Compartilhar via


Geração de código-fonte para marshalling personalizado

O .NET 7 introduz um novo mecanismo para a personalização de como um tipo é manipulado ao usar a interoperabilidade gerada a partir do código-fonte. O gerador de código fonte para P/Invokes reconhece MarshalUsingAttribute e NativeMarshallingAttribute como indicadores para o marshalling personalizado de um tipo.

NativeMarshallingAttribute pode ser aplicado a um tipo para indicar o marshalling personalizado padrão para esse tipo. O MarshalUsingAttribute pode ser aplicado a um parâmetro ou valor de retorno para indicar o marshalling personalizado para aquele uso específico do tipo, tendo precedência sobre qualquer NativeMarshallingAttribute que possa estar no próprio tipo. Ambos os atributos esperam um Typetipo de marshaller de ponto de entrada marcado com um ou mais CustomMarshallerAttribute atributos. Cada CustomMarshallerAttribute indica qual implementação de marshaller deve ser usada para converter o tipo gerenciado especificado para o MarshalMode especificado.

Implementação do Marshaller

As implementações personalizadas de marshaller podem ser com estado ou sem estado. Se o tipo marshaller for uma static classe, ele será considerado sem estado e os métodos de implementação não deverão acompanhar o estado entre chamadas. Se for um tipo de valor, ele será considerado como possuindo estado, e uma instância desse marshaller será usada para realizar marshaling de um parâmetro específico ou valor de retorno. O uso de uma instância exclusiva permite que o estado seja preservado no processo de serialização e desserialização.

Formas Marshaller

O conjunto de métodos que o gerador de marshalling espera de um tipo marshaller personalizado é conhecido como a forma marshaller. Para dar suporte a tipos de marshaller personalizados estáticos e sem estado no .NET Standard 2.0 (que não dá suporte a métodos de interface estática) e melhorar o desempenho, os tipos de interface não são usados para definir e implementar as formas marshaller. Em vez disso, as estruturas são documentadas no artigo Shapes Personalizadas do Marshaller. Os métodos esperados (ou forma) dependem se o marshaller é sem estado ou com estado, e se ele dá suporte ao marshalling de gerenciado para não gerenciado, não gerenciado para gerenciado ou ambos (declarados com CustomMarshallerAttribute.MarshalMode). O SDK do .NET inclui analisadores e corretores de código para ajudar na implementação de marshallers que estão em conformidade com as estruturas necessárias.

MarshalMode

O MarshalMode especificado em um CustomMarshallerAttribute determina o suporte de marshalling esperado e a forma para a implementação do marshaller. Todos os modos dão suporte a implementações de marshaller sem estado. Os modos de marshalling de elementos não dão suporte a implementações de marshaller com estado.

MarshalMode Suporte esperado Aplica-se a Pode ser com estado
ManagedToUnmanagedIn Gerenciado para não gerenciado Parâmetros por valor e in em P/Invoke Sim
ManagedToUnmanagedRef Gerenciado para não gerenciado e não gerenciado para gerenciado ref parâmetros em P/Invoke Sim
ManagedToUnmanagedOut Não gerenciado para gerenciado out parâmetros e valores retornados em P/Invoke Sim
UnmanagedToManagedIn Não gerenciado para gerenciado Por valor e in parâmetros em Inverso P/Invoke Sim
UnmanagedToManagedRef Gerenciado para não gerenciado e não gerenciado para gerenciado ref parâmetros em P/Invoke inverso Sim
UnmanagedToManagedOut Gerenciado para não gerenciado out parâmetros e valores retornados em Reverse P/Invoke Sim
ElementIn Gerenciado para não gerenciado Elementos de coleções passadas com in ou por valor Não
ElementRef Gerenciado para não gerenciado e não gerenciado para gerenciado Elementos de coleções passadas com ref Não
ElementOut Não gerenciado para gerenciado Elementos de coleções passadas com out Não

Observação

