Edit

Share via


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

Custom marshaller implementations can either be stateless or stateful. If the marshaller type is a static class, it's considered stateless, and the implementation methods shouldn't track state across calls. 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. Using a unique instance allows for state to be preserved across the marshalling and unmarshalling process.

Marshaller shapes

The set of methods that the marshalling generator expects from a custom marshaller type is referred to as the marshaller shape. To support stateless, static custom marshaller types in .NET Standard 2.0 (which doesn't support static interface methods), and improve performance, interface types are not used to define and implement the marshaller shapes. Instead, the shapes are documented in the Custom marshaller shapes article. The expected methods (or shape) depends on whether the marshaller is stateless or stateful, and whether it supports marshalling from managed to unmanaged, unmanaged to managed, or both (declared with CustomMarshallerAttribute.MarshalMode). The .NET SDK includes analyzers and code fixers to help with implementing marshallers that conform to the required 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

Use MarshalMode.Default to indicate that the marshaller implementation applies to any supported mode, based on the methods it implements. If you specify a marshaller for a more specific MarshalMode, that marshaller takes precedence over one marked as Default.

Basic usage

Marshalling a single value

To create a custom marshaller for a type, you need to define an entry-point marshaller type that implements the required marshalling methods. The entry-point marshaller type can be a static class or a struct, and it must be marked with CustomMarshallerAttribute.

For example, consider a simple type that you want to marshal between managed and unmanaged code:

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

Define the marshaller type

You can create a type called ExampleMarshaller that's marked with CustomMarshallerAttribute to indicate that it's the entry-point marshaller type that provides custom marshalling information for the Example type. The first argument of the CustomMarshallerAttribute is the managed type that the marshaller targets. The second argument is the MarshalMode that the marshaller supports. The third argument is the marshaller type itself, that is, the type that implements the methods in the expected shape.

[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;
    }
}

The ExampleMarshaller shown here implements stateless marshalling from the managed Example type to a blittable representation in the format that the native code expects (ExampleUnmanaged) and back. The Free method is used to release any unmanaged resources allocated during the marshalling process. The marshalling logic is entirely controlled by the marshaller implementation. Marking fields on a struct with MarshalAsAttribute has no effect on the generated code.

Here, ExampleMarshaller is both the entry-point type and the implementation type. However, if necessary, you can customize the marshalling for different modes by creating separate marshaller types for each mode. Add a new CustomMarshallerAttribute for each mode like in the following class. Typically, this is only necessary for stateful marshallers, where the marshaller type is a struct that maintains state across calls. By convention, the implementation types are nested inside the entry-point marshaller type.

[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();
    }
}

Declare which marshaller to use

Once you've created the marshaller type, you can use the MarshalUsingAttribute on the interop method signature to indicate that you want to use this marshaller for a specific parameter or return value. The MarshalUsingAttribute takes the entry-point marshaller type as an argument, in this case ExampleMarshaller.

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

To avoid having to specify the marshaller type for every usage of the Example type, you can also apply the NativeMarshallingAttribute to the Example type itself. This indicates that the specified marshaller should be used by default for all usages of the Example type in interop source generation.

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

The Example type can then be used in source-generated P/Invoke methods without specifying the marshaller type. 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 parameter or return value 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);

Marshalling collections

Non-generic collections

For collections that aren't generic over the type of the element, you should create a simple marshaller type like shown previously.

Generic collections

To create a custom marshaller for a generic collection type, you can use the ContiguousCollectionMarshallerAttribute attribute. This attribute indicates that the marshaller is for contiguous collections, such as arrays or lists, and it provides a set of methods that the marshaller must implement to support marshalling of the collection's elements. The element type of the collection marshalled must also have a marshaller defined for it using the methods described previously.

Apply the ContiguousCollectionMarshallerAttribute to a marshaller entry-point type to indicate that it's for contiguous collections. The marshaller entry-point 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 one of the marshaller shapes expected for custom marshalling of a collection. (Note that it's an incomplete example.)

[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);
    }
}

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 collection container for the parameter from managed to unmanaged and to marshal the collection container for the return value from unmanaged to managed. The source generator will generate code to copy the elements from the parameter list to the container provided by the marshaller. Since int is blittable, the elements themselves do not need to be marshalled. 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 = nameof(numValues))]
internal static partial List<int> ConvertList(
    [MarshalUsing(typeof(ListMarshaller<,>))] List<int> list,
    out int numValues);

When the element type of the collection is a custom type, you can specify the element marshaller for that using an additional MarshalUsingAttribute with ElementIndirectionDepth = 1. The ListMarshaller will handle the collection container and ExampleMarshaller will marshal each element from unmanaged to managed and vice versa. The ElementIndirectionDepth indicates that the marshaller should be applied to the elements of the collection, which are one level deeper than the collection itself.

[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);

See also