Funktionszeiger
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 delegate
callvirt
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 managed
sein. 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
unsafe
Kontext. - Methoden, die einen
delegate*
Parameter oder Rückgabetyp enthalten, können nur aus einemunsafe
Kontext aufgerufen werden. - Kann nicht in
object
konvertiert 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 alsparams
markiert 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
F0
zu einem anderen funcptr_typeF1
, sofern alle folgenden Punkte zutreffen:F0
undF1
haben die gleiche Anzahl von Parametern, und jeder ParameterD0n
inF0
hat die gleichenref
,out
oderin
Modifizierer wie der entsprechende ParameterD1n
inF1
.- Für jeden Wertparameter (ein Parameter ohne
ref
,out
, oderin
Modifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp inF0
auf den entsprechenden Parametertyp inF1
. - Für jeden
ref
-,out
- oderin
-Parameter entspricht der Parametertyp inF0
dem entsprechenden Parametertyp inF1
. - Wenn der Rückgabetyp ein Wert ist (kein
ref
oderref readonly
), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp vonF1
zum Rückgabetyp vonF0
. - Wenn der Rückgabetyp eine Referenz ist (
ref
oderref readonly
), sind der Rückgabetyp und dieref
-Modifikatoren vonF1
die gleichen wie der Rückgabetyp und dieref
-Modifikatoren vonF0
. - Die Aufrufkonvention von
F0
entspricht 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:
M
undF
haben die gleiche Anzahl von Parametern, und jeder Parameter inM
hat die gleichenref
,out
oderin
Modifizierer wie der entsprechende Parameter inF
.- Für jeden Wertparameter (ein Parameter ohne
ref
,out
, oderin
Modifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp inM
auf den entsprechenden Parametertyp inF
. - Für jeden
ref
-,out
- oderin
-Parameter entspricht der Parametertyp inM
dem entsprechenden Parametertyp inF
. - Wenn der Rückgabetyp ein Wert ist (kein
ref
oderref readonly
), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp vonF
zum Rückgabetyp vonM
. - Wenn der Rückgabetyp eine Referenz ist (
ref
oderref readonly
), sind der Rückgabetyp und dieref
-Modifikatoren vonF
die gleichen wie der Rückgabetyp und dieref
-Modifikatoren vonM
. - Die Aufrufkonvention von
M
entspricht der Aufrufkonvention vonF
. Dies beinhaltet sowohl das Aufrufkonventions-Bit als auch alle Aufrufkonventions-Flags, die im nicht verwalteten Bezeichner angegeben sind. M
ist 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
M
wird ausgewählt, die einem Methodenaufruf des FormularsE(A)
mit den folgenden Änderungen entspricht:- Die Liste der Argumente
A
ist eine Liste von Ausdrücken, die jeweils als Variable klassifiziert und mit dem Typ und Modifikator (ref
,out
oderin
) 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
M
mit derselben Anzahl von Parametern wieF
, und die Konvertierung wird als vorhanden betrachtet. - Die ausgewählte Methode
M
muss (wie oben definiert) mit dem FunktionszeigertypF
kompatibel 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
static
gekennzeichnet sind. - Nicht-
static
lokale 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
stackalloc
Operator 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
E
eine Adresse einer Methodengruppe undT
ein Funktionspointer-Typ ist, dann sind alle Parametertypen vonT
Eingabetypen vonE
mit dem TypT
.
Ausgabetypen
Es wird Folgendes hinzugefügt:
Wenn
E
eine adress-of-Method-Gruppe ist undT
ein Funktions-Pointer-Typ ist, dann ist der Rückgabetyp vonT
ein Ausgabetyp vonE
mit TypT
.
Output-Typ-Inferenzen (Ausgabetypableitung)
Der folgende Aufzählungspunkt wird zwischen den Aufzählungspunkten 2 und 3 eingefügt:
- Wenn
E
eine adress-of-Methodengruppe ist undT
ein Funktionspointer-Typ mit den ParametertypenT1...Tk
und dem RückgabetypTb
ist und die Überladungsauflösung vonE
mit den TypenT1..Tk
eine einzelne Methode mit dem RückgabetypU
ergibt, dann wird eine Unterschrankeninferenz vonU
nachTb
durchgeführt.
Bessere Konvertierung eines Ausdrucks
Der folgende Unteraufzählungspunkt wird als Fall zu Aufzählungspunkt 2 hinzugefügt:
V
ist ein Funktions-Pointer-Typdelegate*<V2..Vk, V1>
undU
ist ein Funktions-Pointer-Typdelegate*<U2..Uk, U1>
und die Aufrufkonvention vonV
ist identisch mitU
und die Widerstandsfähigkeit vonVi
ist identisch mitUi
.
Untergrenzableitungen
In Aufzählungspunkt 3 wird der folgende Fall hinzugefügt:
V
ist ein Funktionspointer vom Typdelegate*<V2..Vk, V1>
und es gibt einen Funktionspointer vom Typdelegate*<U2..Uk, U1>
, sodassU
identisch mitdelegate*<U2..Uk, U1>
ist, und die Aufrufkonvention vonV
identisch mitU
ist, und die Referenz vonVi
identisch mitUi
ist.
Der erste Aufzählungspunkt der Schlussfolgerung von Ui
zu Vi
wird geändert in:
- Wenn
U
kein Funktionspointer-Typ ist undUi
nicht als Referenztyp bekannt ist, oder wennU
ein Funktionspointer-Typ ist undUi
nicht 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
V
delegate*<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:
U
ist ein Funktions-Pointer-Typdelegate*<U2..Uk, U1>
undV
ist ein Funktions-Pointer-Typ, der identisch ist mitdelegate*<V2..Vk, V1>
und die Aufrufkonvention vonU
ist identisch mitV
und die Referenz vonUi
ist identisch mitVi
.
Der erste Aufzählungspunkt der Schlussfolgerung von Ui
zu Vi
wird geändert in:
- Wenn
U
kein Funktionspointer-Typ ist undUi
nicht als Referenztyp bekannt ist, oder wennU
ein Funktionspointer-Typ ist undUi
nicht 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
U
gleichdelegate*<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
in
behandelt. - Bei Anwendung auf den Rückgabetyp ref specifier wird der Rückgabetyp als
ref readonly
behandelt.
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,
OutAttribute
als modreq auf einen Rückgabetyp anzuwenden. - Es ist ein Fehler, sowohl
InAttribute
als auchOutAttribute
als 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 default
CallKind
. 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
, Stdcall
und Fastcall
, die unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
und unmanaged fastcall
entsprechen. 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
identifier
die ZeichenfolgeCallConv
voran. - 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.Object
definiert 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.Object
definiert. - Der Typ wird im
System.Runtime.CompilerServices
Namespace 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_type
ist. - Es ist ein Fehler, wenn eine mit dem Attribut gekennzeichnete Methode Typparameter hat, selbst wenn diese Typparameter auf
unmanaged
beschrä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.CallConvs
anzugeben, die nicht den Anforderungen für die Aufrufkonventionmodopt
s 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
CallKind
wieunmanaged ext
behandelt, ohne die Aufrufkonventionmodopt
am Anfang des Funktionspointer-Typs. - Wenn ein Typ angegeben ist und dieser Typ den Namen
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
oderCallConvFastcall
trägt, wird derCallKind
behandelt wieunmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
oderunmanaged fastcall
, bzw. ohne Aufrufkonventionmodopt
am Anfang des Funktionspointer-Typs. - Wenn mehrere Typen angegeben sind oder der einzelne Typ nicht einer der oben genannten Typen ist, wird
CallKind
alsunmanaged ext
behandelt, wobei die Vereinigung der angegebenen Typen alsmodopt
am 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
delegate
bereits eine ausreichende Option. - Keine Sicherheit bei der Entladung der Baugruppe: Verwenden Sie eine
delegate*
. Dies kann in einstruct
eingebunden werden, um die Verwendung außerhalb einesunsafe
-Kontextes im restlichen Code zu ermöglichen.
C# feature specifications