Compartir vía


Punteros a funciones

Nota

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .

Resumen

Esta propuesta proporciona construcciones de lenguaje que exponen códigos de operación il a los que actualmente no se puede acceder de forma eficaz, o en absoluto, en C#: ldftn y calli. Estos códigos de operación de IL pueden ser importantes en código de alto rendimiento y los desarrolladores necesitan una manera eficaz de acceder a ellos.

Motivación

Las motivaciones y antecedentes de esta característica se describen en el siguiente asunto, así como una posible implementación de la característica.

dotnet/csharplang#191

Esta es una propuesta de diseño alternativa a los intrínsecos del compilador

Diseño detallado

Punteros de función

El lenguaje permitirá la declaración de punteros de función mediante la sintaxis delegate*. La sintaxis completa se describe en detalle en la sección siguiente, pero está pensada para parecerse a la sintaxis usada por declaraciones de tipo Func y Action.

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

Estos tipos se representan mediante el tipo de puntero de función como se describe en ECMA-335. Esto significa que la invocación de un delegate* usará calli donde la invocación de un delegate usará callvirt en el método Invoke. Syntácticamente, la invocación es idéntica para ambas construcciones.

La definición ECMA-335 de punteros de método incluye la convención de llamada como parte de la firma de tipo (sección 7.1). La convención de llamada predeterminada será managed. Las convenciones de llamada no administradas se pueden especificar colocando una palabra clave unmanaged después de la sintaxis delegate*, que usará la predeterminada de la plataforma de tiempo de ejecución. A continuación, se pueden especificar convenciones no administradas específicas entre corchetes a la palabra clave unmanaged especificando cualquier tipo que empiece por CallConv en el espacio de nombres System.Runtime.CompilerServices, dejando el prefijo CallConv. Estos tipos deben proceder de la biblioteca principal del programa y el conjunto de combinaciones válidas depende de la plataforma.

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

Las conversiones entre tipos delegate* se realiza en base a su firma incluyendo la convención de llamada.

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 delegate* es un tipo de puntero, lo que significa que tiene todas las funcionalidades y restricciones de un tipo de puntero estándar:

  • Solo válido en un contexto unsafe.
  • Los métodos que contienen un parámetro delegate* o un tipo de valor devuelto solo se pueden llamar desde un contexto de unsafe.
  • No se puede convertir en object.
  • No se puede usar como argumento genérico.
  • Puede convertir implícitamente delegate* en void*.
  • Puede convertir explícitamente de void* a delegate*.

Restricciones:

  • Los atributos personalizados no se pueden aplicar a una delegate* ni a ninguno de sus elementos.
  • Un parámetro delegate* no se puede marcar como params
  • Un tipo delegate* tiene todas las restricciones de un tipo de puntero normal.
  • La aritmética de punteros no puede realizarse directamente sobre tipos de punteros de función.

Sintaxis de los punteros de función

La sintaxis del puntero de función completa se representa mediante la siguiente gramática:

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 no se proporciona ningún calling_convention_specifier, el valor predeterminado es managed. La codificación precisa de metadatos del calling_convention_specifier y qué identifier son válidos en el unmanaged_calling_convention se trata en Representación de metadatos de convenciones de llamada.

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

Conversiones de punteros de función

En un contexto no seguro, el conjunto de conversiones implícitas disponibles (conversiones implícitas) se extiende para incluir las siguientes conversiones de puntero implícitas:

  • Conversiones existentes - (§23.5)
  • De funcptr_typeF0 a otra funcptr_typeF1, siempre que se cumplan todas las siguientes condiciones:
    • F0 y F1 tienen el mismo número de parámetros, y cada parámetro D0n de F0 tiene los mismos modificadores ref, outo in que el parámetro correspondiente D1n en F1.
    • Para cada parámetro de valor (un parámetro sin ref, outo in modificador), existe una conversión de identidad, conversión de referencia implícita o conversión de puntero implícita del tipo de parámetro en F0 al tipo de parámetro correspondiente en F1.
    • Para cada parámetro ref, outo in, el tipo de parámetro de F0 es el mismo que el tipo de parámetro correspondiente en F1.
    • Si el tipo de valor devuelto es por valor (no ref ni ref readonly), existe una conversión de identidad, de referencia implícita o de puntero implícito del tipo de valor devuelto de F1 al tipo de valor devuelto de F0.
    • Si el tipo de valor devuelto es por referencia (ref o ref readonly), el tipo de valor devuelto y los modificadores ref de F1 son los mismos que el tipo de valor devuelto y los modificadores ref de F0.
    • La convención de llamada de F0 es la misma que la convención de llamada de F1.

