Condividi tramite


Puntatori a funzione

Nota

Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle note pertinenti del meeting di progettazione del linguaggio (LDM) .

Puoi trovare maggiori informazioni sul processo di adozione delle speclet di funzionalità nello standard del linguaggio C# nell'articolo sulle specifiche di .

Sommario

Questa proposta fornisce costrutti di linguaggio che espongono opcode IL ai quali attualmente non è possibile accedere in modo efficiente, o per niente, in C# oggi: ldftn e calli. Questi opcode IL possono essere importanti nel codice ad alte prestazioni e gli sviluppatori necessitano di un modo efficiente per accedervi.

Motivazione

Le motivazioni e le informazioni di base per questa funzionalità sono descritte nella questione seguente, insieme a una potenziale implementazione della funzionalità.

dotnet/csharplang#191

Questa è una proposta di progetto alternativa per le intrinseche del compilatore

Progettazione dettagliata

Puntatori a funzione

Il linguaggio consentirà la dichiarazione di puntatori a funzione usando la sintassi delegate*. La sintassi completa è descritta in dettaglio nella sezione successiva, ma dovrebbe assomigliare alla sintassi utilizzata dalle dichiarazioni di tipo Func e Action.

unsafe class Example
{
    void M(Action<int> a, delegate*<int, void> f)
    {
        a(42);
        f(42);
    }
}

Questi tipi sono rappresentati usando il tipo di puntatore a funzione come descritto in ECMA-335. Ciò significa che la chiamata di un delegate* userà calli in cui la chiamata di un delegate userà callvirt nel metodo Invoke. Sintatticamente, tuttavia, la chiamata è identica per entrambi i costrutti.

La definizione ECMA-335 dei puntatori ai metodi include la convenzione di chiamata come parte della firma del tipo (sezione 7.1). La convenzione di chiamata predefinita sarà managed. Le convenzioni di chiamata non gestite possono essere specificate inserendo una parola chiave unmanaged dopo la sintassi delegate*, che utilizzerà l'impostazione predefinita della piattaforma di runtime. È quindi possibile specificare convenzioni non gestite specifiche tra parentesi quadre alla parola chiave unmanaged specificando qualsiasi tipo che inizia con CallConv nello spazio dei nomi System.Runtime.CompilerServices, lasciando fuori il prefisso CallConv. Questi tipi devono provenire dalla libreria principale del programma e il set di combinazioni valide dipende dalla piattaforma.

//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>;

Le conversioni tra tipi delegate* vengono eseguite in base alla loro firma, inclusa la convenzione di chiamata.

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
    }
}

Un tipo di delegate* è un tipo di puntatore, il che significa che ha tutte le funzionalità e le restrizioni di un tipo di puntatore standard:

  • Valido solo in un contesto di unsafe.
  • I metodi che contengono un parametro delegate* o un tipo restituito possono essere chiamati solo da un contesto di unsafe.
  • Impossibile convertire in object.
  • Non può essere utilizzato come argomento generico.
  • Può convertire in modo implicito delegate* in void*.
  • Può convertire in modo esplicito da void* a delegate*.

Restrizioni:

  • Gli attributi personalizzati non possono essere applicati a un delegate* o a uno dei relativi elementi.
  • Un parametro delegate* non può essere contrassegnato come params
  • Un tipo delegate* ha tutte le restrizioni di un tipo di puntatore normale.
  • L'aritmetica del puntatore non può essere eseguita direttamente sui tipi di puntatore a funzione.

Sintassi dei puntatori a funzione

La sintassi completa del puntatore a funzione è rappresentata dalla grammatica seguente:

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'
    ;

Se non viene specificata alcuna calling_convention_specifier, il valore predefinito è managed. La codifica precisa dei metadati del calling_convention_specifier e delle identifiervalide nella unmanaged_calling_convention è descritta in rappresentazione dei metadati delle convenzioni di chiamata.

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>>;

Conversioni del puntatore a funzione

