COM 可调用包装

当 COM 客户端调用 .NET 对象时,公共语言运行时将为该对象创建托管对象和 COM 可调用包装器(CCW)。 无法直接引用 .NET 对象,COM 客户端使用 CCW 作为托管对象的代理。

无论请求其服务的 COM 客户端数量如何,运行时都为托管对象创建一个 CCW。 如下图所示,多个 COM 客户端可以保持对公开 INew 接口的 CCW 的引用。 反之,CCW 保持对实现该接口的托管对象的单一引用,并被垃圾回收。 COM 和 .NET 客户端都可以同时在同一托管对象上发出请求。

多个 COM 客户端保留对公开 INew 的 CCW 的引用。

COM 可调用包装器对 .NET 运行时中运行的其他类不可见。 其主要目的是协调托管代码和非托管代码之间的调用;然而,CCW 也负责管理它们所包装的托管对象的身份和生命周期。

对象标识

运行时从其垃圾回收堆中为 .NET 对象分配内存,从而使运行时能够根据需要在内存中移动对象。 与此相反,运行时为非回收堆中的 CCW 分配内存,使 COM 客户端直接引用包装器成为可能。

对象生存期

与其包装的 .NET 客户端不同,CCW 在传统 COM 方式中会进行引用计数。 当 CCW 上的引用计数达到零时,包装器将释放其对托管对象的引用。 将在下一个垃圾回收周期期间收集无剩余引用的托管对象。

模拟 COM 接口

CCW 以符合 COM 强制实施基于接口的交互的方式向 COM 客户端公开所有公共的 COM 可见接口、数据类型和返回值。 对于 COM 客户端,对 .NET 对象调用方法与在 COM 对象上调用方法相同。

为了创建这种无缝方法,CCW 生产传统的 COM 接口,如 IUnknownIDispatch。 如下图所示,CCW 保持对其包装的 .NET 对象的单一引用。 COM 客户端和 .NET 对象通过 CCW 的代理和存根构造相互交互。

显示 CCW 如何制造 COM 接口的图示。

除了公开由托管环境中的类显式实现的接口外,.NET 运行时还代表对象提供下表中列出的 COM 接口的实现。 .NET 类可以通过提供这些接口的自身实现来替代默认行为。 但是,运行时始终为 IUnknownIDispatch 接口提供实现。

接口 DESCRIPTION
IDispatch 为晚期绑定到类型提供一个机制。
IErrorInfo 提供错误的详细说明、错误的源、帮助文件、帮助上下文以及定义错误的接口的 GUID(对于 .NET 类,始终为 GUID_NULL)。
IProvideClassInfo 使 COM 客户端能够访问托管类实现的 ITypeInfo 接口。 对于未从 COM 导入的类型,在 .NET Core 上返回 COR_E_NOTSUPPORTED
ISupportErrorInfo 使 COM 客户端能够确定托管对象是否支持 IErrorInfo 接口。 如果是,则允许客户端获取指向最新异常对象的指针。 所有托管类型都支持 IErrorInfo 接口。
ITypeInfo (仅限.NET Framework) 为与 Tlbexp.exe生成的类型信息完全相同的类提供类型信息。
IUnknown 提供 IUnknown 接口的标准实现,COM 客户端使用该接口管理 CCW 的生存期并提供类型强制转换。

托管类还可以提供下表中所述的 COM 接口。

接口 DESCRIPTION
(_classname) 类接口 接口由运行时公开,未显式定义,公开在托管对象上显式公开的所有公共接口、方法、属性和字段。
IConnectionPointIConnectionPointContainer 以基于委托的事件(用于注册事件订阅服务器的接口)为源的对象的接口。
IDispatchEx (仅限.NET Framework) 如果类实现 IExpando,则由运行时提供接口。 IDispatchEx 接口是 IDispatch 接口的扩展,与 IDispatch 不同,它允许枚举、添加、删除和区分大小写的成员调用。
IEnumVARIANT 集合类型类的接口,如果类实现 IEnumerable,则枚举集合中的对象。

类接口简介

在托管代码中未显式定义的类接口是一个接口,用于公开在 .NET 对象上显式公开的所有公共方法、属性、字段和事件。 此接口可以是双接口或仅用于调度的接口。 类接口接收 .NET 类本身的名称,前面是下划线。 例如,对于类“哺乳动物”,类接口_Mammal。

对于派生类,类接口还公开基类的所有公共方法、属性和字段。 派生类还公开每个基类的类接口。 例如,如果类 Mammal 扩展了类 MammalSuperclass(它本身扩展 System.Object),则 .NET 对象会向 COM 客户端公开三个名为_Mammal、_MammalSuperclass和_Object类接口。

例如,请考虑以下 .NET 类:

' Applies the ClassInterfaceAttribute to set the interface to dual.
<ClassInterface(ClassInterfaceType.AutoDual)> _
' Implicitly extends System.Object.
Public Class Mammal
    Sub Eat()
    Sub Breathe()
    Sub Sleep()
End Class
// Applies the ClassInterfaceAttribute to set the interface to dual.
[ClassInterface(ClassInterfaceType.AutoDual)]
// Implicitly extends System.Object.
public class Mammal
{
    public void Eat() {}
    public void Breathe() {}
    public void Sleep() {}
}