Os nomes de membro seguem o padrão {CallDirection}{DataFlow}:

  • A direção da chamada (ManagedToUnmanaged ou UnmanagedToManaged) indica qual lado inicia a chamada. ManagedToUnmanaged refere-se a código gerenciado chamando código não gerenciado (P/Invoke). UnmanagedToManaged significa código não gerenciado que chama código gerenciado (Reverse P/Invoke, COM).
  • O fluxo de dados (Inou OutRef) indica como os dados se movem em relação à chamada. In significa que os dados fluem do chamador para o chamado. Out significa que os dados fluem do chamado de volta para o chamador, incluindo os parâmetros out e os valores de retorno. Ref significa fluxos de dados em ambas as direções.

Por exemplo, ManagedToUnmanagedOut são usados para out parâmetros e valores de retorno quando o código gerenciado chama o código não gerenciado. Embora o nome comece com "ManagedToUnmanaged", o marshaller para esse modo converte da forma não gerenciada para a forma gerenciada, porque os dados retornam ao chamador gerenciado.

Use MarshalMode.Default para indicar que a implementação do marshaller se aplica a qualquer modo com suporte, com base nos métodos que ele implementa. Se você especificar um marshaller para um mais específico MarshalMode, esse marshaller terá precedência sobre um marcado como Default.

Uso Básico

Organizando um único valor

Para criar um marshaller personalizado para um tipo, você precisa definir um tipo de marshaller de ponto de entrada que implemente os métodos de marshalling necessários. O tipo marshaller de ponto de entrada pode ser uma static classe ou um struct, e deve ser marcado com CustomMarshallerAttribute.

Por exemplo, considere um tipo simples que você deseja realizar marshalização entre código gerenciado e não gerenciado.

public struct Example
{
    public string Message;
    public int Flags;
}

Definir o tipo marshaller

Você pode criar um tipo chamado ExampleMarshaller que está marcado como CustomMarshallerAttribute para indicar que é o tipo de marshaller de ponto de entrada que fornece informações de marshalling personalizadas para o tipo Example. O primeiro argumento do CustomMarshallerAttribute é o tipo gerenciado que o marshaller tem como destino. O segundo argumento é o MarshalMode que o marshaller suporta. O terceiro argumento é o próprio tipo marshaller, ou seja, o tipo que implementa os métodos na forma esperada.

[CustomMarshaller(typeof(Example), MarshalMode.Default, typeof(ExampleMarshaller))]
internal static unsafe class ExampleMarshaller
{
    public static ExampleUnmanaged ConvertToUnmanaged(Example managed)
    {
        return new ExampleUnmanaged()
        {
            Message = (IntPtr)Utf8StringMarshaller.ConvertToUnmanaged(managed.Message),
            Flags = managed.Flags
        };
    }

    public static Example ConvertToManaged(ExampleUnmanaged unmanaged)
    {
        return new Example()
        {
            Message = Utf8StringMarshaller.ConvertToManaged((byte*)unmanaged.Message),
            Flags = unmanaged.Flags
        };
    }

    public static void Free(ExampleUnmanaged unmanaged)
    {
        Utf8StringMarshaller.Free((byte*)unmanaged.Message);
    }

    internal struct ExampleUnmanaged
    {
        public IntPtr Message;
        public int Flags;
    }
}

O ExampleMarshaller mostrado aqui implementa a transmissão sem estado do tipo gerenciado Example para uma representação blittable no formato que o código nativo espera (ExampleUnmanaged) e novamente de volta para o tipo original. O Free método é usado para liberar todos os recursos não gerenciados alocados durante o processo de marshalling. A lógica de marshalling é totalmente controlada pela implementação do marshaller. Marcar campos em uma struct com MarshalAsAttribute não tem efeito no código gerado.

Aqui, ExampleMarshaller é tanto o tipo de ponto de entrada quanto o tipo de implementação. No entanto, se necessário, você pode personalizar o marshalling para diferentes modos criando tipos de marshaller separados para cada modo. Adicione um novo CustomMarshallerAttribute para cada modo, como na classe a seguir. Normalmente, isso só é necessário para marshallers com estado interno, em que o tipo marshaller é um struct que mantém o estado entre chamadas. Por convenção, os tipos de implementação são aninhados dentro do tipo marshaller de ponto de entrada.