In un contesto insicuro, il set di conversioni implicite disponibili (conversioni implicite) è ampliato per includere le seguenti conversioni di puntatore implicite:

  • Conversioni Esistenti - (§23.5)
  • Da funcptr_typeF0 a un altro funcptr_typeF1, purché siano soddisfatte tutte le condizioni seguenti:
    • F0 e F1 hanno lo stesso numero di parametri e ogni parametro D0n in F0 ha gli stessi ref, outo i modificatori in come il parametro corrispondente D1n in F1.
    • Per ogni parametro di valore (un parametro senza ref, outo modificatore in), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro in F0 al tipo di parametro corrispondente in F1.
    • Per ogni parametro ref, outo in, il tipo di parametro in F0 corrisponde al tipo di parametro corrispondente in F1.
    • Se il tipo restituito è per valore (nessun ref o ref readonly), esiste una conversione d'identità, riferimento implicito o puntatore implicito dal tipo restituito di F1 al tipo restituito di F0.
    • Se il tipo restituito è per riferimento (ref o ref readonly), il tipo restituito e i modificatori ref di F1 sono uguali al tipo restituito e ai modificatori ref di F0.
    • La convenzione di chiamata di F0 corrisponde alla convenzione di chiamata di F1.

Consenti la funzionalità address-of per i metodi

I gruppi di metodi saranno ora consentiti come argomenti in un'espressione di tipo address-of. Il tipo di tale espressione sarà un delegate* con la firma equivalente del metodo di destinazione e una convenzione di chiamata gestita:

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 un contesto non sicuro, un metodo M è compatibile con un tipo di puntatore a funzione F se sono soddisfatte tutte le condizioni seguenti:

  • M e F hanno lo stesso numero di parametri ed ogni parametro in M ha gli stessi modificatori ref, outo in come il parametro corrispondente in F.
  • Per ogni parametro di valore (un parametro senza ref, outo modificatore in), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro in M al tipo di parametro corrispondente in F.
  • Per ogni parametro ref, outo in, il tipo di parametro in M corrisponde al tipo di parametro corrispondente in F.
  • Se il tipo restituito è per valore (nessun ref o ref readonly), esiste una conversione di identità, di riferimento implicito o di puntatore implicito dal tipo restituito di F a quello di M.
  • Se il tipo restituito è per riferimento (ref o ref readonly), il tipo restituito e i modificatori ref di F sono uguali al tipo restituito e ai modificatori ref di M.
  • La convenzione di chiamata di M corrisponde alla convenzione di chiamata di F. Sono inclusi sia il bit della convenzione di chiamata sia i flag di convenzione di chiamata specificati nell'identificatore non gestito.
  • M è un metodo statico.

In un contesto non sicuro esiste una conversione implicita da un'espressione di indirizzo che ha come destinazione un gruppo di metodi E a un tipo di puntatore a funzione compatibile F se E contiene almeno un metodo applicabile nella sua forma normale a un elenco di argomenti costruito usando i tipi di parametro e modificatori di Fcome descritto di seguito.

  • Viene selezionato un singolo metodo M corrispondente a una chiamata al metodo del modulo E(A) con le modifiche seguenti:
    • L'elenco di argomenti A è un elenco di espressioni, ognuna classificata come variabile e con il tipo e il modificatore (ref, outo in) del funcptr_parameter_list corrispondente di F.
    • I metodi candidati sono solo i metodi applicabili nella forma normale, non quelli applicabili nel formato espanso.
    • I metodi candidati sono solo i metodi statici.
  • Se l'algoritmo di risoluzione dell'overload genera un errore, si verifica un errore in fase di compilazione. In caso contrario, l'algoritmo produce un singolo miglior metodo M che ha lo stesso numero di parametri di F e la conversione viene considerata esistente.
  • Il metodo selezionato M deve essere compatibile (come definito in precedenza) con il tipo di puntatore a funzione F. In caso contrario, si verifica un errore in fase di compilazione.
  • Il risultato della conversione è un puntatore a funzione di tipo F.

Ciò significa che gli sviluppatori possono dipendere dalle regole di risoluzione dell'overload per funzionare in combinazione con l'operatore address-of:

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;
    }
}

L'operatore address-of verrà implementato usando l'istruzione ldftn.

Restrizioni di questa funzionalità:

  • Si applica solo ai metodi contrassegnati come static.
  • Le funzioni locali non-static non possono essere usate in &. I dettagli di implementazione di questi metodi non vengono deliberatamente specificati dal linguaggio. Ciò include se sono statici rispetto all'istanza o esattamente alla firma con cui vengono generati.

