Ponteiros de função
Nota
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).
Você pode saber mais sobre o processo de adoção de especificações detalhadas de recursos no padrão da linguagem C# no artigo sobre as especificações de .
Resumo
Esta proposta fornece construções de linguagem que expõem opcodes IL que não podem ser acessados de forma eficiente ou de forma alguma em C# atualmente: ldftn
e calli
. Esses opcodes il podem ser importantes no código de alto desempenho e os desenvolvedores precisam de uma maneira eficiente de acessá-los.
Motivação
As motivações e o contexto desse recurso são descritos na seguinte questão (bem como uma possível implementação do recurso):
Esta é uma proposta alternativa de design para os intrínsecos do compilador
Design detalhado
Ponteiros de função
A linguagem permitirá a declaração de ponteiros de função usando a sintaxe delegate*
. A sintaxe completa é descrita em detalhes na próxima seção, mas deve ser semelhante à sintaxe usada por declarações de tipo Func
e Action
.
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
Esses tipos são representados usando o tipo de ponteiro de função, conforme descrito no ECMA-335. Isso significa que a invocação de um delegate*
usará calli
em que a invocação de um delegate
usará callvirt
no método Invoke
.
Sintaticamente, a invocação é idêntica para ambas as construções.
A definição de ponteiros de método da ECMA-335 inclui a convenção de chamada como parte da assinatura do tipo (seção 7.1).
A convenção de chamada padrão será managed
. Convenções de chamada não gerenciadas podem ser especificadas colocando uma unmanaged
palavra-chave após a sintaxe delegate*
, que usará o padrão da plataforma de tempo de execução. Convenções não gerenciadas específicas podem ser especificadas entre colchetes para a palavra-chave unmanaged
especificando qualquer tipo que comece com CallConv
no namespace System.Runtime.CompilerServices
, deixando de fora o prefixo CallConv
. Esses tipos devem vir da biblioteca principal do programa e o conjunto de combinações válidas depende da 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>;
As conversões entre tipos de delegate*
são feitas com base em sua assinatura, incluindo a convenção de chamada.
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
}
}
Um tipo de delegate*
é um tipo de ponteiro, o que significa que ele tem todas as funcionalidades e restrições de um tipo de ponteiro padrão:
- Válido apenas em um contexto de
unsafe
. - Métodos que contêm um parâmetro
delegate*
ou tipo de retorno só podem ser chamados em um contextounsafe
. - Não é possível converter em
object
. - Não pode ser usado como um argumento genérico.
- Pode converter implicitamente
delegate*
emvoid*
. - Pode converter explicitamente de
void*
paradelegate*
.
Restrições:
- Atributos personalizados não podem ser aplicados a um
delegate*
ou a nenhum de seus elementos. - Um parâmetro
delegate*
não pode ser marcado comoparams
- Um tipo de
delegate*
tem todas as restrições de um tipo de ponteiro normal. - A aritmética de ponteiros não pode ser realizada diretamente em tipos de ponteiros de função.
Sintaxe do ponteiro de função
A sintaxe do ponteiro de função completa é representada pela seguinte 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'
;
Se nenhum calling_convention_specifier
for fornecido, o padrão será managed
. A codificação precisa de metadados do calling_convention_specifier
e quais identifier
são válidos no unmanaged_calling_convention
é abordada na representação de metadados de convenções de chamada.
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>>;
Conversões de ponteiro de função
Em um contexto não seguro, o conjunto de conversões implícitas disponíveis é estendido para incluir as seguintes conversões de ponteiro implícitas:
- Conversões existentes – (§23,5)
- De funcptr_type
F0
para outro funcptr_typeF1
, desde que todas as condições a seguir sejam verdadeiras:F0
eF1
têm o mesmo número de parâmetros e cada parâmetroD0n
emF0
tem os mesmos modificadoresref
,out
ouin
que o parâmetro correspondenteD1n
emF1
.- Para cada parâmetro de valor (um parâmetro sem
ref
,out
ou modificador dein
), existe uma conversão de identidade, conversão de referência implícita ou conversão de ponteiro implícito do tipo de parâmetro emF0
para o tipo de parâmetro correspondente emF1
. - Para cada parâmetro
ref
,out
ouin
, o tipo de parâmetro emF0
é o mesmo que o tipo de parâmetro correspondente emF1
. - Se o tipo de retorno for por valor (sem
ref
ouref readonly
), uma identidade, referência implícita ou conversão de ponteiro implícito existirá do tipo de retorno deF1
para o tipo de retorno deF0
. - Se o tipo de retorno for por referência (
ref
ouref readonly
), o tipo de retorno e os modificadoresref
deF1
são os mesmos que o tipo de retorno e os modificadoresref
deF0
. - A convenção de chamada de
F0
é a mesma da convenção de chamada deF1
.
Permitir o endereço de métodos de destino
Os grupos de métodos agora serão permitidos como argumentos para um endereço de expressão. O tipo de tal expressão será um delegate*
que tem a assinatura equivalente do método de destino e uma convenção de chamada gerenciada:
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;
}
}
Em um contexto não seguro, um método M
é compatível com um tipo de ponteiro de função F
se todos os seguintes forem verdadeiros:
M
eF
têm o mesmo número de parâmetros e cada parâmetro emM
tem os mesmos modificadoresref
,out
ouin
que o parâmetro correspondente emF
.- Para cada parâmetro de valor (um parâmetro sem
ref
,out
ou modificador dein
), existe uma conversão de identidade, conversão de referência implícita ou conversão de ponteiro implícito do tipo de parâmetro emM
para o tipo de parâmetro correspondente emF
. - Para cada parâmetro
ref
,out
ouin
, o tipo de parâmetro emM
é o mesmo que o tipo de parâmetro correspondente emF
. - Se o tipo de retorno for por valor (sem
ref
ouref readonly
), uma identidade, referência implícita ou conversão de ponteiro implícito existirá do tipo de retorno deF
para o tipo de retorno deM
. - Se o tipo de retorno for por referência (
ref
ouref readonly
), o tipo de retorno e os modificadores deref
deF
serão os mesmos que o tipo de retorno e os modificadores deref
deM
. - A convenção de chamada de
M
é a mesma da convenção de chamada deF
. Isso inclui o bit de convenção de chamada e todos os sinalizadores de convenção de chamada especificados no identificador não gerenciado. M
é um método estático.
Em um contexto não seguro, existe uma conversão implícita de uma expressão de endereço cujo destino é um grupo de métodos E
para um tipo de ponteiro de função compatível F
se E
contiver pelo menos um método aplicável em sua forma normal a uma lista de argumentos construída pelo uso dos tipos de parâmetro e modificadores de F
, conforme descrito no seguinte.
- Um único método
M
é selecionado correspondente a uma invocação de método do formulárioE(A)
com as seguintes modificações:- A lista de argumentos
A
é uma lista de expressões, cada uma classificada como uma variável e com o tipo e modificador (ref
,out
ouin
) do funcptr_parameter_list correspondente deF
. - Os métodos candidatos são apenas aqueles que são aplicáveis em sua forma normal, não aqueles aplicáveis em sua forma expandida.
- Os métodos candidatos são apenas aqueles que são estáticos.
- A lista de argumentos
- Se o algoritmo de resolução de sobrecarga produzir um erro, ocorrerá um erro de tempo de compilação. Caso contrário, o algoritmo produzirá um único melhor método
M
, que terá o mesmo número de parâmetros queF
, e a conversão será considerada existente. - O método selecionado
M
deve ser compatível (conforme definido acima) com o tipo de ponteiro de funçãoF
. Caso contrário, ocorrerá um erro de tempo de compilação. - O resultado da conversão é um ponteiro de função do tipo
F
.
Isso significa que os desenvolvedores podem depender das regras de resolução de sobrecarga para trabalhar em conjunto com o operador de endereço de:
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;
}
}
O operador de obtenção de endereço será implementado usando a instrução ldftn
.
Restrições deste recurso:
- Aplica-se apenas a métodos marcados como
static
. - Funções locais não
static
não podem ser usadas em&
. Os detalhes de implementação desses métodos não são especificados deliberadamente pelo idioma. Isso inclui se eles são estáticos versus instância ou exatamente com qual assinatura eles são emitidos.
Operadores em tipos de ponteiro de função
A seção sobre expressões em código não seguro é modificada da seguinte forma:
Em um contexto inseguro, várias construções estão disponíveis para operar em todos os _pointer_type_s que não sejam _funcptr_type_s:
- O operador
*
pode ser usado para realizar a indireção de ponteiro (§23.6.2).- O operador
->
pode ser usado para acessar um membro de um struct por meio de um ponteiro (§23.6.3).- O operador
[]
pode ser usado para indexar um ponteiro (§23.6.4).- O operador
&
pode ser usado para obter o endereço de uma variável (§23.6.5).- Os operadores
++
e--
podem ser usados para incrementar e diminuir ponteiros (§23.6.6).- Os operadores
+
e-
podem ser usados para executar aritmética de ponteiro (§23.6.7).- Os operadores
==
,!=
,<
,>
,<=
e=>
podem ser usados para comparar ponteiros (§23.6.8).- O operador
stackalloc
pode ser usado para alocar memória da pilha de chamadas (§23.8).- A instrução
fixed
pode ser usada para corrigir temporariamente uma variável para que seu endereço possa ser obtido (§23,7).Em um contexto não seguro, vários constructos estão disponíveis para operação em todas as _funcptr_type_s:
- O operador
&
pode ser usado para obter o endereço de métodos estáticos (Permitir o endereço dos métodos de destino)- Os operadores
==
,!=
,<
,>
,<=
e=>
podem ser usados para comparar ponteiros (§23.6.8).
Além disso, modificamos todas as seções em Pointers in expressions
para proibir tipos de ponteiro de função, com exceção de Pointer comparison
e The sizeof operator
.
Membro de função Better
§12.6.4.3 Melhor membro da função será alterado para incluir a seguinte linha:
Um
delegate*
é mais específico do quevoid*
Isso significa que é possível sobrecarregar o void*
e o delegate*
e ainda assim usar o operador de endereço de maneira sensata.
Inferência de tipos
No código não seguro, as seguintes alterações são feitas nos algoritmos de inferência de tipo:
Tipos de entrada
O seguinte é adicionado:
Se
E
é um grupo de métodos de endereço de eT
é um tipo de ponteiro de função, então todos os tipos de parâmetros deT
são tipos de entrada deE
com o tipoT
.
Tipos de saída
O seguinte é adicionado:
Se
E
é um grupo de endereços de métodos eT
é um tipo de ponteiro de função, o tipo de retorno deT
será um tipo de saída deE
com o tipoT
.
Inferências de tipo de saída
O marcador a seguir é adicionado entre os marcadores 2 e 3:
- Se
E
é um grupo de métodos de endereço de eT
é um tipo de ponteiro de função com tipos de parâmetrosT1...Tk
e o tipo de retornoTb
, e resolução de sobrecarga deE
com tiposT1..Tk
produz um único método com tipo de retornoU
, então uma inferência de limite inferior é feita deU
paraTb
.
Melhor conversão da expressão
O sub-marcador a seguir é adicionado como um caso ao marcador 2:
V
é um tipo de ponteiro de funçãodelegate*<V2..Vk, V1>
eU
é um tipo de ponteiro de funçãodelegate*<U2..Uk, U1>
, e a convenção de chamada deV
é idêntica aU
e a refez deVi
é idêntica aUi
.
Inferências de limite inferior
O seguinte caso é adicionado ao marcador 3:
V
é um tipo de ponteiro de funçãodelegate*<V2..Vk, V1>
e há um tipo de ponteiro de funçãodelegate*<U2..Uk, U1>
de modo queU
é idêntico adelegate*<U2..Uk, U1>
, e a convenção de chamada deV
é idêntica aU
, e a referência deVi
é idêntica aUi
.
O primeiro marcador de inferência de Ui
para Vi
foi modificado para:
- Se
U
não é um tipo de ponteiro de função eUi
não é conhecido como um tipo de referência, ou seU
é um tipo de ponteiro de função eUi
não é conhecido como um tipo de ponteiro de função ou um tipo de referência, então uma inferência exata é feita.
Em seguida, adicionado após o 3° item de inferência de Ui
para Vi
:
- Caso contrário, se
V
fordelegate*<V2..Vk, V1>
então a inferência dependerá do parâmetro i-th dedelegate*<V2..Vk, V1>
:
- Se V1:
- Se o retorno for por valor, então será feita uma inferência de limite inferior .
- Se o retorno for por referência, então uma inferência exata é feita.
- Se V2..Vk:
- Se o parâmetro for por valor, então uma inferência de limite superior é feita.
- Se o parâmetro for por referência, então uma inferência exata é feita.
Inferências de limite superior
O seguinte caso é adicionado ao marcador 2:
U
é um tipo de ponteiro de funçãodelegate*<U2..Uk, U1>
eV
é um tipo de ponteiro de função que é idêntico adelegate*<V2..Vk, V1>
, e a convenção de chamada deU
é idêntico aV
, e a referência deUi
é idêntico aVi
.
O primeiro marcador de inferência de Ui
para Vi
foi modificado para:
- Se
U
não é um tipo de ponteiro de função eUi
não é conhecido como um tipo de referência, ou seU
é um tipo de ponteiro de função eUi
não é conhecido como um tipo de ponteiro de função ou um tipo de referência, então uma inferência exata é feita.
Em seguida, adicionado após o 3° item de inferência de Ui
para Vi
:
- Caso contrário, se
U
fordelegate*<U2..Uk, U1>
então a inferência dependerá do parâmetro i-th dedelegate*<U2..Uk, U1>
:
- Se U1:
- Se o retorno é por valor, então uma inferência de limite superior é feita.
- Se o retorno for por referência, então uma inferência exata é feita.
- Se U2..Uk:
- Se o parâmetro for por valor, então uma inferência de limite inferior é feita.
- Se o parâmetro for por referência, então uma inferência exata é feita.
Representação de metadados de parâmetros de in
, out
e ref readonly
e tipos de retorno
As assinaturas de ponteiro de função não têm localização de sinalizadores de parâmetro, portanto, devemos codificar se os parâmetros e o tipo de retorno são in
, out
ou ref readonly
usando modreqs.
in
Reutilizamos System.Runtime.InteropServices.InAttribute
, aplicados como um modreq
ao especificador ref em um parâmetro ou tipo de retorno, para significar o seguinte:
- Se aplicado a um especificador de ref de parâmetro, esse parâmetro será tratado como
in
. - Se aplicado ao especificador ref do tipo de retorno, o tipo de retorno é tratado como
ref readonly
.
out
Usamos System.Runtime.InteropServices.OutAttribute
, aplicado como um modreq
ao especificador ref em um tipo de parâmetro, para significar que o parâmetro é um parâmetro out
.
Erros
- É um erro aplicar
OutAttribute
como um modreq a um tipo de retorno. - É um erro aplicar
InAttribute
eOutAttribute
como um modreq a um tipo de parâmetro. - Se um dos dois for especificado via modopt, eles serão ignorados.
Representação de metadados de convenções de chamada
As convenções de chamada são codificadas em uma assinatura de método em metadados por uma combinação do sinalizador CallKind
na assinatura e zero ou mais modopt
no início da assinatura. Atualmente, o ECMA-335 declara os seguintes elementos no sinalizador CallKind
:
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
Destes, os ponteiros de função em C# vão suportar todos, exceto varargs
.
Além disso, o runtime (e possivelmente o 335) será atualizado para incluir um novo CallKind
em novas plataformas. Isso não tem um nome formal no momento, mas este documento usará unmanaged ext
como espaço reservado para representar o novo formato de convenção de chamada extensível. Sem modopt
, a convenção de chamada padrão da plataforma é unmanaged ext
, unmanaged
sem colchetes.
Mapeando o calling_convention_specifier
para um CallKind
Um calling_convention_specifier
omitido ou especificado como managed
é mapeado para o default
CallKind
. Esse é o CallKind
padrão de qualquer método não atribuído com UnmanagedCallersOnly
.
O C# reconhece 4 identificadores especiais que mapeiam para CallKind
específicos não gerenciados existentes do ECMA 335. Para que esse mapeamento ocorra, esses identificadores devem ser especificados por conta própria, sem outros identificadores, e esse requisito é codificado na especificação para unmanaged_calling_convention
s. Esses identificadores são Cdecl
, Thiscall
, Stdcall
e Fastcall
, que correspondem a unmanaged cdecl
, unmanaged thiscall
, unmanaged stdcall
e unmanaged fastcall
, respectivamente. Se mais de um identifer
for especificado ou o único identifier
não for dos identificadores especialmente reconhecidos, executaremos uma pesquisa de nome especial no identificador com as seguintes regras:
- Anexamos o
identifier
com a cadeia de caracteresCallConv
- Analisamos apenas os tipos definidos no namespace
System.Runtime.CompilerServices
. - Analisamos apenas os tipos definidos na biblioteca principal do aplicativo, que é a biblioteca que define
System.Object
e não tem dependências. - Analisamos apenas os tipos públicos.
Se a pesquisa tiver êxito em todos os identifier
especificados em um unmanaged_calling_convention
, codificamos o CallKind
como unmanaged ext
e codificamos cada um dos tipos resolvidos no conjunto de modopt
no início da assinatura do ponteiro de função. Como observação, essas regras significam que os usuários não podem prefixar esses identifier
com CallConv
, pois isso resultará na busca de CallConvCallConvVectorCall
.
Ao interpretar metadados, primeiro analisamos o CallKind
. Se for algo diferente de unmanaged ext
, ignoraremos todas as modopt
no tipo de retorno para fins de determinação da convenção de chamada e use somente o CallKind
. Se o CallKind
é unmanaged ext
, examinaremos os modopts no início do tipo de ponteiro de função, tomando a união de todos os tipos que atendem aos seguintes requisitos:
- O é definido na biblioteca principal, que é a biblioteca que não faz referência a nenhuma outra biblioteca e define
System.Object
. - O tipo é definido no namespace
System.Runtime.CompilerServices
. - O tipo começa com o prefixo
CallConv
. - O tipo é público.
Eles representam os tipos que devem ser encontrados ao realizar uma pesquisa dos identifier
dentro de um unmanaged_calling_convention
ao definir um tipo de ponteiro de função na origem.
É um erro tentar usar um ponteiro de função com um CallKind
de unmanaged ext
se o runtime de destino não der suporte ao recurso. Isso será determinado procurando a presença da constante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind
. Se essa constante estiver presente, considera-se que o runtime suporta o recurso.
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
é um atributo usado pelo CLR para indicar que um método deve ser chamado com uma convenção de chamada específica. Por isso, apresentamos o seguinte suporte para trabalhar com o atributo:
- É um erro chamar diretamente um método anotado com esse atributo de C#. Os usuários devem obter um ponteiro de função para o método e, em seguida, invocar esse ponteiro.
- É um erro aplicar o atributo a qualquer coisa que não seja um método estático comum ou uma função local estática comum. O compilador C# marcará todos os métodos não estáticos ou estáticos não comuns importados de metadados com esse atributo como sem suporte pelo idioma.
- É um erro que um método marcado com o atributo tenha um parâmetro ou tipo de retorno que não seja um
unmanaged_type
. - É um erro para um método marcado com o atributo ter parâmetros de tipo, mesmo que esses parâmetros de tipo sejam restritos a
unmanaged
. - É um erro que um método de um tipo genérico esteja marcado com o atributo.
- É um erro converter um método marcado com o atributo em um tipo delegado.
- É um erro especificar qualquer tipo para
UnmanagedCallersOnly.CallConvs
que não atenda aos requisitos da convenção de chamadamodopt
nos metadados.
Ao determinar a convenção de chamada de um método marcado com um atributo de UnmanagedCallersOnly
válido, o compilador executa as seguintes verificações nos tipos especificados na propriedade CallConvs
para determinar os CallKind
e modopt
efetivos que devem ser usados para determinar a convenção de chamada:
- Se nenhum tipo for especificado, o
CallKind
é tratado comounmanaged ext
, sem nenhuma convenção de chamadamodopt
no início do tipo de ponteiro de função. - Se houver um tipo especificado e esse tipo for chamado
CallConvCdecl
,CallConvThiscall
,CallConvStdcall
ouCallConvFastcall
, oCallKind
será tratado comounmanaged cdecl
,unmanaged thiscall
,unmanaged stdcall
ouunmanaged fastcall
, respectivamente, sem nenhuma convenção de chamadamodopt
no início do tipo de ponteiro de função. - Se vários tipos forem especificados ou se o tipo único não for nomeado como um dos tipos especialmente chamados acima, o
CallKind
é tratado comounmanaged ext
, com a união dos tipos especificados tratados comomodopt
no início do tipo de ponteiro de função.
Em seguida, o compilador analisa essa coleção efetiva de CallKind
e modopt
e usa as regras normais de metadados para determinar a convenção de chamada final do tipo de ponteiro de função.
Perguntas abertas
Detectando suporte de runtime para unmanaged ext
https://github.com/dotnet/runtime/issues/38135 monitora a adição deste sinalizador. Dependendo do comentário da revisão, usaremos a propriedade especificada no problema ou usaremos a presença de UnmanagedCallersOnlyAttribute
como o sinalizador que determina se os runtimes dão suporte a unmanaged ext
.
Considerações
Permitir métodos de instância
A proposta pode ser estendida para dar suporte aos métodos de instância aproveitando a convenção de chamada da CLI EXPLICITTHIS
(denominada instance
no código C#). Essa forma de ponteiros de função da CLI coloca o parâmetro this
como o primeiro parâmetro explícito na sintaxe do ponteiro de função.
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
Isso é bom, mas adiciona alguma complicação à proposta. Especialmente porque ponteiros de função que diferem pela convenção de chamada instance
e managed
seriam incompatíveis, embora ambos os casos sejam usados para invocar métodos gerenciados com a mesma assinatura C#. Além disso, em todos os casos considerados onde seria valioso ter isso, havia uma solução simples: usar uma função local static
.
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
Não exija práticas inseguras na declaração
Em vez de exigir unsafe
em cada uso de um delegate*
, exija-o apenas no ponto em que um grupo de métodos é convertido em um delegate*
. É nesse ponto que as principais questões de segurança entram em ação (saber que o conjunto de contenção não pode ser descarregado enquanto o valor estiver vivo). Exigir unsafe
nos outros locais pode ser visto como excessivo.
Foi assim que o design foi originalmente pretendido. Mas as regras de linguagem resultantes pareciam muito estranhas. É impossível ocultar o fato de que esse é um valor de ponteiro e ele continuou a aparecer mesmo sem a palavra-chave unsafe
. Por exemplo, a conversão para object
não pode ser permitida, não pode ser membro de um class
, etc... O design do C# deve exigir unsafe
para todos os usos de ponteiro e, portanto, esse design segue isso.
Os desenvolvedores ainda poderão apresentar um wrapper seguro sobre os valores delegate*
da mesma forma que fazem atualmente para tipos de ponteiro normais. Considere:
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
Usando delegados
Em vez de usar um novo elemento de sintaxe, delegate*
, basta usar tipos existentes de delegate
com um *
seguindo o tipo:
Func<object, object, bool>* ptr = &object.ReferenceEquals;
O tratamento da convenção de chamada pode ser feito anotando os tipos de delegate
com um atributo que especifica um valor CallingConvention
. A ausência de um atributo indicaria a convenção de chamada gerenciada.
Codificar isso no IL é problemático. O valor subjacente precisa ser representado como um ponteiro, mas também deve:
- Tenha um tipo exclusivo para permitir sobrecargas com diferentes tipos de ponteiros de função.
- Ser equivalente para fins de OHI em todos os limites de assembly.
O último ponto é particularmente problemático. Isso significa que cada assembly que usa Func<int>*
deve codificar um tipo equivalente em metadados, mesmo que Func<int>*
seja definido em um assembly sobre o qual não tem controle.
Além disso, qualquer outro tipo definido com o nome System.Func<T>
em um assembly que não seja mscorlib deve ser diferente da versão definida em mscorlib.
Uma opção que foi explorada foi emitir um ponteiro como mod_req(Func<int>) void*
. Isso não funciona, porém, como um mod_req
não pode se associar a um TypeSpec
e, portanto, não pode direcionar instanciações genéricas.
Ponteiros de função nomeados
A sintaxe do ponteiro de função pode ser complicada, principalmente em casos complexos, como ponteiros de função aninhados. Em vez de exigir que os desenvolvedores digitem a assinatura toda vez, o idioma poderia permitir declarações nomeadas de ponteiros de função, como é feito com delegate
.
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
Parte do problema aqui é que o primitivo da CLI subjacente não tem nomes, portanto, isso seria puramente uma invenção em C# e exigiria um pouco de trabalho de metadados para habilitar. Isso é factível, mas é um trabalho significativo. Essencialmente, requer que C# tenha um auxiliar para a tabela de definição de tipos exclusivamente para esses nomes.
Além disso, quando os argumentos para ponteiros de função nomeados foram examinados, descobrimos que eles poderiam se aplicar igualmente bem a vários outros cenários. Por exemplo, seria igualmente conveniente declarar tuplas nomeadas para reduzir a necessidade de digitar a assinatura completa em todos os casos.
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
Após discutirmos, decidimos não permitir a declaração nomeada dos tipos delegate*
. Se descobrirmos que há uma necessidade significativa para isso com base nos comentários de uso dos clientes, investigaremos uma solução de nomenclatura que funcione para ponteiros de função, tuplas, genéricos etc. É provável que isso seja semelhante em forma a outras sugestões, como o suporte completo ao typedef
na linguagem.
Considerações futuras
delegados estáticos
Isso se refere a proposta para permitir a declaração de tipos de delegate
que só podem se referir a membros static
. A vantagem é que essas instâncias delegate
podem ser livres de alocação e oferecer uma melhor performance em cenários sensíveis.
Se o recurso de ponteiro de função for implantado, a proposta static delegate
provavelmente será fechada. A vantagem proposta desse recurso é a natureza livre de alocação. No entanto, investigações recentes descobriram que não é possível alcançar isso devido ao descarregamento do assembly. Deve haver um identificador forte do static delegate
até o método ao qual ele se refere para impedir que o assembly seja descarregado inapropriadamente.
Para manter cada instância static delegate
seria necessário alocar um novo identificador que seja contrário às metas da proposta. Houve alguns projetos em que a alocação poderia ser amortizada para uma única alocação por site de chamada, mas isso era um pouco complexo e não parecia valer a pena a troca.
Isso significa que os desenvolvedores essencialmente precisam decidir entre as seguintes compensações:
- Segurança diante do descarregamento do assembly: isso requer alocações e, portanto,
delegate
já é uma opção suficiente. - Sem segurança durante o descarregamento da montagem: use um
delegate*
. Isso pode ser encapsulado em umastruct
para permitir o uso fora de um contexto deunsafe
no restante do código.
C# feature specifications