Pointeurs fonction
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Résumé
Cette proposition fournit des constructions de langage qui exposent des opcodes IL qui ne peuvent actuellement pas être accessibles efficacement, ou du tout, en C# aujourd’hui : ldftn
et calli
. Ces opcodes IL peuvent être importants dans le code hautes performances et les développeurs ont besoin d’un moyen efficace d’y accéder.
Motivation
Les motivations et l’arrière-plan de cette fonctionnalité sont décrites dans le problème suivant (comme il s’agit d’une implémentation potentielle de la fonctionnalité) :
Ceci est une proposition de conception alternative aux éléments intrinsèques du compilateur>
Conception détaillée
Pointeurs de fonction
Le langage permet la déclaration des pointeurs de fonction à l’aide de la syntaxe delegate*
. La syntaxe complète est décrite en détail dans la section suivante, mais elle est destinée à ressembler à la syntaxe utilisée par Func
et Action
déclarations de type.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Ces types sont représentés à l’aide du type de pointeur de fonction comme décrit dans ECMA-335. Cela signifie que l’appel d’un delegate*
utilisera calli
où l’appel d’un delegate
utilisera callvirt
sur la méthode Invoke
.
Syntaxiquement, l’appel est identique pour les deux structures.
La définition ECMA-335 des pointeurs de méthode inclut la convention d’appel dans le cadre de la signature de type (section 7.1).
La convention d’appel par défaut sera managed
. Les conventions d’appel non gérées peuvent être spécifiées en plaçant un mot-clé unmanaged
après la syntaxe delegate*
, qui utilisera la plateforme d'exécution par défaut. Des conventions non managées spécifiques peuvent ensuite être spécifiées entre crochets au mot clé unmanaged
en spécifiant n’importe quel type commençant par CallConv
dans l’espace de noms System.Runtime.CompilerServices
, en laissant le préfixe CallConv
. Ces types doivent provenir de la bibliothèque principale du programme, et l’ensemble de combinaisons valides dépend de la plateforme.
//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>;
Les conversions entre les types delegate*
sont effectuées en fonction de leur signature comme la convention d’appel.
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 type delegate*
est un type de pointeur, ce qui signifie qu’il a toutes les fonctionnalités et restrictions d’un type de pointeur standard :
- Valide uniquement dans un contexte
unsafe
. - Les méthodes qui contiennent un paramètre
delegate*
ou un type de retour ne peuvent être appelées qu’à partir d’un contexteunsafe
. - Impossible de convertir en
object
. - Impossible d’utiliser comme argument générique.
- Peut convertir implicitement
delegate*
envoid*
. - Peut effectuer une conversion explicite de
void*
endelegate*
.
Restrictions :
- Les attributs personnalisés ne peuvent pas être appliqués à un
delegate*
ou à l’un de ses éléments. - Un paramètre
delegate*
ne peut pas être marqué commeparams
- Un type
delegate*
a toutes les restrictions d’un type de pointeur normal. - L’arithmétique du pointeur ne peut pas être effectuée directement sur les types de pointeur de fonction.
Syntaxe du pointeur de fonction
La syntaxe complète du pointeur de fonction est représentée par la grammaire suivante :
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'
;
Si aucune calling_convention_specifier
n’est fournie, la valeur par défaut est managed
. L’encodage précis des métadonnées du calling_convention_specifier
et les identifier
valides dans le unmanaged_calling_convention
sont abordés dans Représentation des métadonnées des conventions d’appel.
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>>;
Conversions des pointeurs de fonction
Dans un contexte non sécurisé, l’ensemble des conversions implicites disponibles (conversions implicites) est étendu pour inclure les conversions de pointeur implicite suivantes :
- Conversions existantes - (§23.5)
- De funcptr_type
F0
à une autre funcptr_typeF1
, à condition que toutes les conditions suivantes soient remplies :F0
etF1
ont le même nombre de paramètres, et chaque paramètreD0n
dansF0
a le mêmeref
,out
ou modificateursin
que le paramètre correspondantD1n
dansF1
.- Pour chaque paramètre de valeur (un paramètre sans
ref
,out
ouin
modificateur), une conversion d’identité, une conversion de référence implicite ou une conversion de pointeur implicite existe du type de paramètre dansF0
au type de paramètre correspondant dansF1
. - Pour chaque
ref
,out
ou paramètrein
, le type de paramètre dansF0
est identique au type de paramètre correspondant dansF1
. - Si le type de retour est par valeur (aucune
ref
ouref readonly
), une identité, une référence implicite ou une conversion de pointeur implicite existe du type de retour deF1
au type de retour deF0
. - Si le type de retour est par référence (
ref
ouref readonly
), le type de retour et les modificateursref
deF1
sont identiques à ceux du type de retour et des modificateursref
deF0
. - La convention d’appel de
F0
est la même que la convention d’appel deF1
.
Permettre de cibler des méthodes avec une prise d’adresse
Les groupes de méthodes seront désormais autorisés comme arguments pour une expression de prise d’adresse. Le type d’une telle expression est un delegate*
qui a la signature équivalente de la méthode cible et une convention d’appel managée :
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;
}
}
Dans un contexte non sécurisé, une méthode M
est compatible avec un type de pointeur de fonction F
si toutes les valeurs suivantes sont vraies :
M
etF
ont le même nombre de paramètres, et chaque paramètre deM
a les mêmesref
,out
ou modificateursin
que le paramètre correspondant dansF
.- Pour chaque paramètre de valeur (un paramètre sans
ref
,out
ouin
modificateur), une conversion d’identité, une conversion de référence implicite ou une conversion de pointeur implicite existe du type de paramètre dansM
au type de paramètre correspondant dansF
. - Pour chaque
ref
,out
ou paramètrein
, le type de paramètre dansM
est identique au type de paramètre correspondant dansF
. - Si le type de retour est par valeur (aucune
ref
ouref readonly
), une identité, une référence implicite ou une conversion de pointeur implicite existe du type de retour deF
au type de retour deM
. - Si le type de retour est par référence (
ref
ouref readonly
), le type de retour et les modificateurs deref
et deF
sont identiques au type de retour et aux modificateurs deref
deM
. - La convention d’appel de
M
est la même que la convention d’appel deF
. Cela inclut à la fois le bit de convention d’appel, ainsi que les indicateurs de convention d’appel spécifiés dans l’identificateur non managé. M
est une méthode statique.
Dans un contexte non sécurisé, une conversion implicite existe à partir d’une expression d’adresse dont la cible est un groupe de méthodes E
en type de pointeur de fonction compatible F
si E
contient au moins une méthode applicable sous sa forme normale à une liste d’arguments construite à l’aide des types de paramètres et des modificateurs de F
, comme décrit dans la section suivante.
- Une méthode unique
M
est sélectionnée correspondant à un appel de méthode du formulaireE(A)
avec les modifications suivantes :- La liste d’arguments
A
est une liste d’expressions, chacune classifiée en tant que variable et avec le type et le modificateur (ref
,out
ouin
) du funcptr_parameter_list correspondant deF
. - Les méthodes candidates sont uniquement celles qui s’appliquent sous leur forme normale, et non celles applicables dans leur forme développée.
- Les méthodes candidates sont uniquement celles qui sont statiques.
- La liste d’arguments
- Si l’algorithme de résolution de surcharge génère une erreur, une erreur au moment de la compilation se produit. Dans le cas contraire, l’algorithme produit une méthode optimale
M
avoir le même nombre de paramètres queF
et la conversion est considérée comme existante. - La méthode sélectionnée
M
doit être compatible (comme défini ci-dessus) avec le type de pointeur de fonctionF
. Sinon, une erreur au moment de la compilation se produit. - Le résultat de la conversion est un pointeur de fonction de type
F
.
Cela signifie que les développeurs peuvent se fier aux règles de résolution de surcharge pour fonctionner conjointement avec l’opérateur de prise d’adresse :
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’opérateur de prise d’adresse sera implémenté à l’aide de l’instruction ldftn
.
Restrictions de cette fonctionnalité :
- S’applique uniquement aux méthodes marquées comme
static
. - Les fonctions locales non
static
ne peuvent pas être utilisées dans&
. Les détails de l’implémentation de ces méthodes ne sont délibérément pas spécifiés par la langue. Cela inclut si elles sont statiques ou d'instance, ou exactement quelle signature elles ont lorsqu'elles sont émises.
Opérateurs sur les types de pointeurs de fonction
La section du code non sécurisé sur les expressions est modifiée comme suit :
Dans un contexte non sécurisé (unsafe), plusieurs constructions sont disponibles pour opérer sur tous les _pointer_type_s qui ne sont pas _funcptr_type_s :
- L’opérateur
*
peut être utilisé pour effectuer une indirection de pointeur (§23.6.2).- L’opérateur
->
peut être utilisé pour accéder à un membre d’un struct via un pointeur (§23.6.3).- L’opérateur
[]
peut être utilisé pour indexer un pointeur (§23.6.4).- L’opérateur
&
peut être utilisé pour obtenir l’adresse d’une variable (§23.6.5).- Les opérateurs
++
et--
peuvent être utilisés pour incrémenter et décrémenter des pointeurs (§23.6.6).- Les opérateurs
+
et-
peuvent être utilisés pour effectuer des arithmétiques de pointeur (§23.6.7).- Les opérateurs
==
,!=
,<
,>
,<=
et=>
peuvent être utilisés pour comparer les pointeurs (§23.6.8).- L’opérateur
stackalloc
peut être utilisé pour allouer de la mémoire à partir de la pile des appels (§23.8).- L’instruction
fixed
peut être utilisée pour corriger temporairement une variable afin que son adresse puisse être obtenue (§23.7).Dans un contexte non sécurisé, plusieurs constructions sont disponibles pour fonctionner sur toutes les _funcptr_type_s :
- L’opérateur
&
peut être utilisé pour obtenir l’adresse des méthodes statiques (Autoriser l’adresse des méthodes cibles)- Les opérateurs
==
,!=
,<
,>
,<=
et=>
peuvent être utilisés pour comparer les pointeurs (§23.6.8).
En outre, nous modifions toutes les sections de Pointers in expressions
pour interdire les types de pointeurs de fonction, sauf Pointer comparison
et The sizeof operator
.
Meilleur membre de fonction
§12.6.4.3 Membre de fonction amélioré sera modifié afin d'inclure la ligne suivante :
Un
delegate*
est plus spécifique quevoid*
Cela signifie qu’il est possible de surcharger avec un void*
et un delegate*
tout en utilisant de manière sensée l’opérateur de prise d’adresse.
Inférence de type
Dans le code non sécurisé, les modifications suivantes sont apportées aux algorithmes d’inférence de type :
Types d’entrée
Les éléments suivants sont ajoutés :
Si
E
est un groupe de méthodes d’adresse etT
est un type de pointeur de fonction, tous les types de paramètres deT
sont des types d’entrée deE
avec le typeT
.
Types de sortie
Les éléments suivants sont ajoutés :
Si
E
est un groupe de méthodes avec prise d’adresse etT
est un type de pointeur de fonction, alors le type de retour deT
est un type de sortie deE
avec un typeT
.
Inférences de type de résultat
La puce suivante est ajoutée entre les puces 2 et 3 :
- Si
E
est un groupe de méthodes avec prise d’adresse etT
est un type de pointeur de fonction avec des types de paramètresT1...Tk
et un type de retourTb
, et que la résolution de surcharge deE
avec les typesT1..Tk
aboutit à une méthode unique avec un type de retourU
, alors une inférence de borne inférieure est effectuée deU
versTb
.
Meilleure conversion à partir d’une expression
La sous-puce suivante est ajoutée comme cas à la puce 2 :
V
est un type de pointeur de fonctiondelegate*<V2..Vk, V1>
etU
est un type de pointeur de fonctiondelegate*<U2..Uk, U1>
, et la convention d’appel deV
est identique àU
, et la refness deVi
est identique àUi
.
Inférences de borne inférieure
Le cas suivant est ajouté au point 3 :
V
est un type de pointeur de fonctiondelegate*<V2..Vk, V1>
, et il existe un type de pointeur de fonctiondelegate*<U2..Uk, U1>
tel queU
soit identique àdelegate*<U2..Uk, U1>
, que la convention d’appel deV
soit identique àU
, et que la référencialité deVi
soit identique àUi
.
La première puce de l’inférence de Ui
vers Vi
est modifiée pour :
- Si
U
n’est pas un type de pointeur de fonction et queUi
n’est pas connu pour être un type de référence, ou siU
est un type de pointeur de fonction etUi
n’est pas connu pour être un type de pointeur de fonction ou un type de référence, une inférence exacte est effectuée.
Puis, ajouté après la 3ᵉ puce de l’inférence de Ui
vers Vi
:
- Sinon, si
V
estdelegate*<V2..Vk, V1>
l’inférence dépend du paramètre i-th dedelegate*<V2..Vk, V1>
:
- Si V1 :
- Si le retour se fait par valeur, alors une inférence de borne inférieure est effectuée.
- Si le retour se fait par référence, alors une inférence exacte est effectuée.
- Si V2..Vk :
- Si le paramètre est par valeur, alors une inférence de borne supérieure est effectuée.
- Si le paramètre est par référence, alors une inférence exacte est effectuée.
Inférences de borne supérieure
Le cas suivant est ajouté au point 2 :
U
est un type de pointeur de fonctiondelegate*<U2..Uk, U1>
etV
est un type de pointeur de fonction identique àdelegate*<V2..Vk, V1>
, et la convention d’appel deU
est identique àV
, et la refness deUi
est identique àVi
.
La première puce de l’inférence de Ui
vers Vi
est modifiée pour :
- Si
U
n’est pas un type de pointeur de fonction et queUi
n’est pas connu comme un type de référence, ou siU
est un type de pointeur de fonction et queUi
n’est pas connu comme un type de pointeur de fonction ou un type de référence, alors une inférence exacte est effectuée.
Puis ajouté après la 3ᵉ puce de l’inférence de Ui
vers Vi
:
- Sinon, si
U
estdelegate*<U2..Uk, U1>
l’inférence dépend du paramètre i-th dedelegate*<U2..Uk, U1>
:
- Si U1 :
- Si le retour se fait par valeur, alors une inférence de borne supérieure est effectuée.
- Si le retour se fait par référence, alors une inférence exacte est effectuée.
- Si U2..Uk :
- Si le paramètre est par valeur, alors une inférence de borne inférieure est effectuée.
- Si le paramètre est par référence, alors une inférence exacte est effectuée.
Représentation des métadonnées des paramètres in
, des out
et des paramètres ref readonly
et des types de retour
Les signatures de pointeur de fonction n’ont aucun emplacement d’indicateur de paramètre. Nous devons donc encoder si les paramètres et le type de retour sont in
, out
ou ref readonly
à l’aide de modreqs.
in
Nous réutilisons System.Runtime.InteropServices.InAttribute
, appliqué en tant que modreq
au spécificateur ref sur un paramètre ou un type de retour, pour signifier ce qui suit :
- S’il est appliqué à un spécificateur ref de paramètre, ce paramètre est traité comme
in
. - Si appliqué au spécificateur de type de retour ref, le type de retour est traité comme
ref readonly
.
out
Nous utilisons System.Runtime.InteropServices.OutAttribute
, appliqué en tant que modreq
au spécificateur ref sur un type de paramètre, pour signifier que le paramètre est un paramètre out
.
Erreurs
- Appliquer
OutAttribute
comme modreq à un type de retour constitue une erreur. - Il s’agit d’une erreur d’appliquer à la fois
InAttribute
etOutAttribute
en tant que modreq à un type de paramètre. - Si les deux sont spécifiés via modopt, ils sont ignorés.
Représentation des métadonnées des conventions d’appel
Les conventions d’appel sont encodées dans une signature de méthode dans les métadonnées par une combinaison de l’indicateur CallKind
dans la signature et zéro ou plusieurs modopt
au début de la signature. ECMA-335 déclare actuellement les éléments suivants dans l’indicateur de CallKind
:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
Parmi ceux-ci, les pointeurs de fonction en C# prendront en charge tout sauf varargs
.
En outre, le runtime (et éventuellement 335) sera mis à jour de manière à inclure un nouveau CallKind
sur de nouvelles plateformes. Cela n’a pas de nom formel actuellement, mais ce document utilise unmanaged ext
comme espace réservé pour représenter le nouveau format de convention d’appel extensible. Sans modopt
s, unmanaged ext
est la convention d’appel par défaut de la plateforme, unmanaged
sans crochets.
Mappage du calling_convention_specifier
à un CallKind
Un calling_convention_specifier
qui est omis, ou indiqué comme managed
, est associé au default
CallKind
. Par défaut, c'est CallKind
de toute méthode non attribuée par UnmanagedCallersOnly
.
C# reconnaît 4 identificateurs spéciaux qui correspondent à des CallKind
non managés spécifiques provenant d’ECMA 335. Pour que ce mappage se produise, ces identificateurs doivent être spécifiés par eux-mêmes, sans aucun autre identificateur, et cette exigence est encodée dans la spécification de unmanaged_calling_convention
s. Ces identificateurs sont Cdecl
, Thiscall
, Stdcall
et Fastcall
, qui correspondent respectivement à unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
et unmanaged fastcall
. Si plusieurs identifer
sont spécifiées ou si l'identifier
unique n’est pas des identificateurs spécialement reconnus, nous effectuons une recherche de nom spéciale sur l’identificateur avec les règles suivantes :
- Nous préfixons
identifier
avec la chaîneCallConv
. - Nous examinons uniquement les types définis dans l’espace de noms
System.Runtime.CompilerServices
. - Nous examinons uniquement les types définis dans la bibliothèque principale de l’application, qui est la bibliothèque qui définit
System.Object
et n’a aucune dépendance. - Nous examinons uniquement les types publics.
Si la recherche réussit pour tous les identifier
spécifiés dans un unmanaged_calling_convention
, nous encodons le CallKind
comme unmanaged ext
et encodons chacun des types résolus dans l’ensemble des modopt
au début de la signature de pointeur de fonction. En guise de remarque, ces règles signifient que les utilisateurs ne peuvent pas préfixer ces identifier
avec CallConv
, car cela entraînera la recherche de CallConvCallConvVectorCall
.
Lors de l’interprétation des métadonnées, nous examinons d’abord le CallKind
. S'il ne s'agit pas de unmanaged ext
, nous ignorons tous les modopt
sur le type de retour pour déterminer la convention d'appel et nous n'utilisons que les CallKind
. Si CallKind
est unmanaged ext
, nous examinons les modopts au début du type de pointeur de fonction, en prenant l’union de tous les types qui remplissent les critères suivants :
- Il est défini dans la bibliothèque principale, qui est la bibliothèque qui ne fait référence à aucune autre bibliothèque et définit
System.Object
. - Le type est défini dans l’espace de noms
System.Runtime.CompilerServices
. - Le type commence par le préfixe
CallConv
. - Le type est public.
Ceux-ci représentent les types qui doivent être trouvés lors de l’exécution de recherches sur les identifier
s dans un unmanaged_calling_convention
lors de la définition d’un type de pointeur de fonction dans la source.
Essayer d’utiliser un pointeur de fonction avec un CallKind
de unmanaged ext
constitue une erreur si le runtime cible ne prend pas en charge la fonctionnalité. Cela sera déterminé en recherchant la présence de la constante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind
. Si cette constante est présente, le runtime est considéré comme supportant la fonctionnalité.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
est un attribut utilisé par le CLR pour indiquer qu’une méthode doit être appelée avec une convention d’appel spécifique. Pour cette raison, nous présentons la prise en charge suivante pour l’utilisation de l’attribut :
- Il s’agit d’une erreur d’appeler directement une méthode annotée avec cet attribut à partir de C#. Les utilisateurs doivent obtenir un pointeur de fonction vers la méthode, puis appeler ce pointeur.
- Il s’agit d’une erreur d’application de l’attribut à quelque chose d’autre qu’une méthode statique ordinaire ou une fonction locale statique ordinaire. Le compilateur C# marque toutes les méthodes non statiques ou statiques non ordinaires importées à partir de métadonnées avec cet attribut comme non prises en charge par le langage.
- Il est incorrect qu'une méthode marquée par l'attribut ait un paramètre ou un type de retour qui n'est pas un
unmanaged_type
. - C'est une erreur qu'une méthode marquée de l'attribut ait des paramètres de type, même si ces paramètres de type sont contraints à
unmanaged
. - C'est une erreur qu'une méthode dans un type générique soit marquée avec l'attribut.
- C'est une erreur de convertir une méthode portant l’attribut à un type délégué.
- Spécifier des types pour
UnmanagedCallersOnly.CallConvs
qui ne remplissent pas les critères des conventions d’appelmodopt
dans les métadonnées constitue une erreur.
Lorsque vous déterminez la convention d’appel d’une méthode marquée avec un attribut UnmanagedCallersOnly
valide, le compilateur effectue les vérifications suivantes sur les types spécifiés dans la propriété CallConvs
pour déterminer les CallKind
effectifs et les modopt
qui doivent être utilisés pour déterminer la convention d’appel :
- Si aucun type n’est spécifié, la
CallKind
est traitée commeunmanaged ext
, sans convention d’appelmodopt
s au début du type de pointeur de fonction. - S’il existe un type spécifié et que ce type est nommé
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
ouCallConvFastcall
, leCallKind
est traité commeunmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
ouunmanaged fastcall
, respectivement, sans convention d’appelmodopt
s au début du type de pointeur de fonction. - Si plusieurs types sont spécifiés ou si le type unique n’est pas nommé l’un des types spécialement appelés ci-dessus, le
CallKind
est traité commeunmanaged ext
, avec l’union des types spécifiés commemodopt
s au début du type de pointeur de fonction.
Le compilateur examine ensuite cette collection effective CallKind
et modopt
et utilise des règles de métadonnées normales pour déterminer la convention d’appel finale du type de pointeur de fonction.
Questions ouvertes
Détection de la prise en charge du runtime pour unmanaged ext
https://github.com/dotnet/runtime/issues/38135 effectue le suivi de l’ajout de cet indicateur. Selon les retours de la vérification, nous utiliserons soit la propriété spécifiée dans l’élément de suivi (issue), soit la présence de UnmanagedCallersOnlyAttribute
comme indicateur déterminant si le runtime prend en charge unmanaged ext
.
Considérations
Autoriser les méthodes d’instance
La proposition peut être étendue pour prendre en charge les méthodes d’instance en tirant parti de la convention d’appel cli EXPLICITTHIS
(nommée instance
dans le code C#). Cette forme de pointeurs de fonction CLI place le paramètre this
comme premier paramètre explicite de la syntaxe du pointeur de fonction.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
Cela est solide, mais ajoute une complication à la proposition. En particulier, les pointeurs de fonction qui diffèrent par la convention d’appel instance
et managed
seraient incompatibles même si les deux cas sont utilisés pour appeler des méthodes managées avec la même signature C#. En outre, dans tous les cas considérés où il serait utile d'avoir une solution simple, il existe un moyen simple : utiliser une fonction locale static
.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
Ne pas exiger unsafe à la déclaration
Au lieu d’exiger unsafe
à chaque utilisation d’une delegate*
, ne le nécessite qu’au moment où un groupe de méthodes est converti en delegate*
. C’est ici que les problèmes de sécurité de base entrent en jeu (sachant que l’assembly contenant ne peut pas être déchargé tant que la valeur est active). Exiger unsafe
dans les autres emplacements peut être considéré comme excessif.
C’est ainsi que la conception a été initialement prévue. Mais les règles linguistiques issues du processus étaient très maladroites. Il est impossible de cacher que c’est une valeur de pointeur, et elle restait visible même sans le mot clé unsafe
. Par exemple, la conversion en object
ne peut pas être autorisée, elle ne peut pas être membre d’un class
, etc... La conception C# doit nécessiter unsafe
pour tous les utilisations du pointeur, ce qui suit cette conception.
Les développeurs pourront toujours présenter un wrapper « safe » sur des valeurs delegate*
, de la même manière qu’ils le font aujourd’hui pour les types de pointeurs normaux. Considérer:
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Utilisation de délégués
Au lieu d’utiliser un nouvel élément de syntaxe, delegate*
, utilisez simplement des types delegate
existants avec un *
suivant le type :
Func<object, object, bool>* ptr = &object.ReferenceEquals;
La gestion de la convention d’appel peut être effectuée en annotant les types delegate
avec un attribut qui spécifie une valeur CallingConvention
. L’absence d’un attribut signifierait la convention d’appel managée.
L’encodage de cela en IL est problématique. La valeur sous-jacente doit être représentée en tant que pointeur, mais elle doit également :
- Avoir un type unique pour autoriser des surcharges avec différents types de pointeurs de fonction.
- Être équivalent aux fins OHI à travers les limites d’assembly.
Le dernier point est particulièrement problématique. Cela signifie que chaque assemblage qui utilise Func<int>*
doit encoder un type équivalent dans les métadonnées, même si Func<int>*
est défini dans un assemblage que l'on ne contrôle pas.
En outre, tout autre type défini avec le nom System.Func<T>
dans un assembly qui n’est pas mscorlib doit être différent de la version définie dans mscorlib.
Une option explorée a émis un pointeur tel que mod_req(Func<int>) void*
. Cela ne fonctionne pas en tant que mod_req
ne peut pas être lié à un TypeSpec
et ne peut donc pas cibler les instanciations génériques.
Pointeurs de fonction nommés
La syntaxe du pointeur de fonction peut être fastidieuse, en particulier dans des cas complexes tels que des pointeurs de fonction imbriqués. Plutôt que les développeurs ne doivent taper la signature à chaque fois, le langage pourrait permettre l'utilisation de déclarations nommées de pointeurs de fonction, comme cela se fait avec delegate
.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Une partie du problème ici est que la primitive CLI sous-jacente n’a pas de noms, ce serait purement une invention C# et exiger un peu de travail de métadonnées pour permettre. C’est faisable, mais c’est un sujet important de travail. Il exige essentiellement que C# dispose d’un compagnon de la table def de type uniquement pour ces noms.
En outre, lorsque les arguments des pointeurs de fonction nommés ont été examinés, nous avons constaté qu’ils pouvaient s’appliquer également bien à un certain nombre d’autres scénarios. Par exemple, il serait tout aussi pratique de déclarer des tuples nommés pour réduire la nécessité de saisir la signature complète dans tous les cas.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
Après en avoir discuté, nous avons décidé de ne pas autoriser la déclaration nommée de types delegate*
. Si nous trouvons qu’il existe un besoin important pour cela en fonction des commentaires d’utilisation des clients, nous allons examiner une solution de nommage qui fonctionne pour les pointeurs de fonction, les tuples, les génériques, etc. Il est probable qu’il s’agit d’une forme similaire à d’autres suggestions comme la prise en charge complète typedef
dans la langue.
Considérations futures
délégués statiques
Cela fait référence à la proposition visant à autoriser la déclaration de types delegate
qui ne peuvent faire référence qu’à des membres static
. L'avantage étant que ces instances delegate
peuvent être sans allocation et plus performantes dans les scénarios sensibles à la performance.
Si la fonctionnalité de pointeur de fonction est implémentée, la proposition static delegate
sera probablement clôturée. L'avantage proposé de cette fonctionnalité est qu'elle ne nécessite pas d'allocation de mémoire. Cependant, de récentes investigations ont montré qu’il n’est pas possible de le réaliser en raison du déchargement d’assembly. Il doit exister un handle solide depuis le static delegate
vers la méthode à laquelle il fait référence afin d’empêcher le déchargement de l’assembly.
Pour conserver chaque instance static delegate
, il serait nécessaire d’allouer un nouveau handle, ce qui va à l’encontre des objectifs de la proposition. Il y avait des conceptions où l’allocation pouvait être amortie à une allocation unique par site d’appel, mais c'était un peu compliqué et ne semblait pas valoir le compromis.
Cela signifie que les développeurs doivent essentiellement décider entre les compromis suivants :
- La sécurité face au déchargement d’assembly : cela nécessite des allocations et, par conséquent,
delegate
est déjà une option suffisante. - Aucune sécurité face au déchargement d’assembly : utilisez un
delegate*
. Cela peut être encapsulé dans unstruct
pour autoriser l’utilisation en dehors d’un contexte deunsafe
dans le reste du code.
C# feature specifications