Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
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
, 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 de código nos locais de uso e usado para determinar o tipo de marcador para a marcação da variável com atributos.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 usado para indicar qual marshaller deve ser usado para o tipo atribuído. Isso é útil para autores de bibliotecas que fornecem tipos e marshallers associados para esses tipos.
Esses atributos, no entanto, não são os únicos mecanismos disponíveis ao autor de um marshaller personalizado. O gerador de código 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 corretor do gerador de código fonte
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 sequência de caracteres codificada em UTF-32, uma codificação de sequência de caracteres que o .NET historicamente não processava.
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 de string
"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 marçalizadores embutidos para o string
tipo que efetuam o marshaling como UTF-8, UTF-16, ANSI, e até mesmo como o tipo Windows BSTR
. No entanto, não existe 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 tipo string
, que é o tipo gerido a ser convertido, o segundo é o modo, que indica quando usar o conversor, e o terceiro tipo é Utf32StringMarshaller
, o tipo a ser usado para a conversão. 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 do sistema permitem otimizações e empacotamento personalizado para um modo específico. 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 recebe um Span
de um unmanaged
tipo. A propriedade BufferSize
indica a quantidade de espaço de pilha — o comprimento do Span
a ser passado para FromManaged
— que o marshaller gostaria de obter durante a chamada de marshalling.
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 marshallizador sem estado ou com estado deve ser usado. Isso é tratado pelo implementador ao especificar o(s) atributo(s) MarshalMode
no CustomMarshaller
do marshaller. O gerador de código-fonte selecionará o marshaller mais adequado com base no contexto em que o MarshalUsing
é aplicado, com o MarshalMode.Default
como 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
O empacotamento de um tipo definido pelo utilizador requer definir não apenas a lógica de empacotamento, mas também o tipo em C# para empacotar 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 por definir um marshaller para a direção de entrada. 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 o seu marshaller UTF-32 para converter o campo string
em um uint*
.
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, não-coleção, é 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 fizeste para o modo "in". 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 "sem estado", sendo sem estado um requisito para todos os modos "Elemento". Também irá perceber que existe um método ConvertToUnmanaged
semelhante ao 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ê integrará um error_data
em 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 callee não gerenciado retorna e fornece um objeto ErrorDataUnmanaged
, você pode inspecioná-lo usando o seu estilo de marshaller ElementOut
e verificar se está marcado como um erro fatal. Se for esse o caso, esse é o seu sinal para lançar 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 um marshaller implícito a ErrorData
sempre que usado em um P/Invoke adicionando [NativeMarshalling(typeof(ErrorDataMarshaller))]
à definição de ErrorData
. 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 os seus marshallers usando MarshalUsing
no ponto de utilização.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }