.NET 7 引入了一种新的机制,用于在使用源生成的互操作时自定义如何封送类型。 P/Invoke 的源生成器将 MarshalUsingAttribute 和 NativeMarshallingAttribute 识别为类型的自定义封送的指示器。
NativeMarshallingAttribute 可以应用于一种类型,来指示该类型的默认自定义封送处理。 MarshalUsingAttribute 可应用于参数或返回值,以指示该类型的特定用法的自定义封送,优先于可能在该类型本身上的任何 NativeMarshallingAttribute。 这两个属性都需要使用一个或多个 CustomMarshallerAttribute 属性标记的 Type(入口点封送处理程序类型)。 每个 CustomMarshallerAttribute 指示应使用哪个封送处理程序实现来封送指定 MarshalMode 的指定托管类型。
封送处理程序实现
自定义封送器实现可以是无状态实现,也可以是有状态的。 如果封送器类型是类 static
,则被视为无状态,并且实现方法不应跨调用跟踪状态。 如果它是值类型,则被视为有状态,并且该封送器的一个实例将用于封送特定参数或返回值。 使用唯一实例可以在整个编组和解编过程中保留状态。
封送程序形状
封送生成器期望来自自定义封送器类型的方法集称为 封送器形状。 为了支持 .NET Standard 2.0 中的无状态、静态自定义封送器类型(不支持静态接口方法),并改进性能,接口类型不用于定义和实现封送器形状。 相反,形状记录在 自定义封送器图形 文章中。 预期的方法(或形状)取决于封送程序是无状态还是有状态的,以及它是否支持从托管到非托管、从非托管到托管的封送,或者两者都支持(用 CustomMarshallerAttribute.MarshalMode
声明)。 .NET SDK 包括分析器和代码修复程序,可帮助实现符合所需形状的封送器。
MarshalMode
CustomMarshallerAttribute 中指定的 MarshalMode 确定了封送处理程序实现的预期封送支持和形状。 所有模式都支持无状态封送处理程序实现。 元素封送模式不支持有状态的封送器实现。
MarshalMode |
预期支持 | 可以是有状态的 |
---|---|---|
ManagedToUnmanagedIn | 托管到非托管 | 是的 |
ManagedToUnmanagedRef | 托管到非托管和非托管到托管 | 是的 |
ManagedToUnmanagedOut | 非托管到托管 | 是的 |
UnmanagedToManagedIn | 非托管到托管 | 是的 |
UnmanagedToManagedRef | 托管到非托管和非托管到托管 | 是的 |
UnmanagedToManagedOut | 托管到非托管 | 是的 |
ElementIn | 托管到非托管 | 否 |
ElementRef | 托管到非托管和非托管到托管 | 否 |
ElementOut | 非托管到托管 | 否 |
使用 MarshalMode.Default 来指示封送器的实现适用于所有支持的模式,具体取决于它实现的方法。 如果为更具体的MarshalMode
指定了封送器,该封送器将优先于标记为Default
的封送器。
基本用法
封送单个值
若要为类型创建自定义封送器,需要定义实现所需封送方法的入口点封送器类型。 入口点封送器类型可以是 static
类或 struct
类,并且必须用 CustomMarshallerAttribute 标记。
例如,考虑一个要在托管代码和非托管代码之间封送的简单类型:
public struct Example
{
public string Message;
public int Flags;
}
定义封送程序类型
可以创建一个名为ExampleMarshaller
的类型,并使用CustomMarshallerAttribute标记它,指示它是为Example
类型提供自定义封送信息的入口点封送器类型。 CustomMarshallerAttribute
的第一个参数是封送程序所针对的托管类型。 第二个 MarshalMode
参数是封送器支持的参数。 第三个参数是封送器类型本身,即实现预期形状中方法的类型。
[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;
}
}
此处显示的 ExampleMarshaller
实现了从托管 Example
类型到本机代码期望的格式 (ExampleUnmanaged
) 的 blittable 表示的无状态封送,然后返回。 Free
方法用于释放封送过程中分配的任何非托管资源。 封送逻辑完全由封送器的实现控制。 使用MarshalAsAttribute标记结构体中的字段对生成的代码没有影响。
ExampleMarshaller
此处是入口点类型和实现类型。 但是如有必要,可以通过为每种模式创建单独的封送程序类型,自定义不同模式的封送。 为每种模式添加一个新的CustomMarshallerAttribute
,如以下类所示。 通常,这仅适用于有状态封送器,其中封送器类型是 struct
,能够在调用之间保持状态。 按照约定,实现类型嵌套在入口点封送器类型内。
[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();
}
}
指定要使用的封送程序
创建封送器类型后,可以在互操作方法签名中使用 MarshalUsingAttribute 来指示要将该封送器用于特定的参数或返回值。 MarshalUsingAttribute 将入口点封送程序类型作为参数,在本例中为 ExampleMarshaller
。
[LibraryImport("nativelib")]
[return: MarshalUsing(typeof(ExampleMarshaller))]
internal static partial Example ConvertExample(
[MarshalUsing(typeof(ExampleMarshaller))] Example example);
为了避免每次使用 Example
类型时都要指定封送器类型,您还可以将 NativeMarshallingAttribute 应用于 Example
类型本身。 这表示在互操作源生成中,默认情况下应将指定的封送程序用于 Example
类型的所有用法。
[NativeMarshalling(typeof(ExampleMarshaller))]
public struct Example
{
public string Message;
public int Flags;
}
然后,可以在源生成的 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 属性。 此属性指示封送器用于连续集合(如数组或列表),并提供封送器必须实现的一组方法来支持对集合的元素进行封送。 集合封送的元素类型还必须使用前面所述的方法为其定义封送器。
将 ContiguousCollectionMarshallerAttribute 应用于封送处理程序入口点类型以指示它用于连续集合。 封送程序入口点类型必须比关联的托管类型多一个类型参数。 最后一个类型参数是一个占位符,将由源生成器使用集合元素类型的非托管类型填充。
例如,可以为 List<T> 指定自定义封送。 在以下代码中, ListMarshaller
是入口点和实现。 它符合集合的自定义封送所需的封送程序形状之一。 (请注意,这是一个不完整的示例。
[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);
}
}
示例中的 ListMarshaller
是一个无状态集合封送处理程序,可实现对 List<T> 从托管到非托管以及从非托管到托管的封送的支持。 在以下 P/Invoke 示例中,ListMarshaller
将用于封送参数的集合容器从托管到非托管,同时也用于封送返回值的集合容器从非托管到托管。 源生成器将生成代码,将元素从参数 list
复制到封送器提供的容器。 由于 int
是 blittable,因此元素本身不需要封送处理。 CountElementName 指示在将返回值从非托管封送到托管时,应使用 numValues
参数作为元素计数。
[LibraryImport("nativelib")]
[return: MarshalUsing(typeof(ListMarshaller<,>), CountElementName = nameof(numValues))]
internal static partial List<int> ConvertList(
[MarshalUsing(typeof(ListMarshaller<,>))] List<int> list,
out int numValues);
当集合的元素类型为自定义类型时,可以使用附加的 MarshalUsingAttribute 与 ElementIndirectionDepth = 1
指定该元素的封送器。
ListMarshaller
将处理集合容器,而 ExampleMarshaller
将每个元素从非托管转换为托管,反之亦然。 ElementIndirectionDepth
指示封送程序应应用于集合中的元素,这些元素比集合本身深一层。
[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);