Freigeben über


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):

dotnet/csharplang#191

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 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 unsafe Kontext.
  • Methoden, die einen delegate* Parameter oder Rückgabetyp enthalten, können nur aus einem unsafe Kontext aufgerufen werden.
  • Kann nicht in objectkonvertiert werden.
  • Kann nicht als generisches Argument verwendet werden.
  • Kann implizit delegate* in void*konvertieren.
  • Kann explizit von void* in delegate*konvertiert werden.

Einschränkungen:

  • Benutzerdefinierte Attribute können nicht auf ein delegate* oder eines seiner Elemente angewendet werden.
  • Ein delegate*-Parameter kann nicht als params 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_typeF0 zu einem anderen funcptr_typeF1, sofern alle folgenden Punkte zutreffen:
    • F0 und F1 haben die gleiche Anzahl von Parametern, und jeder Parameter D0n in F0 hat die gleichen ref, outoder in Modifizierer wie der entsprechende Parameter D1n in F1.
    • Für jeden Wertparameter (ein Parameter ohne ref, out, oder in Modifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp in F0 auf den entsprechenden Parametertyp in F1.
    • Für jeden ref-, out- oder in-Parameter entspricht der Parametertyp in F0 dem entsprechenden Parametertyp in F1.
    • Wenn der Rückgabetyp ein Wert ist (kein ref oder ref readonly), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp von F1 zum Rückgabetyp von F0.
    • Wenn der Rückgabetyp eine Referenz ist (ref oder ref readonly), sind der Rückgabetyp und die ref-Modifikatoren von F1 die gleichen wie der Rückgabetyp und die ref-Modifikatoren von F0.
    • Die Aufrufkonvention von F0 entspricht der Aufrufkonvention von F1.

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 und F haben die gleiche Anzahl von Parametern, und jeder Parameter in M hat die gleichen ref, outoder in Modifizierer wie der entsprechende Parameter in F.
  • Für jeden Wertparameter (ein Parameter ohne ref, out, oder in Modifikator), eine Identitätsumwandlung, eine implizite Referenzumwandlung oder eine implizite Pointer-Umwandlung vom Parametertyp in M auf den entsprechenden Parametertyp in F.
  • Für jeden ref-, out- oder in-Parameter entspricht der Parametertyp in M dem entsprechenden Parametertyp in F.
  • Wenn der Rückgabetyp ein Wert ist (kein ref oder ref readonly), existiert eine Identitäts-, implizite Referenz- oder implizite Pointer-Konvertierung vom Rückgabetyp von F zum Rückgabetyp von M.
  • Wenn der Rückgabetyp eine Referenz ist (ref oder ref readonly), sind der Rückgabetyp und die ref-Modifikatoren von F die gleichen wie der Rückgabetyp und die ref-Modifikatoren von M.
  • Die Aufrufkonvention von M entspricht der Aufrufkonvention von F. 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 Formulars E(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 oder in) der entsprechenden funcptr_parameter_list von F.
    • 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.
  • 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 wie F, und die Konvertierung wird als vorhanden betrachtet.
  • Die ausgewählte Methode M muss (wie oben definiert) mit dem Funktionszeigertyp Fkompatibel 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-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:

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 als void*

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

§12.6.3.4

Es wird Folgendes hinzugefügt:

Wenn E eine Adresse einer Methodengruppe und T ein Funktionspointer-Typ ist, dann sind alle Parametertypen von T Eingabetypen von E mit dem Typ T.

Ausgabetypen

§12.6.3.5

Es wird Folgendes hinzugefügt:

Wenn E eine adress-of-Method-Gruppe ist und T ein Funktions-Pointer-Typ ist, dann ist der Rückgabetyp von T ein Ausgabetyp von E mit Typ T.

Output-Typ-Inferenzen (Ausgabetypableitung)

§12.6.3.7

Der folgende Aufzählungspunkt wird zwischen den Aufzählungspunkten 2 und 3 eingefügt:

  • Wenn E eine adress-of-Methodengruppe ist und T ein Funktionspointer-Typ mit den Parametertypen T1...Tk und dem Rückgabetyp Tb ist und die Überladungsauflösung von E mit den Typen T1..Tk eine einzelne Methode mit dem RückgabetypU ergibt, dann wird eine Unterschrankeninferenz von U nach Tb durchgeführt.

Bessere Konvertierung eines Ausdrucks

§12.6.4.5

Der folgende Unteraufzählungspunkt wird als Fall zu Aufzählungspunkt 2 hinzugefügt:

  • V ist ein Funktions-Pointer-Typ delegate*<V2..Vk, V1> und U ist ein Funktions-Pointer-Typ delegate*<U2..Uk, U1> und die Aufrufkonvention von V ist identisch mit U und die Widerstandsfähigkeit von Vi ist identisch mit Ui.

Untergrenzableitungen

§12.6.3.10

In Aufzählungspunkt 3 wird der folgende Fall hinzugefügt:

  • V ist ein Funktionspointer vom Typ delegate*<V2..Vk, V1> und es gibt einen Funktionspointer vom Typ delegate*<U2..Uk, U1>, sodass U identisch mit delegate*<U2..Uk, U1> ist, und die Aufrufkonvention von V identisch mit U ist, und die Referenz von Vi identisch mit Ui ist.

Der erste Aufzählungspunkt der Schlussfolgerung von Ui zu Vi wird geändert in:

  • Wenn U kein Funktionspointer-Typ ist und Ui nicht als Referenztyp bekannt ist, oder wenn U ein Funktionspointer-Typ ist und Ui 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 von delegate*<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)

