Partilhar via


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 DllImporto , 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, .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 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 stringde "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 { ... }

Consulte também