Partilhar via


Ponteiros de função

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações 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 Language Design Meeting (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Resumo

Esta proposta fornece construções de linguagem que expõem opcodes IL que atualmente não podem ser acessados de forma eficiente, ou de todo, hoje em C#: ldftn e calli. Esses opcodes IL podem ser importantes em código de alto desempenho e os desenvolvedores precisam de uma maneira eficiente de acessá-los.

Motivação

As motivações e antecedentes para este recurso são descritos na seguinte edição (assim como uma possível implementação do recurso):

dotnet/csharplang#191

Esta é uma proposta alternativa de design para os intrínsecos do compilador

Projeto 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 é projetada para assemelhar-se à sintaxe usada pelas declarações de tipos 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 em ECMA-335. Isso significa que a invocação de um delegate* usará calli onde a invocação de um delegate usará callvirt no método Invoke. Sintaticamente, porém, a invocação é idêntica para ambas as construções.

A definição ECMA-335 de ponteiros de método inclui a convenção de chamada como parte da assinatura de tipo (seção 7.1). A convenção de chamada padrão será managed. As convenções de chamadas não geridas podem ser especificadas colocando uma palavra-chave unmanaged após a sintaxe delegate*, utilizando assim 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 começando 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 os tipos delegate* são feitas com base na 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 todos os recursos e restrições de um tipo de ponteiro padrão:

  • Válido somente num contexto unsafe.
  • Os métodos que contêm um parâmetro delegate* ou tipo de retorno só podem ser chamados a partir de um contexto unsafe.
  • Não pode ser convertido em object.
  • Não pode ser usado como um argumento genérico.
  • Pode implicitamente converter delegate* em void*.
  • Pode converter explicitamente de void* para delegate*.

Restrições:

  • Os atributos personalizados não podem ser aplicados a um delegate* ou a qualquer um 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.
  • Operações de aritmética de ponteiros não podem ser executadas diretamente em tipos de ponteiro de função.

Sintaxe do ponteiro da função

A sintaxe completa do ponteiro da função é 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 são válidos no unmanaged_calling_convention é abordada em 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 inseguro, o conjunto de conversões implícitas disponíveis é estendido para incluir as seguintes conversões implícitas de ponteiro:

  • 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 in), existe uma conversão de identidade, conversão de referência implícita ou conversão de ponteiro implícita 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), existe uma conversão de identidade, referência implícita ou ponteiro implícito 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), os modificadores ref e o tipo de retorno de F1 são iguais aos modificadores ref e ao tipo de retorno de F0.
    • A convenção de chamada de F0 é a mesma que a convenção de chamada de F1.

Permitir métodos de endereçamento para 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 tenha 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 inseguro, um método M é compatível com um tipo de ponteiro de função F se todos os itens a seguir forem verdadeiros:

  • M e F têm o mesmo número de parâmetros, e cada parâmetro em M tem os mesmos modificadores de ref, outou in que o parâmetro correspondente em F.
  • Para cada parâmetro de valor (um parâmetro sem ref, outou modificador in), existe uma conversão de identidade, conversão de referência implícita ou conversão de ponteiro implícita 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), existe uma conversão de identidade, referência implícita ou ponteiro implícito 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), os modificadores ref e o tipo de retorno de F são os mesmos que os modificadores ref e o tipo de retorno de M.
  • A convenção de chamada de M é a mesma que a convenção de chamada de F. Isso inclui tanto o bit da convenção de chamada quanto quaisquer sinalizadores de convenção de chamada especificados no identificador não gerenciado.
  • M é um método estático.

Em um contexto inseguro, existe uma conversão implícita de um endereço de expressã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 que seja aplicável em sua forma normal a uma lista de argumentos construída pelo uso dos tipos de parâmetros e modificadores de F, conforme descrito a seguir.

  • Um único método M é selecionado correspondendo 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) da funcptr_parameter_list correspondente de F.
    • Os métodos candidatos são apenas os métodos aplicáveis na sua forma normal, não os aplicáveis na sua forma expandida.
    • Os métodos candidatos são apenas aqueles que são estáticos.
  • Se o algoritmo de resolução de sobrecarga produz um erro, então ocorre um erro em tempo de compilação. Caso contrário, o algoritmo produz um único melhor método M que tem o mesmo número de parâmetros que F, e a conversão passa a 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 em tempo de compilação.
  • O resultado da conversão é um ponteiro de função do tipo F.

Isso significa que os desenvolvedores podem depender de regras de resolução de sobrecarga para trabalhar em conjunto com o operador de endereço:

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 endereço do operador será implementado usando a instrução ldftn.

