为 COM 互操作限定 .NET 类型

向 COM 公开 .NET 类型

若要向 COM 应用程序公开程序集中的类型,请考虑 COM 互操作在设计时的需求。 如果符合以下准则,托管类型(类、接口、结构和枚举)将与 COM 类型无缝集成:

  • 类应显式实现接口。

    尽管 COM 互操作提供了一种机制,用于自动生成包含类的所有成员及其基类成员的接口,但最好还是提供显式接口。 自动生成的接口称为类接口。 有关指南,请参阅类接口简介

    可以使用 Visual Basic、C# 和 C++ 将接口定义合并在代码中,而无需使用接口定义语言 (IDL) 或其等效语言。 有关语法的详细信息,请参见语言文档。

  • 托管的类型必须是公共的。

    只有程序集中的公共类型才会注册并导出到类型库中。 因此只有公共类型才对 COM 可见。

    托管类型会向其他未向 COM 公开的托管代码公开功能。 例如,参数化的构造函数、静态方法和常数字段不会向 COM 客户端公开。 此外,运行时在类型中和类型外封送数据时,数据可能会被复制或转换。

  • 方法、属性、字段和事件必须是公共的。

    如果要对 COM 可见,公共类型的成员也必须是公共的。 通过应用 ComVisibleAttribute,可以限制程序集、公共类型或公共类型的公共成员的可见性。 默认情况下,所有公共类型和成员都是可见的。

  • 具备公共无参数构造函数的类型才能从 COM 中激活。

    托管的公共类型对于 COM 是可见的。 但是如果没有公共无参数构造函数(不带任何参数的构造函数),COM 客户端无法创建该类型。 如果该类型由其他方法激活,COM 客户端仍可使用该类型。

  • 类型不能是抽象的。

    COM 客户端和 .NET 客户端都不能创建抽象的类型。

导出到 COM 后,托管类型的继承层次结构将被展平。 托管和非托管环境之间的版本控制也会有所不同。 向 COM 公开的类型不具有其他托管类型相同的版本控制特性。

使用 .NET 的 COM 类型

如果打算使用 .NET 中的 COM 类型,并且不想使用 Tlbimp.exe(类型库导入程序)等工具,则必须遵循以下准则:

  • 接口必须应用 ComImportAttribute
  • 接口必须使用 COM 接口的接口 ID 应用 GuidAttribute
  • 接口应应用 InterfaceTypeAttribute 来指定此接口的基接口类型(IUnknownIDispatchIInspectable)。
    • 默认选项是设置 IDispatch 的基类型,并将声明的方法追加到接口的预期虚拟函数表。
    • 只有 .NET Framework 支持指定 IInspectable 的基类型。

这些准则提供了常见方案的最低要求。 应用互操作属性中介绍了更多自定义选项。

在 .NET 中定义 COM 接口

当 .NET 代码尝试通过具有 ComImportAttribute 特性的接口对 COM 对象调用方法时,它需要构建一个虚拟函数表(也称为 vtable 或 vftable)来形成接口的 .NET 定义,以确定要调用的本机代码。 此过程很复杂。 以下示例演示了一些简单案例。

请考虑使用以下几种方法的 COM 接口:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

对于此接口,下表描述了其虚拟函数表布局:

IComInterface 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

每个方法都按声明顺序添加到虚拟函数表。 特定顺序由 C++ 编译器定义,但对于没有重载的简单情况,由声明顺序定义表中的顺序。

声明与此接口对应的 .NET 接口,如下所示:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(/* The IID for IComInterface */)]
interface IComInterface
{
    void Method();
    void Method2();
}

InterfaceTypeAttribute 指定基接口。 它提供几个选项:

ComInterfaceType 基接口类型 特性化接口上成员的行为
InterfaceIsIUnknown IUnknown 虚拟函数表首先具有 IUnknown 的成员,然后按声明顺序具有此接口的成员。
InterfaceIsIDispatch IDispatch 成员不会添加到虚拟函数表。 只能通过 IDispatch 访问它们。
InterfaceIsDual IDispatch 虚拟函数表首先具有 IDispatch 的成员,然后按声明顺序具有此接口的成员。
InterfaceIsIInspectable IInspectable 虚拟函数表首先具有 IInspectable 的成员,然后按声明顺序具有此接口的成员。 仅在 .NET Framework 上受支持。

COM 接口继承和 .NET

使用 ComImportAttribute 的 COM 互操作系统不会与接口继承交互,因此除非采取一些缓解步骤,否则可能会导致意外行为。

使用 System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 特性的 COM 源生成器确实会与接口继承交互,因此它的行为更符合预期。

C++ 中的 COM 接口继承

在 C++ 中,开发人员可以声明派生自其他 COM 接口的 COM 接口,如下所示:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

struct IComInterface2 : public IComInterface
{
    STDMETHOD(Method3)() = 0;
};

此声明样式经常用作一种机制,可以在不更改现有接口的情况下向 COM 对象添加方法(这是一种中断性变更)。 此继承机制会生成以下虚拟函数表布局:

IComInterface 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

因此,可以轻松地从 IComInterface2* 调用 IComInterface 上定义的方法。 具体而言,在基接口上调用方法不需要调用 QueryInterface 即可获取指向基接口的指针。 此外,明确定义 C++ 支持从 IComInterface2* 隐式转换到 IComInterface*,这可以避免再次调用 QueryInterface。 因此,在 C 或 C++ 中,如果不想调用 QueryInterface,则无需执行该操作即可获取基类型,这可以提高一些性能。

注意

WinRT 接口不遵循此继承模型。 它们被定义为遵循与 .NET 中基于 [ComImport] 的 COM 互操作模型相同的模型。

ComImportAttribute 的接口继承

在 .NET 中,类似于接口继承的 C# 代码实际上并不是接口继承。 考虑下列代码:

interface I
{
    void Method1();
}
interface J : I
{
    void Method2();
}

此代码的作用不是“J 实现 I”。 其实际作用为“任何实现 J 的类型也必须实现 I。”这种差异会导致对基于 ComImportAttribute 的互操作中的接口继承的基本设计决策不合理。 始终单独考虑接口;接口的基接口列表不会影响用于确定给定 .NET 接口的虚拟函数表的任何计算。

因此,与前面的 C++ COM 接口示例的自然等效项会生成不同的虚拟函数表布局。

C# 代码:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

虚拟函数表布局:

IComInterface 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

由于这些虚拟函数表与 C++ 示例不同,因此这会导致运行时出现严重问题。 具有 ComImportAttribute 的 .NET 中这些接口的正确定义如下所示:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    new void Method();
    new void Method2();
    void Method3();
}

在元数据级别,IComInterface2 不实现 IComInterface,仅指定 IComInterface2 的实现者也必须实现 IComInterface。 因此,必须重新声明基接口类型中的每个方法。

GeneratedComInterfaceAttribute 的接口继承(.NET 8 及更高版本)

GeneratedComInterfaceAttribute 触发的 COM 源生成器会将 C# 接口继承实现为 COM 接口继承,因此虚拟函数表的布局符合预期。 如果采用前面的示例,则具有 System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 的 .NET 中这些接口的正确定义如下所示:

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

基接口的方法不需要重新声明,也不应重新声明。 下表描述了生成的虚拟函数表:

IComInterface 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 虚拟函数表槽 方法名称
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

如你所看到的,这些表与 C++ 示例匹配,因此这些接口将正常工作。

请参阅