Permitir direcciones de métodos de destino

Ahora se permitirán grupos de métodos como argumentos de una expresión address-of. El tipo de esta expresión será un delegate* que tiene la firma equivalente del método de destino y una convención de llamada administrada:

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

En un contexto no seguro, un método M es compatible con un tipo de puntero de función F si se cumplen todas las siguientes opciones:

  • M y F tienen el mismo número de parámetros y cada parámetro de M tiene los mismos modificadores ref, outo in como parámetro correspondiente en F.
  • Para cada parámetro de valor (un parámetro sin ref, outo in modificador), existe una conversión de identidad, conversión de referencia implícita o conversión de puntero implícita del tipo de parámetro en M al tipo de parámetro correspondiente en F.
  • Para cada parámetro ref, outo in, el tipo de parámetro de M es el mismo que el tipo de parámetro correspondiente en F.
  • Si el tipo de retorno es por valor (no ref ni ref readonly), existe una conversión de identidad, una conversión de referencia implícita o una conversión de puntero implícita del tipo de retorno de F al tipo de retorno de M.
  • Si el tipo de valor devuelto es por referencia (ref o ref readonly), el tipo de valor devuelto y los modificadores ref de F son los mismos que el tipo de valor devuelto y los modificadores ref de M.
  • La convención de llamada de M es la misma que la convención de llamada de F. Esto incluye el bit de convención de llamada, así como los indicadores de convención de llamada especificados en el identificador no gestionado.
  • M es un método estático.

En un contexto no seguro, existe una conversión implícita desde una dirección de expresión cuyo destino es un grupo de métodos E a un tipo de puntero de función compatible F si E contiene al menos un método que se aplica en su forma normal a una lista de argumentos construida mediante el uso de los tipos de parámetros y modificadores de F, como se describe en lo siguiente.

  • Se selecciona un único método M correspondiente a una invocación de método del formulario E(A) con las siguientes modificaciones:
    • La lista de argumentos A es una lista de expresiones, cada una clasificada como una variable, y tiene el tipo y el modificador (ref, out, o in) del funcptr_parameter_list correspondiente de F.
    • Los métodos candidatos son solo los métodos que son aplicables en su forma normal, no los aplicables en su forma expandida.
    • Los métodos candidatos son solo los métodos estáticos.
  • Si el algoritmo de resolución de sobrecarga genera un error, se produce un error en tiempo de compilación. De lo contrario, el algoritmo genera un único método mejor M que tiene el mismo número de parámetros que F y se considera que la conversión existe.
  • El método seleccionado M debe ser compatible (tal como se definió anteriormente) con el tipo de puntero de función F. De lo contrario, se produce un error en tiempo de compilación.
  • El resultado de la conversión es un puntero de función de tipo F.

Esto significa que los desarrolladores pueden depender de las reglas de resolución de sobrecargas para trabajar en conjunto con el operador 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;
    }
}

El operador address-of se implementará mediante la instrucción ldftn.

Restricciones de esta característica:

  • Solo se aplica a los métodos marcados como static.
  • Las funciones no locales destatic no se pueden usar en &. El lenguaje no especifica deliberadamente los detalles de implementación de estos métodos. Esto incluye si son estáticos o de instancia o exactamente con qué firma se emiten.

Operadores sobre tipos de puntero de función

La sección del código no seguro en las expresiones se modifica como tal:

En un contexto inseguro, hay varias construcciones disponibles para operar sobre todos los _pointer_type_s que no son _funcptr_type_s:

  • El operador * se puede usar para realizar la direccionamiento indirecto del puntero (§23.6.2).
  • El operador -> se puede usar para acceder a un miembro de una estructura a través de un puntero (§23.6.3).
  • El operador [] se puede usar para indexar un puntero (§23.6.4).
  • El operador & se puede usar para obtener la dirección de una variable (§23.6.5).
  • Los operadores ++ y -- se pueden usar para incrementar y disminuir punteros (§23.6.6).
  • Los operadores + y - se pueden usar para realizar la aritmética del puntero (§23.6.7).
  • Los operadores ==, !=, <, >, <=y => se pueden usar para comparar punteros (§23.6.8).
  • El operador stackalloc se puede usar para asignar memoria desde la pila de llamadas (§23.8).
  • La instrucción fixed se puede usar para corregir temporalmente una variable para que se pueda obtener su dirección (§23.7).