Operatori sui tipi di puntatore a funzione

La sezione relativa al codice non sicuro per quanto riguarda le espressioni viene modificata come segue.

In un contesto non sicuro, sono disponibili diversi costrutti per operare su tutti i "_pointer_type_s" che non sono "_funcptr_type_s".

  • L'operatore * può essere utilizzato per eseguire l'indirezione del puntatore (§23.6.2).
  • L'operatore -> può essere usato per accedere a un membro di uno struct tramite un puntatore (§23.6.3).
  • L'operatore [] può essere usato per indicizzare un puntatore (§23.6.4).
  • L'operatore & può essere usato per ottenere l'indirizzo di una variabile (§23.6.5).
  • Gli operatori ++ e -- possono essere usati per incrementare e decrementare i puntatori (23.6.6 ).
  • Gli operatori + e - possono essere usati per eseguire l'aritmetica del puntatore (§23.6.7).
  • Gli operatori ==, !=, <, >, <=e => possono essere usati per confrontare i puntatori (§23.6.8).
  • L'operatore stackalloc può essere usato per allocare memoria dallo stack di chiamate (§23.8).
  • L'istruzione fixed può essere usata per correggere temporaneamente una variabile in modo che sia possibile ottenere il relativo indirizzo (§23.7).

In un contesto non sicuro sono disponibili diversi costrutti per l'uso in tutti i _funcptr_type_s:

Inoltre, tutte le sezioni di Pointers in expressions vengono modificate in modo da impedire i tipi di puntatore a funzione, ad eccezione di Pointer comparison e The sizeof operator.

Membro di funzione migliore

Il miglior membro di funzione §12.6.4.3 verrà modificato per includere la seguente riga:

Un delegate* è più specifico di void*

Ciò significa che è possibile eseguire l'overload su void* e un delegate* e usare comunque l'operatore address-of.

Inferenza di Tipo

Nel codice unsafe vengono apportate le modifiche seguenti agli algoritmi di inferenza del tipo:

Tipi di input

§12.6.3.4

Viene aggiunto quanto segue:

Se E è un gruppo di metodi indicati tramite indirizzo e T è un tipo di puntatore a funzione, allora tutti i tipi di parametro di T sono tipi di input di E con tipo T.

Tipi di output

§12.6.3.5

Viene aggiunto quanto segue:

Se E è un gruppo di metodi 'address-of' e T è un tipo di puntatore di funzione, il tipo restituito di T è un tipo di output di E con tipo T.

Inferenze dei tipi di output

§12.6.3.7

Il punto elenco seguente viene aggiunto tra i punti elenco 2 e 3:

  • Se E è un gruppo di metodi di indirizzo e T è un tipo di puntatore a funzione con tipi di parametro T1...Tk e tipo di ritorno Tb, e la risoluzione dell'overload di E con i tipi T1..Tk restituisce un singolo metodo con tipo di ritorno U, quindi viene effettuata un'inferenza di limite inferiore da U a Tb.

Conversione migliore dall'espressione

§12.6.4.5

Il sotto-punto elenco seguente viene aggiunto come case al punto 2:

  • V è un tipo di puntatore a funzione delegate*<V2..Vk, V1> e U è un tipo di puntatore a funzione delegate*<U2..Uk, U1>e la convenzione di chiamata di V è identica a Ue il riferimento di Vi è identico a Ui.

Inferenze con limiti inferiori

§12.6.3.10

Il caso seguente viene aggiunto al punto 3:

  • V è un tipo di puntatore a funzione delegate*<V2..Vk, V1> ed esiste un tipo di puntatore a funzione delegate*<U2..Uk, U1> in modo che U sia identico a delegate*<U2..Uk, U1>e la convenzione di chiamata di V sia identica a Ue il riferimento di Vi sia identico a Ui.

Il primo punto elenco di inferenza da Ui a Vi viene modificato in:

  • Se U non è un tipo di puntatore a funzione e Ui non è noto come un tipo di riferimento, o se U è un tipo di puntatore a funzione e Ui non è noto come un tipo di puntatore a funzione o un tipo di riferimento, viene eseguita un'inferenza esatta

Quindi, aggiunto dopo il terzo punto elenco di inferenza da Ui a Vi:

  • In caso contrario, se V è delegate*<V2..Vk, V1> l'inferenza dipende dal parametro i-th di delegate*<V2..Vk, V1>:
    • Se V1:
      • Se il valore restituito è , viene eseguita una inferenza con limite inferiore.
      • Se il valore restituito è per riferimento, viene eseguita una inferenza esatta.
    • Se V2..Vk:
      • Se il parametro è per valore, viene eseguita un'inferenza del limite superiore .
      • Se il parametro è passato per riferimento, viene fatta un'inferenza esatta .

Inferenze con limite superiore

§12.6.3.11

Il caso seguente viene aggiunto al punto 2:

  • U è un tipo di puntatore a funzione delegate*<U2..Uk, U1> e V è un tipo di puntatore a funzione identico a delegate*<V2..Vk, V1>e la convenzione di chiamata di U è identica a Ve il riferimento di Ui è identico a Vi.

Il primo punto elenco di inferenza da Ui a Vi viene modificato in:

  • Se U non è un tipo di puntatore a funzione e Ui non è noto come tipo riferimento o se U è un tipo di puntatore a funzione e Ui non è noto come un tipo di puntatore a funzione o un tipo riferimento, viene eseguita un''inferenza esatta

Aggiunto quindi dopo il terzo punto elenco di inferenza da Ui a Vi:

  • In caso contrario, se U è delegate*<U2..Uk, U1> l'inferenza dipende dal parametro i-th di delegate*<U2..Uk, U1>:
    • Se U1:
      • Se il ritorno è per valore, viene eseguita un'inferenza limite superiore .
      • Se il valore restituito è per riferimento, viene eseguita una inferenza esatta.
    • Se U2..Uk:
      • Se il parametro è per valore, viene eseguita un'inferenza limite inferiore.
      • Se il parametro è per riferimento, viene eseguita un''inferenza esatta.

Rappresentazione dei metadati dei parametri in, oute ref readonly e dei tipi restituiti

Le signature del puntatore a funzione non prevedono flag di parametro, pertanto è necessario codificare se i parametri e il tipo restituito sono in, outo ref readonly usando modreqs.

in

Riutilizziamo System.Runtime.InteropServices.InAttribute, applicato come modreq al specificatore di riferimento su un parametro o un tipo di ritorno, per significare quanto segue:

  • Se applicato a un identificatore di riferimento di parametro, questo parametro viene considerato come in.
  • Se applicato all'identificatore ref del tipo restituito, il tipo restituito viene considerato come ref readonly.

out

Viene usato System.Runtime.InteropServices.OutAttribute, applicato come modreq all'identificatore di riferimento in un tipo di parametro, per indicare che il parametro è un parametro out.

Errori

  • È un errore applicare OutAttribute come modreq a un tipo restituito.
  • È un errore applicare sia InAttribute che OutAttribute come modreq a un tipo di parametro.
  • Se uno dei due elementi viene specificato tramite modopt, vengono ignorati.

Rappresentazione dei metadati delle convenzioni di chiamata

Le convenzioni di chiamata vengono codificate in una firma del metodo nei metadati mediante una combinazione del flag CallKind nella firma e zero o più modoptall'inizio della firma. ECMA-335 dichiara attualmente gli elementi seguenti nel flag CallKind:

CallKind
   : default
   | unmanaged cdecl
   | unmanaged fastcall
   | unmanaged thiscall
   | unmanaged stdcall
   | varargs
   ;

Tra questi, i puntatori a funzione in C# supporteranno tutti, tranne varargs.

Inoltre, il runtime (e eventualmente 335) verrà aggiornato per includere una nuova CallKind su piattaforme nuove. Questo non ha attualmente un nome formale, ma questo documento userà unmanaged ext come segnaposto per supportare il nuovo formato di convenzione di chiamata estendibile. Senza modopt, unmanaged ext è la convenzione di chiamata predefinita della piattaforma, mentre unmanaged è senza parentesi quadre.

Mappatura del calling_convention_specifier a un CallKind

Un calling_convention_specifier omesso o specificato come managedcorrisponde al defaultCallKind. Questo è il default CallKind di qualsiasi metodo non attribuito con UnmanagedCallersOnly.

