Source generation for custom marshalling

.NET 7 introduces a new mechanism for customization of how a type is marshalled when using source-generated interop. The source generator for P/Invokes recognizes MarshalUsingAttribute and NativeMarshallingAttribute as indicators for custom marshalling of a type.

NativeMarshallingAttribute can be applied to a type to indicate the default custom marshalling for that type. The MarshalUsingAttribute can be applied to a parameter or return value to indicate the custom marshalling for that particular usage of the type, taking precedence over any NativeMarshallingAttribute that may be on the type itself. Both of these attributes expect a Type—the entry-point marshaller type—that's marked with one or more CustomMarshallerAttribute attributes. Each CustomMarshallerAttribute indicates which marshaller implementation should be used to marshal the specified managed type for the specified MarshalMode.

Marshaller implementation

Marshaller implementations can either be stateless or stateful. If the marshaller type is a static class, it's considered stateless. If it's a value type, it's considered stateful and one instance of that marshaller will be used to marshal a specific parameter or return value. Different shapes for the marshaller implementation are expected based on whether a marshaller is stateless or stateful and whether it supports marshalling from managed to unmanaged, unmanaged to managed, or both. The .NET SDK includes analyzers and code fixers to help with implementing marshallers that conform to the require shapes.

MarshalMode

The MarshalMode specified in a CustomMarshallerAttribute determines the expected marshalling support and shape for the marshaller implementation. All modes support stateless marshaller implementations. Element marshalling modes do not support stateful marshaller implementations.

MarshalMode Expected support Can be stateful
ManagedToUnmanagedIn Managed to unmanaged Yes
ManagedToUnmanagedRef Managed to unmanaged and unmanaged to managed Yes
ManagedToUnmanagedOut Unmanaged to managed Yes
UnmanagedToManagedIn Unmanaged to managed Yes
UnmanagedToManagedRef Managed to unmanaged and unmanaged to managed Yes
UnmanagedToManagedOut Managed to unmanaged Yes
ElementIn Managed to unmanaged No
ElementRef Managed to unmanaged and unmanaged to managed No
ElementOut Unmanaged to managed No

MarshalMode.Default indicates that the marshaller implementation should be used for any mode that it supports. If a marshaller implementation for a more specific MarshalMode is also specified, it takes precedence over MarshalMode.Default.

Basic usage

We can specify NativeMarshallingAttribute on a type, pointing at an entry-point marshaller type that is either a static class or a struct.

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

ExampleMarshaller, the entry-point marshaller type, is marked with CustomMarshallerAttribute, pointing at a marshaller implementation type. In this example, ExampleMarshaller is both the entry point and the implementation. It conforms to marshaller shapes expected for custom marshalling of a value.

[CustomMarshaller(typeof(Example), MarshalMode.Default, typeof(ExampleMarshaller))]
internal static class ExampleMarshaller
{
    public static ExampleUnmanaged ConvertToUnmanaged(Example managed)
        => throw new NotImplementedException();

    public static Example ConvertToManaged(ExampleUnmanaged unmanaged)
        => throw new NotImplementedException();

    public static void Free(ExampleUnmanaged unmanaged)
        => throw new NotImplementedException();

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

The ExampleMarshaller in the example is a stateless marshaller that implements support for marshalling from managed to unmanaged and from unmanaged to managed. The marshalling logic is entirely controlled by your marshaller implementation. Marking fields on a struct with MarshalAsAttribute has no effect on the generated code.

The Example type can then be used in P/Invoke source generation. In the following P/Invoke example, ExampleMarshaller will be used to marshal the parameter from managed to unmanaged. It will also be used to marshal the return value from unmanaged to managed.

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

To use a different marshaller for a specific usage of the Example type, specify MarshalUsingAttribute at the use site. In the following P/Invoke example, ExampleMarshaller will be used to marshal the parameter from managed to unmanaged. OtherExampleMarshaller will be used to marshal the return value from unmanaged to managed.

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

Collections

Apply the ContiguousCollectionMarshallerAttribute to a marshaller entry-point type to indicate that it's for contiguous collections. The type must have one more type parameter than the associated managed type. The last type parameter is a placeholder and will be filled in by the source generator with the unmanaged type for the collection's element type.

For example, you can specify custom marshalling for a List<T>. In the following code, ListMarshaller is both the entry point and the implementation. It conforms to marshaller shapes expected for custom marshalling of a collection.

[ContiguousCollectionMarshaller]
[CustomMarshaller(typeof(List<>), MarshalMode.Default, typeof(ListMarshaller<,>))]
public unsafe static class ListMarshaller<T, TUnmanagedElement> where TUnmanagedElement : unmanaged
{
    public static byte* AllocateContainerForUnmanagedElements(List<T> managed, out int numElements)
        => throw new NotImplementedException();

    public static ReadOnlySpan<T> GetManagedValuesSource(List<T> managed)
        => throw new NotImplementedException();

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

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

    public static Span<T> GetManagedValuesDestination(List<T> managed)
        => throw new NotImplementedException();

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

    public static void Free(byte* unmanaged)
        => throw new NotImplementedException();
}

The ListMarshaller in the example is a stateless collection marshaller that implements support for marshalling from managed to unmanaged and from unmanaged to managed for a List<T>. In the following P/Invoke example, ListMarshaller will be used to marshal the parameter from managed to unmanaged and to marshal the return value from unmanaged to managed. CountElementName indicates that the numValues parameter should be used as the element count when marshalling the return value from unmanaged to managed.

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

See also