Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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à.
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 diunsafe. - Impossibile convertire in
object. - Non può essere utilizzato come argomento generico.
- Può convertire in modo implicito
delegate*invoid*. - Può convertire in modo esplicito da
void*adelegate*.
Restrizioni:
- Gli attributi personalizzati non possono essere applicati a un
delegate*o a uno dei relativi elementi. - Un parametro
delegate*non può essere contrassegnato comeparams - 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_type
F0a un altro funcptr_typeF1, purché siano soddisfatte tutte le condizioni seguenti:-
F0eF1hanno lo stesso numero di parametri e ogni parametroD0ninF0ha gli stessiref,outo i modificatoriincome il parametro corrispondenteD1ninF1. - Per ogni parametro di valore (un parametro senza
ref,outo modificatorein), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro inF0al tipo di parametro corrispondente inF1. - Per ogni parametro
ref,outoin, il tipo di parametro inF0corrisponde al tipo di parametro corrispondente inF1. - Se il tipo restituito è per valore (nessun
reforef readonly), esiste una conversione d'identità, riferimento implicito o puntatore implicito dal tipo restituito diF1al tipo restituito diF0. - Se il tipo restituito è per riferimento (
reforef readonly), il tipo restituito e i modificatorirefdiF1sono uguali al tipo restituito e ai modificatorirefdiF0. - La convenzione di chiamata di
F0corrisponde alla convenzione di chiamata diF1.
-
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:
-
MeFhanno lo stesso numero di parametri ed ogni parametro inMha gli stessi modificatoriref,outoincome il parametro corrispondente inF. - Per ogni parametro di valore (un parametro senza
ref,outo modificatorein), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro inMal tipo di parametro corrispondente inF. - Per ogni parametro
ref,outoin, il tipo di parametro inMcorrisponde al tipo di parametro corrispondente inF. - Se il tipo restituito è per valore (nessun
reforef readonly), esiste una conversione d'identità, riferimento implicito o puntatore implicito dal tipo restituito diFal tipo restituito diM. - Se il tipo restituito è per riferimento (
reforef readonly), il tipo restituito e i modificatorirefdiFsono uguali al tipo restituito e ai modificatorirefdiM. - La convenzione di chiamata di
Mcorrisponde alla convenzione di chiamata diF. 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
Mcorrispondente a una chiamata al metodo del moduloE(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,outoin) del funcptr_parameter_list corrispondente diF. - 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.
- L'elenco di argomenti
- 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
Mche ha lo stesso numero di parametri diFe la conversione viene considerata esistente. - Il metodo selezionato
Mdeve essere compatibile (come definito in precedenza) con il tipo di puntatore a funzioneF. 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-
staticnon 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
stackallocpuò essere usato per allocare memoria dallo stack di chiamate (§23.8).- L'istruzione
fixedpuò 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:
- L'operatore
&può essere usato per ottenere l'indirizzo dei metodi statici (Consentire l'indirizzamento ai metodi di destinazione)- Gli operatori
==,!=,<,>,<=e=>possono essere usati per confrontare i puntatori (§23.6.8).
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 divoid*
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
Viene aggiunto quanto segue:
Se
Eè un gruppo di metodi indicati tramite indirizzo eTè un tipo di puntatore a funzione, allora tutti i tipi di parametro diTsono tipi di input diEcon tipoT.
Tipi di output
Viene aggiunto quanto segue:
Se
Eè un gruppo di metodi 'address-of' eTè un tipo di puntatore di funzione, il tipo restituito diTè un tipo di output diEcon tipoT.
Inferenze dei tipi di output
Il punto elenco seguente viene aggiunto tra i punti elenco 2 e 3:
- Se
Eè un gruppo di metodi di indirizzo eTè un tipo di puntatore a funzione con tipi di parametroT1...Tke tipo di ritornoTb, e la risoluzione dell'overload diEcon i tipiT1..Tkrestituisce un singolo metodo con tipo di ritornoU, quindi viene effettuata un'inferenza di limite inferiore daUaTb.
Conversione migliore dall'espressione
Il sotto-punto elenco seguente viene aggiunto come case al punto 2:
Vè un tipo di puntatore a funzionedelegate*<V2..Vk, V1>eUè un tipo di puntatore a funzionedelegate*<U2..Uk, U1>e la convenzione di chiamata diVè identica aUe il riferimento diViè identico aUi.
Inferenze con limiti inferiori
Il caso seguente viene aggiunto al punto 3:
Vè un tipo di puntatore a funzionedelegate*<V2..Vk, V1>ed esiste un tipo di puntatore a funzionedelegate*<U2..Uk, U1>in modo cheUsia identico adelegate*<U2..Uk, U1>e la convenzione di chiamata diVsia identica aUe il riferimento diVisia identico aUi.
Il primo punto elenco di inferenza da Ui a Vi viene modificato in:
- Se
Unon è un tipo di puntatore a funzione eUinon è noto come un tipo di riferimento, o seUè un tipo di puntatore a funzione eUinon è 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 didelegate*<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
Il caso seguente viene aggiunto al punto 2:
Uè un tipo di puntatore a funzionedelegate*<U2..Uk, U1>eVè un tipo di puntatore a funzione identico adelegate*<V2..Vk, V1>e la convenzione di chiamata diUè identica aVe il riferimento diUiè identico aVi.
Il primo punto elenco di inferenza da Ui a Vi viene modificato in:
- Se
Unon è un tipo di puntatore a funzione eUinon è noto come un tipo di riferimento, o seUè un tipo di puntatore a funzione eUinon è noto come un tipo di puntatore a funzione o un tipo di 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 didelegate*<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 è passato per riferimento, viene fatta 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
OutAttributecome modreq a un tipo restituito. - È un errore applicare sia
InAttributecheOutAttributecome 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
identifierla stringaCallConv - 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.Objecte 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 identifiera CallConv, 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.CallConvsche non soddisfano i requisiti della convenzione di chiamatamodoptnei 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
CallKindviene considerato comeunmanaged ext, senza convenzioni di chiamatamodopts all'inizio del tipo di puntatore di funzione. - Se è specificato un tipo e tale tipo è denominato
CallConvCdecl,CallConvThiscall,CallConvStdcalloCallConvFastcall, l'CallKindviene considerato comeunmanaged cdecl,unmanaged thiscall,unmanaged stdcallounmanaged fastcallrispettivamente, senza convenzioni di chiamatamodopts 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 comeunmanaged ext, con l'unione dei tipi specificati considerata comemodoptall'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:
- Disporre di un tipo univoco per consentire gli overload con diversi tipi di puntatore a funzione.
- 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:
- Sicurezza in caso di scaricamento dell'assemblaggio: questo richiede allocazioni e quindi
delegateè già un'opzione sufficiente. - Nessuna protezione in presenza di scarico dell'assemblaggio: utilizzare un
delegate*. Può essere racchiuso in unstructper consentire l'utilizzo al di fuori di un contestounsafenel resto del codice.
C# feature specifications