限定 COM 互作的 .NET 类型

向 COM 公开 .NET 类型

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

  • 类应显式实现接口。

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

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

  • 托管类型必须为公共类型。

    只有程序集中的公共类型已注册并导出到类型库。 因此,COM 仅显示公共类型。

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

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

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

  • 类型必须具有可从 COM 激活的公共无参数构造函数。

    COM 可以看到托管的公共类型。 但是,如果没有公共无参数构造函数(没有参数的构造函数),COM 客户端将无法创建类型。 如果 COM 客户端被其他某种方式激活,则仍可使用该类型。

  • 类型不能是抽象的。

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

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

从 .NET 使用 COM 类型

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

  • 接口必须 ComImportAttribute 应用。
  • 接口必须使用 GuidAttribute COM 接口的接口 ID 应用。
  • 接口应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

因此,从中IComInterface定义IComInterface2*的方法很容易调用 。 具体而言,在基接口上调用方法不需要调用即可 QueryInterface 获取指向基接口的指针。 此外,C++允许从隐式转换 IComInterface2*IComInterface*该转换,这是明确定义的,并允许你避免再次调用 QueryInterface 。 因此,在 C 或C++中,如果不想调用基类型,则永远不必调用 QueryInterface 该基类型,这可以允许一些性能改进。

注释

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

接口继承与 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++示例,这将导致运行时出现严重问题。 .NET ComImportAttribute 中这些接口的正确定义如下所示:

[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 实现 C# 接口继承触发的 COM 源生成器作为 COM 接口继承,因此虚拟函数表按预期布局。 如果采用前面的示例,则 .NET System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 中这些接口的正确定义如下所示:

[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++示例匹配,因此这些接口能够正常运行。

另请参阅