Compartilhar via


Geração de origem para marshalling personalizado

O .NET 7 apresenta um novo mecanismo para personalização de como o marshalling de um tipo é feito ao usar a interoperabilidade gerada pela origem. O gerador de origem 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 retornado para indicar o marshalling personalizado para esse uso específico do tipo, tendo precedência sobre qualquer NativeMarshallingAttribute que possa estar no próprio tipo. Esses dois atributos esperam um Type – o tipo de marshaller do ponto de entrada –, que é marcado com um ou mais atributos CustomMarshallerAttribute. Cada CustomMarshallerAttribute indica qual implementação do marshaller deve ser usada para realizar marshalling do tipo gerenciado especificado para o MarshalMode especificado.

Implementação do Marshaller

As implementações personalizadas do marshaller podem ser sem estado ou com 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 com estado e uma instância desse marshaller será usada para realizar marshalling de um valor retornado ou parâmetro específico. O uso de uma instância única permite a preservação do estado ao longo do processo de marshalling e unmarshalling.

Formas de Marshaller

O conjunto de métodos que o gerador de marshalling espera de um tipo de marshaller personalizado é referido como a estrutura do marshaller. Para oferecer suporte a tipos personalizados de marshaller estáticos e sem estado no .NET Standard 2.0 (que não dá suporte a métodos estáticos de interface) e melhorar o desempenho, tipos de interface não são utilizados para definir e implementar as estruturas do marshaller. Em vez disso, as formas são documentadas no artigo Formas personalizadas de marshaller. Os métodos esperados (ou forma) dependem se o marshaller é sem 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 reparadores de código para ajudar a implementar marshallers que estão em conformidade com as formas 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 dos elementos não dão suporte a implementações de marshaller com estado.

MarshalMode Suporte esperado Pode ser com estado
ManagedToUnmanagedIn Gerenciado para não gerenciado Sim
ManagedToUnmanagedRef Gerenciado para não gerenciado e não gerenciado para gerenciado Sim
ManagedToUnmanagedOut Não gerenciado para gerenciado Sim
UnmanagedToManagedIn Não gerenciado para gerenciado Sim
UnmanagedToManagedRef Gerenciado para não gerenciado e não gerenciado para gerenciado Sim
UnmanagedToManagedOut Gerenciado para não gerenciado Sim
ElementIn Gerenciado para não gerenciado Não
ElementRef Gerenciado para não gerenciado e não gerenciado para gerenciado Não
ElementOut Não gerenciado para gerenciado Não

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

Marshalling de um único valor

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

Por exemplo, considere um tipo simples que você quer transferir entre o código gerenciado e o não gerenciado:

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

Definir o tipo de marshaller

Você pode criar um tipo chamado ExampleMarshaller que está marcado com CustomMarshallerAttribute para indicar que é o tipo marshaller do 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 dá suporte. O terceiro argumento é o tipo de marshaller em si, ou seja, o tipo que implementa os métodos no formato esperado.

[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 marshalling sem estado do tipo gerenciado Example para uma representação blittable no formato que o código nativo espera (ExampleUnmanaged) e vice-versa. O método Free é usado para liberar quaisquer recursos não gerenciados alocados durante o processo de empacotamento. 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, onde o tipo de marshaller é um struct que preserva o estado entre as chamadas. Por convenção, os tipos de implementação são aninhados dentro do tipo de marshaller do 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 será usado

Depois de criar o tipo marshaller, você pode usar o MarshalUsingAttribute na assinatura do método de interoperabilidade para indicar que deseja utilizar esse marshaller para um parâmetro ou valor de retorno específico. O MarshalUsingAttribute aceita 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 de marcador para cada uso do tipo Example, você também pode aplicar o NativeMarshallingAttribute ao tipo Example em si. 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 tipo Example pode ser usado em métodos P/Invoke gerados pela origem sem especificar o tipo de marshaller. No exemplo de P/Invoke a seguir, ExampleMarshaller será usado para realizar marshaling do parâmetro de gerenciado para não gerenciado. Ele também será usado para realizar marshaling do valor retornado de não gerenciado para 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 local de uso. No exemplo de P/Invoke a seguir, ExampleMarshaller será usado para realizar marshaling do parâmetro de gerenciado para não gerenciado. OtherExampleMarshaller será usado para realizar marshaling do valor retornado de não gerenciado para gerenciado.

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

Marshalling de coleções

Coleções não genéricas

Para coleções que não são genéricas quanto ao tipo do elemento, você deve criar um tipo simples de marshaller, 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 serve 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 com marshalling também deve ter um marshaller definido para ele usando os métodos descritos anteriormente.

Aplique o ContiguousCollectionMarshallerAttribute a um tipo de ponto de entrada de marshaller para indicar que ele é para coleções contíguas. O tipo de ponto de entrada de 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 de 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á utilizado para realizar o marshaling do contêiner de coleção do parâmetro, de gerenciado para não gerenciado, e do contêiner de coleção do valor de retorno, 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, não é necessário empacotar os elementos. CountElementName indica que o parâmetro numValues deve ser usado como a contagem de elementos ao realizar marshaling do 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 é personalizado, você pode especificar o marshaller de elemento para isso usando mais um MarshalUsingAttribute com ElementIndirectionDepth = 1. O ListMarshaller manipulará o contêiner de coleção e ExampleMarshaller fará o marshaling de cada elemento de não gerenciado para gerenciado e vice-versa. O ElementIndirectionDepth indica que o marshaller deve ser aplicado aos elementos da coleção, que estão um nível abaixo da 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