C# riconosce 4 identificatori speciali che corrispondono a specifici CallKindesistenti non gestiti da ECMA 335. Affinché questo mapping venga eseguito, questi identificatori devono essere specificati autonomamente, senza altri identificatori e questo requisito viene codificato nella specifica per unmanaged_calling_conventions. Questi identificatori sono Cdecl, Thiscall, Stdcalle Fastcall, che corrispondono rispettivamente a unmanaged cdecl, unmanaged thiscall, unmanaged stdcalle unmanaged fastcall. Se viene specificato più di un identifer o il singolo identifier non è degli identificatori riconosciuti in modo speciale, viene eseguita una ricerca di nomi speciali sull'identificatore con le regole seguenti:

  • Anteponiamo alla identifier la stringa CallConv
  • Vengono esaminati solo i tipi definiti nello spazio dei nomi System.Runtime.CompilerServices.
  • Vengono esaminati solo i tipi definiti nella libreria principale dell'applicazione, ovvero la libreria che definisce System.Object e non ha dipendenze.
  • Vengono esaminati solo i tipi pubblici.

Se la ricerca ha esito positivo in tutti i identifierspecificati in un unmanaged_calling_convention, il CallKind viene codificato come unmanaged ext, e ciascuno dei tipi risolti viene codificato nell'insieme di modoptall'inizio della firma del puntatore di funzione. È importante notare che queste regole implicano che gli utenti non possono anteporre CallConva identifier, in quanto ciò comporterà la ricerca di CallConvCallConvVectorCall.

Quando si interpretano i metadati, esaminiamo prima il CallKind. Se è diverso da unmanaged ext, tutti i modoptnel tipo restituito vengono ignorati per determinare la convenzione di chiamata e usare solo il CallKind. Se il CallKind è unmanaged ext, esaminiamo i "modopt" all'inizio del tipo di puntatore di funzione, prendendo l'unione di tutti i tipi che soddisfano i seguenti requisiti:

  • è definito nella libreria principale, ovvero la libreria che non fa riferimento ad altre librerie e definisce System.Object.
  • Il tipo è definito nello spazio dei nomi System.Runtime.CompilerServices.
  • Il tipo inizia con il prefisso CallConv.
  • Il tipo è pubblico.

Questi rappresentano i tipi che devono essere trovati durante l'esecuzione della ricerca dei identifierin un unmanaged_calling_convention quando si definisce un tipo di puntatore a funzione nel codice sorgente.

È un errore tentare di usare un puntatore a funzione con un CallKind di unmanaged ext se il runtime di destinazione non supporta la funzionalità. La determinazione verrà effettuata cercando la presenza della costante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind. Se questa costante è presente, il runtime viene considerato in grado di supportare la funzionalità.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute è un attributo usato da CLR per indicare che un metodo deve essere chiamato con una convenzione di chiamata specifica. Per questo motivo, viene introdotto il supporto seguente per l'uso dell'attributo :

  • È un errore chiamare direttamente un metodo annotato con questo attributo in C#. Gli utenti devono ottenere un puntatore di funzione al metodo e quindi invocare tale puntatore.
  • Si tratta di un errore per applicare l'attributo a qualsiasi elemento diverso da un normale metodo statico o una normale funzione locale statica. Il compilatore C# contrassegnerà qualsiasi metodo non statico o statico non ordinario importato dai metadati con questo attributo come non supportato dal linguaggio.
  • È un errore che un metodo contrassegnato dall'attributo abbia un parametro o un tipo di ritorno che non sia un unmanaged_type.
  • È un errore per un metodo contrassegnato con l'attributo di avere parametri di tipo, anche se tali parametri di tipo sono vincolati a unmanaged.
  • È un errore per un metodo di un tipo generico essere contrassegnato con l'attributo.
  • Si tratta di un errore durante la conversione di un metodo contrassegnato con l'attributo in un tipo delegato.
  • È un errore specificare tipi per UnmanagedCallersOnly.CallConvs che non soddisfano i requisiti della convenzione di chiamata modoptnei metadati.