COM 客户端可以获取指向名为 _Mammal 的类接口的指针。 在 .NET Framework 上,可以使用 类型库导出程序(Tlbexp.exe) 工具生成包含 _Mammal 接口定义的类型库。 .NET Core 不支持类型库导出程序。 Mammal如果类实现了一个或多个接口,则接口将显示在 coclass 下。

[odl, uuid(…), hidden, dual, nonextensible, oleautomation]
interface _Mammal : IDispatch
{
    [id(0x00000000), propget] HRESULT ToString([out, retval] BSTR*
        pRetVal);
    [id(0x60020001)] HRESULT Equals([in] VARIANT obj, [out, retval]
        VARIANT_BOOL* pRetVal);
    [id(0x60020002)] HRESULT GetHashCode([out, retval] short* pRetVal);
    [id(0x60020003)] HRESULT GetType([out, retval] _Type** pRetVal);
    [id(0x6002000d)] HRESULT Eat();
    [id(0x6002000e)] HRESULT Breathe();
    [id(0x6002000f)] HRESULT Sleep();
}
[uuid(…)]
coclass Mammal
{
    [default] interface _Mammal;
}

生成类接口是可选的。 默认情况下,COM 互操作会为你导出到类型库中的每个类生成仅支持调度的接口。 可以通过向类应用 ClassInterfaceAttribute 来阻止或修改此接口的自动创建。 尽管类接口可以简化向 COM 公开托管类的任务,但其用途有限。

谨慎

使用类接口(而不是显式定义自己的接口)可能会使托管类的未来版本控制复杂化。 在使用类接口之前,请阅读以下准则。

为 COM 客户端定义要使用的显式接口,而不是生成类接口。

由于 COM 互作自动生成类接口,因此对类的后期版本更改可以更改公共语言运行时公开的类接口的布局。 由于 COM 客户端通常未准备好处理接口布局中的更改,因此如果更改类的成员布局,它们会中断。

此准则强化了向 COM 客户端公开的接口必须保持不变的概念。 若要通过无意中重新排序接口布局来降低破坏 COM 客户端的风险,请通过显式定义接口将类的所有更改与接口布局隔离开来。

使用 ClassInterfaceAttribute 解除类接口的自动生成并实现类的显式接口,如以下代码片段所示:

<ClassInterface(ClassInterfaceType.None)>Public Class LoanApp
    Implements IExplicit
    Sub M() Implements IExplicit.M
…
End Class
[ClassInterface(ClassInterfaceType.None)]
public class LoanApp : IExplicit
{
    int IExplicit.M() { return 0; }
}

ClassInterfaceType.None 值可防止类元数据导出到类型库时生成类接口。 在前面的示例中,COM 客户端只能通过LoanApp接口访问IExplicit该类。

避免缓存调度标识符 (DispIds)

对于脚本化客户端、Microsoft Visual Basic 6.0 客户端或不缓存接口成员的 DispId 的任何后期绑定客户端,可接受使用类接口。 DispId 标识接口成员,以启用后期绑定。

对于类接口,DispIds 的生成基于接口中成员的位置。 如果更改成员的顺序并将类导出到类型库,则会更改类接口中生成的 DispId。

若要避免在使用类接口时中断后期绑定 COM 客户端,请应用具有 ClassInterfaceAttribute 值的 ClassInterfaceType.AutoDispatch。 此值实现仅调度类接口,但省略类型库中的接口说明。 如果没有接口说明,客户端无法在编译时缓存 DispId。 尽管这是类接口的默认接口类型,但可以显式应用属性值。

<ClassInterface(ClassInterfaceType.AutoDispatch)> Public Class LoanApp
    Implements IAnother
    Sub M() Implements IAnother.M
…
End Class
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class LoanApp
{
    public int M() { return 0; }
}

若要在运行时获取接口成员的 DispId,COM 客户端可以调用 IDispatch.GetIdsOfNames。 若要在接口上调用方法,请将返回的 DispId 作为参数传递给 IDispatch.Invoke

限制使用类接口的双重接口选项。

双重接口通过 COM 客户端启用对接口成员的早期绑定和后期绑定。 在设计时和测试期间,你可能会发现将类接口设置为双接口很有用。 对于永远不会修改的托管类(及其基类),也可以接受此选项。 在所有其他情况下,请避免将类接口设置为双。

在极少数情况下,自动生成的双接口可能合适;但是,它通常会创建与版本相关的复杂性。 例如,使用派生类的类接口的 COM 客户端在基类发生更改时很容易出现问题。 当第三方提供基类时,类接口的布局已失控。 此外,与仅调度接口不同,双接口(ClassInterfaceType.AutoDual)在导出的类型库中提供了类接口的说明。 此类说明会促使后期绑定的客户端在编译时缓存 DispId。

确保所有 COM 事件通知都是后期绑定的。

默认情况下,COM 类型信息直接嵌入到托管程序集内,这就无需使用主互操作程序集(PIA)。 但是,嵌入式类型信息的一个限制是它不支持通过早期绑定的 vtable 调用传递 COM 事件通知,而仅支持后期绑定的 IDispatch::Invoke 调用。

如果应用程序需要对 COM 事件接口方法进行早期绑定调用,可以将 Visual Studio 中的 “嵌入互作类型” 属性设置为 true,或将以下元素包含在项目文件中:

<EmbedInteropTypes>True</EmbedInteropTypes>

另请参阅