.NET和COM之间的互操作还是比较顺滑的。
COM是一种语言无关的接口描述以及调用机制,也可以应用在.NET平台上。 COM和.NET的差别可以简单概括为:
- 前者是基于接口的,后者是基于类型的
- 前者是基于引用管理的,后者是受托管的
主要是学习下Native interoperability的COM Interop in .NET。
Expose .NET Core components to COM
按流程来,挺简单的。
Embedding type libraries in the COM host
不支持生成COM Type Library。需要手动写IDL(然后使用MIDL编译)或者直接写C/C++头文件。
.NET 6以及后续版本,支持将编译后的TLBs嵌入COM host。
Sample
Additional notes
激活后,含有COM组件的配装件会在单独的AssemblyLoadContext中加载。
不支持Self-contained deployments的COM组件。 仅支持framework-dependent deployments的COM组件。
在同一进程同时加载.NET Framework和.NET Core有诊断上的限制,文中有述。
COM Wrappers
COM与.NET在运行时的对象模型有如下差异
- COM对象的使用端必须管理所用对象的生命周期;而.NET的CLR则自行管理所辖对象的生命周期
- COM对象的使用端请求的是接口,返回的是接口指针;.NET对象使用端可以通过反射获取对象的功能描述
- .NET对象归CLR管理。CLR有可能会动态移动对象在内存中所处的位置并更新指向旧位置的引用;COM对象不能被随便移动,以保证使用端的指针值不变
为了抹平这些差异,COM和.NET对象之间的互操作需要添加中间层(代理):
- RCW(runtime callable wrapper)是.NET使用COM对象的中间层
- CCW(COM callble wrapper)是COM使用.NET对象的中间层
RCW和CCW需要使用到叫做marshaling的操作来在两端为对象做映射。
Runtime Callable Wrapper
在同一进程内,CLR为每个COM对象创建一个RCW,不管有多少引用指向此对象。
当客户端和服务端的数据表示形式不一致时,RCW要对数据进行配搭以确保各端能看到所需的数据类型。 比如将.NET的string类型配搭COM的BSTR类型。
Marshaling selected interfaces
RCW的首要目的时抹平.NET和COM在对象使用和参数传递上的差距。
RCW会耗用COM对象的如下接口
- IDispatch,提供反射机制延迟到COM对象的绑定的
- IErrorInfo,提供更详细的错误描述
- IProvideClassInfo,如果提供了的话,会被调用以获得更好的类型标识
- IUnknown,用于对象标识,类型胁制,生命周期管理
RCW或会耗用如下接口
- IConnectionPoint 或IConnectionPointContainer
- IDispatchEx (仅用于.NET Framework)
- IEnumVARIANT,对于支持枚举的COM类型,可以将它们当作合集(collection)使用
COM Callable Wrapper
对于每个托管的对象,不管有多少COM使用端,只会创建一个CCW。
Object Identity
受调用的.NET对象是在具有垃圾回收的堆上创建的,可以自由移动。CCW对象则是在不具有垃圾回收的堆上创建的。
Object Lifetime
CCW对象采用引用计数方式管理。
Simulating COM interfaces
.NET运行时默认提供如下接口
- IDispatch
- IErrorInfo
- IProvideClassInfo
- ISupportErrorInfo
- ITypeInfo ((.NET Framework only)
- IUnknown
一个.NET类可以自行实现并覆盖除了IUnknown和IDispatch以外的接口。
一个.NET类也可以提供如下额外的接口
- The (_classname) class interface ,对应所有的公开接口、方法、领属、辖域
- IConnectionPoint/IConnectionPointContainer
- IDispatchEx (只适用于.NET Framework)
- IEnumVARIANT
Introducing the class interface
不须定义,一个.NET对象即可通过class interface导出所有的公开接口、方法、领属、辖域。 此接口可以是dual或者dispatch-only的,并且提供.NET类的名字,但前面会加一个下划线。
对于派生类,class interface也会到处所有基类的所有的公开接口、方法、领属、辖域。
生成class interface是一个可选操作。默认情况下,COM interop会生成一个dispatch-only的接口。可以通过ClassInterfaceAttribute调整
Define an explicit interface for COM clients to use rather than generating the class interface.
通过class interface自动导出类成员的问题是,如果成员发生了变动,会影响到COM接口的布局,有可能会导致客户端无法适配。
通过ClassInterfaceAttribute 关掉class interface的自动导出:
[ClassInterface(ClassInterfaceType.None)]
public class LoanApp : IExplicit
{
int IExplicit.M() { return 0; }
}
Avoid caching dispatch identifiers (DispIds)
class interface的自动导出适用于安歇不会缓存接口成员的DispId的客户端(比如脚本、VB6.0等)。
自动导出的DispId依赖于成员在接口中出现的顺序。
可以通过ClassInterfaceType.AutoDispatch让导出的成员只能通过dispatch的方式获得:
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class LoanApp
{
public int M() { return 0; }
}
这样,客户端必须通过IDispatch.GetIdsOfNames
获得成员的名字,然后通过IDispatch.Invoke
来调用成员。
Restrict using the dual interface option for the class interface.
Dual的方式同时开启早绑定和迟绑定。会在导出的type library中对接口进行描述。此描述会鼓励客户端在编译的时候缓存DispId,即便这个客户端时迟绑定的。
不过Dual的方式可能在开发的过程中管用。
Ensure that all COM event notifications are late-bound.
默认情况下,COM类型信息会嵌入托管的配装件(assembly),从而不需要PIA(primary interop assemblies)。但是,有一个限制,它不支持通过早绑定的vtable调用的COM事件通知,而只支持迟绑定的IDispatch::Invoke 调用。
如果需要早绑定,以支持COM事件接口方法,需要在Visual Studio工程中开启:
<EmbedInteropTypes>True</EmbedInteropTypes>
Working with Interop Exceptions in Unmanaged Code
非托管代码异常方面的互操作只在Windows上支持。Unix上的ABI没有描述异常信息。
不支持与使用setjmp和longjmp的C函数进行互操作。
Tutorial: Use the ComWrappers API
ComWrappers Class允许你自行实现类似RCW或者CCW的功能。
如何通过创建ComWrappers的子类来提供一个优化的,AOT友好额COM interop解决方案。
……
其他
参考链接
- .NET - COM Interoperability,2003年的文章,不过几个图示画的挺好的,结论也比较清晰。
- Applying Interop Attributes,列举了COM相关的属性。
- Qualifying .NET Types for COM Interoperation用.NET作成COM的注意事项。
- COM in .NET,有一些例子可供参考。
- Creating a Microsoft .Net COM Visible DLL ,YouTube视频可供参考
(完)