ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

.NET 源生成 COM 互操作(`[GeneratedComInterface]`)中的属性与索引器:vtable 布局、受支持语法面与 ABI 设计约束

.NET 源生成 COM 互操作(`[GeneratedComInterface]`)中的属性与索引器:vtable 布局、受支持语法面与 ABI 设计约束 .NET 源生成 COM 互操作[GeneratedComInterface]中的属性与索引器vtable 布局、受支持语法面与 ABI 设计约束【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime[GeneratedComInterface]是 .NET 运行时仓库dotnet/runtime中新一代源生成 COM 互操作的核心机制它让开发者可以用纯 C# 语法声明 COM 接口而不必手写get_X/set_X方法对。本文以 docs/design/libraries/ComInterfaceGenerator/Properties.md 设计文档为骨架结合本仓库的生成器实现源码src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/与真实测试资产完整讲解属性property和索引器indexer在[GeneratedComInterface]接口上的受支持语法面、产生的 ABIvtable形状、继承与遮蔽规则以及默认实现成员DIM这一逃生舱机制。读完本文你将能判断一个属性/索引器声明能否被源生成能精确推算出它占据哪些 vtable 槽位并能正确处理[MarshalUsing]、[IndexerName]、new遮蔽等易错点。设计目标与非目标目标用自然 C# 语法替代手写访问器对用户可以直接写int Count { get; set; }由生成器展开为底层的get_Count/set_CountABI 方法无需手工维护两套方法签名。与内置 COM CCW 保持线缆兼容生成的 vtable 布局必须与内置 CLR 为[ComVisible(true)]托管接口生成的布局一致从而保证源生成 COM 与内置 COM 在常见形状下可以互通。复用既有 per-method ABI 管线访问器本质上就是恰好支撑一个属性声明的IMethodSymbol。属性/索引器的访问器存根走与普通方法完全相同的逐方法 ABI 生成管线同一套封送marshalling规则、同一套 HRESULT 翻译逻辑实现层面只是在其上加了一层薄薄的包装。提供 DIM 逃生舱通过默认实现成员见 DefaultImplementedMembers.md用户可以声明纯托管糖sugar属性或方法它们不占用 vtable 槽位从而让一个属性可以包装一对互不相关或不相邻的 ABI 方法。非目标明确的边界不在 vtable 层面区分propput与propputrefpropputref概念存在于 TLB 元数据和IDispatch::Invoke中在纯IUnknownvtable 中没有对应表示。所有 setter 统一映射到一个 vtable 槽位。不做IDispatchdispid 集成[GeneratedComInterface]上的属性只通过IUnknownvtable 暴露不参与IDispatch的 dispatch id 体系。vtable 布局属性如何映射为槽位对于按源码顺序排在第k位的属性生成器会在前序成员槽位之后紧跟着发出一个或两个 vtable 槽位属性声明形状占用槽位数布局说明T Foo { get; set; }2 个连续槽位getter 槽在前setter 槽在后T Foo { get; }1 个槽位仅 getterT Foo { set; }1 个槽位仅 setter槽位签名遵循 COM 自动化OLE Automation的经典形状getter 槽HRESULT get_Foo(out T value)setter 槽HRESULT set_Foo(T value)访问器存根由与普通方法完全相同的逐方法 ABI 管线生成底层是VirtualMethodIndexAttribute这个可独立使用的 vtable 构建块详见 VTableStubs.md因此方法与访问器共享同一套封送规则以及同一套PreserveSig风格的 HRESULT 翻译机制。getter 在前、setter 在后、按源码声明顺序排列这一约定与内置 CLR 为[ComVisible(true)]托管接口生成的布局完全一致只读或只写属性只产生单个槽位同样与内置布局对齐。这一点在仓库的测试资产中有非常直观的体现。测试共享接口 IProperties.cs 声明了 6 个属性而原生测试资产 NativeExports/ComInterfaceGenerator/Properties.cs 中手工构造的 vtable 显示前 3 个槽0、1、2为IUnknown的QueryInterface/AddRef/Release槽 3、4 为IntProperty的 get/set槽 5 为只读的ReadOnlyInt槽 6 为只写的WriteOnlyInt槽 7、8 为GuidProperty槽 9、10 为StringProperty槽 11、12 为Self——每个属性严格按源码顺序占据连续槽位读写属性各占 2 个、只读/只写各占 1 个。继承与 vtable 布局继承属性遵循与继承方法完全相同的规则详见 DerivedComInterfaces.md一个[GeneratedComInterface]若从另一个[GeneratedComInterface]派生则继承基类所有访问器槽位并保持其在原接口中的索引不变派生接口自己的访问器槽位追加在它们之后。派生接口还会收到生成器发出的用户可见的遮蔽shadow声明覆盖每个继承来的属性访问器。这样T value derived.BaseProperty;这类调用不需要QueryInterface回到基接口类型即可完成分发——这是为了规避内置[ComImport]模型中接口继承不是真的继承、必须手写new重声明每个基类方法这个长期痛点见 DerivedComInterfaces.md 中的性能设计讨论。仓库中的 DerivedProperties.cs 手工构造了两个 vtable 来验证这一点基接口 vtable 有 13 个槽3 个IUnknown 10 个访问器槽派生 vtable 有 18 个槽其中 3~12 槽与基接口 vtable逐字节一致代码注释明确写着 Inherited slots — must match the IProperties vtable layout above byte-for-byte派生新增属性DerivedIntProperty、DerivedStringProperty、DerivedReadOnlyInt的访问器槽从 13 开始追加。受支持的属性语法面Supported property surface生成器刻意将受支持面收得很窄超出该清单的任何声明都会被SYSLIB1091member will not be source generated拒绝以便未来在不破坏现有用户的前提下逐步扩展受支持的语义。允许的写法无方法体的自动属性访问器三种访问器组合均可[GeneratedComInterface, Guid(…)] public partial interface IFoo { int Count { get; set; } // 两个 vtable 槽 string Name { get; } // 一个 vtable 槽 bool Verbose { set; } // 一个 vtable 槽 }属性级访问修饰符public、internal、private、protected这些只存在于 C# 表层ABI 形状不变internal int Count { get; set; } // 仍然是两个 vtable 槽注意访问器级访问修饰符如int Count { get; private set; }在访问器为抽象时会被 C# 语言本身以CS0442拒绝根本到不了生成器。要收窄访问器可见性必须给访问器加方法体——这会把属性推入下面的默认实现路径DIM。属性声明上的unsafe修饰符生成的访问器存根本来就位于unsafepartial 接口内因此这基本是免费的它允许属性的值类型是指针。属性声明上的new修饰符用于显式遮蔽继承来的 COM 属性。基类访问器槽位保持在原索引处派生接口为遮蔽访问器追加全新的槽位——这与new关键字遮蔽方法的行为一致[GeneratedComInterface, Guid(…)] public partial interface IBase { int Value { get; set; } } [GeneratedComInterface, Guid(…)] public partial interface IFoo : IBase { new int Value { get; set; } // 追加两个全新槽位IBase.Value 的槽位保留 }默认实现属性访问器带方法体见 DefaultImplementedMembers.md。禁止的写法以SYSLIB1091报告属性上的extern与required修饰符。init访问器init与set的区别只是 C# 调用侧的语法区别在 COM vtable 中没有对应表示。在[GeneratedComInterface]属性上声明init访问器无论是否带方法体都会被拒绝。若访问器应参与 vtable请使用set若只是想要init风格的托管辅助请通过独立的托管专用抽象例如 DIM实现。混合访问器形状一个访问器带方法体而另一个不带例如int Mixed { get; set { … } }。这是默认实现成员章节中SYSLIB1091PropertyAccessorsMustBeAllOrNothing诊断对应的场景在生成器分析之前C# 编译器也会用CS0525接口上的自动属性限制先行拦截。属性级封送特性除[MarshalUsing]之外的任何形式[MarshalAs]不允许直接放在属性上。此外virtual、abstract、sealed修饰符目前不支持用于[GeneratedComInterface]属性——属性的修饰符约束比方法更严格abstract是接口默认状态无需书写接口属性上的virtual要求方法体会落入 DIM 路径但当前不被接受sealed直接拒绝。生成器源码中的印证ComMethodInfo.cssrc/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComMethodInfo.cs在解析成员时检查访问器是否全部一致否则报告GeneratorDiagnostics.PropertyAccessorsMustBeAllOrNothing诊断描述符定义在 GeneratorDiagnostics.cs其规则 ID 即SYSLIB1091。属性上的封送marshalling特性[MarshalUsing]可以直接施加在属性上[GeneratedComInterface, Guid(…)] public partial interface IFoo { [MarshalUsing(typeof(MyMarshaller))] MyType Item { get; set; } }属性级封送信息在生成器构建逐访问器存根时会同时传播到两端getter 作为返回值信息return-value info、setter 作为参数信息parameter info。属性级写法是推荐风格因为它用一处声明表达了封送这个属性的完整意图。旧的逐访问器写法getter 上[return: MarshalUsing(...)]、setter 上[param: MarshalUsing(...)]仍然支持当两种形式同时存在时逐访问器形式优先。[MarshalAs]不允许放在属性声明上BCL 中MarshalAsAttribute的[AttributeUsage]并不包含AttributeTargets.Property因此 C# 编译器会在生成器运行之前就以CS0592拒绝该写法。如确实需要请把[MarshalAs]施加到单个访问器的返回值/参数上。派生接口中的遮蔽成员会剥离[MarshalUsing]与[MarshalAs]生成器在派生接口上自动发出的遮蔽成员shadow不会复制这两个封送特性——遮蔽成员通过托管调用把调用转发给基访问器在其上重复挂载封送信息是冗余的。索引器IndexersC# 索引器T this[I0 i0, …, In in] { get; set; }受支持且走与普通属性完全相同的逐访问器管线每个访问器各自成为自己的 vtable 槽位索引器的 getter 槽位于其 setter 槽之前索引器槽位与其他成员一起按源码声明顺序追加。槽位签名使用默认的[IndexerName(Item)]时getter 槽HRESULT get_Item(I0 i0, …, In in, out T value)setter 槽HRESULT set_Item(I0 i0, …, In in, T value)索引参数在 setter 上位于末尾的 value 参数之前——这与 C# 语言及内置 CLR 通过 COM 呈现索引器时的参数顺序一致。只读与只写this[I i] { get; }产生一个 getter 槽this[I i] { set; }产生一个 setter 槽——与属性行为相同。重载Overloading一个[GeneratedComInterface]可以声明多个以索引参数类型区分的索引器重载每个重载的访问器按源码顺序获得自己连续的一对槽位。C# 语言要求同一类型上的所有索引器共享一个有效的[IndexerName]编译器以CS0668强制因此各重载槽位上的 IL 方法名完全相同重载消歧完全依赖参数列表[GeneratedComInterface, Guid(…)] public partial interface IFoo { int this[int i] { get; set; } // 槽 3、4get_Item(int, out int) / set_Item(int, int) int this[int i, int j] { get; set; } // 槽 5、6get_Item(int, int, out int) / set_Item(int, int, int) int this[long l] { get; } // 槽 7 get_Item(long, out int) int this[short s] { set; } // 槽 8 set_Item(short, int) }槽位编号从 3 开始是因为 0~2 被IUnknown的QueryInterface/AddRef/Release占据。仓库测试对这一点覆盖得非常细致。共享接口 IIndexers.cs 声明了 5 个索引器int单参、int,int双参、long只读、short只写、string键值原生测试资产 NativeExports/ComInterfaceGenerator/Indexers.cs 手工构造的 vtable 注释逐槽列出了预期布局槽 3~6 是两个读写重载的 get/set 对槽 7 是get_Item(long)槽 8 是set_Item(short)槽 9、10 是 string 索引器的 get/set——与文档中的规则完全吻合。[IndexerName]索引器上的[IndexerName(...)]特性会重命名 IL 访问器方法例如get_Element/set_Element替代get_Item/set_Item。生成器在ABI 槽位生成和派生接口上自动发出的转发遮蔽成员两处都会尊重该特性。这里有一个必须遵守的约束派生[GeneratedComInterface]若用new修饰符重新声明继承来的索引器必须重复基接口的[IndexerName]值若基接口使用默认名则派生接口应省略该特性两个值不能分歧。原因在于生成器会把继承来的索引器以显式接口实现的形式int IBase.this[…] throw new UnreachableException();emit 到派生接口上这会被 C# 编译器视为派生类型上的一个索引器成员从而触发 CS0668 的同一类型所有索引器共享一个[IndexerName]规则。如果用户声明的new遮蔽与基类的[IndexerName]不一致编译器会以CS0668、CS0111及相关诊断拒绝该派生接口。因此若要把一个接口拆分成多个以[IndexerName]区分的形状必须声明在彼此无关的独立接口上。仓库测试中的 IRenamedIndexer 接口正是为覆盖此路径而设它使用[IndexerName(Element)]让生成的 IL 访问器名为get_Element/set_Element从而验证[IndexerName]在派生接口遮蔽成员发射器中的传播。支持的修饰符与封送受支持的属性语法面 中允许/禁止的修饰符清单原样适用于索引器只需把int Foo { … }换成int this[I i] { … }。索引器上的[MarshalUsing]会以与属性完全相同的方式传播到两个访问器存根value 参数作为参数信息、getter 作为返回值信息。默认实现索引器与属性一致索引器的访问器若全部带方法体则被视为默认实现成员不分配 vtable 槽位。混合访问器体int this[int i] { get; set { … } }由 C# 语言本身以CS0501拒绝对应属性的等价形状触发的是CS0525总之用户可见的诊断都会在生成器分析之前浮现。默认实现成员DIM不占槽位的托管糖[GeneratedComInterface]上的属性访问器、索引器访问器和方法都可以携带用户提供的方法体生成器将它们视为纯托管糖不分配 vtable 槽位。完整契约规则、诊断、get_X/set_X名称预留陷阱见 DefaultImplementedMembers.md核心要点如下[GeneratedComInterface, Guid(…)] public partial interface IFoo { // 两个 vtable 槽——ABI 方法 double ReadValue(); void WriteValue(double value); // 零个 vtable 槽——包装上面两个 ABI 方法的托管糖 double Value { get ReadValue(); set WriteValue(value); } // 也是零个 vtable 槽——纯托管辅助方法 double DoubleIt() ReadValue() * 2; }DIM 是方法→一个 vtable 槽 / 属性→一或两个相邻 vtable 槽这一规范规则无法表达的场景的逃生舱典型用途包括包装名称不符合规范get_X/set_X命名的 ABI 方法包装位于不相邻 vtable 槽上的 ABI 方法添加线缆 ABI 不需要知道的辅助方法。DIM 的关键规则所有访问器必须一致属性要么全部抽象访问器int X { get; set; }要么全部带方法体int X { get …; set …; }。混用会触发SYSLIB1091PropertyAccessorsMustBeAllOrNothing。C# 编译器对裸get;搭配带体的set { … }还会先以CS0525拒绝。DIM 上的[MarshalUsing]/[MarshalAs]是警告DIM 从不参与封送任何挂在 DIM 上的封送特性都会产生SYSLIB1091警告MarshalAttributeOnDefaultImplementedComInterfaceMember——生成器不为 DIM 生成代码挂封送特性具有误导性。继承的 DIM 会从基类 CCW 分发中跳过生成器为继承接口生成 CCW 分发时会排除非IsAbstract的访问器方法即继承的 DIM运行时通过托管对象上的普通虚分发解析它们。get_X/set_X名称预留陷阱一个看起来很自然的 DIM 模式是用一对名为get_X/set_X的 ABI 方法去支撑属性X——但这无法编译[GeneratedComInterface, Guid(…)] public partial interface IFoo { double get_Value(); // ABI 方法 void set_Value(double value); // ABI 方法 double Value { get get_Value(); set set_Value(value); } // DIM 包装 ABI 方法 —— 编译失败 }只要 C# 接口声明了属性Value语言就会为属性的访问器预留 IL 名称get_Value和set_Value在同一接口上再显式声明double get_Value()方法会以CS0082type already reserves a member called get_Value失败。正确的做法是让 ABI 方法避开属性访问器名称由 DIM 通过调用而非同名来包装[GeneratedComInterface, Guid(…)] public partial interface IFoo { double ReadValue(); // ABI 方法 void WriteValue(double value); // ABI 方法 double Value { get ReadValue(); set WriteValue(value); } // DIM }该约束同样适用于派生接口遮蔽派生接口不能用 DIM 属性X去遮蔽一对继承自基类的名为get_X/set_X的 ABI 方法——ABI 方法与同名属性不能在同一 C# 接口链上共存。实现层面的印证访问器如何汇入逐方法管线从生成器实现可以确认属性/索引器访问器并不是一条独立代码路径而是既有 per-method ABI 管线的成员形态扩展ComInterfaceGenerator.cs 在处理成员存根时区分MemberKind.IsPropertyOrIndexerAccessor()并处理继承存根的遮蔽发射IncrementalMethodStubGenerationContext.cs 定义了IsPropertyOrIndexerAccessor()与IsPropertyAccessorName(string)等辅助判定将访问器识别为属性形状的成员VirtualMethodPointerStubGenerator.cs 在生成存根时对IsPropertyOrIndexerAccessor()的成员同样参与函数指针调用的发射底层的VirtualMethodIndexAttribute定义见 VTableStubs.md提供了Index、ImplicitThisParameter、Direction、StringMarshalling、SetLastError等原始能力COM 生成器在其上叠加了属性/索引器的访问器语义。也就是说一个属性的 getter/setter 与一个普通方法在调用 vtable 中某个偏移处的函数指针这个层面毫无区别区别只在于访问器的槽位是成对出现、按 get-before-set 排序的且共享属性级封送信息。参考资料与进一步阅读本仓库docs/design/libraries/ComInterfaceGenerator/目录下的配套设计文档Compatibility.md — 逐版本发布的语义兼容性说明rolling notes。DefaultImplementedMembers.md —[GeneratedComInterface]上属性、索引器、方法的 DIM 完整契约。DerivedComInterfaces.md — 属性声明所继承的继承与遮蔽规则以及自动遮蔽成员的性能动机。VTableStubs.md — 访问器存根最终消费的底层VirtualMethodIndexAttribute构建块。感兴趣的读者还可以直接阅读生成器实现src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/与测试资产src/libraries/System.Runtime.InteropServices/tests/Common/ComInterfaces/ 中的IProperties.cs、IIndexers.cs以及 src/libraries/System.Runtime.InteropServices/tests/TestAssets/NativeExports/ComInterfaceGenerator/ 中的Properties.cs、Indexers.cs、DerivedProperties.cs其中手工构造的 vtable 是理解槽位分配规则的绝佳实物对照。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表