En un contexto inseguro, hay varias construcciones disponibles para operar con todos los _funcptr_type_s:

Además, modificamos todas las secciones de Pointers in expressions para prohibir los tipos de puntero de función, excepto Pointer comparison y The sizeof operator.

Mejor miembro de función

§12.6.4.3 Mejor miembro de función se cambiará para incluir la siguiente línea:

Un delegate* es más específico que void*

Esto significa que es posible sobrecargar en void* y un delegate* y seguir utilizando sensatamente el operador address-of.

Inferencia de tipos

En el código no seguro, se realizan los siguientes cambios en los algoritmos de inferencia de tipos:

Tipos de entrada

§12.6.3.4

Se agrega lo siguiente:

Si E es un grupo de métodos de referencia y T es un tipo de puntero de función, entonces todos los tipos de parámetros de T son tipos de entrada de E con el tipo T.

Tipos de salida

§12.6.3.5

Se agrega lo siguiente:

Si E es un grupo de métodos address-of y T es un tipo de puntero de función entonces el tipo de retorno de T es un tipo de salida de E con tipo T.

Inferencias de tipo de salida

§12.6.3.7

Se añade el siguiente punto entre los puntos 2 y 3:

  • Si E es una dirección de un grupo de métodos y T es un tipo de puntero de función con tipos de parámetros T1...Tk y tipo de retorno Tb, y la resolución de sobrecarga de E con los tipos T1..Tk produce un único método con tipo de retorno U, entonces se hace una inferencia de límite inferior de U a Tb.

Mejor conversión a partir de expresión

§12.6.4.5

Se añade el siguiente subpunto como caso al punto 2:

  • V es un tipo puntero de función delegate*<V2..Vk, V1> y U es un tipo de puntero de función delegate*<U2..Uk, U1>, y la convención de llamada de V es idéntica a U, y la referencia de Vi es idéntica a Ui.

Inferencias de límite inferior

§12.6.3.10

El siguiente caso se agrega al punto 3:

  • V es un tipo de puntero de función delegate*<V2..Vk, V1> y existe un tipo de puntero de función delegate*<U2..Uk, U1> tal que U es idéntico a delegate*<U2..Uk, U1>, la convención de llamada de V es idéntica a U, y el estado de referencia de Vi es idéntico a Ui.

El primer punto de la inferencia de Ui a Vi se modifica a:

  • Si U no es un tipo de puntero de función y Ui no se sabe que es un tipo de referencia, o si U es un tipo de puntero de función y no se sabe que Ui sea un tipo de puntero de función o un tipo de referencia, se realiza una inferencia exacta

Entonces, se añade después del tercer punto de la inferencia de Ui a Vi:

  • De lo contrario, si V es delegate*<V2..Vk, V1>, la inferencia depende del parámetro i-th de delegate*<V2..Vk, V1>:
    • Si V1:
      • Si el retorno es por valor, entonces se hace una inferencia de límite inferior.
      • Si el retorno es por referencia, entonces se hace una inferencia exacta.
    • Si V2..Vk:
      • Si el parámetro es un valor, se realiza una inferencia de límite superior.
      • Si el parámetro es una referencia, se realiza una inferencia exacta.

Inferencias de límite superior

§12.6.3.11

El siguiente caso se agrega al punto 2:

  • U es un tipo de puntero de función delegate*<U2..Uk, U1>, y V es un tipo de puntero de función que es idéntico a delegate*<V2..Vk, V1>, la convención de llamada de U es idéntica a la de V, y la referencialidad de Ui es idéntica a la de Vi.

El primer punto de la inferencia de Ui a Vi se modifica a:

  • Si U no es un tipo de puntero de función y Ui no se sabe que es un tipo de referencia, o si U es un tipo de puntero de función y no se sabe que Ui sea un tipo de puntero de función o un tipo de referencia, se realiza una inferencia exacta

Entonces, se añade después del tercer punto de la inferencia de Ui a Vi:

  • De lo contrario, si U es delegate*<U2..Uk, U1>, la inferencia depende del parámetro i-th de delegate*<U2..Uk, U1>:
    • Si U1:
      • Si el retorno se realiza por valor, entonces se realiza una inferencia de límite superior .
      • Si el valor devuelto es por referencia, se realiza una inferencia exacta .
    • Si U2..Uk:
      • Si el parámetro es por valor, entonces se hace una inferencia de límite inferior.
      • Si el parámetro es una referencia, se realiza una inferencia exacta.

Representación de metadatos de los parámetros in, outy ref readonly, y tipos de retorno.

Las firmas de puntero de función no tienen ubicación de indicadores de parámetro, por lo que debemos codificar si los parámetros y el tipo de retorno son in, out, o ref readonly utilizando modreqs.

in

Reutilizamos System.Runtime.InteropServices.InAttribute, aplicado como modreq al especificador ref en un parámetro o tipo de retorno, para significar lo siguiente:

  • Si se aplica a un especificador de referencia de parámetros, este parámetro se trata como in.
  • Si se aplica al especificador ref de tipo de retorno, el tipo de retorno se trata como ref readonly.

out

Usamos System.Runtime.InteropServices.OutAttribute, aplicado como modreq al especificador ref en un tipo de parámetro, para indicar que el parámetro es un parámetro out.

Errores

  • Es un error aplicar OutAttribute como modreq a un tipo de valor devuelto.
  • Es un error aplicar tanto InAttribute como OutAttribute como modreq a un tipo de parámetro.
  • Si se especifica cualquiera de ellos a través de modopt, se omiten.

Representación de metadatos de convenciones de llamada

Las convenciones de llamada se codifican en una firma de método en metadatos mediante una combinación de la marca CallKind en la firma y cero o más modopt al principio de la firma. ECMA-335 declara actualmente los siguientes elementos en la marca CallKind:

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

De estos, los punteros de función en C# soportarán todos menos varargs.

Además, el tiempo de ejecución (y eventualmente 335) se actualizará para incluir un nuevo CallKind en las nuevas plataformas. Esto no tiene un nombre formal actualmente, pero este documento usará unmanaged ext como marcador de posición para representar el nuevo formato de convención de llamadas extensible. Sin modopts, unmanaged ext es la convención de llamada predeterminada de la plataforma, unmanaged sin corchetes.

Asignación de calling_convention_specifier a CallKind

Un calling_convention_specifier que se omite, o se especifica como managed, se asigna a defaultCallKind. Este es el valor por defecto CallKind de cualquier método que no tiene el atributo UnmanagedCallersOnly.

C# reconoce 4 identificadores especiales que se corresponden con componentes CallKindespecíficos existentes no administrados de ECMA 335. Para que se produzca esta asignación, estos identificadores deben especificarse por sí solos, sin ningún otro identificador, y este requisito está codificado en la especificación de unmanaged_calling_convention. Estos identificadores son Cdecl, Thiscall, Stdcally Fastcall, que corresponden a unmanaged cdecl, unmanaged thiscall, unmanaged stdcally unmanaged fastcall, respectivamente. Si se especifica más de un identifer o el único identifier no es de los identificadores especialmente reconocidos, realizamos una búsqueda de nombres especial en el identificador con las siguientes reglas:

  • Anteponemos el identifier con la cadena CallConv
  • Solo se examinan los tipos definidos en el espacio de nombres System.Runtime.CompilerServices.
  • Solo se examinan los tipos definidos en la biblioteca principal de la aplicación, que es la biblioteca que define System.Object y no tiene dependencias.
  • Solo se examinan los tipos públicos.

Si la búsqueda tiene éxito en todos los identifier especificados en un unmanaged_calling_convention, codificamos el CallKind como unmanaged ext, y codificamos cada uno de los tipos resueltos en el conjunto de modopt al principio de la firma del puntero de función. Como nota, estas reglas significan que los usuarios no pueden prefijar estos identifier con CallConv, ya que esto resultará en la búsqueda de CallConvCallConvVectorCall.

Al interpretar los metadatos, primero observamos el CallKind. Si es algo distinto de unmanaged ext, omitimos todos los modopten el tipo de retorno para determinar la convención de llamada y usamos solo el CallKind. Si el CallKind es unmanaged ext, miramos los modopts al principio del tipo del puntero de función, tomando la unión de todos los tipos que cumplan los siguientes requisitos:

  • El está definido en la biblioteca principal, que es la biblioteca que no hace referencia a otras bibliotecas y define System.Object.
  • El tipo se define en el espacio de nombres System.Runtime.CompilerServices.
  • El tipo comienza con el prefijo CallConv.
  • El tipo es público.

Estos representan los tipos que deben ser encontrados al realizar la búsqueda en el identifier en un cuando se define un tipo de puntero unmanaged_calling_convention de función en la fuente.

