自定义封送的源生成

.NET 7 引入了一种新的机制,用于在使用源生成的互操作时自定义如何封送类型。 P/Invoke 的源生成器MarshalUsingAttributeNativeMarshallingAttribute 识别为类型的自定义封送的指示器。

NativeMarshallingAttribute 可应用于类型,以指示该类型的默认自定义封送。 MarshalUsingAttribute 可应用于参数或返回值,以指示该类型的特定用法的自定义封送,优先于可能在该类型本身上的任何 NativeMarshallingAttribute。 这两个属性都需要使用一个或多个 CustomMarshallerAttribute 属性标记的 Type(入口点封送处理程序类型)。 每个 CustomMarshallerAttribute 指示应使用哪个封送处理程序实现来封送指定 MarshalMode 的指定托管类型。

封送处理程序实现

封送处理程序实现可以是无状态的,也可以是有状态的。 如果封送处理程序类型为 static 类,则被视为无状态。 如果是值类型,则被视为有状态,并且该封送处理程序的一个实例将用于封送特定参数或返回值。 根据封送处理程序是无状态的还是有状态的,以及它是否支持从托管到非托管、非托管到托管或两者同时进行封送,预计会有不同的封送处理程序实现形状。 .NET SDK 包括分析器和代码修复程序,以帮助实现符合所需形状的封送处理程序。

MarshalMode

CustomMarshallerAttribute 中指定的 MarshalMode 确定了封送处理程序实现的预期封送支持和形状。 所有模式都支持无状态封送处理程序实现。 元素封送模式不支持有状态封送处理程序实现。

MarshalMode 预期支持 可以是有状态的
ManagedToUnmanagedIn 托管到非托管
ManagedToUnmanagedRef 托管到非托管和非托管到托管
ManagedToUnmanagedOut 非托管到托管
UnmanagedToManagedIn 非托管到托管
UnmanagedToManagedRef 托管到非托管和非托管到托管
UnmanagedToManagedOut 托管到非托管
ElementIn 托管到非托管
ElementRef 托管到非托管和非托管到托管
ElementOut 非托管到托管

MarshalMode.Default 指示封送处理程序实现应用于它支持的任何模式。 如果还指定了更具体的 MarshalMode 的封送处理程序实现,则优先于 MarshalMode.Default

基本用法

我们可以在类型上指定 NativeMarshallingAttribute,指向入口点封送处理程序类型,即 static 类或 struct

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

ExampleMarshaller 是入口点封送处理程序类型,使用 CustomMarshallerAttribute 标记,指向封送处理程序实现类型。 在此示例中,ExampleMarshaller 是入口点和实现。 它符合值的自定义封送所需的封送处理程序形状

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

示例中的 ExampleMarshaller 是一个无状态封送处理程序,可实现对从托管到非托管以及从非托管到托管的封送的支持。 封送逻辑完全由封送处理程序实现控制。 使用 MarshalAsAttribute 标记结构上的字段对生成的代码没有影响。

然后可以在 P/Invoke 源生成中使用 Example 类型。 在以下 P/Invoke 示例中,ExampleMarshaller 将用于将参数从托管封送到非托管。 它还将用于将返回值从非托管封送到托管。

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

若要将不同的封送处理程序用于 Example 类型的特定用法,请在使用站点指定 MarshalUsingAttribute。 在以下 P/Invoke 示例中,ExampleMarshaller 将用于将参数从托管封送到非托管。 OtherExampleMarshaller 将用于将返回值从非托管封送到托管。

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

集合

ContiguousCollectionMarshallerAttribute 应用于封送处理程序入口点类型以指示它用于连续集合。 该类型必须比关联的托管类型多一个类型参数。 最后一个类型参数是一个占位符,将由源生成器使用集合元素类型的非托管类型填充。

例如,可以为 List<T> 指定自定义封送。 在以下代码中,ListMarshaller 是入口点和实现。 它符合集合的自定义封送所需的封送处理程序形状

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

示例中的 ListMarshaller 是一个无状态集合封送处理程序,可实现对 List<T> 从托管到非托管以及从非托管到托管的封送的支持。 在以下 P/Invoke 示例中,ListMarshaller 将用于将参数从托管封送到非托管,并将返回值从非托管封送到托管。 CountElementName 指示在将返回值从非托管封送到托管时,应使用 numValues 参数作为元素计数。

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

另请参阅