Partager via


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

dotnet/csharplang#191

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 contexte unsafe.
  • Impossible de convertir en object.
  • Impossible d’utiliser comme argument générique.
  • Peut convertir implicitement delegate* en void*.
  • Peut effectuer une conversion explicite de void* en delegate*.

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é comme params
  • 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_typeF0 à une autre funcptr_typeF1, à condition que toutes les conditions suivantes soient remplies :
    • F0 et F1 ont le même nombre de paramètres, et chaque paramètre D0n dans F0 a le même ref, outou modificateurs in que le paramètre correspondant D1n dans F1.
    • Pour chaque paramètre de valeur (un paramètre sans ref, outou in modificateur), une conversion d’identité, une conversion de référence implicite ou une conversion de pointeur implicite existe du type de paramètre dans F0 au type de paramètre correspondant dans F1.
    • Pour chaque ref, outou paramètre in, le type de paramètre dans F0 est identique au type de paramètre correspondant dans F1.
    • Si le type de retour est par valeur (aucune ref ou ref readonly), une identité, une référence implicite ou une conversion de pointeur implicite existe du type de retour de F1 au type de retour de F0.
    • Si le type de retour est par référence (ref ou ref readonly), le type de retour et les modificateurs ref de F1 sont identiques à ceux du type de retour et des modificateurs ref de F0.
    • La convention d’appel de F0 est la même que la convention d’appel de F1.

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 et F ont le même nombre de paramètres, et chaque paramètre de M a les mêmes ref, outou modificateurs in que le paramètre correspondant dans F.
  • Pour chaque paramètre de valeur (un paramètre sans ref, outou in modificateur), une conversion d’identité, une conversion de référence implicite ou une conversion de pointeur implicite existe du type de paramètre dans M au type de paramètre correspondant dans F.
  • Pour chaque ref, outou paramètre in, le type de paramètre dans M est identique au type de paramètre correspondant dans F.
  • Si le type de retour est par valeur (aucune ref ou ref readonly), une identité, une référence implicite ou une conversion de pointeur implicite existe du type de retour de F au type de retour de M.
  • Si le type de retour est par référence (ref ou ref readonly), le type de retour et les modificateurs de ref et de F sont identiques au type de retour et aux modificateurs de ref de M.
  • La convention d’appel de M est la même que la convention d’appel de F. 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 formulaire E(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, outou in) du funcptr_parameter_list correspondant de F.
    • 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.
  • 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 que F 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 fonction F. 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 nonstatic 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 :

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

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

§12.6.3.4

Les éléments suivants sont ajoutés :

Si E est un groupe de méthodes d’adresse et T est un type de pointeur de fonction, tous les types de paramètres de T sont des types d’entrée de E avec le type T.

Types de sortie

§12.6.3.5

Les éléments suivants sont ajoutés :

Si E est un groupe de méthodes avec prise d’adresse et T est un type de pointeur de fonction, alors le type de retour de T est un type de sortie de E avec un type T.

Inférences de type de résultat

§12.6.3.7

La puce suivante est ajoutée entre les puces 2 et 3 :

  • Si E est un groupe de méthodes avec prise d’adresse et T est un type de pointeur de fonction avec des types de paramètres T1...Tk et un type de retour Tb, et que la résolution de surcharge de E avec les types T1..Tk aboutit à une méthode unique avec un type de retour U, alors une inférence de borne inférieure est effectuée de U vers Tb.

Meilleure conversion à partir d’une expression

§12.6.4.5

La sous-puce suivante est ajoutée comme cas à la puce 2 :

  • V est un type de pointeur de fonction delegate*<V2..Vk, V1> et U est un type de pointeur de fonction delegate*<U2..Uk, U1>, et la convention d’appel de V est identique à U, et la refness de Vi est identique à Ui.

Inférences de borne inférieure

§12.6.3.10

Le cas suivant est ajouté au point 3 :

  • V est un type de pointeur de fonction delegate*<V2..Vk, V1>, et il existe un type de pointeur de fonction delegate*<U2..Uk, U1> tel que U soit identique à delegate*<U2..Uk, U1>, que la convention d’appel de V soit identique à U, et que la référencialité de Vi 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 que Ui n’est pas connu pour être un type de référence, ou si U est un type de pointeur de fonction et Ui 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 est delegate*<V2..Vk, V1> l’inférence dépend du paramètre i-th de delegate*<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

§12.6.3.11

Le cas suivant est ajouté au point 2 :

  • U est un type de pointeur de fonction delegate*<U2..Uk, U1> et V est un type de pointeur de fonction identique à delegate*<V2..Vk, V1>, et la convention d’appel de U est identique à V, et la refness de Ui 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 que Ui n’est pas connu comme un type de référence, ou si U est un type de pointeur de fonction et que Ui 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 est delegate*<U2..Uk, U1> l’inférence dépend du paramètre i-th de delegate*<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 outet 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, outou 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 et OutAttribute 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 modopts, 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 defaultCallKind. 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 CallKindnon 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_conventions. Ces identificateurs sont Cdecl, Thiscall, Stdcallet Fastcall, qui correspondent respectivement à unmanaged cdecl, unmanaged thiscall, unmanaged stdcallet 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îne CallConv.
  • 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 identifieravec 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 modoptsur 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 identifiers 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’appel modopt 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 modoptqui doivent être utilisés pour déterminer la convention d’appel :

  • Si aucun type n’est spécifié, la CallKind est traitée comme unmanaged ext, sans convention d’appel modopts au début du type de pointeur de fonction.
  • S’il existe un type spécifié et que ce type est nommé CallConvCdecl, CallConvThiscall, CallConvStdcallou CallConvFastcall, le CallKind est traité comme unmanaged cdecl, unmanaged thiscall, unmanaged stdcallou unmanaged fastcall, respectivement, sans convention d’appel modopts 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é comme unmanaged ext, avec l’union des types spécifiés comme modopts 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 :

  1. Avoir un type unique pour autoriser des surcharges avec différents types de pointeurs de fonction.
  2. Ê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 :

  1. 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.
  2. Aucune sécurité face au déchargement d’assembly : utilisez un delegate*. Cela peut être encapsulé dans un struct pour autoriser l’utilisation en dehors d’un contexte de unsafe dans le reste du code.