Compartilhar via


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

dotnet/csharplang#191

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 contexto unsafe.
  • Não é possível converter em object.
  • Não pode ser usado como um argumento genérico.
  • Pode converter implicitamente delegate* em void*.
  • Pode converter explicitamente de void* para delegate*.

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 como params
  • 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 identifiersã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_typeF0 para outro funcptr_typeF1, desde que todas as condições a seguir sejam verdadeiras:
    • F0 e F1 têm o mesmo número de parâmetros e cada parâmetro D0n em F0 tem os mesmos modificadores ref, outou in que o parâmetro correspondente D1n em F1.
    • Para cada parâmetro de valor (um parâmetro sem ref, outou modificador de in), 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 em F0 para o tipo de parâmetro correspondente em F1.
    • Para cada parâmetro ref, outou in, o tipo de parâmetro em F0 é o mesmo que o tipo de parâmetro correspondente em F1.
    • Se o tipo de retorno for por valor (sem ref ou ref readonly), uma identidade, referência implícita ou conversão de ponteiro implícito existirá do tipo de retorno de F1 para o tipo de retorno de F0.
    • Se o tipo de retorno for por referência (ref ou ref readonly), o tipo de retorno e os modificadores ref de F1 são os mesmos que o tipo de retorno e os modificadores ref de F0.
    • A convenção de chamada de F0 é a mesma da convenção de chamada de F1.

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 e F têm o mesmo número de parâmetros e cada parâmetro em M tem os mesmos modificadores ref, outou in que o parâmetro correspondente em F.
  • Para cada parâmetro de valor (um parâmetro sem ref, outou modificador de in), 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 em M para o tipo de parâmetro correspondente em F.
  • Para cada parâmetro ref, outou in, o tipo de parâmetro em M é o mesmo que o tipo de parâmetro correspondente em F.
  • Se o tipo de retorno for por valor (sem ref ou ref readonly), uma identidade, referência implícita ou conversão de ponteiro implícito existirá do tipo de retorno de F para o tipo de retorno de M.
  • Se o tipo de retorno for por referência (ref ou ref readonly), o tipo de retorno e os modificadores de ref de F serão os mesmos que o tipo de retorno e os modificadores de ref de M.
  • A convenção de chamada de M é a mesma da convenção de chamada de F. 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ário E(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, outou in) do funcptr_parameter_list correspondente de F.
    • 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.
  • 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 que F, 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ção F. 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ãostatic 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:

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

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

§12.6.3.4

O seguinte é adicionado:

Se E é um grupo de métodos de endereço de e T é um tipo de ponteiro de função, então todos os tipos de parâmetros de T são tipos de entrada de E com o tipo T.

Tipos de saída

§12.6.3.5

O seguinte é adicionado:

Se E é um grupo de endereços de métodos e T é um tipo de ponteiro de função, o tipo de retorno de T será um tipo de saída de E com o tipo T.

Inferências de tipo de saída

§12.6.3.7

O marcador a seguir é adicionado entre os marcadores 2 e 3:

  • Se E é um grupo de métodos de endereço de e T é um tipo de ponteiro de função com tipos de parâmetros T1...Tk e o tipo de retorno Tb, e resolução de sobrecarga de E com tipos T1..Tk produz um único método com tipo de retorno U, então uma inferência de limite inferior é feita de U para Tb.

Melhor conversão da expressão

§12.6.4.5

O sub-marcador a seguir é adicionado como um caso ao marcador 2:

  • V é um tipo de ponteiro de função delegate*<V2..Vk, V1> e U é um tipo de ponteiro de função delegate*<U2..Uk, U1>, e a convenção de chamada de V é idêntica a Ue a refez de Vi é idêntica a Ui.

Inferências de limite inferior

§12.6.3.10

O seguinte caso é adicionado ao marcador 3:

  • V é um tipo de ponteiro de função delegate*<V2..Vk, V1> e há um tipo de ponteiro de função delegate*<U2..Uk, U1> de modo que U é idêntico a delegate*<U2..Uk, U1>, e a convenção de chamada de V é idêntica a U, e a referência de Vi é idêntica a Ui.

O primeiro marcador de inferência de Ui para Vi foi modificado para:

  • Se U não é um tipo de ponteiro de função e Ui não é conhecido como um tipo de referência, ou se U é um tipo de ponteiro de função e Ui 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 for delegate*<V2..Vk, V1> então a inferência dependerá do parâmetro i-th de delegate*<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

§12.6.3.11

O seguinte caso é adicionado ao marcador 2:

  • U é um tipo de ponteiro de função delegate*<U2..Uk, U1> e V é um tipo de ponteiro de função que é idêntico a delegate*<V2..Vk, V1>, e a convenção de chamada de U é idêntico a V, e a referência de Ui é idêntico a Vi.

O primeiro marcador de inferência de Ui para Vi foi modificado para:

  • Se U não é um tipo de ponteiro de função e Ui não é conhecido como um tipo de referência, ou se U é um tipo de ponteiro de função e Ui 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 for delegate*<U2..Uk, U1> então a inferência dependerá do parâmetro i-th de delegate*<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, oute 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 e OutAttribute 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 modoptno 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 defaultCallKind. Esse é o CallKind padrão de qualquer método não atribuído com UnmanagedCallersOnly.

O C# reconhece 4 identificadores especiais que mapeiam para CallKindespecí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_conventions. Esses identificadores são Cdecl, Thiscall, Stdcalle Fastcall, que correspondem a unmanaged cdecl, unmanaged thiscall, unmanaged stdcalle 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 caracteres CallConv
  • 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 identifierespecificados em um unmanaged_calling_convention, codificamos o CallKind como unmanaged exte codificamos cada um dos tipos resolvidos no conjunto de modoptno início da assinatura do ponteiro de função. Como observação, essas regras significam que os usuários não podem prefixar esses identifiercom 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 modoptno 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 identifierdentro 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 chamada modoptnos 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 modoptefetivos que devem ser usados para determinar a convenção de chamada:

  • Se nenhum tipo for especificado, o CallKind é tratado como unmanaged ext, sem nenhuma convenção de chamada modoptno início do tipo de ponteiro de função.
  • Se houver um tipo especificado e esse tipo for chamado CallConvCdecl, CallConvThiscall, CallConvStdcallou CallConvFastcall, o CallKind será tratado como unmanaged cdecl, unmanaged thiscall, unmanaged stdcallou unmanaged fastcall, respectivamente, sem nenhuma convenção de chamada modoptno 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 como unmanaged ext, com a união dos tipos especificados tratados como modoptno 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:

  1. Tenha um tipo exclusivo para permitir sobrecargas com diferentes tipos de ponteiros de função.
  2. 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:

  1. Segurança diante do descarregamento do assembly: isso requer alocações e, portanto, delegate já é uma opção suficiente.
  2. Sem segurança durante o descarregamento da montagem: use um delegate*. Isso pode ser encapsulado em uma struct para permitir o uso fora de um contexto de unsafe no restante do código.