.NET LibraryImportGenerator 中 `(ReadOnly)Span<T>` 互操作编组设计解析:从内联支持到 `LinearCollection` 可扩展模型

发布时间:2026/9/18 23:13:07
.NET LibraryImportGenerator 中 `(ReadOnly)Span<T>` 互操作编组设计解析:从内联支持到 `LinearCollection` 可扩展模型 .NET LibraryImportGenerator 中(ReadOnly)SpanT互操作编组设计解析从内联支持到LinearCollection可扩展模型【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime(ReadOnly)SpanT是 .NET 高性能代码的核心类型如何让它在源生成source-generatedP/Invoke 场景中跨越托管/非托管边界是 dotnet/runtime 中 LibraryImportGenerator 实验退出标准的重要一环。本文以 SpanMarshallers.md 为骨架完整梳理该特性从内联内建支持Design 1到基于 Native Type Marshalling 的LinearCollection可扩展模型Design 2的两次设计演进并对照 UserTypeMarshallingV2.md 与当前仓库源码讲清MarshalUsingAttribute的计数参数、GenericPlaceholder泛型占位、集合 marshaller 形状shape等核心机制。读完你既能理解这套设计的来龙去脉也能直接照着示例在LibraryImport中使用 span/集合参数编组。背景LibraryImportGenerator 与 span 编组的引入动机LibraryImportGenerator是 dotnet/runtime 中用于生成高性能 P/Invoke 存根stub的源生成器开发者声明[LibraryImport]标记的partial方法编译器在构建期生成非托管调用代码从而绕开运行时内置的编组marshalling管线。作为该实验的退出标准之一团队决定为生成的存根引入System.SpanT与System.ReadOnlySpanT的编组支持。此文档docs/design/libraries/LibraryImportGenerator/SpanMarshallers.md记录的正是实现这些 marshaller 过程中的设计决策与之配套的还有 Pipeline.md、Compatibility.md 等设计文档。需要特别说明的是文档开头即声明NOTE: These design docs are kept for historical purposes. The designs in this file are superseded by the designs in UserTypeMarshallingV2.md.即本文介绍的两版设计属于历史设计稿最终落地形态被 UserTypeMarshallingV2.mdV2 设计取代。但读懂这两版设计的取舍恰恰是理解 V2 为什么长成今天这个样子的钥匙下文将沿着这条演进线展开。Design 1对(ReadOnly)SpanT的内建支持第一种方案是在编组存根stub中直接内联inlinespan 的默认编组逻辑其思路完全建立在既有数组array支持模式之上。默认行为默认情况下SpanT与ReadOnlySpanT的编组方式与数组类型高度一致核心策略按优先级依次是固定pin数据并直接传递尽可能把 span 背后的数据固定住将指针直接传给原生代码这是性能最优路径栈上分配临时空间无法固定时优先在栈上分配 scratch space避免堆分配Marshal.AllocCoTaskMem分配原生内存与数组编组的兼容路径保持一致。对于**原生到托管native → managed**方向的编组则分配一个托管数组把原生内存中的数据拷贝进数组。此外文档明确当 span 从原生方向编组回来时将支持数组今天已支持的同款MarshalAsAttribute属性如SizeParamIndex、SizeConst。空 spanEmpty spans的语义决策团队决定让默认行为完全对齐托管侧(ReadOnly)SpanT的语义空 span 对应传null指针为 span 用户在互操作场景提供最平滑的默认体验。同时为了帮助想从数组参数迁移到 span 参数的开发者额外提供一个按需启用opt-in的 in-source marshaller它允许包装了空数组的 span以非空指针形式传递。附加的 in-source marshaller 提案基于 StructMarshalling.md 中设计的自定义类型编组模型文档还提议两个内置 marshaller空 span 以非空指针编组的 marshaller由于它无法正确表示非 blittable 元素类型的非空 span因此仅支持空 span将原生内存指针包装成 span 而非拷贝进托管数组的 marshaller按设计只支持 blittable 元素。这是一个 opt-in 的高级场景要求用户手动释放内存因此手动释放这一额外负担对使用它的用户而言是可以理解的。文档同时坦诚地留了一个未决问题由于没有机制提供集合长度原生侧如何告知 span 长度尚未解决曾考虑总是给一个 length 为 1 的 span、由用户自行创建正确大小的新 span但这被认为是糟糕的设计。Design 1 的取舍优点建立在已有数组支持之上实现经验可直接复用实现更简单沿用数组已支持的同一套MarshalAs属性开发者可以几乎零成本地把源生成 P/Invoke 中的数组参数迁移到 span 参数。缺点对非 blittable 元素类型的非空 span无法泛型化地定义自定义 marshaller因为元素的编组规则在 marshaller 编写时是未知的非内置类型的 span 元素无法自定义非默认编组把 span 编组逻辑完全内联进存根会增加磁盘上 IL 的体积该设计无法让开发者轻松为自己的集合类型定义自定义编组支持MarshalAs属性在非源生成的DllImport上对 span 依旧不生效——这是旧MarshalAs模型首次被套用到新类型上而且既有的 native type marshalling 支持无法处理未知编写时不可知非 blittable 元素类型的集合也无法在反编组时为集合指定元素计数。Design 2LinearCollection——面向连续集合的可扩展编组模型与其把 span 编组完全内联第二种方案选择扩展 StructMarshalling.md 的自定义类型编组模型让模型天生支持集合类类型。这样 Design 1 的诸多缺点尤其集合元素不可泛型化都能被解决span 编组仍实现前述空 span语义非nullspan marshaller 与无分配no-allocspan marshaller 也能在所有场景而非仅空 span中使用。新增CustomTypeMarshallerKind.LinearCollection编组类别设计在 BCL 中引入一个新的 marshaller kindnamespace System.Runtime.InteropServices { [AttributeUsage(AttributeTargets.Struct)] public sealed class CustomTypeMarshallerAttribute : Attribute { /// summary /// This type is used as a placeholder for the first generic parameter when generic parameters cannot be used /// to identify the managed type (i.e. when the marshaller type is generic over T and the managed type is T[]) /// /summary public struct GenericPlaceholder { } } public enum CustomTypeMarshallerKind { Value, LinearCollection } }配套的用法是集合类型如SpanT通过NativeTypeMarshalling指向一个泛型 marshaller该 marshaller 再用CustomTypeMarshaller声明自己负责的托管类型与编组类别[NativeTypeMarshalling(typeof(DefaultSpanMarshaller))] public ref struct SpanT { ... } [CustomTypeMarshaller(typeof(Span), CustomTypeMarshallerKind.LinearCollection)] public ref struct DefaultSpanMarshallerT { ... }CustomTypeMarshallerKind.LinearCollection要求泛型 marshaller 遵循下文描述的 LinearCollection marshaller shape。泛型支持与GenericPlaceholder由于属性attribute中无法使用泛型参数设计允许NativeTypeMarshallingAttribute与CustomTypeMarshallerAttribute中出现开放泛型类型前提是其元数arity与挂载属性的类型一致并且传给该类型的泛型参数也能用于构造作为参数传入的类型。当CustomTypeMarshaller标记的类型是指针、数组或指针与数组的组合的 marshaller 时可用CustomTypeMarshallerAttribute.GenericPlaceholder占据 marshaller 类型的第一个泛型参数位。文档给出了四种典型组合[CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder), Direction CustomTypeMarshallerDirection.In)] struct MarshallerT { public Marshaller(T managed); } [CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder[]), Direction CustomTypeMarshallerDirection.In)] struct MarshallerT { public Marshaller(T[] managed); } [CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder*), Direction CustomTypeMarshallerDirection.In)] struct MarshallerT where T : unmanaged { public Marshaller(T* managed); } [CustomTypeMarshaller(typeof(CustomTypeMarshallerAttribute.GenericPlaceholder*[]), Direction CustomTypeMarshallerDirection.In)] struct MarshallerT where T : unmanaged { public Marshaller(T*[] managed); }这里的模式非常清晰GenericPlaceholder是T单值、GenericPlaceholder[]数组、GenericPlaceholder*指针等形态的占位符让一个泛型 marshaller 可以同时覆盖T、T[]、T*、T*[]这些托管形态。LinearCollection marshaller shape一个泛型集合 marshaller 除满足CustomTypeMarshallerKind.Value形状对 marshaller 类型的要求构造函数除外外还必须具备如下形状[CustomTypeMarshaller(typeof(GenericCollection, , ,...), CustomTypeMarshallerKind.LinearCollection)] public struct GenericContiguousCollectionMarshallerImplT, U, V,... { // this constructor is required if marshalling from native to managed is supported. public GenericContiguousCollectionMarshallerImpl(int nativeSizeOfElement); // these constructors are required if marshalling from managed to native is supported. public GenericContiguousCollectionMarshallerImpl(GenericCollectionT, U, V, ... collection, int nativeSizeOfElement); public GenericContiguousCollectionMarshallerImpl(GenericCollectionT, U, V, ... collection, Spanbyte stackSpace, int nativeSizeOfElement); // optional /// summary /// A span that points to the memory where the managed values of the collection are stored. /// /summary public ReadOnlySpanTCollectionElement GetManagedValuesSource(); /// summary /// A span that points to the memory where the unmarshalled managed values of the collection should be stored. /// /summary public SpanTCollectionElement GetManagedValuesDestination(int length); /// summary /// A span that points to the memory where the native values of the collection are stored after the native call. /// /summary public ReadOnlySpanbyte GetNativeValuesSource(int length); /// summary /// A span that points to the memory where the native values of the collection should be stored. /// /summary public Spanbyte GetNativeValuesDestination(); // The requirements on the TNative type are the same as when used with NativeTypeMarshallingAttribute. // The property is required with the generic collection marshalling. public TNative ToNativeValue(); public void FromNativeValue(TNative value); }关键设计点构造函数额外接收一个int参数表示集合单个元素的原生尺寸。上文的TCollectionElement是 marshaller 自定义的任意元素类型——因为元素可能被编组为与托管侧尺寸不同的原生类型这样集合 marshaller 的作者就无需知道如何编组元素只需处理集合本身的结构。元素编组时生成代码的行为分两种元素为 blittable对GetManagedValuesSource/GetNativeValuesSource到GetNativeValuesDestination/GetManagedValuesDestination之间的 span 做块拷贝block copy元素非 blittable生成循环把托管 span 中的元素逐个编组后写入NativeValueStoragespan。该设计在元素为 blittable 时能达到与数组现有支持以及 Design 1 span 支持相近的性能指标。为集合编组提供附加信息MarshalUsingAttribute扩展集合反编组unmarshalling时存根代码生成器需要知道原生集合中有多少个元素为了与旧系统对齐还需要描述集合元素如何编组。为此设计对MarshalUsingAttribute做了如下扩展- [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Field, AllowMultipletrue)] public class MarshalUsingAttribute : Attribute { public MarshalUsingAttribute() {} public MarshalUsingAttribute(Type nativeType) {} public string CountElementName { get; set; } public int ConstantElementCount { get; set; } public int ElementIndirectionLevel { get; set; } public const string ReturnsCountValue return-value; }CountElementName指向某个参数或在结构体编组上下文中指向某个字段其值表示原生集合元素个数当值为ReturnsCountValue即return-value时表示元素个数来自返回值ConstantElementCount提供一个常量集合长度参数less 构造函数表示使用属性中的信息但对类型本身采用默认编组规则。Open Question: 是否允许在同一个属性里同时组合CountElementName与ConstantElementCount在MarshalAs编组中SizeParamIndex与SizeConst是可以组合的最终尺寸为paramValue(SizeParamIndex) SizeConst。ElementIndirectionLevel用于为集合中的元素类型提供编组信息。例如把ListListFoo从托管传到原生时可以分别给外层、内层 List 与Foo指定编组规则private static partial void Bar([MarshalUsing(typeof(ListAsArrayMarshallerListFoo), CountElementName nameof(count)), MarshalUsing(ConstantElementCount 10, ElementIndirectionLevel 1), MarshalUsing(typeof(FooMarshaller), ElementIndirectionLevel 2)] ListListFoo foos, int count);规则约束同一参数/返回值上可放多个MarshalUsing但只有当ElementIndirectionLevel取不同值时才允许每个参数/返回值允许恰好一个MarshalUsing不设置该属性它控制作为参数传入的集合对象本身的编组。某一级ElementIndirectionLevel对应的托管类型由上一级集合 marshaller 的ManagedValuesspan 元素类型推导而来——例如上面的ElementIndirectionLevel 1对应托管类型就是ListAsArrayMarshallerListFoo.ManagedValues[0]的类型。文档也对比了另一种替代方案用Type ElementNativeType { get; set; }属性代替ElementIndirectionLevel直接指定元素原生类型——但该方案会阻断集合的集合的编组支持因此被否决。示例用泛型集合编组支持 span下面是一个简化的SpanTmarshaller 示例为清晰起见省略了栈分配等全部优化[CustomTypeMarshaller(typeof(Span), CustomTypeMarshallerKind.LinearCollection, Features CustomTypeMarshallerFeatures.UnmanagedResources | CustomTypeMarshallerFeatures.TwoStageMarshalling)] public ref struct SpanMarshallerT { private SpanT managedCollection; private int nativeElementSize; private IntPtr Value { get; set; } public SpanMarshaller(SpanT collection, int nativeSizeOfElement) { managedCollection collection; Value Marshal.AllocCoTaskMem(collection.Length * nativeSizeOfElement); nativeElementSize nativeSizeOfElement; nativeElementSize nativeSizeOfElement; } public ReadOnlySpanT GetManagedValuesSource() managedCollection; public SpanT GetManagedValuesDestination(int length) managedCollection new T[length]; public unsafe Spanbyte GetNativeValuesDestination() MemoryMarshal.CreateSpan(ref *(byte*)(Value), managedCollection.Length); public unsafe Spanbyte GetNativeValuesSource(int length) MemoryMarshal.CreateSpan(ref *(byte*)(Value), length); public SpanT ToManaged() managedCollection; public IntPtr ToNativeValue() Value; public void FromNativeValue(IntPtr value) Value value; public void FreeNative() { if (Value ! IntPtr.Zero) { Marshal.FreeCoTaskMem(Value); } } }该 marshaller 通过UnmanagedResources特性声明自己持有需要释放的非托管资源FreeNative负责FreeCoTaskMem通过TwoStageMarshalling特性把IntPtr作为真正传给原生代码的值。假设SpanT带有[NativeMarshalling(typeof(SpanMarshaller))]特性那么对下面的签名生成的存根应如下所示struct WrappedInt { private int value; public WrappedInt(int w) { value w.i; } public int ToManaged() value; } [LibraryImport(Native)] [return:MarshalUsing(CountElementName nameof(length))] public static partial Spanint DuplicateValues([MarshalUsing(typeof(WrappedInt), ElementIndirectionLevel 1)] Spanint values, int length); // Generated stub: public static partial unsafe Spanint DuplicateValues(Spanint values, int length) { SpanMarshallerint __values_marshaller new SpanMarshallerint(values, sizeof(WrappedInt)); { ReadOnlySpanint __values_managedSpan __values_marshaller.GetManagedValuesSource(); Spanbyte __values_nativeSpan __values_marshaller.GetNativeValuesDestination(); for (int i 0; i __values_managedSpan.Length; i) { WrappedInt native new WrappedInt(__values_managedSpan[i]); MemoryMarshal.Write(__values_nativeSpan.Slice(sizeof(WrappedInt) * i), ref native); } } IntPtr __retVal_native __PInvoke__(__values_marshaller.ToNativeValue(), length); SpanMarshallerint __retVal_marshaller new(); __retVal_marshaller.FromNativeValue(__retVal_native); MemoryMarshal.Castbyte, int(__retVal_marshaller.GetNativeValuesSource(length)).CopyTo(__retVal_marshaller.GetManagedValuesDestination(length)); return __retVal_marshaller.ToManaged(); [DllImport(Native, EntryPointDuplicateValues)] static extern IntPtr __PInvoke__(IntPtr values, int length); }这个存根把设计中的每个环节串了起来值得逐段对照理解入参方向new SpanMarshallerint(values, sizeof(WrappedInt))分配原生缓冲区并记录元素原生尺寸随后遍历GetManagedValuesSource()得到的托管 span把每个元素经WrappedInt编组后MemoryMarshal.Write进GetNativeValuesDestination()返回的原生 span 的对应偏移处——这就是非 blittable 元素逐元素编组的体现调用阶段把__values_marshaller.ToNativeValue()即IntPtr指针作为参数传给__PInvoke__出参/返回方向通过FromNativeValue接收原生指针GetNativeValuesSource(length)给出原生数据视图length由CountElementName nameof(length)提供MemoryMarshal.Castbyte, int块拷贝到GetManagedValuesDestination(length)分配的托管数组blittable 元素的块拷贝路径最后由ToManaged()返回。设计同样可以推广到内置数组编组如果希望把数组编组逻辑从存根中搬到共享代码里直接套用这套LinearCollection模型即可。未来扩展非连续集合GenericCollection如果集合在托管或原生侧采用非连续non-contiguous元素布局开发者目前只能在互操作边界转换成数组/span 类型。文档提案新增GenericCollectionmarshaller kind并用一组按索引访问的方法替换 span 形态的方法- [CustomTypeMarshaller(typeof(Span), CustomTypeMarshallerKind.LinearCollection)] [CustomTypeMarshaller(typeof(Span), CustomTypeMarshallerKind.GenericCollection)] public struct GenericContiguousCollectionMarshallerImplT, U, V,... { // these constructors are required if marshalling from managed to native is supported. public GenericContiguousCollectionMarshallerImpl(GenericCollectionT, U, V, ... collection, int nativeSizeOfElements); public GenericContiguousCollectionMarshallerImpl(GenericCollectionT, U, V, ... collection, Spanbyte stackSpace, int nativeSizeOfElements); // optional public TNative ToNativeValue(); public void FromNativeValue(TNative value); - public ReadOnlySpanTCollectionElement GetManagedValuesSource(); - public SpanTCollectionElement GetManagedValuesDestination(int length); - public ReadOnlySpanbyte GetNativeValuesSource(int length); - public Spanbyte GetNativeValuesDestination(); public ref byte GetOffsetForNativeValueAtIndex(int index); public TCollectionElement GetManagedValueAtIndex(int index); public TCollectionElement SetManagedValueAtIndex(int index); public int Count { get; set; } }托管 → 原生生成代码用GetManagedValueAtIndex逐索引取出Count个元素把各自编组结果写入GetOffsetForNativeValueAtIndex(index)返回的地址最后ToNativeValue()取得传给原生代码的值原生 → 托管Countsetter 被设置为原生集合元素个数FromNativeValue接收原生返回值随后按Count次迭代用GetOffsetForNativeValueAtIndex定位原生值、用SetManagedValueAtIndex写回反编组后的托管值。由于不再假定连续布局链表linked list这类集合也能被支持。Design 2 的取舍优点集合类型的拥有者无需知道如何编组集合的元素非 blittable 类型集合的自定义非默认编组与 blittable 类型使用同一套代码集合编组逻辑共享减小磁盘 IL 体积开发者可以不修改源生成器就为自己的集合类型启用编组对原生集合布局不做假设链表等集合可轻松支持。缺点向 BCL 引入更多属性类型增加编组类型模型的复杂度——文档建议值得把必需成员构造函数除外用接口描述出来以降低心智负担例如托管→原生成员、原生→托管成员、顺序访问专用成员三组接口可以取代新的 marshaller kind基础提案只支持连续集合当时团队倾向接受请开发者在互操作边界自行转换数组/span这一限制。演进结局V2 模型如何取代两版设计随着该模型在 dotnet/runtime、ASP.NET Core、WinForms 及早期采用者中被广泛使用团队收到大量反馈详见 UserTypeMarshallingV2.md主要痛点包括状态机复杂marshaller 方法调用顺序难以定义异常场景容易泄漏内存集合元素场景下的状态保持极其困难简单 marshaller 被过度设计透明结构Transparent Structures支持反而增加了基础设计的负担概念过载可选特性与多种 shape 的组合让新人难以入门特化能力受限V1 把一个托管类型映射到一个 marshaller 类型导致栈分配缓冲这类场景优化要让所有场景买单一个 marshaller 只能服务一个托管类型如string与char无法共享同一编组概念。V2 的应对方案在源码中已有完整落地。核心变化包括无状态stateless与有状态stateful两种 shape无状态 shape 覆盖 90% 场景避免状态维护问题有状态 shape 目前不允许出现在集合元素场景MarshalMode枚举取代CustomTypeMarshallerDirection按 C# 语法in/ref/out、正/反 P/Invoke划分ManagedToUnmanagedIn/Ref/Out、UnmanagedToManagedIn/Ref/Out、ElementIn/Ref/Out与Default共 10 种模式避免开发者记忆方向枚举与 C# 关键字之间的映射CustomMarshallerAttribute入口点类型 每个场景指定实现类型入口点类型如Utf8StringMarshaller命名编组概念允许一个入口点提供多托管类型、多场景的特化支持ContiguousCollectionMarshallerAttribute标记连续集合 marshaller 入口点对应本主题 V1 的LinearCollection概念构造函数不再承载编组逻辑改为FromManaged/ToUnmanaged、FromUnmanaged/ToManaged等方法序列且每个参数只有一个 marshaller 实例并始终赋给局部变量简化异常处理与资源释放的指导。例如ArrayMarshaller在 V2 模型中的完整形态节选自 System.Runtime.InteropServices.cs 引用程序集[System.CLSCompliant(false)] [System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), System.Runtime.InteropServices.Marshalling.MarshalMode.Default, typeof(System.Runtime.InteropServices.Marshalling.ArrayMarshaller,))] [System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), System.Runtime.InteropServices.Marshalling.MarshalMode.ManagedToUnmanagedIn, typeof(System.Runtime.InteropServices.Marshalling.ArrayMarshaller,.ManagedToUnmanagedIn))] [System.Runtime.InteropServices.Marshalling.ContiguousCollectionMarshaller] public static class ArrayMarshallerT, TUnmanagedElement where TUnmanagedElement : unmanaged { public static TUnmanagedElement* AllocateContainerForUnmanagedElements(T[]? managed, out int numElements) { ... } public static System.ReadOnlySpanT GetManagedValuesSource(T[]? managed) { ... } public static System.SpanTUnmanagedElement GetUnmanagedValuesDestination(TUnmanagedElement* unmanaged, int numElements) { ... } public static T[]? AllocateContainerForManagedElements(TUnmanagedElement* unmanaged, int numElements) { ... } public static System.SpanT GetManagedValuesDestination(T[]? managed) { ... } public static System.ReadOnlySpanTUnmanagedElement GetUnmanagedValuesSource(TUnmanagedElement* unmanagedValue, int numElements) { ... } public static void Free(TUnmanagedElement* unmanaged) { } public ref struct ManagedToUnmanagedIn { public static int BufferSize { get; } public void FromManaged(T[]? array, System.SpanTUnmanagedElement buffer) { } public System.ReadOnlySpanT GetManagedValuesSource() { ... } public System.SpanTUnmanagedElement GetUnmanagedValuesDestination() { ... } public ref TUnmanagedElement GetPinnableReference() { ... } public static ref T GetPinnableReference(T[]? array) { ... } public TUnmanagedElement* ToUnmanaged() { ... } public void Free() { } } }对照即可发现 V2 形状对 V1 的继承关系GetManagedValuesSource/GetUnmanagedValuesDestination这些 span 访问成员被保留额外增加了一个集合 marshaller 末尾多一个泛型参数TUnmanagedElement可约束为unmanaged的约定由系统用集合元素非托管类型的泛型兼容表示填充指针类型用nint。PointerArrayMarshallerT, TUnmanagedElementref 文件同区域则是同一模型的指针数组变体。而 V1 中为集合计数提供的MarshalUsingAttribute扩展最终以如下形态进入 BCL见 System.Runtime.InteropServices.cs 引用程序集[System.AttributeUsageAttribute(System.AttributeTargets.Parameter | System.AttributeTargets.Property | System.AttributeTargets.ReturnValue, AllowMultiple true)] public sealed partial class MarshalUsingAttribute : System.Attribute { public MarshalUsingAttribute() { } public MarshalUsingAttribute(System.Type nativeType) { } public System.Type? NativeType { get; } public string CountElementName { get; set; } public int ConstantElementCount { get; set; } public int ElementIndirectionDepth { get; set; } public const string ReturnsCountValue return-value; }注意一个细节V1 设计文档中的ElementIndirectionLevel属性在最终实现里更名为ElementIndirectionDepth且属性目标加入了AttributeTargets.Property——这正是设计稿到实际产品之间演进的直接证据。源码佐证生成器如何识别与解析集合 marshaller在源生成器侧集合 marshaller 的识别逻辑集中在 ManualTypeMarshallingHelper.csIsLinearCollectionEntryPoint约 L85-L88通过检查入口点类型是否带ContiguousCollectionMarshallerAttribute来判断其是否为连续集合 marshallerTryGetLinearCollectionMarshallersFromEntryType约 L152-L170以isLinearCollectionMarshalling: true进入统一的解析管线同时要求提供元素编组信息获取回调getMarshallingInfoForElement因为集合元素的编组由元素自身类型决定泛型元数解析约 L280-L350体现了 V2 的多一个泛型参数约定集合 marshaller 解析时numOriginalArgsSubstituted ! 1/extraArgumentsInTemplate ! 1视为不匹配在GetStatelessMarshallerDataForType/GetStatefulMarshallerDataForType约 L492-L627中当isLinearCollectionMarshaller为真时元素类型取自GetManagedValuesSource返回的ReadOnlySpanT的泛型参数——即 shape 约定的ManagedValuesspan 元素类型决定元素编组目标类型。实战用法结合测试用例理解属性如何组合仓库中的集成测试 CollectionTests.cs 是理解这套属性到底怎么写的最佳范例。它展示了MarshalUsingAttribute各种组合的真实用法// 入参 元素计数来自另一参数 public static partial int Sum([MarshalUsing(typeof(ListMarshaller,))] Listint values, int numValues); // 双向(ref) CountElementName public static partial void Duplicate([MarshalUsing(typeof(ListMarshaller,), CountElementName numValues)] ref Listint values, int numValues); // 返回值 计数来自入参 [return: MarshalUsing(typeof(ListMarshaller,), CountElementName numValues)] public static partial Listint CreateRange(...); // 元素级编组ElementIndirectionDepth 1 作用于元素类型 public static partial int SumWithFreeTracking( [MarshalUsing(typeof(ListMarshaller,)), MarshalUsing(typeof(IntWrapperMarshallerWithFreeCounts), ElementIndirectionDepth 1)] ListIntWrapper values, int numValues); // 常量计数 [return: MarshalUsing(typeof(ListMarshaller,), ConstantElementCount sizeof(long))] public static partial long GetLongBytes(long l); // 直接对 ReadOnlySpanint 使用默认集合编组 返回值计数 [return: MarshalUsing(CountElementName nameof(numValues))] public static partial ReadOnlySpanint CreateRangeReadOnlySpan(int start, int end, out int numValues); // 有状态 marshaller 同样支持全套组合 public static partial int Sum([MarshalUsing(typeof(ListMarshallerStateful,))] Listint values, int numValues);从这些用例可以总结出可复用的实战模式入参方向[MarshalUsing(typeof(MyMarshaller,))]元素个数要么由原生约定blittable 元素可固定/按指针传递要么通过额外参数传入反编组需要知道长度CountElementName xxx指向参数名或ConstantElementCount N常量返回值场景用[return: MarshalUsing(CountElementName ...)]元素需要自定义编组叠加[MarshalUsing(typeof(ElementMarshaller), ElementIndirectionDepth 1)]多级嵌套逐级增加深度开箱即用的集合SpanT/ReadOnlySpanT与数组类型在LibraryImport中已有默认支持对应上文ArrayMarshaller与内建的 span 支持复杂自定义集合类型则通过[CustomMarshaller(...)][ContiguousCollectionMarshaller]声明入口点即可无需修改生成器。总结回顾(ReadOnly)SpanT编组支持的设计旅程可以看到一条清晰的演进脉络Design 1内建内联优先复用数组模式通过固定/栈分配/AllocCoTaskMem三级策略追求性能但对非 blittable 元素集合与自定义集合类型无能为力且增大 IL 体积Design 2LinearCollection出线模型引入CustomTypeMarshallerKind.LinearCollection、GenericPlaceholder与MarshalUsingAttribute的计数/深度扩展把集合结构编组与元素编组解耦实现了集合类型的可扩展支持并预留了非连续集合GenericCollection的扩展点V2 落地UserTypeMarshallingV2.md在广泛采用后的反馈驱动下收敛为MarshalModeCustomMarshallerAttributeContiguousCollectionMarshallerAttribute的入口点模型无状态/有状态双 shape最终在 System.Runtime.InteropServices 引用程序集 与 源生成器实现 中定型。对开发者而言今天在使用LibraryImport时最直接的收益是SpanT、ReadOnlySpanT、数组乃至自定义连续集合类型都可以声明式地获得高性能、可组合、可扩展的编组支持而当你需要为自有集合类型编写 marshaller 时V1 文档中的LinearCollection形状与MarshalUsingAttribute计数语义依然是理解生成器行为的最佳入门教材——它们在 V2 中以ContiguousCollectionMarshaller与ElementIndirectionDepth的形式延续至今。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考