Partilhar via


Tutorial: Usar marshallers personalizados em P/Invokes gerados a partir do 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, 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, .soou 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 { ... }

Ver também