Es un error intentar utilizar un puntero de función con un CallKind de unmanaged ext si el tiempo de ejecución de destino no soporta la característica. Esto se determinará buscando la presencia de la constante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind. Si esta constante está presente, el tiempo de ejecución se considera compatible con la característica.

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute

System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute es un atributo usado por CLR para indicar que se debe llamar a un método con una convención de llamada específica. Por este motivo, presentamos el siguiente soporte para trabajar con el atributo:

  • Es un error llamar directamente a un método anotado con este atributo desde C#. Los usuarios deben obtener un puntero de función para el método y luego invocar ese puntero.
  • Se trata de un error para aplicar el atributo a cualquier cosa que no sea un método estático normal o una función local estática normal. El compilador de C# marcará los métodos no estáticos o estáticos no normales importados de metadatos con este atributo como no admitido por el lenguaje.
  • Es un error que un método marcado con el atributo tenga un parámetro o un tipo de retorno que no sea un unmanaged_type.
  • Es un error que un método marcado con el atributo tenga parámetros de tipo, incluso si esos parámetros de tipo están restringidos a unmanaged.
  • Es un error que un método de un tipo genérico esté marcado con el atributo.
  • Es un error convertir un método marcado con el atributo en un tipo delegado.
  • Es un error especificar cualquier tipo para UnmanagedCallersOnly.CallConvs que no cumpla con los requisitos de la convención de invocación modopten los metadatos.

Al determinar la convención de llamada de un método marcado con un atributo UnmanagedCallersOnly válido, el compilador realiza las siguientes comprobaciones sobre los tipos especificados en la propiedad CallConvs para determinar los CallKind efectivos y modopts que se deben usar para determinar la convención de llamada:

  • Si no se especifican tipos, el CallKind se trata como unmanaged ext, sin la convención de llamada modopt al comienzo del tipo de puntero de función.
  • Si hay un tipo especificado y ese tipo se denomina CallConvCdecl, CallConvThiscall, CallConvStdcallo CallConvFastcall, el CallKind se trata como unmanaged cdecl, unmanaged thiscall, unmanaged stdcallo unmanaged fastcall, respectivamente, sin convención de llamada modopts al principio del tipo de puntero de función.
  • Si se especifican múltiples tipos o el único tipo no se llama uno de los tipos especialmente llamados arriba, el CallKind se trata como unmanaged ext, con la unión de los tipos especificados tratados como modopt al comienzo del tipo de puntero de función.

A continuación, el compilador examina esta colección efectiva de CallKind y modopt y usa reglas de metadatos normales para determinar la convención de llamada final del puntero de función tipo.

Preguntas abiertas

La detección de soporte en tiempo de ejecución para unmanaged ext

https://github.com/dotnet/runtime/issues/38135 realiza un seguimiento de la adición de esta marca. Dependiendo de los resultados de la revisión, utilizaremos la propiedad especificada en la cuestión, o utilizaremos la presencia de UnmanagedCallersOnlyAttribute como la marca que determina si los tiempos de ejecución soportan unmanaged ext.

Consideraciones

Permitir métodos de instancia