Quando si determina la convenzione di chiamata di un metodo contrassegnato con un attributo UnmanagedCallersOnly valido, il compilatore esegue i controlli seguenti sui tipi specificati nella proprietà CallConvs per determinare il CallKind effettivo e i modoptche devono essere utilizzati per determinare la convenzione di chiamata:

  • Se non vengono specificati tipi, il CallKind viene considerato come unmanaged ext, senza convenzioni di chiamata modopts all'inizio del tipo di puntatore di funzione.
  • Se è specificato un tipo e tale tipo è denominato CallConvCdecl, CallConvThiscall, CallConvStdcallo CallConvFastcall, l'CallKind viene considerato come unmanaged cdecl, unmanaged thiscall, unmanaged stdcallo unmanaged fastcallrispettivamente, senza convenzioni di chiamata modopts all'inizio del tipo di puntatore di funzione.
  • Se vengono specificati più tipi o se il singolo tipo non è uno di quelli indicati specificamente sopra, il CallKind è trattato come unmanaged ext, con l'unione dei tipi specificati considerata come modoptall'inizio del tipo di puntatore di funzione.

Il compilatore esamina quindi questa raccolta effettiva di CallKind e modopt e utilizza le normali regole dei metadati per determinare la convenzione di chiamata finale del tipo di puntatore a funzione.

Domande aperte

Rilevamento del supporto di runtime per unmanaged ext

https://github.com/dotnet/runtime/issues/38135 tiene traccia dell'aggiunta di questo indicatore. A seconda del feedback ottenuto dalla revisione, si userà la proprietà specificata nel problema oppure si userà la presenza di UnmanagedCallersOnlyAttribute come flag che determina se i runtime supportano unmanaged ext.

Considerazioni

Consenti metodi di istanza

La proposta potrebbe essere estesa per supportare i metodi di istanza sfruttando la convenzione di chiamata CLI EXPLICITTHIS (denominata instance nel codice C#). Questa forma di puntatori a funzione CLI inserisce il parametro this come primo parametro esplicito della sintassi del puntatore di funzione.

unsafe class Instance {
    void Use() {
        delegate* instance<Instance, string> f = &ToString;
        f(this);
    }
}

Questo è sensato, ma aggiunge un po' di complicazione alla proposta. In particolare, poiché i puntatori a funzione diversi dalla convenzione di chiamata instance e managed sarebbero incompatibili anche se entrambi i casi vengono usati per richiamare metodi gestiti con la stessa firma C#. In ogni caso in cui si è ritenuto che fosse utile avere questa funzione, c'era una semplice soluzione alternativa: utilizzare una funzione locale static.

unsafe class Instance {
    void Use() {
        static string toString(Instance i) => i.ToString();
        delegate*<Instance, string> f = &toString;
        f(this);
    }
}

Non richiedere unsafe nella dichiarazione

Anziché richiedere unsafe a ogni uso di un delegate*, è necessario solo nel punto in cui un gruppo di metodi viene convertito in un delegate*. Questo è il momento in cui entrano in gioco i principali problemi di sicurezza (sapendo che l'assembly contenitore non può essere scaricato finché il valore rimane attivo). La richiesta di unsafe in altre posizioni può essere considerata eccessiva.

Questo è il modo in cui il design è stato originariamente progettato. Ma le regole linguistiche risultanti risultavano molto innaturali. È impossibile nascondere il fatto che si tratta di un valore puntatore e continuava a trapelare anche senza la parola chiave unsafe. Ad esempio, la conversione in object non può essere consentita, non può essere membro di un classe così via... La progettazione C# deve richiedere unsafe per tutti gli usi del puntatore e di conseguenza questa progettazione ne segue.

Gli sviluppatori saranno ancora in grado di presentare un wrapper sicuro sopra i valori di delegate* allo stesso modo in cui lo fanno oggi per i normali tipi di puntatori. Considerare:

unsafe struct Action {
    delegate*<void> _ptr;

    Action(delegate*<void> ptr) => _ptr = ptr;
    public void Invoke() => _ptr();
}

Uso dei delegati

Invece di usare un nuovo elemento della sintassi, delegate*, è sufficiente usare i tipi di delegate esistenti con un * che segue il tipo:

Func<object, object, bool>* ptr = &object.ReferenceEquals;

La gestione della convenzione di chiamata può essere eseguita annotando i tipi di delegate con un attributo che specifica un valore CallingConvention. La mancanza di un attributo indicherebbe la convenzione di chiamata gestita.

La codifica in IL è problematica. Il valore sottostante deve essere rappresentato come puntatore, ma deve anche:

  1. Disporre di un tipo univoco per consentire gli overload con diversi tipi di puntatore a funzione.
  2. Essere equivalente per scopi OHI oltre i limiti dell'assembly.

L'ultimo punto è particolarmente problematico. Ciò significa che ogni assembly che usa Func<int>* deve codificare un tipo equivalente nei metadati anche se Func<int>* è definito in un assembly anche se non controlla. Inoltre, qualsiasi altro tipo definito con il nome System.Func<T> in un assembly che non è mscorlib deve essere diverso dalla versione definita in mscorlib.

Un'opzione che è stata esplorata era emettere un puntatore di questo tipo come mod_req(Func<int>) void*. Questo non funziona, perché un mod_req non può legarsi a un TypeSpec e quindi non può mirare a istanze generiche.

Puntatori a funzioni denominate

La sintassi del puntatore di funzione può essere complessa, in particolare in casi complessi come puntatori a funzioni annidate. Invece di far digitare agli sviluppatori la firma della funzione ogni volta, il linguaggio potrebbe consentire dichiarazioni denominate di puntatori a funzione, come avviene con delegate.

func* void Action();

unsafe class NamedExample {
    void M(Action a) {
        a();
    }
}

Parte del problema qui è che la primitiva CLI sottostante non ha nomi, pertanto sarebbe una pura invenzione di C# e richiederebbe un po' di lavoro di metadati per abilitare. Questo è fattibile, ma richiede un notevole impegno di lavoro. È essenzialmente necessario che C# abbia un accompagnamento alla tabella 'type def' puramente per questi nomi.

Inoltre, quando sono stati esaminati gli argomenti per i puntatori a funzione con nome, abbiamo scoperto che si potevano applicare altrettanto bene a numerosi altri scenari. Ad esempio, sarebbe altrettanto conveniente dichiarare tuple denominate per ridurre la necessità di digitare la firma completa in tutti i casi.

(int x, int y) Point;

class NamedTupleExample {
    void M(Point p) {
        Console.WriteLine(p.x);
    }
}

Dopo aver discusso, abbiamo deciso di non permettere la dichiarazione esplicita dei tipi di delegate*. Se riscontriamo una necessità significativa basata sul feedback sull'utilizzo dei clienti, prenderemo in considerazione una soluzione di denominazione che funzioni per i puntatori a funzioni, le tuple, i generici, eccetera. Questa sarà probabilmente simile nella forma ad altre proposte come un supporto completo per typedef nel linguaggio.

Considerazioni future

delegati statici

Ciò si riferisce a la proposta di consentire la dichiarazione di tipi di delegate che possono fare riferimento solo ai membri static. Il vantaggio è che tali istanze di delegate possono essere senza necessità di allocazione e migliori per scenari sensibili alle prestazioni.

Se la funzionalità dei puntatori a funzioni viene implementata, la proposta di static delegate sarà probabilmente chiusa. Il vantaggio proposto di tale funzionalità è la natura che non richiede allocazione. Tuttavia, recenti indagini hanno rilevato che non è possibile raggiungere l'obiettivo a causa del rilascio dell'assemblaggio. Deve essere presente un handle sicuro dal static delegate al metodo a cui fa riferimento per evitare che l'assembly venga scaricato da sotto di esso.

Per mantenere ogni istanza static delegate, sarebbe necessario allocare un nuovo handle, che è contrario agli obiettivi della proposta. C'erano alcuni design in cui l'allocazione poteva essere ammortizzata a una singola allocazione per ogni call-site, ma era un po' complesso e non sembrava valere il compromesso.

Ciò significa che gli sviluppatori devono essenzialmente decidere tra i compromessi seguenti:

  1. Sicurezza in caso di scaricamento dell'assemblaggio: questo richiede allocazioni e quindi delegate è già un'opzione sufficiente.
  2. Nessuna protezione in presenza di scarico dell'assemblaggio: utilizzare un delegate*. Può essere racchiuso in un struct per consentire l'utilizzo al di fuori di un contesto unsafe nel resto del codice.