Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Anmerkung
Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.
Weitere Informationen über den Prozess zur Aufnahme von Feature Speclets in den C#-Sprachstandard finden Sie in dem Artikel über die Spezifikationen.
Zusammenfassung
Dieser Vorschlag enthält Sprachkonstrukte, die IL-Opcodes offenlegen, auf die heute in C# nicht effizient oder überhaupt nicht zugegriffen werden kann: ldftn und calli. Diese IL-Opcodes können bei leistungsstarkem Code wichtig sein, und Entwickler benötigen eine effiziente Möglichkeit, auf sie zuzugreifen.
Motivation
Die Motivationen und Hintergründe für diese Funktion werden im folgenden Thema beschrieben (ebenso wie eine mögliche Implementierung der Funktion):
Dies ist ein alternativer Designvorschlag für Compiler-Intrinsics
Detailliertes Design
Funktionszeiger
Die Sprache ermöglicht die Deklaration von Funktionszeigern mithilfe der delegate*-Syntax. Die vollständige Syntax wird im nächsten Abschnitt detailliert beschrieben, aber sie soll der Syntax von Func- und Action-Typdeklarationen ähneln.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Diese Typen werden mithilfe des Funktionszeigertyps dargestellt, wie in ECMA-335 beschrieben. Das bedeutet, dass der Aufruf von delegate*calli verwendet, während der Aufruf von delegatecallvirt für die Methode Invoke verwendet.
Syntaktisch ist der Aufruf für beide Konstrukte jedoch identisch.
Die ECMA-335-Definition von Methodenzeigern enthält die Aufrufkonvention als Teil der Typsignatur (Abschnitt 7.1).
Die Standardaufrufkonvention wird managedsein. Nicht verwaltete Aufrufkonventionen können durch ein unmanaged-Schlüsselwort vor der delegate*-Syntax angegeben werden, wodurch die Standardeinstellung der Runtime-Plattform verwendet wird. Bestimmte nicht verwaltete Konventionen können dann in eckigen Klammern für das Schlüsselwort unmanaged angegeben werden, indem ein beliebiger Typ angegeben wird, der mit CallConv im System.Runtime.CompilerServices Namespace beginnt und das präfix CallConv verlässt. Diese Typen müssen aus der Kernbibliothek des Programms stammen, und die Gruppe gültiger Kombinationen ist plattformabhängig.
//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;
// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;
// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;
// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;
Die Konvertierung zwischen den delegate*-Typen erfolgt auf der Grundlage ihrer Signatur einschließlich der Aufrufkonvention.
unsafe class Example {
void Conversions() {
delegate*<int, int, int> p1 = ...;
delegate* managed<int, int, int> p2 = ...;
delegate* unmanaged<int, int, int> p3 = ...;
p1 = p2; // okay p1 and p2 have compatible signatures
Console.WriteLine(p2 == p1); // True
p2 = p3; // error: calling conventions are incompatible
}
}
Ein delegate* Typ ist ein Zeigertyp, was bedeutet, dass er über alle Funktionen und Einschränkungen eines Standardzeigertyps verfügt:
- Gültig nur in einem
unsafeKontext. - Methoden, die einen
delegate*Parameter oder Rückgabetyp enthalten, können nur aus einemunsafeKontext aufgerufen werden. - Kann nicht in
objectkonvertiert werden. - Kann nicht als generisches Argument verwendet werden.
- Kann implizit
delegate*invoid*konvertieren. - Kann explizit von
void*indelegate*konvertiert werden.
Einschränkungen:
- Benutzerdefinierte Attribute können nicht auf ein
delegate*oder eines seiner Elemente angewendet werden. - Ein
delegate*-Parameter kann nicht alsparamsmarkiert werden. - Ein
delegate*Typ weist alle Einschränkungen eines normalen Zeigertyps auf. - Zeigerarithmetik kann nicht direkt auf Funktionszeigertypen ausgeführt werden.
Syntax des Funktionszeigers
Die vollständige Funktionszeiger-Syntax wird durch die folgende Grammatik ausgedrückt:
pointer_type
: ...
| funcptr_type
;
funcptr_type
: 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
;
calling_convention_specifier
: 'managed'
| 'unmanaged' ('[' unmanaged_calling_convention ']')?
;
unmanaged_calling_convention
: 'Cdecl'
| 'Stdcall'
| 'Thiscall'
| 'Fastcall'
| identifier (',' identifier)*
;
funptr_parameter_list
: (funcptr_parameter ',')*
;
funcptr_parameter
: funcptr_parameter_modifier? type
;
funcptr_return_type
: funcptr_return_modifier? return_type
;
funcptr_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
funcptr_return_modifier
: 'ref'
| 'ref readonly'
;
Wenn kein calling_convention_specifier angegeben wird, ist der Standardwert managed. Die genaue Metadatenkodierung der calling_convention_specifier und wasidentifier in der unmanaged_calling_convention gültig ist, wird in Metadaten-Darstellung der Aufrufkonventionen behandelt.
delegate int Func1(string s);
delegate Func1 Func2(Func1 f);
// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;
// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;
Konvertierung von Funktions-Pointers
In einem unsicheren Kontext wird der Satz der verfügbaren impliziten Konvertierungen (implizite Konvertierungen) erweitert, um die folgenden impliziten Zeigerkonvertierungen einzuschließen:
- Bestehende Konversionen - (§23.5)
- Von funcptr_type
F0zu einem anderen funcptr_typeF1, sofern alle folgenden Punkte zutreffen:-
F0undF1haben die gleiche Anzahl von Parametern, und jeder ParameterD0ninF0hat die gleichenref,outoderinModifizierer wie der entsprechende ParameterD1ninF1. - Für jeden Wertparameter (ein Parameter ohne
ref,out, oderinModifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp inF0auf den entsprechenden Parametertyp inF1. - Für jeden
ref-,out- oderin-Parameter entspricht der Parametertyp inF0dem entsprechenden Parametertyp inF1. - Wenn der Rückgabetyp ein Wert ist (kein
refoderref readonly), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp vonF1zum Rückgabetyp vonF0. - Wenn der Rückgabetyp eine Referenz ist (
refoderref readonly), sind der Rückgabetyp und dieref-Modifikatoren vonF1die gleichen wie der Rückgabetyp und dieref-Modifikatoren vonF0. - Die Aufrufkonvention von
F0entspricht der Aufrufkonvention vonF1.
-
Zulassen der Adressierung von Zielmethoden
Methodengruppen sind jetzt als Argumente für einen address-of-Ausdruck zulässig. Der Typ eines solchen Ausdrucks ist ein delegate*, das die entsprechende Signatur der Zielmethode und eine verwaltete Aufrufkonvention hat:
unsafe class Util {
public static void Log() { }
void Use() {
delegate*<void> ptr1 = &Util.Log;
// Error: type "delegate*<void>" not compatible with "delegate*<int>";
delegate*<int> ptr2 = &Util.Log;
}
}
In einem unsicheren Kontext ist eine Methode M mit einem Funktionszeigertyp F kompatibel, wenn alle folgenden Werte zutreffen:
-
MundFhaben die gleiche Anzahl von Parametern, und jeder Parameter inMhat die gleichenref,outoderinModifizierer wie der entsprechende Parameter inF. - Für jeden Wertparameter (ein Parameter ohne
ref,out, oderinModifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp inMauf den entsprechenden Parametertyp inF. - Für jeden
ref-,out- oderin-Parameter entspricht der Parametertyp inMdem entsprechenden Parametertyp inF. - Wenn der Rückgabetyp ein Wert ist (kein
refoderref readonly), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp vonFzum Rückgabetyp vonM. - Wenn der Rückgabetyp eine Referenz ist (
refoderref readonly), sind der Rückgabetyp und dieref-Modifikatoren vonFdie gleichen wie der Rückgabetyp und dieref-Modifikatoren vonM. - Die Aufrufkonvention von
Mentspricht der Aufrufkonvention vonF. Dies beinhaltet sowohl das Aufrufkonventions-Bit als auch alle Aufrufkonventions-Flags, die im nicht verwalteten Bezeichner angegeben sind. -
Mist eine statische Methode.
In einem unsicheren Kontext gibt es eine implizite Konvertierung von einem address-of-Ausdruck, dessen Ziel eine Methodengruppe E ist, in einen kompatiblen Funktionspointer-Typ F, wenn E mindestens eine Methode enthält, die in ihrer normalen Form auf eine Argumentliste anwendbar ist, die unter Verwendung der Parametertypen und Modifikatoren von F konstruiert wurde, wie im Folgenden beschrieben.
- Eine einzelne Methode
Mwird ausgewählt, die einem Methodenaufruf des FormularsE(A)mit den folgenden Änderungen entspricht:- Die Liste der Argumente
Aist eine Liste von Ausdrücken, die jeweils als Variable klassifiziert und mit dem Typ und Modifikator (ref,outoderin) der entsprechenden funcptr_parameter_list vonF. - Die Kandidatenmethoden sind nur die Methoden, die in ihrer normalen Form anwendbar sind, nicht die, die in ihrer erweiterten Form anwendbar sind.
- Die Kandidatenmethoden sind nur die Methoden, die statisch sind.
- Die Liste der Argumente
- Wenn der Algorithmus der Überladungsauflösung einen Fehler erzeugt, tritt ein Kompilierungszeitfehler auf. Andernfalls erzeugt der Algorithmus eine einzige beste Methode
Mmit derselben Anzahl von Parametern wieF, und die Konvertierung wird als vorhanden betrachtet. - Die ausgewählte Methode
Mmuss (wie oben definiert) mit dem FunktionszeigertypFkompatibel sein. Andernfalls tritt ein Kompilierungszeitfehler auf. - Das Ergebnis der Konvertierung ist ein Funktionszeiger vom Typ
F.
Das bedeutet, dass Entwickler sich auf Regeln zur Auflösung von Überlasten verlassen können, die in Verbindung mit dem address-of-Operator funktionieren:
unsafe class Util {
public static void Log() { }
public static void Log(string p1) { }
public static void Log(int i) { }
void Use() {
delegate*<void> a1 = &Log; // Log()
delegate*<int, void> a2 = &Log; // Log(int i)
// Error: ambiguous conversion from method group Log to "void*"
void* v = &Log;
}
}
Der address-of-Operator wird mit der Anweisung ldftn implementiert.
Einschränkungen dieses Features:
- Gilt nur für Methoden, die als
staticgekennzeichnet sind. - Nicht-
staticlokale Funktionen können in&nicht verwendet werden. Die Implementierungsdetails dieser Methoden werden absichtlich nicht in der Sprache spezifiziert. Dazu gehört, ob sie statisch oder instanziell sind oder mit welcher Signatur sie ausgegeben werden.
Operatoren auf Funktions-Pointer-Typen
Der Abschnitt im unsicheren Code über Ausdrücke wird wie folgt geändert:
In einem unsicheren Kontext stehen mehrere Konstrukte zur Verfügung, um mit all _pointer_type_s zu arbeiten, die nicht _funcptr_type_s sind:
- Der
*Operator kann verwendet werden, um eine Pointer-Umleitung durchzuführen (§23.6.2).- Der
->-Operator kann für den Zugriff auf ein Mitglied einer Struktur über einen Zeiger verwendet werden (§23.6.3).- Der
[]-Operator darf zum Indizieren eines Zeigers verwendet werden (§23.6.4).- Der
&-Operator kann verwendet werden, um die Adresse einer Variablen zu erhalten (§23.6.5).- Die Operatoren
++und--können verwendet werden, um Zeiger zu erhöhen und zu verringern (§23.6.6).- Die operatoren
+und-können verwendet werden, um Zeigerarithmetik durchzuführen (§23.6.7).- Die Operatoren
==,!=,<,>,<=und=>können zum Vergleichen von Zeigern verwendet werden (§23.6.8).- Der
stackallocOperator kann verwendet werden, um Speicher aus dem Aufrufstapel zuzuweisen (§23.8).- Die
fixed-Anweisung kann verwendet werden, um eine Variable vorübergehend zu korrigieren, damit ihre Adresse abgerufen werden kann (§23.7).In einem unsicheren Kontext stehen mehrere Konstrukte zur Verfügung, um mit all _funcptr_type_s zu arbeiten:
- Der
&Operator kann verwendet werden, um die Adresse von statischen Methoden zu erhalten (Zulassen der Adressierung von Zielmethoden)- Die Operatoren
==,!=,<,>,<=und=>können zum Vergleichen von Zeigern verwendet werden (§23.6.8).
Darüber hinaus ändern wir alle Abschnitte in Pointers in expressions, um Funktionszeigertypen zu verbieten, mit Ausnahme von Pointer comparison und The sizeof operator.
Besseres Funktionselement
§12.6.4.3 Better function member (Besseres Funktionelement) wird geändert, um die folgende Zeile aufzunehmen
Ein
delegate*ist spezifischer alsvoid*
Das bedeutet, dass es möglich ist, auf void* und einen delegate* zu überladen, und trotzdem den address-of-Operator sinnvoll zu verwenden.
Typableitung
Im unsicheren Code werden die folgenden Änderungen an den Typinferenzalgorithmen vorgenommen:
Eingabetypen
Es wird Folgendes hinzugefügt:
Wenn
Eeine Adresse einer Methodengruppe undTein Funktionspointer-Typ ist, dann sind alle Parametertypen vonTEingabetypen vonEmit dem TypT.
Ausgabetypen
Es wird Folgendes hinzugefügt:
Wenn
Eeine adress-of-Method-Gruppe ist undTein Funktions-Pointer-Typ ist, dann ist der Rückgabetyp vonTein Ausgabetyp vonEmit TypT.
Output-Typ-Inferenzen (Ausgabetypableitung)
Der folgende Aufzählungspunkt wird zwischen den Aufzählungspunkten 2 und 3 eingefügt:
- Wenn
Eeine adress-of-Methodengruppe ist undTein Funktionspointer-Typ mit den ParametertypenT1...Tkund dem RückgabetypTbist und die Überladungsauflösung vonEmit den TypenT1..Tkeine einzelne Methode mit dem RückgabetypUergibt, dann wird eine Unterschrankeninferenz vonUnachTbdurchgeführt.
Bessere Konvertierung eines Ausdrucks
Der folgende Unteraufzählungspunkt wird als Fall zu Aufzählungspunkt 2 hinzugefügt:
Vist ein Funktions-Pointer-Typdelegate*<V2..Vk, V1>undUist ein Funktions-Pointer-Typdelegate*<U2..Uk, U1>und die Aufrufkonvention vonVist identisch mitUund die Widerstandsfähigkeit vonViist identisch mitUi.
Untergrenzableitungen
In Aufzählungspunkt 3 wird der folgende Fall hinzugefügt:
Vist ein Funktionspointer vom Typdelegate*<V2..Vk, V1>und es gibt einen Funktionspointer vom Typdelegate*<U2..Uk, U1>, sodassUidentisch mitdelegate*<U2..Uk, U1>ist, und die Aufrufkonvention vonVidentisch mitUist, und die Referenz vonViidentisch mitUiist.
Der erste Aufzählungspunkt der Schlussfolgerung von Ui zu Vi wird geändert in:
- Wenn
Ukein Funktionspointer-Typ ist undUinicht als Referenztyp bekannt ist, oder wennUein Funktionspointer-Typ ist undUinicht als Funktionspointer-Typ oder Referenztyp bekannt ist, dann wird eine exakte Inferenz durchgeführt
Außerdem wird nach dem 3. Aufzählungspunkt der Rückschluss von Ui auf Vi hinzugefügt:
- Andernfalls, wenn
Vdelegate*<V2..Vk, V1>ist, hängt die Schlussfolgerung vom i-ten Parameter vondelegate*<V2..Vk, V1>ab:
- Wenn V1:
- Wenn die Rückgabe durch einen Wert erfolgt, dann wird eine Untergrenze abgeleitet.
- Wenn die Rückgabe per Referenz erfolgt, wird eine exakte Interferenz durchgeführt.
- Wenn V2..Vk:
- Wenn der Parameter einen Wert hat, dann wird eine obere Grenze abgeleitet.
- Wenn der Parameter eine Referenz ist, dann wird eine exakte Inferenz durchgeführt.
Upper-bound inferences (Obergrenzenableitung)
In Aufzählungspunkt 2 wird der folgende Fall hinzugefügt:
Uist ein Funktions-Pointer-Typdelegate*<U2..Uk, U1>undVist ein Funktions-Pointer-Typ, der identisch ist mitdelegate*<V2..Vk, V1>und die Aufrufkonvention vonUist identisch mitVund die Referenz vonUiist identisch mitVi.
Der erste Aufzählungspunkt der Schlussfolgerung von Ui zu Vi wird geändert in:
- Wenn
Ukein Funktionspointer-Typ ist undUinicht als Referenztyp bekannt ist, oder wennUein Funktionspointer-Typ ist undUinicht als Funktionspointer-Typ oder Referenztyp bekannt ist, dann wird eine exakte Inferenz durchgeführt
Außerdem wird nach dem 3. Aufzählungspunkt der Rückschluss von Ui auf Vi hinzugefügt:
- Andernfalls, wenn
Udelegate*<U2..Uk, U1>ist, hängt die Schlussfolgerung vom i-ten Parameter vondelegate*<U2..Uk, U1>ab:
- Wenn U1:
- Wenn die Rückgabe einen Wert hat, dann wird eine obere Grenze abgeleitet.
- Wenn die Rückgabe per Referenz erfolgt, wird eine exakte Interferenz durchgeführt.
- Wenn U2..Uk:
- Wenn der Parameter ein Wert ist, dann wird eine Untergrenze abgeleitet.
- Wenn der Parameter eine Referenz ist, dann wird eine exakte Inferenz durchgeführt.
Metadaten-Darstellung von in, out und ref readonly Parameter und Rückgabetypen
Funktionspointer-Signaturen haben keine Parameter-Flags, so dass wir kodieren müssen, ob Parameter und Rückgabetyp in, out oder ref readonly durch die Verwendung von modreqs.
in
Wir verwenden System.Runtime.InteropServices.InAttribute wieder, das als modreq auf den Referenzbezeichner eines Parameters oder Rückgabetyps angewendet wird, um Folgendes darzustellen:
- Wenn dies auf einen Parameter ref specifier angewendet wird, wird dieser Parameter als
inbehandelt. - Bei Anwendung auf den Rückgabetyp ref specifier wird der Rückgabetyp als
ref readonlybehandelt.
out
Wir verwenden System.Runtime.InteropServices.OutAttribute, angewandt als modreq auf den ref-Spezifizierer eines Parametertyps, um anzuzeigen, dass der Parameter ein out-Parameter ist.
Irrtümer
- Es ist ein Fehler,
OutAttributeals modreq auf einen Rückgabetyp anzuwenden. - Es ist ein Fehler, sowohl
InAttributeals auchOutAttributeals Modreq auf einen Parametertyp anzuwenden. - Wenn einer von beiden über modopt angegeben wird, werden sie ignoriert.
Metadaten zur Darstellung von Aufrufkonventionen
Aufrufkonventionen werden in einer Methodensignatur in den Metadaten durch eine Kombination aus dem CallKind-Flag in der Signatur und null oder mehr modopt am Anfang der Signatur kodiert. ECMA-335 deklariert derzeit die folgenden Elemente im CallKind-Flag:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
Von diesen werden Funktionszeiger in C# alle bis auf varargs unterstützt.
Außerdem wird die Runtime (und eventuell 335) aktualisiert, um ein neues CallKind auf neuen Plattformen einzubinden. Dies hat derzeit keinen formalen Namen, aber dieses Dokument verwendet unmanaged ext als Platzhalter, um für das neue erweiterbare Aufrufkonventionsformat zu stehen. Ohne modopt ist unmanaged ext die Standardaufrufkonvention der Plattform, unmanaged ohne die eckigen Klammern.
Zuordnung der calling_convention_specifier zu einer CallKind
Ein calling_convention_specifier, das weggelassen oder als managed angegeben wird, entspricht dem defaultCallKind. Dies ist die Standardeinstellung CallKind für alle Methoden, die nicht mit UnmanagedCallersOnly gekennzeichnet sind.
C# kennt 4 spezielle Bezeichner, die auf bestimmte bestehende nicht verwaltete CallKind aus ECMA 335 abgebildet werden. Damit diese Zuordnung erfolgen kann, müssen diese Bezeichner allein und ohne andere Bezeichner angegeben werden. Diese Anforderung ist in der Spezifikation für unmanaged_calling_convention enthalten. Diese Bezeichner sind Cdecl, Thiscall, Stdcallund Fastcall, die unmanaged cdecl, unmanaged thiscall, unmanaged stdcallund unmanaged fastcallentsprechen. Wenn mehr als ein identifer angegeben wird oder der einzelne identifier nicht zu den speziell anerkannten Bezeichnern gehört, führen wir eine spezielle Namenssuche für den Bezeichner nach den folgenden Regeln durch:
- Wir stellen dem
identifierdie ZeichenfolgeCallConvvoran. - Wir betrachten nur Typen, die im
System.Runtime.CompilerServices-Namespace definiert sind. - Wir betrachten nur Typen, die in der Kernbibliothek der Anwendung definiert sind, bei der es sich um die Bibliothek handelt, die
System.Objectdefiniert und keine Abhängigkeiten aufweist. - Wir betrachten nur die öffentlichen Typen.
Wenn die Suche bei allen in einem identifier angegebenen unmanaged_calling_convention erfolgreich ist, kodieren wir CallKind als unmanaged ext und kodieren jeden der aufgelösten Typen in der Menge von modopt am Anfang der Funktionspointer-Signatur. Bitte beachten Sie, dass diese Regeln bedeuten, dass Benutzer diesen identifier kein CallConv voranstellen können, da dies zu einer Suche nach CallConvCallConvVectorCall führt.
Wenn wir Metadaten interpretieren, schauen wir uns zunächst die CallKind an. Wenn es sich um etwas anderes als unmanaged ext handelt, ignorieren wir alle modopt des Rückgabetyps für die Zwecke der Bestimmung der Aufrufkonvention und verwenden nur CallKind. Wenn CallKind ist unmanaged ext, sehen wir uns die modopts am Anfang des Funktionspointer-Typs an und nehmen die Kombination aller Typen, die die folgenden Anforderungen erfüllen:
- Die ist in der Core-Bibliothek definiert, also der Bibliothek, die keine anderen Bibliotheken referenziert und
System.Objectdefiniert. - Der Typ wird im
System.Runtime.CompilerServicesNamespace definiert. - Der Typ beginnt mit dem Präfix
CallConv. - Der Typ ist öffentlich.
Diese stellen die Typen dar, die bei der Suche nach dem identifier in einem unmanaged_calling_convention gefunden werden müssen, wenn ein Funktionszeigertyp im Quellcode definiert wird.
Es ist ein Fehler, wenn Sie versuchen, einen Funktionspointer mit einem CallKind von unmanaged ext zu verwenden, wenn die Ziel-Laufzeitumgebung diese Funktion nicht unterstützt. Dies wird durch die Suche nach dem Vorhandensein der System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind-Konstante bestimmt. Wenn diese Konstante vorhanden ist, wird angenommen, dass die Laufzeit die Funktion unterstützt.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute ist ein Attribut, das vom CLR verwendet wird, um anzugeben, dass eine Methode mit einer bestimmten Aufrufkonvention aufgerufen werden soll. Aus diesem Grund wird die folgende Unterstützung für die Arbeit mit dem Attribut eingeführt:
- Es ist ein Fehler, eine Methode, die mit diesem Attribut aus C# kommentiert wurde, direkt aufzurufen. Benutzer müssen einen Funktionspointer auf die Methode erhalten und dann diesen Pointer aufrufen.
- Es ist ein Fehler, das Attribut auf eine andere als eine normale statische Methode oder gewöhnliche statische lokale Funktion anzuwenden. Der C#-Compiler markiert alle nicht-statischen oder statischen nicht-gewöhnlichen Methoden, die aus Metadaten mit diesem Attribut importiert werden, als von Language nicht unterstützt.
- Es ist ein Fehler, wenn eine Methode, die mit dem Attribut gekennzeichnet ist, einen Parameter oder Rückgabetyp hat, der nicht
unmanaged_typeist. - Es ist ein Fehler, wenn eine mit dem Attribut gekennzeichnete Methode Typparameter hat, selbst wenn diese Typparameter auf
unmanagedbeschränkt sind. - Es ist ein Fehler, wenn eine Methode in einem generischen Typ mit dem Attribut gekennzeichnet ist.
- Es ist ein Fehler, eine mit dem Attribut gekennzeichnete Methode in einen Delegatentyp umzuwandeln.
- Es ist ein Fehler, alle Typen für
UnmanagedCallersOnly.CallConvsanzugeben, die nicht den Anforderungen für die Aufrufkonventionmodopts in Metadaten entsprechen.
Bei der Bestimmung der Aufrufkonvention einer Methode, die mit einem gültigen UnmanagedCallersOnly-Attribut gekennzeichnet ist, führt der Compiler die folgenden Überprüfungen an den in der CallConvs-Eigenschaft angegebenen Typen durch, um die effektiven CallKind und modopt zu bestimmen, die zur Bestimmung der Aufrufkonvention verwendet werden sollen:
- Wenn keine Typen angegeben werden, wird
CallKindwieunmanaged extbehandelt, ohne die Aufrufkonventionmodoptam Anfang des Funktionspointer-Typs. - Wenn ein Typ angegeben ist und dieser Typ den Namen
CallConvCdecl,CallConvThiscall,CallConvStdcalloderCallConvFastcallträgt, wird derCallKindbehandelt wieunmanaged cdecl,unmanaged thiscall,unmanaged stdcalloderunmanaged fastcall, bzw. ohne Aufrufkonventionmodoptam Anfang des Funktionspointer-Typs. - Wenn mehrere Typen angegeben sind oder der einzelne Typ nicht einer der oben genannten Typen ist, wird
CallKindalsunmanaged extbehandelt, wobei die Vereinigung der angegebenen Typen alsmodoptam Anfang des Funktionspointer-Typs behandelt wird.
Der Compiler sieht sich dann diese effektive CallKind- und modopt-Sammlung an und verwendet normale Metadatenregeln, um die endgültige Aufrufkonvention des Funktionspointer-Typs zu bestimmen.
Offene Fragen
Erkennung der Laufzeitunterstützung für unmanaged ext
https://github.com/dotnet/runtime/issues/38135 protokolliert das Hinzufügen dieser Kennzeichnung. Abhängig von den Rückmeldungen aus der Überprüfung werden wir entweder die in der Frage angegebene Eigenschaft verwenden oder das Vorhandensein von UnmanagedCallersOnlyAttribute als Flag verwenden, das bestimmt, ob die Laufzeiten unmanaged ext unterstützen.
Überlegungen
Instanzmethoden zulassen
Der Vorschlag könnte erweitert werden, um Instanzmethoden zu unterstützen, indem die EXPLICITTHIS CLI-Aufrufkonvention (mit dem Namen instance im C#-Code) genutzt wird. Diese Form von CLI-Funktionszeigern platziert den this Parameter als expliziten ersten Parameter der Funktionszeigersyntax.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
Dies ist solide, fügt aber dem Vorschlag einige Komplikationen hinzu. Insbesondere, weil Funktionszeiger, die sich in den Aufrufkonventionen instance und managed unterscheiden, inkompatibel wären, auch wenn in beiden Fällen verwaltete Methoden mit derselben C#-Signatur aufgerufen werden. Außerdem gab es in allen Fällen, in denen dies sinnvoll war, eine einfache Lösung: die Verwendung einer lokalen static-Funktion.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
Verlangt kein „unsafe“ bei der Anmeldung
Anstatt bei jeder Verwendung eines unsafe ein delegate* zu verlangen, genügt es, es an dem Punkt zu verlangen, an dem eine Methodengruppe in ein delegate* umgewandelt wird. Hier kommen die zentralen Sicherheitsaspekte ins Spiel (das Wissen, dass die enthaltende Komponente nicht entladen werden kann, solange der Wert aktiv ist). Die Forderung nach unsafe an den anderen Standorten kann als exzessiv angesehen werden.
So wurde das Design ursprünglich beabsichtigt. Aber die daraus resultierenden Sprachregeln fühlten sich sehr ungünstig an. Es ist unmöglich, die Tatsache auszublenden, dass es sich um einen Pointer-Wert handelt, der auch ohne das Schlüsselwort unsafe immer wieder durchscheint. Zum Beispiel kann die Umwandlung in object nicht erlaubt sein, es kann kein Bestandteil von class sein, usw. ... Das C#-Design sieht vor, dass unsafe für alle Pointer-Verwendungen erforderlich ist, und daher folgt dieses Design dieser Vorgabe.
Entwickler werden weiterhin in der Lage sein, einen sicheren Wrapper über delegate*-Werte zu legen, genauso wie sie es heute für normale Pointertypen tun. Berücksichtigen Sie:
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Verwendung von Delegaten
Anstatt ein neues Syntaxelement, delegate*, zu verwenden, verwenden Sie einfach die vorhandenen delegate-Typen mit einem * nach dem Typ:
Func<object, object, bool>* ptr = &object.ReferenceEquals;
Die Handhabung der Aufrufkonvention kann erfolgen, indem die delegate-Typen mit einem Attribut versehen werden, das einen CallingConvention-Wert angibt. Ein fehlendes Attribut würde bedeuten, dass die Konvention für verwaltete Anrufe gilt.
Die Codierung in IL ist problematisch. Der zugrunde liegende Wert muss als Pointer dargestellt werden, aber er muss auch Folgendes erfüllen:
- Haben einen eindeutigen Typ, um Überladungen mit verschiedenen Funktionspointer-Typen zu ermöglichen.
- Für OHI-Zwecke über die Baugruppengrenzen hinweg gleichwertig sein.
Der letzte Punkt ist besonders problematisch. Das bedeutet, dass jede Assembly, die Func<int>* verwendet, einen entsprechenden Typ in den Metadaten kodieren muss, auch wenn Func<int>* in einer Assembly definiert ist, die nicht kontrolliert wird.
Darüber hinaus muss jeder andere Typ, der mit dem Namen System.Func<T> definiert ist, in einer Assembly, bei der es sich nicht um mscorlib handelt, von der in mscorlib definierten Version verschieden sein.
Eine Möglichkeit, die untersucht wurde, war die Ausgabe eines solchen Pointers als mod_req(Func<int>) void*. Das funktioniert jedoch nicht, da ein mod_req sich nicht an ein TypeSpec binden kann und daher keine generischen Instanziierungen als Ziel hat.
Pointer für benannte Funktionen
Die Syntax von Funktionszeigern kann umständlich sein, insbesondere in komplexen Fällen wie bei geschachtelten Funktionzeigern. Anstatt die Entwickler jedes Mal die Signatur abtippen zu lassen, könnte Language benannte Deklarationen von Funktionspointern erlauben, wie dies bei delegate der Fall ist.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Ein Teil des Problems hier ist der zugrunde liegende CLI-Grundtyp hat keine Namen, daher wäre dies rein eine C#-Erfindung und erfordert ein bisschen Metadatenarbeit, um dies zu ermöglichen. Das ist machbar, erfordert aber eine beträchtliche Menge an Arbeit. Im Grunde genommen muss C# ein Pendant zur Tabelle type def nur für diese Namen haben.
Auch als die Argumente für benannte Funktionszeiger untersucht wurden, stellten wir fest, dass sie sich ebenso gut auf eine Vielzahl anderer Szenarien anwenden ließen. Es wäre zum Beispiel genauso sinnvoll, benannte Tupel zu deklarieren, damit Sie nicht in jedem Fall die vollständige Signatur abtippen müssen.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
Nach eingehender Beratung haben wir beschlossen, die namentliche Deklaration von delegate*-Typen nicht zuzulassen. Wenn wir aufgrund des Feedbacks unserer Kunden feststellen, dass ein erheblicher Bedarf besteht, werden wir eine Lösung für die Benennung von Funktionspointern, Tupeln, Generika usw. untersuchen. Dies wird wahrscheinlich eine ähnliche Form haben wie andere Vorschläge wie die vollständige Unterstützung von typedef in Language.
Zukünftige Überlegungen
statische Delegaten
Dies bezieht sich auf den Vorschlag, die Angabe von delegate-Typen zuzulassen, die sich nur auf static- Elemente beziehen können. Der Vorteil ist, dass solche delegate-Instanzen frei von Zuweisungen sein können und in leistungssensiblen Szenarien besser sind.
Wenn die Funktion des Funktionszeigers implementiert wird, wird der static delegate-Vorschlag wahrscheinlich geschlossen werden. Der vorgeschlagene Vorteil dieser Funktion ist, dass sie keine Zuweisungen erfordert. Jüngste Untersuchungen haben jedoch ergeben, dass dies aufgrund der Entladung der Baugruppen nicht machbar ist. Um zu verhindern, dass die Baugruppe unter ihr entladen wird, muss ein stabiler Mechanismus von der static delegate bis zu der Methode, auf die sie sich bezieht, vorhanden sein.
Um jede static delegate-Instanz zu pflegen, müsste ein neues Handle zugewiesen werden, was den Zielen des Vorschlags zuwiderläuft. Es gab einige Entwürfe, bei denen die Zuteilung auf eine einzige Zuteilung pro Call-Site zurückgeführt werden konnte, aber das war etwas kompliziert und schien den Nachteil nicht wert zu sein.
Das bedeutet, dass Entwickler im Wesentlichen zwischen den folgenden Kompromissen entscheiden müssen:
- Sicherheit bei der Entladung von Baugruppen: Dies erfordert eine Zuteilung und daher ist
delegatebereits eine ausreichende Option. - Keine Sicherheit bei der Entladung der Baugruppe: Verwenden Sie eine
delegate*. Dies kann in einstructeingebunden werden, um die Verwendung außerhalb einesunsafe-Kontextes im restlichen Code zu ermöglichen.
C# feature specifications