[CustomMarshaller(typeof(Example), MarshalMode.ManagedToUnmanagedIn, typeof(ExampleMarshaller.ManagedToUnmanagedIn))]
[CustomMarshaller(typeof(Example), MarshalMode.ManagedToUnmanagedOut, typeof(ExampleMarshaller.UnmanagedToManagedOut))]
internal static class ExampleMarshaller
{
    internal struct ManagedToUnmanagedIn
    {
        public void FromManaged(TManaged managed) => throw new NotImplementedException();

        public TNative ToUnmanaged() => throw new NotImplementedException();

        public void Free() =>  throw new NotImplementedException()
    }

    internal struct UnmanagedToManagedOut
    {
        public void FromUnmanaged(TNative unmanaged) => throw new NotImplementedException();

        public TManaged ToManaged() => throw new NotImplementedException();

        public void Free() => throw new NotImplementedException();
    }
}

Declarar qual marshaller usar

Depois de criar o tipo marshaller, você pode usar o MarshalUsingAttribute na assinatura do método de interoperabilidade para indicar que deseja usar este marshaller para um parâmetro específico ou valor de retorno. O MarshalUsingAttribute recebe o tipo de marshaller de ponto de entrada como argumento, neste caso ExampleMarshaller.

[LibraryImport("nativelib")]
[return: MarshalUsing(typeof(ExampleMarshaller))]
internal static partial Example ConvertExample(
    [MarshalUsing(typeof(ExampleMarshaller))] Example example);

Para evitar a necessidade de especificar o tipo do marshaller para cada uso do tipo Example, você também pode aplicar o NativeMarshallingAttribute no próprio tipo Example. Isso indica que o marshaller especificado deve ser usado por padrão para todos os usos do tipo Example na geração de código-fonte de interoperabilidade.

[NativeMarshalling(typeof(ExampleMarshaller))]
public struct Example
{
    public string Message;
    public int Flags;
}

Em seguida, o Example tipo pode ser usado em métodos P/Invoke gerados pela origem sem especificar o tipo marshaller. No exemplo P/Invoke a seguir, ExampleMarshaller será usado para realizar o processo de marshaling do parâmetro de gerenciado para não gerenciado. Ele também será usado para realizar marshaling do valor retornado do ambiente não gerenciado para o gerenciado.

[LibraryImport("nativelib")]
internal static partial Example ConvertExample(Example example);

Para usar um marshaller diferente para um parâmetro específico ou valor retornado do tipo Example, especifique MarshalUsingAttribute no ponto de uso. No exemplo P/Invoke a seguir, ExampleMarshaller será usado para realizar o processo de marshaling do parâmetro de gerenciado para não gerenciado. OtherExampleMarshaller será usado para realizar o marshaling do valor retornado de não gerenciado para gerenciado.

[LibraryImport("nativelib")]
[return: MarshalUsing(typeof(OtherExampleMarshaller))]
internal static partial Example ConvertExample(Example example);

**Serialização de coleções**

Coleções não genéricas

Para coleções que não são genéricas sobre o tipo do elemento, você deve criar um tipo de marshaller simples, como mostrado anteriormente.

Coleções genéricas

Para criar um marshaller personalizado para um tipo de coleção genérico, você pode usar o ContiguousCollectionMarshallerAttribute atributo. Esse atributo indica que o marshaller é para coleções contíguas, como matrizes ou listas, e fornece um conjunto de métodos que o marshaller deve implementar para dar suporte ao marshalling dos elementos da coleção. O tipo de elemento da coleção em processo de marshaling também deve ter um Marshaller definido para ele, usando os métodos descritos anteriormente.

Aplique o ContiguousCollectionMarshallerAttribute ao tipo de ponto de entrada de marshaller para indicar que é destinado a coleções contíguas. O tipo de ponto de entrada marshaller deve ter um parâmetro de tipo a mais do que o tipo gerenciado associado. O último parâmetro de tipo é um espaço reservado e será preenchido pelo gerador de origem com o tipo não gerenciado para o tipo de elemento da coleção.

