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à.
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 identifier
valide 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
F0
a un altro funcptr_typeF1
, purché siano soddisfatte tutte le condizioni seguenti:-
F0
eF1
hanno lo stesso numero di parametri e ogni parametroD0n
inF0
ha gli stessiref
,out
o i modificatoriin
come il parametro corrispondenteD1n
inF1
. - Per ogni parametro di valore (un parametro senza
ref
,out
o modificatorein
), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro inF0
al tipo di parametro corrispondente inF1
. - Per ogni parametro
ref
,out
oin
, il tipo di parametro inF0
corrisponde al tipo di parametro corrispondente inF1
. - Se il tipo restituito è per valore (nessun
ref
oref readonly
), esiste una conversione d'identità, riferimento implicito o puntatore implicito dal tipo restituito diF1
al tipo restituito diF0
. - Se il tipo restituito è per riferimento (
ref
oref readonly
), il tipo restituito e i modificatoriref
diF1
sono uguali al tipo restituito e ai modificatoriref
diF0
. - La convenzione di chiamata di
F0
corrisponde 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:
-
M
eF
hanno lo stesso numero di parametri ed ogni parametro inM
ha gli stessi modificatoriref
,out
oin
come il parametro corrispondente inF
. - Per ogni parametro di valore (un parametro senza
ref
,out
o modificatorein
), esiste una conversione di identità, una conversione implicita del riferimento o una conversione implicita del puntatore dal tipo di parametro inM
al tipo di parametro corrispondente inF
. - Per ogni parametro
ref
,out
oin
, il tipo di parametro inM
corrisponde al tipo di parametro corrispondente inF
. - Se il tipo restituito è per valore (nessun
ref
oref readonly
), esiste una conversione di identità, di riferimento implicito o di puntatore implicito dal tipo restituito diF
a quello diM
. - Se il tipo restituito è per riferimento (
ref
oref readonly
), il tipo restituito e i modificatoriref
diF
sono uguali al tipo restituito e ai modificatoriref
diM
. - La convenzione di chiamata di
M
corrisponde 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 F
come descritto di seguito.
- Viene selezionato un singolo metodo
M
corrispondente 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
,out
oin
) 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
M
che ha lo stesso numero di parametri diF
e la conversione viene considerata esistente. - Il metodo selezionato
M
deve 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-
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:
- 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 diT
sono tipi di input diE
con 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 diE
con 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...Tk
e tipo di ritornoTb
, e la risoluzione dell'overload diE
con i tipiT1..Tk
restituisce un singolo metodo con tipo di ritornoU
, quindi viene effettuata un'inferenza di limite inferiore daU
aTb
.
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 aU
e 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 cheU
sia identico adelegate*<U2..Uk, U1>
e la convenzione di chiamata diV
sia identica aU
e il riferimento diVi
sia identico aUi
.
Il primo punto elenco di inferenza da Ui
a Vi
viene modificato in:
- Se
U
non è un tipo di puntatore a funzione eUi
non è noto come un tipo di riferimento, o seU
è un tipo di puntatore a funzione eUi
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 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 aV
e il riferimento diUi
è identico aVi
.
Il primo punto elenco di inferenza da Ui
a Vi
viene modificato in:
- Se
U
non è un tipo di puntatore a funzione eUi
non è noto come tipo riferimento o seU
è un tipo di puntatore a funzione eUi
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 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 è per riferimento, viene eseguita un''inferenza esatta.
Rappresentazione dei metadati dei parametri in
, out
e 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
, out
o 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
cheOutAttribute
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ù modopt
all'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 managed
corrisponde al default
CallKind
. Questo è il default CallKind
di qualsiasi metodo non attribuito con UnmanagedCallersOnly
.
C# riconosce 4 identificatori speciali che corrispondono a specifici CallKind
esistenti 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_convention
s. Questi identificatori sono Cdecl
, Thiscall
, Stdcall
e Fastcall
, che corrispondono rispettivamente a unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
e 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 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.Object
e non ha dipendenze. - Vengono esaminati solo i tipi pubblici.
Se la ricerca ha esito positivo in tutti i identifier
specificati in un unmanaged_calling_convention
, il CallKind
viene codificato come unmanaged ext
, e ciascuno dei tipi risolti viene codificato nell'insieme di modopt
all'inizio della firma del puntatore di funzione. È importante notare che queste regole implicano che gli utenti non possono anteporre CallConv
a 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 modopt
nel 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 identifier
in 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 chiamatamodopt
nei 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 modopt
che devono essere utilizzati per determinare la convenzione di chiamata:
- Se non vengono specificati tipi, il
CallKind
viene considerato comeunmanaged ext
, senza convenzioni di chiamatamodopt
s all'inizio del tipo di puntatore di funzione. - Se è specificato un tipo e tale tipo è denominato
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
oCallConvFastcall
, l'CallKind
viene considerato comeunmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
ounmanaged fastcall
rispettivamente, senza convenzioni di chiamatamodopt
s 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 comemodopt
all'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 class
e 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 unstruct
per consentire l'utilizzo al di fuori di un contestounsafe
nel resto del codice.
C# feature specifications