§12.6.3.11

In Aufzählungspunkt 2 wird der folgende Fall hinzugefügt:

  • U ist ein Funktions-Pointer-Typ delegate*<U2..Uk, U1> und V ist ein Funktions-Pointer-Typ, der identisch ist mit delegate*<V2..Vk, V1> und die Aufrufkonvention von U ist identisch mit V und die Referenz von Ui ist identisch mit Vi.

Der erste Aufzählungspunkt der Schlussfolgerung von Ui zu Vi wird geändert in:

  • Wenn U kein Funktionspointer-Typ ist und Ui nicht als Referenztyp bekannt ist, oder wenn U ein Funktionspointer-Typ ist und Ui 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 gleich delegate*<U2..Uk, U1> ist, hängt die Schlussfolgerung vom i-ten Parameter von delegate*<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 auch OutAttribute 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 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 identifier die Zeichenfolge CallConv 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_typeist.
  • 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 Aufrufkonvention modopts 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 wie unmanaged ext behandelt, ohne die Aufrufkonvention modopt am Anfang des Funktionspointer-Typs.
  • Wenn ein Typ angegeben ist und dieser Typ den Namen CallConvCdecl, CallConvThiscall, CallConvStdcall oder CallConvFastcall trägt, wird der CallKind behandelt wie unmanaged cdecl, unmanaged thiscall, unmanaged stdcall oder unmanaged fastcall, bzw. ohne Aufrufkonvention modopt am Anfang des Funktionspointer-Typs.
  • Wenn mehrere Typen angegeben sind oder der einzelne Typ nicht einer der oben genannten Typen ist, wird CallKind als unmanaged ext behandelt, wobei die Vereinigung der angegebenen Typen als modopt 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:

  1. Haben einen eindeutigen Typ, um Überladungen mit verschiedenen Funktionspointer-Typen zu ermöglichen.
  2. 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:

  1. Sicherheit bei der Entladung von Baugruppen: Dies erfordert eine Zuteilung und daher ist delegate bereits eine ausreichende Option.
  2. Keine Sicherheit bei der Entladung der Baugruppe: Verwenden Sie eine delegate*. Dies kann in ein struct eingebunden werden, um die Verwendung außerhalb eines unsafe-Kontextes im restlichen Code zu ermöglichen.