Por exemplo, você pode especificar o marshalling personalizado para um List<T>. No código a seguir, ListMarshaller é tanto o ponto de entrada como a implementação. Ele está em conformidade com uma das formas marshaller esperadas para o marshalling personalizado de uma coleção. (Observe que é um exemplo incompleto.)

[ContiguousCollectionMarshaller]
[CustomMarshaller(typeof(List<>), MarshalMode.Default, typeof(ListMarshaller<,>.DefaultMarshaller))]
public unsafe static class ListMarshaller<T, TUnmanagedElement> where TUnmanagedElement : unmanaged
{
    public static class DefaultMarshaller
    {
        public static byte* AllocateContainerForUnmanagedElements(List<T> managed, out int numElements)
        {
            numElements = managed.Count;
            nuint collectionSizeInBytes = managed.Count * /* size of T */;
            return (byte*)NativeMemory.Alloc(collectionSizeInBytes);
        }

        public static ReadOnlySpan<T> GetManagedValuesSource(List<T> managed)
            => CollectionsMarshal.AsSpan(managed);

        public static Span<TUnmanagedElement> GetUnmanagedValuesDestination(byte* unmanaged, int numElements)
            => new Span<TUnmanagedElement>((TUnmanagedElement*)unmanaged, numElements);

        public static List<T> AllocateContainerForManagedElements(byte* unmanaged, int length)
            => new List<T>(length);

        public static Span<T> GetManagedValuesDestination(List<T> managed)
            => CollectionsMarshal.AsSpan(managed);

        public static ReadOnlySpan<TUnmanagedElement> GetUnmanagedValuesSource(byte* nativeValue, int numElements)
            => new ReadOnlySpan<TUnmanagedElement>((TUnmanagedElement*)nativeValue, numElements);

        public static void Free(byte* unmanaged)
            => NativeMemory.Free(unmanaged);
    }
}

O ListMarshaller no exemplo é um marshaller de coleção sem estado que implementa o suporte para marshalling de gerenciado para não gerenciado e de não gerenciado para gerenciado para um List<T>. No exemplo de P/Invoke a seguir, ListMarshaller será usado para fazer marshaling do contêiner de coleção para o parâmetro de gerenciado para não gerenciado e para fazer marshaling do contêiner de coleção para o valor retornado de não gerenciado para gerenciado. O gerador de origem gerará código para copiar os elementos do parâmetro list para o contêiner fornecido pelo marshaller. Como int é blittable, os próprios elementos não precisam ser empacotados. CountElementName indica que o numValues parâmetro deve ser usado como a contagem de elementos ao agrupar o valor retornado de não gerenciado para gerenciado.

[LibraryImport("nativelib")]
[return: MarshalUsing(typeof(ListMarshaller<,>), CountElementName = nameof(numValues))]
internal static partial List<int> ConvertList(
    [MarshalUsing(typeof(ListMarshaller<,>))] List<int> list,
    out int numValues);

Quando o tipo de elemento da coleção é um tipo personalizado, você pode especificar o marcador de elemento para isso usando um adicional MarshalUsingAttribute com ElementIndirectionDepth = 1. ListMarshaller lidará com o contêiner de coleção e ExampleMarshaller fará o marshaling de cada elemento do ambiente não gerenciado para o ambiente gerenciado e vice-versa. ElementIndirectionDepth indica que o marshaller deve ser aplicado aos elementos da coleção, que estão um nível mais profundo do que a própria coleção.

[LibraryImport("nativelib")]
[MarshalUsing(typeof(ListMarshaller<,>), CountElementName = nameof(numValues))]
[MarshalUsing(typeof(ExampleMarshaller), ElementIndirectionDepth = 1)]
internal static partial void ConvertList(
    [MarshalUsing(typeof(ListMarshaller<,>))]
    [MarshalUsing(typeof(ExampleMarshaller), ElementIndirectionDepth = 1)]
    List<Example> list,
    out int numValues);

Consulte também