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