La propuesta podría ampliarse para admitir métodos de instancia aprovechando la convención de llamada de la CLI de EXPLICITTHIS (denominada instance en código de C#). Esta forma de punteros de función de la CLI coloca el parámetro this como un primer parámetro explícito de la sintaxis del puntero de función.

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

Esto es sólido, pero añade cierta complicación a la propuesta. Especialmente porque los punteros de función que difieren por la convención de llamada instance y managed serían incompatibles a pesar de que ambos casos se utilizan para invocar métodos administrados con la misma firma C#. Además, en todos los casos considerados en los que sería valioso tener esto, había una solución sencilla: utilizar una función local static.

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

No exigir unsafe en la declaración

En lugar de requerir unsafe en cada uso de un delegate*, solo es necesario en el punto en el que se convierte un grupo de métodos en un delegate*. Aquí es donde los problemas de seguridad entran en juego (sabiendo que el ensamblado que contiene no puede ser descargado mientras el valor está vivo). Requerir unsafe en las otras localizaciones puede ser visto como excesivo.

Así es como se diseñó originalmente el diseño. Pero las reglas de lenguaje resultantes parecían muy incómodas. Es imposible ocultar el hecho de que se trata de un valor puntero y que seguía asomándose incluso sin la palabra clave unsafe. Por ejemplo, no se puede permitir la conversión a object, no puede ser miembro de un class, etc. El diseño de C# es requerir unsafe para todos los usos del puntero y, por lo tanto, este diseño sigue esto.

Los desarrolladores seguirán siendo capaces de presentar una contenedor seguro sobre los valores delegate* de la misma forma que lo hacen actualmente para los tipos de puntero normales. Ten en cuenta:

unsafe struct Action {
    delegate*<void> _ptr;

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

Uso de delegados

En lugar de utilizar un nuevo elemento sintáctico, delegate*, simplemente se utilizarían los tipos existentes delegate con un * a continuación del tipo:

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

Para controlar la convención de llamada, se pueden anotar los tipos de delegate con un atributo que especifica un valor de CallingConvention. La falta de un atributo significaría la convención de llamada administrada.

La codificación de esto en IL es problemática. El valor subyacente debe representarse como puntero, pero también debe:

  1. Tener un tipo único para permitir sobrecargas con diferentes tipos de puntero de función.
  2. Ser equivalente para propósitos OHI a través de los límites de los ensamblados.

El último punto es especialmente problemático. Esto significa que cada ensamblado que utilice Func<int>* debe codificar un tipo equivalente en metadatos aunque Func<int>* esté definido en un ensamblado que no controle. Además, cualquier otro tipo definido con el nombre System.Func<T> en un ensamblado que no sea mscorlib debe ser diferente de la versión definida en mscorlib.

Una opción que se exploró fue la de emitir dicho puntero como mod_req(Func<int>) void*. Esto no funciona como una mod_req no se puede enlazar a un TypeSpec y, por tanto, no puede tener como destino instancias genéricas.

Punteros de función con nombre

La sintaxis de los punteros de función puede ser engorrosa, especialmente en casos complejos como los punteros de función anidados. En lugar de que los desarrolladores tengan que escribir la firma cada vez, el lenguaje podría permitir declaraciones nombradas de punteros de función, tal como se hace con delegate.

func* void Action();

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

Parte del problema aquí es que el primitivo de la CLI subyacente no tiene nombres, por lo que esto sería puramente una invención de C# y requiere un poco de trabajo de metadatos para habilitar. Eso es factible, pero supone una cantidad significativa de trabajo. Básicamente, requiere que C# tenga un complemento para la tabla def de tipo exclusivamente para estos nombres.

Además, cuando se examinaron los argumentos de los punteros de función con nombre, descubrimos que podían aplicarse igualmente bien a una serie de otros escenarios. Por ejemplo, sería igual de conveniente declarar tuplas con nombre para reducir la necesidad de escribir la firma completa en todos los casos.

(int x, int y) Point;

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

Tras debatirlo, decidimos no permitir la declaración de tipos delegate* con nombre. Si descubrimos que existe una necesidad significativa de ello, basándonos en los comentarios de los clientes, investigaremos una solución de nomenclatura que funcione para punteros de función, tuplas, genéricos, etc. Es probable que esto sea similar a otras sugerencias, como el soporte completo en el lenguaje typedef.

Consideraciones futuras

delegados estáticos

Se refiere a la propuesta de permitir la declaración de tipos delegate que solo pueden referirse a miembros static. La ventaja es que instancias como delegate pueden no requerir asignación, lo que mejora el rendimiento en escenarios sensibles.

Si se implementa la característica de apuntador de función, probablemente se cerrará la propuesta static delegate. La ventaja propuesta de esa característica es su naturaleza que no requiere asignación. Sin embargo, investigaciones recientes han descubierto que no es posible conseguirlo debido a la descarga del conjunto. Tiene que haber un manejador fuerte desde el método static delegate al que se refiere para evitar que el ensamblaje se descargue por debajo de él.

Para mantener cada instancia static delegate sería necesario asignar un nuevo manejador que va en contra de los objetivos de la propuesta. Hubo algunos diseños en los que la asignación podría ser amortizada a una sola asignación por sitio de llamada, pero eso era un poco complejo y no parecía valer la pena.

Esto significa que los desarrolladores tienen que decidir esencialmente entre las siguientes compensaciones:

  1. Seguridad frente a la descarga del ensamblaje: requiere asignaciones y, por tanto, delegate ya es una opción suficiente.
  2. Sin seguridad frente a la descarga de ensamblajes: utilizar un delegate*. Esto se puede encapsular en un struct para permitir el uso fuera de un contexto de unsafe en el resto del código.