Restrições deste recurso:

  • Aplica-se apenas aos métodos marcados como static.
  • Funções locais nãostatic não podem ser usadas em &. Os detalhes de implementação desses métodos deliberadamente não são especificados pela linguagem. 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 no código não seguro sobre expressões é 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 são _funcptr_type_s:

  • O operador * pode ser usado para executar a indireção do ponteiro (§23.6.2).
  • O operador -> pode ser usado para acessar um membro de uma struct através de um ponteiro (§23.6.3).
  • O operador [] pode ser usado para indexar um ponteiro (§23.6.4).
  • O operador & pode ser utilizado para obter o endereço de uma variável (§23.6.5).
  • Os operadores ++ e -- podem ser utilizados para incrementar e diminuir os ponteiros (§23.6.6).
  • Os operadores + e - podem ser usados para executar a aritmética de ponteiros (§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 declaração fixed pode ser usada para fixar temporariamente uma variável para que seu endereço possa ser obtido (§23.7).

Em um contexto inseguro, várias construções estão disponíveis para operação em todos os _funcptr_type_s:

Além disso, modificamos todas as seções em Pointers in expressions para proibir tipos de ponteiro de função, exceto Pointer comparison e The sizeof operator.

Melhor membro de função

§12.6.4.3 A função membro melhor será alterada para incluir a seguinte linha:

Um delegate* é mais específico do que void*

Isso significa que é possível sobrecarregar em void* e um delegate* e ainda usar sensatamente o endereço do operador.

Inferência de tipo

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

É aditado o seguinte:

Se E é um grupo de endereços de método 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 tipo T.

Tipos de saída

§12.6.3.5

É aditado o seguinte:

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

Inferências de tipo de saída

§12.6.3.7

O seguinte marcador é adicionado entre os marcadores 2 e 3:

  • Se é um grupo de métodos com endereço e é um tipo de ponteiro de função com tipos de parâmetro e tipo de retorno , e a resolução de sobrecarga do com os tipos produz um único método com tipo de retorno , então faz-se uma inferência de limite inferior para de a .

Melhor conversão a partir da expressão

§12.6.4.5

O seguinte sub-item é adicionado como um caso no ponto 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 à de U, e a natureza de referência de Vi é idêntica à de Ui.

Inferências de limite inferior

§12.6.3.10

Ao ponto 3 é aditado o seguinte caso:

  • 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> tal que U é idêntico a delegate*<U2..Uk, U1>, e a convenção de chamada de V é idêntica a U, e a característica de referência de Vi é idêntica a Ui.

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

  • Se U não é um tipo de ponteiro de função e Ui não é conhecido por ser um tipo de referência, ou se U é um tipo de ponteiro de função e Ui não é conhecido por ser 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º ponto de inferência de Ui para Vi:

  • Caso contrário, se V é delegate*<V2..Vk, V1> então a inferência depende do i-ésimo parâmetro de delegate*<V2..Vk, V1>:
    • Se V1:
      • Se o retorno for por valor, então uma inferência de limite inferior é feita.
      • 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 será 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

Ao ponto 2 é aditado o seguinte caso:

  • 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êntica a V, e a natureza de referência de Ui é idêntica a Vi.

O primeiro ponto de inferência de Ui para Vi é modificado para:

  • Se U não é um tipo de ponteiro de função e Ui não é conhecido por ser um tipo de referência, ou se U é um tipo de ponteiro de função e Ui não é conhecido por ser um tipo de ponteiro de função ou um tipo de referência, então uma de inferência exata é feita

Em seguida, adicionei após o 3º ponto de inferência de Ui até Vi:

  • Caso contrário, se U é delegate*<U2..Uk, U1> então a inferência depende do i-ésimo parâmetro de delegate*<U2..Uk, U1>:
    • Se U1:
      • Se o retorno for 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 é realizada.
      • Se o parâmetro for por referência, então uma inferência exata é feita.

Representação de metadados de parâmetros in, oute ref readonly e tipos de retorno

As assinaturas de ponteiros de função não contêm a localização dos sinalizadores de parâmetro, por isso devemos codificar se os parâmetros e o tipo de retorno são in, outou ref readonly usando modreqs.

in

Reutilizamos System.Runtime.InteropServices.InAttribute, aplicado como modreq ao especificador ref em um parâmetro ou tipo de retorno, para significar o seguinte:

  • Se aplicado a um especificador ref de parâmetro, este parâmetro é 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 modreq a um tipo de retorno.
  • É um erro aplicar InAttribute e OutAttribute como modreq a um tipo de parâmetro.
  • Se ambos forem especificados 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 modopts no início da assinatura. A ECMA-335 declara atualmente os seguintes elementos na bandeira CallKind:

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

Destes, os ponteiros de função em C# suportarão todos, exceto varargs.

Além disso, o runtime (posteriormente também o 335) será atualizado para incluir um novo CallKind em novas plataformas. Isto não tem um nome formal atualmente, mas este documento usará unmanaged ext como um marcador para representar o novo formato de convenção de chamada extensível. Sem modopts, unmanaged ext é a convenção de chamada padrão da plataforma, unmanaged sem os colchetes.

Mapeamento de calling_convention_specifier para CallKind

Quando um calling_convention_specifier é omitido ou especificado como managed, ele é mapeado para o defaultCallKind. Este é CallKind padrão de qualquer método não atribuído a UnmanagedCallersOnly.

O C# reconhece 4 identificadores especiais que mapeiam para CallKindnão gerenciados existentes específicos 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:

  • Nós precedemos o identifier com a cadeia de caracteres CallConv
  • Examinamos apenas os tipos definidos no namespace System.Runtime.CompilerServices.
  • Examinamos apenas os tipos definidos na biblioteca principal do aplicativo, que é a biblioteca que define System.Object e não tem dependências.
  • Olhamos apenas para tipos públicos.

Se a pesquisa for bem-sucedida em todos os identifierespecificados em um unmanaged_calling_convention, codificaremos o CallKind como unmanaged exte codificaremos cada um dos tipos resolvidos no conjunto de modopts no início da assinatura do ponteiro da função. Nota: estas regras significam que os utilizadores não podem prefixar estes identifiers com CallConv, pois isso resultará na procura de CallConvCallConvVectorCall.

Ao interpretar metadados, primeiro olhamos para o CallKind. Se for algo diferente de unmanaged ext, ignoramos todos os modopts no tipo de retorno para determinar a convenção de chamada e usamos apenas o CallKind. Se o CallKind for unmanaged ext, examinamos os modopts no início do tipo de ponteiro de funções, 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 outras bibliotecas 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 executar a pesquisa nos identifiers em unmanaged_calling_convention ao definir um tipo de ponteiro de função no código-fonte.

É um erro tentar usar um ponteiro de função com um CallKind de unmanaged ext se o tempo de execução de destino não suportar esta funcionalidade. Isto será determinado procurando a presença da constante System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind. Se essa constante estiver presente, o tempo de execução é considerado para suportar 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 do C#. Os utilizadores 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á quaisquer métodos não estáticos ou estáticos não ordinários importados de metadados com esse atributo como não suportados pela linguagem.
  • É 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 para um método em um tipo genérico a ser marcado com o atributo.
  • É um erro converter um método marcado por um atributo num tipo delegado.
  • É um erro especificar quaisquer tipos de UnmanagedCallersOnly.CallConvs que não atendam aos requisitos para a convenção de chamada modoptnos metadados.

Ao determinar a convenção de chamada de um método marcado com um atributo UnmanagedCallersOnly válido, o compilador executa as seguintes verificações nos tipos especificados na propriedade CallConvs para determinar os CallKind e modopts efetivos que devem ser usados para determinar a convenção de chamada:

  • Se nenhum tipo for especificado, o CallKind será tratado como unmanaged ext, sem convenção de chamada modopts no início do tipo de ponteiro de função.
  • Se houver um tipo especificado e esse tipo for nomeado CallConvCdecl, CallConvThiscall, CallConvStdcallou CallConvFastcall, o CallKind será tratado como unmanaged cdecl, unmanaged thiscall, unmanaged stdcallou unmanaged fastcall, respectivamente, sem nenhuma convenção de chamada modopts no início do tipo de ponteiro de função.
  • Se vários tipos forem especificados ou se o único tipo não estiver entre os especificamente mencionados acima, o CallKind será tratado como unmanaged ext, com a união dos tipos especificados sendo tratada como modopts no início do tipo de ponteiro de função.

Em seguida, o compilador examina essa coleção efetiva de CallKind e modopt e usa regras normais de metadados para determinar a convenção de chamada final do tipo de ponteiro de função.

Perguntas abertas

Detetando suporte de tempo de execução para unmanaged ext

https://github.com/dotnet/runtime/issues/38135 regista a adição deste indicador. Dependendo do feedback da revisão, usaremos a propriedade especificada no problema ou usaremos a presença de UnmanagedCallersOnlyAttribute como o sinalizador que determina se os tempos de execução suportam unmanaged ext.

Considerações

Permitir métodos de instância

A proposta poderia ser estendida para dar suporte a métodos de instância aproveitando a convenção de chamada de CLI EXPLICITTHIS (chamada instance no código C#). Esta forma de apontadores de função CLI coloca o parâmetro this como o primeiro parâmetro explícito da sintaxe do apontador da função.

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

Isto é sólido, mas acrescenta alguma complicação à proposta. Particularmente porque ponteiros de função que diferiam pela convenção de chamada instance e managed seriam incompatíveis, mesmo que ambos os casos sejam usados para invocar métodos gerenciados com a mesma assinatura C#. Em todos os casos considerados onde seria valioso ter essa funcionalidade, 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 declaração insegura

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*. É aqui que entram em jogo as principais questões de segurança (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 concebido. Mas as regras linguísticas resultantes pareciam muito estranhas. É impossível esconder o facto de que este é um valor de ponteiro e continuou a surgir mesmo sem o uso da palavra-chave unsafe. Por exemplo, a conversão para object não pode ser permitida, não pode ser membro de uma class, etc... O design C# deve exigir unsafe para todos os usos de ponteiro e, portanto, esse design segue isso.

Os desenvolvedores ainda serão capazes de apresentar um invólucro seguro de sobre valores de delegate* da mesma forma que fazem hoje 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*, simplesmente use os tipos existentes de delegate, com um * a seguir ao tipo.

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

A manipulação da convenção de chamada pode ser realizada ao se anotar os tipos delegate com um atributo que define um valor CallingConvention. A falta de um atributo significaria a convenção de chamada gerenciada.

Codificar isso em IL é problemático. O valor subjacente precisa ser representado como um ponteiro, mas também deve:

  1. Existe um tipo exclusivo para permitir sobrecargas com diferentes tipos de ponteiro de função.
  2. Ser equivalente para fins OHI além dos limites de montagem.

O último ponto é particularmente problemático. Isso significa que todo assembly que usa Func<int>* deve codificar um tipo equivalente em metadados, mesmo que Func<int>* esteja definido em um assembly, embora não esteja sob o seu controlo. Além disso, qualquer outro tipo que é definido com o nome System.Func<T> em um assembly que não é mscorlib deve ser diferente da versão definida em mscorlib.

Uma opção que foi explorada foi a utilização de um ponteiro como mod_req(Func<int>) void*. Isso não funciona, pois um mod_req não pode se ligar 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 laboriosa, particularmente em casos complexos, tal como os ponteiros de função aninhados. Em vez de os desenvolvedores escreverem repetidamente a assinatura, a linguagem 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 a primitiva CLI subjacente não tem nomes, portanto, isso seria puramente uma invenção do C# e exigiria um pouco de trabalho de metadados para habilitar. Isso é factível, mas é um trabalho significativo. Requer essencialmente que o C# tenha um companheiro para a tabela de definição de tipo puramente para esses nomes.

Além disso, quando examinámos os argumentos para ponteiros de funções nomeadas, também descobrimos que eles poderiam aplicar-se igualmente bem a uma variedade de 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 discussão, decidimos não permitir a declaração nominal de delegate* tipos. Se descobrirmos que há uma necessidade significativa para isso com base no feedback de uso do cliente, então investigaremos uma solução de nomenclatura que funcione para ponteiros de função, tuplas, genéricos, etc. É provável que isso seja semelhante na forma a outras sugestões, como suporte total typedef no idioma.

Considerações futuras

Delegados estáticos

Isto refere-se a a proposta permitir a declaração de delegate tipos que só podem referir-se a static membros. A vantagem é que essas instâncias delegate podem não requerer alocação e serem melhores em cenários sensíveis ao desempenho.

Se o recurso de ponteiro de função for implementado, a proposta de static delegate provavelmente será fechada. A vantagem proposta desse recurso é a natureza livre de atribuição. No entanto, investigações recentes concluíram que não é possível alcançar isso devido ao desmantelamento da montagem. Deve haver uma alça forte desde o static delegate até o método a que se refere, a fim de evitar que o conjunto seja descarregado por baixo dele.

Para manter todas as instâncias static delegate, seria necessário alocar um novo identificador, o que contraria os objetivos da proposta. Havia alguns projetos em que a alocação poderia ser amortizada para uma única alocação por local de chamada, mas isso era um pouco complexo e não parecia valer a pena.

Isso significa que os desenvolvedores essencialmente têm que decidir entre as seguintes compensações:

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