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.
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 deunsafe
. - No se puede convertir en
object
. - No se puede usar como argumento genérico.
- Puede convertir implícitamente
delegate*
envoid*
. - Puede convertir explícitamente de
void*
adelegate*
.
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 comoparams
- 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_type
F0
a otra funcptr_typeF1
, siempre que se cumplan todas las siguientes condiciones:F0
yF1
tienen el mismo número de parámetros, y cada parámetroD0n
deF0
tiene los mismos modificadoresref
,out
oin
que el parámetro correspondienteD1n
enF1
.- Para cada parámetro de valor (un parámetro sin
ref
,out
oin
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 enF0
al tipo de parámetro correspondiente enF1
. - Para cada parámetro
ref
,out
oin
, el tipo de parámetro deF0
es el mismo que el tipo de parámetro correspondiente enF1
. - Si el tipo de valor devuelto es por valor (no
ref
niref readonly
), existe una conversión de identidad, de referencia implícita o de puntero implícito del tipo de valor devuelto deF1
al tipo de valor devuelto deF0
. - Si el tipo de valor devuelto es por referencia (
ref
oref readonly
), el tipo de valor devuelto y los modificadoresref
deF1
son los mismos que el tipo de valor devuelto y los modificadoresref
deF0
. - La convención de llamada de
F0
es la misma que la convención de llamada deF1
.
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
yF
tienen el mismo número de parámetros y cada parámetro deM
tiene los mismos modificadoresref
,out
oin
como parámetro correspondiente enF
.- Para cada parámetro de valor (un parámetro sin
ref
,out
oin
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 enM
al tipo de parámetro correspondiente enF
. - Para cada parámetro
ref
,out
oin
, el tipo de parámetro deM
es el mismo que el tipo de parámetro correspondiente enF
. - Si el tipo de retorno es por valor (no
ref
niref 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 deF
al tipo de retorno deM
. - Si el tipo de valor devuelto es por referencia (
ref
oref readonly
), el tipo de valor devuelto y los modificadoresref
deF
son los mismos que el tipo de valor devuelto y los modificadoresref
deM
. - La convención de llamada de
M
es la misma que la convención de llamada deF
. 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 formularioE(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
, oin
) del funcptr_parameter_list correspondiente deF
. - 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.
- La lista de argumentos
- 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 queF
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ónF
. 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 de
static
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:
- El operador
&
puede usarse para obtener la dirección de métodos estáticos (Permitir address-of a métodos objetivo)- Los operadores
==
,!=
,<
,>
,<=
y=>
se pueden usar para comparar punteros (§23.6.8).
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 quevoid*
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
Se agrega lo siguiente:
Si
E
es un grupo de métodos de referencia yT
es un tipo de puntero de función, entonces todos los tipos de parámetros deT
son tipos de entrada deE
con el tipoT
.
Tipos de salida
Se agrega lo siguiente:
Si
E
es un grupo de métodos address-of yT
es un tipo de puntero de función entonces el tipo de retorno deT
es un tipo de salida deE
con tipoT
.
Inferencias de tipo de salida
Se añade el siguiente punto entre los puntos 2 y 3:
- Si
E
es una dirección de un grupo de métodos yT
es un tipo de puntero de función con tipos de parámetrosT1...Tk
y tipo de retornoTb
, y la resolución de sobrecarga deE
con los tiposT1..Tk
produce un único método con tipo de retornoU
, entonces se hace una inferencia de límite inferior deU
aTb
.
Mejor conversión a partir de expresión
Se añade el siguiente subpunto como caso al punto 2:
V
es un tipo puntero de funcióndelegate*<V2..Vk, V1>
yU
es un tipo de puntero de funcióndelegate*<U2..Uk, U1>
, y la convención de llamada deV
es idéntica aU
, y la referencia deVi
es idéntica aUi
.
Inferencias de límite inferior
El siguiente caso se agrega al punto 3:
V
es un tipo de puntero de funcióndelegate*<V2..Vk, V1>
y existe un tipo de puntero de funcióndelegate*<U2..Uk, U1>
tal queU
es idéntico adelegate*<U2..Uk, U1>
, la convención de llamada deV
es idéntica aU
, y el estado de referencia deVi
es idéntico aUi
.
El primer punto de la inferencia de Ui
a Vi
se modifica a:
- Si
U
no es un tipo de puntero de función yUi
no se sabe que es un tipo de referencia, o siU
es un tipo de puntero de función y no se sabe queUi
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
esdelegate*<V2..Vk, V1>
, la inferencia depende del parámetro i-th dedelegate*<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
El siguiente caso se agrega al punto 2:
U
es un tipo de puntero de funcióndelegate*<U2..Uk, U1>
, yV
es un tipo de puntero de función que es idéntico adelegate*<V2..Vk, V1>
, la convención de llamada deU
es idéntica a la deV
, y la referencialidad deUi
es idéntica a la deVi
.
El primer punto de la inferencia de Ui
a Vi
se modifica a:
- Si
U
no es un tipo de puntero de función yUi
no se sabe que es un tipo de referencia, o siU
es un tipo de puntero de función y no se sabe queUi
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
esdelegate*<U2..Uk, U1>
, la inferencia depende del parámetro i-th dedelegate*<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
, out
y 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
comoOutAttribute
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 modopt
s, 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 default
CallKind
. 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 CallKind
especí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
, Stdcall
y Fastcall
, que corresponden a unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
y 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 cadenaCallConv
- 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 modopt
en 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ónmodopt
en 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 modopt
s que se deben usar para determinar la convención de llamada:
- Si no se especifican tipos, el
CallKind
se trata comounmanaged ext
, sin la convención de llamadamodopt
al comienzo del tipo de puntero de función. - Si hay un tipo especificado y ese tipo se denomina
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
oCallConvFastcall
, elCallKind
se trata comounmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
ounmanaged fastcall
, respectivamente, sin convención de llamadamodopt
s 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 comounmanaged ext
, con la unión de los tipos especificados tratados comomodopt
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:
- Tener un tipo único para permitir sobrecargas con diferentes tipos de puntero de función.
- 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:
- Seguridad frente a la descarga del ensamblaje: requiere asignaciones y, por tanto,
delegate
ya es una opción suficiente. - Sin seguridad frente a la descarga de ensamblajes: utilizar un
delegate*
. Esto se puede encapsular en unstruct
para permitir el uso fuera de un contexto deunsafe
en el resto del código.
C# feature specifications