Tutorial: Usar marshallers personalizados em P/Invokes gerados pelo código-fonte
Neste tutorial, você aprenderá como implementar um marshaller e usá-lo para empacotamento personalizado em P/Invokes gerados pelo código-fonte.
Você implementará marshallers para um tipo interno, personalizará o marshalling para um parâmetro específico e um tipo definido pelo usuário e especificará o marshalling padrão para um tipo definido pelo usuário.
Todo o código-fonte usado neste tutorial está disponível no repositório dotnet/samples.
Visão geral do LibraryImport
gerador de origem
O System.Runtime.InteropServices.LibraryImportAttribute
tipo é o ponto de entrada do usuário para um gerador de código-fonte introduzido no .NET 7. Este gerador de código-fonte é projetado para gerar todo o código de empacotamento em tempo de compilação em vez de em tempo de execução. Os pontos de entrada têm sido historicamente especificados usando DllImport
o , mas essa abordagem vem com custos que nem sempre podem ser aceitáveis — para obter mais informações, consulte P/Invoke source generation. O LibraryImport
gerador de código-fonte pode gerar todo o código de empacotamento e remover o requisito de geração em tempo de execução intrínseco ao DllImport
.
Para expressar os detalhes necessários para gerar código de empacotamento tanto para o tempo de execução quanto para os usuários personalizarem para seus próprios tipos, vários tipos são necessários. Os seguintes tipos são usados ao longo deste tutorial:
MarshalUsingAttribute
– Atributo que é procurado pelo gerador fonte nos locais de uso e usado para determinar o tipo de marshaller para marshalling a variável atribuída.CustomMarshallerAttribute
– Atributo utilizado para indicar um marshaller para um tipo e o modo como as operações de marshalling devem ser realizadas (por exemplo, by-ref de managed para unmanaged).NativeMarshallingAttribute
– Atributo utilizado para indicar qual marshaller usar para o tipo atribuído. Isso é útil para autores de bibliotecas que fornecem tipos e acompanhantes de marshallers para esses tipos.
Esses atributos, no entanto, não são os únicos mecanismos disponíveis para um autor de marshaller personalizado. O gerador fonte inspeciona o próprio marshaller em busca de várias outras indicações que informem como o marshalling deve ocorrer.
Detalhes completos sobre o design podem ser encontrados no repositório dotnet/runtime .
Analisador e fixador do gerador de origem
Juntamente com o gerador de origem em si, um analisador e um fixador são fornecidos. O analisador e o fixador estão habilitados e disponíveis por padrão desde o .NET 7 RC1. O analisador é projetado para ajudar a orientar os desenvolvedores a usar o gerador de código-fonte corretamente. O fixador fornece conversões automatizadas de muitos DllImport
padrões para a assinatura apropriada LibraryImport
.
Apresentando a biblioteca nativa
Usar o gerador de LibraryImport
código-fonte significaria consumir uma biblioteca nativa ou não gerenciada. Uma biblioteca nativa pode ser uma biblioteca compartilhada (ou seja, .dll
, .so
ou dylib
) que chama diretamente uma API do sistema operacional que não é exposta por meio do .NET. A biblioteca também pode ser fortemente otimizada em uma linguagem não gerenciada que um desenvolvedor .NET deseja consumir. Para este tutorial, você criará sua própria biblioteca compartilhada que expõe uma superfície de API no estilo C. O código a seguir representa um tipo definido pelo usuário e duas APIs que você consumirá do C#. Essas duas APIs representam o modo "in", mas há modos adicionais para explorar no exemplo.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);
O código anterior contém os dois tipos de interesse, char32_t*
e error_data
. char32_t*
representa uma cadeia de caracteres codificada em UTF-32, que não é uma codificação de cadeia de caracteres que o .NET historicamente marshals. error_data
é um tipo definido pelo usuário que contém um campo inteiro de 32 bits, um campo booleano C++ e um campo de cadeia de caracteres codificada UTF-32. Ambos os tipos exigem que você forneça uma maneira para o gerador de código-fonte gerar código de empacotamento.
Personalizar o empacotamento para um tipo interno
Considere o char32_t*
tipo primeiro, uma vez que a organização desse tipo é exigida pelo tipo definido pelo usuário. char32_t*
representa o lado nativo, mas você também precisa de representação no código gerenciado. No .NET, há apenas um tipo string
de "string", . Portanto, você estará organizando uma cadeia de caracteres codificada UTF-32 nativa de e para o string
tipo no código gerenciado. Já existem vários marshallers embutidos para o string
tipo que marshal como UTF-8, UTF-16, ANSI, e até mesmo como o tipo Windows BSTR
. No entanto, não há um para marshalling como UTF-32. É isso que você precisa definir.
O Utf32StringMarshaller
tipo é marcado com um CustomMarshaller
atributo, que descreve o que ele faz com o gerador de origem. O primeiro argumento de tipo para o atributo é o string
tipo, o tipo gerenciado para marechal, o segundo é o modo, que indica quando usar o marshaller, e o terceiro tipo é Utf32StringMarshaller
, o tipo a ser usado para marshalling. Você pode aplicar o CustomMarshaller
várias vezes para especificar melhor o modo e qual tipo de marshaller usar para esse modo.
O exemplo atual mostra um marshaller "apátrida" que recebe algumas entradas e retorna dados na forma empacotada. O Free
método existe para simetria com o marshalling não gerenciado, e o coletor de lixo é a operação "livre" para o marshaller gerenciado. O implementador é livre para executar quaisquer operações desejadas para organizar a entrada para a saída, mas lembre-se de que nenhum estado será explicitamente preservado pelo gerador de origem.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
internal static unsafe class Utf32StringMarshaller
{
public static uint* ConvertToUnmanaged(string? managed)
=> throw new NotImplementedException();
public static string? ConvertToManaged(uint* unmanaged)
=> throw new NotImplementedException();
public static void Free(uint* unmanaged)
=> throw new NotImplementedException();
}
}
As especificidades de como este marshaller em particular realiza a conversão de string
para char32_t*
podem ser encontradas na amostra. Observe que qualquer API .NET pode ser usada (por exemplo, Encoding.UTF32).
Considere um caso em que o estado é desejável. Observe o adicional CustomMarshaller
e observe o modo mais específico, MarshalMode.ManagedToUnmanagedIn
. Este marshaller especializado é implementado como "stateful" e pode armazenar o estado em toda a chamada de interoperabilidade. Mais especialização e estado permitem otimizações e empacotamento personalizado para um modo. Por exemplo, o gerador de origem pode ser instruído a fornecer um buffer alocado por pilha que poderia evitar uma alocação explícita durante o empacotamento. Para indicar o suporte para um buffer alocado por pilha, o marshaller implementa uma BufferSize
propriedade e um FromManaged
método que usa um Span
de um unmanaged
tipo. A BufferSize
propriedade indica a quantidade de espaço de pilha — o comprimento do a ser passado para FromManaged
— que o marshaller gostaria de Span
obter durante a chamada de marechal.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
internal static unsafe class Utf32StringMarshaller
{
//
// Stateless functions removed
//
public ref struct ManagedToUnmanagedIn
{
public static int BufferSize => 0x100;
private uint* _unmanagedValue;
private bool _allocated; // Used stack alloc or allocated other memory
public void FromManaged(string? managed, Span<byte> buffer)
=> throw new NotImplementedException();
public uint* ToUnmanaged()
=> throw new NotImplementedException();
public void Free()
=> throw new NotImplementedException();
}
}
}
Agora você pode chamar a primeira das duas funções nativas usando seus marshallers de cadeia UTF-32. A declaração a seguir usa o LibraryImport
atributo, assim como DllImport
, mas depende do MarshalUsing
atributo para dizer ao gerador de origem qual marshaller usar ao chamar a função nativa. Não há necessidade de esclarecer se o apátrida ou o marshaller apátrida deve ser usado. Isso é tratado pelo implementador definindo o(s) atributo MarshalMode
(s) CustomMarshaller
do marshaller. O gerador fonte selecionará o marshaller mais adequado com base no contexto em que o é aplicado, sendo MarshalMode.Default
o MarshalUsing
fallback.
// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);
Personalizar o empacotamento para um tipo definido pelo usuário
Marshalling um tipo definido pelo usuário requer definir não apenas a lógica de empacotamento, mas também o tipo em C# para marshal de/para. Lembre-se do tipo nativo que estamos tentando controlar.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
Agora, defina como seria idealmente em C#. An int
é do mesmo tamanho no C++ moderno e no .NET. A bool
é o exemplo canônico de um valor booleano no .NET. Construindo sobre o Utf32StringMarshaller
, você pode marshal char32_t*
como um .NET string
. Contabilizando o estilo .NET, o resultado é a seguinte definição em C#:
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
Seguindo o padrão de nomenclatura, nomeie o marshaller ErrorDataMarshaller
. Em vez de especificar um marshaller para MarshalMode.Default
, você só definirá marshallers para alguns modos. Neste caso, se o marshaller for usado para um modo que não é fornecido, o gerador de origem falhará. Comece com a definição de um marshaller para a direção "em". Este é um marshaller "apátrida" porque o marshaller em si consiste apenas em static
funções.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
internal static unsafe class ErrorDataMarshaller
{
// Unmanaged representation of ErrorData.
// Should mimic the unmanaged error_data type at a binary level.
internal struct ErrorDataUnmanaged
{
public int Code; // .NET doesn't support less than 32-bit, so int is 32-bit.
public byte IsFatal; // The C++ bool is defined as a single byte.
public uint* Message; // This could be as simple as a void*, but uint* is closer.
}
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
ErrorDataUnmanaged
imita a forma do tipo não gerenciado. A conversão de um ErrorData
para um ErrorDataUnmanaged
é agora trivial com Utf32StringMarshaller
.
A organização de um int
é desnecessária, uma vez que sua representação é idêntica em código não gerenciado e gerenciado. A representação binária de um bool
valor não é definida no .NET, portanto, use seu valor atual para definir um valor zero e diferente de zero no tipo não gerenciado. Em seguida, reutilize seu marshaller UTF-32 para converter o string
campo em um uint*
arquivo .
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}
Lembre-se de que você está definindo este marshaller como um "in", então você deve limpar todas as alocações realizadas durante o marshalling. Os int
campos e bool
não alocaram nenhuma memória, mas o Message
campo sim. Reutilize Utf32StringMarshaller
novamente para limpar a corda empacotada.
public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);
Vamos considerar brevemente o cenário "fora". Considere o caso em que uma ou várias instâncias de error_data
são retornadas.
extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)
extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);
[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);
Um P/Invoke que retorna um único tipo de instância, non-collection, é categorizado como .MarshalMode.ManagedToUnmanagedOut
Normalmente, você usa uma coleção para retornar vários elementos e, nesse caso, um Array
é usado. O marshaller para um cenário de coleta, correspondente ao MarshalMode.ElementOut
modo, retornará vários elementos e será descrito posteriormente.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class Out
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
A conversão de ErrorDataUnmanaged
para ErrorData
é o inverso do que você fez para o modo "em". Lembre-se de que você também precisa limpar todas as alocações que o ambiente não gerenciado esperava que você executasse. Também é importante notar que as funções aqui estão marcadas static
e, portanto, são "apátridas", ser apátrida é um requisito para todos os modos "Elemento". Você também notará que há um ConvertToUnmanaged
método como no modo "in". Todos os modos "Elemento" requerem manipulação para os modos "in" e "out".
Para o marshaller "out" não gerenciado, você vai fazer algo especial. O nome do tipo de dados que você está organizando é chamado error_data
e o .NET normalmente expressa erros como exceções. Alguns erros são mais impactantes do que outros e os erros identificados como "fatais" geralmente indicam um erro catastrófico ou irrecuperável. Observe que o error_data
tem um campo para verificar se o erro é fatal. Você organizará um error_data
código gerenciado e, se for fatal, lançará uma exceção em vez de apenas convertê-lo em um ErrorData
e devolvê-lo.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class ThrowOnFatalErrorOut
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
Um parâmetro "out" converte de um contexto não gerenciado em um contexto gerenciado, para que você implemente o ConvertToManaged
método. Quando o destinatário não gerenciado retorna e fornece um ErrorDataUnmanaged
objeto, você pode inspecioná-lo usando seu ElementOut
marshaller de modo e verificar se ele está marcado como um erro fatal. Se sim, essa é a sua indicação para jogar em vez de apenas devolver o ErrorData
.
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);
return data;
}
Talvez você não só vá consumir a biblioteca nativa, mas também queira compartilhar seu trabalho com a comunidade e fornecer uma biblioteca de interoperabilidade. Você pode fornecer ErrorData
um marshaller implícito sempre que ele for usado em um P/Invoke adicionando [NativeMarshalling(typeof(ErrorDataMarshaller))]
à ErrorData
definição. Agora, qualquer pessoa que use sua definição desse tipo em uma LibraryImport
chamada terá o benefício de seus marshallers. Eles sempre podem substituir seus marshallers usando MarshalUsing
no local de uso.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }