COM 接口

COM 接口

什么是 COM?

COM 全称为 Component Object Model,是微软为了计算机工业的软件生产更加符合人类的行为方式开发的一种新的软件开发技术。在 COM 构架下,人们可以开发出各种各样的功能专一的组件,然后将它们按照需要组合起来,构成复杂的应用系统。由此带来的好处是多方面的:

  • 可以将系统中的组件用新的替换掉,以便随时进行系统的升级和定制
  • 可以在多个应用系统中重复利用同一个组件
  • 可以方便的将应用系统扩展到网络环境下
  • COM 与语言,平台无关的特性使所有的程序员均可充分发挥自己的才智与专长编写组件模块
  • 等等

接口的结构与描述

COM 把接口与实现分离开的动机主要有两个:

  • 升级。把对象内部的工作细节对客户隐藏起来,使得实现类内部的数据成员的数量、类型以及内部的方法都可以发生变化,而客户程序无需重新编译。客户在运行时询问对象,以便发现对象的扩展功能(是否实现了其他的接口?)
  • 编译器独立性。

COM 规范规定:接口是包含了一组函数的数据结构,通过这组数据结构,客户可以调用组件对象的功能。接口的结构如下所示:
COM 接口

使用 C++/C/Delphi 描述接口

假设我们要做一个简单的计算器,提供以下函数:

int add(int num1, int num2);
int sub(int subtrahend, int minuend);
  • 使用C++的类来描述接口的结构
class ICalculator 
{
public:
    virtual int __stdcall add(int num1, int num2) = 0;
    virtual int __stdcall sub(int subtrahend, int minuend) = 0;
};

类 ICalculator 的内存结构与 COM 接口规范要求的完全一致。见下图:
COM 接口

  • 使用 C 语言描述接口的例子
struct ICalculator { //接口包含一个指针,指针指向一个虚表结构
    struct ICalculatorVtbl *pVtbl;
};

struct ICalculatorVtbl { //虚表由函数指针构成
    int (STDMETHODCALLTYPE *add)(ICalculator* this, int num1, int num2); 
    int (STDMETHODCALLTYPE *sub)(ICalculator* this, int subtrahend, int minuend);
};

实际上其他的语言,只要能描述虚表和虚表指针的结构就能够描述 COM 接口。

  • 使用 Delphi 语言对接口的描述
ICalculator = interface
    function add(num1: Integer, num2: Integer): Integer; stdcall;
    function sub(subtrahend: Integer, minuend: Integer): Integer; stdcall;
end;

IDL

COM 最终的目标是建立二进制级的组件模型。COM 规范只定义了接口的特征,它没有规定编译器,也没有约束语言的使用。我们不仅需要需要编译器独立性,还需要语言的独立性。可以把 C++ 定义的接口翻译到其他的编程语言中。因为 COM 接口的二进制本质上就是一组 vptr/vtbl 虚表指针和虚表,所以,很多语言都可以做到。

但如果为所有已知的语言对所有的接口都产生映射版本,那么

  • 工作量巨大
  • 由于 C++ 语言非常复杂,且很容易产生歧义,它所描述的接口不一定能映射到其他语言上。

因此 COM 提供了这样一种语言,它只用到基本的 C 语法,同时加入了一些能消除歧义的特征,用来描述接口。称为接口定义语言 IDL(Interface Definition Language)。它的意义在于以语言中性的方式准确地描述接口的类型,并且在 IDL 与其他语言之间建立映射,从而作为客户端与服务器端的接口描述标准,使得各方在遵循 IDL 标准的基础上*地选择编程语言。

ICalculator IDL 定义

import “unknwn.idl”;  // 类似于 include,引入其他的 idl 文档

[   object,  // 表明该接口是一个 COM 接口而不是一个 RPC 接口
    uuid(54BF6568-1007-11D1-B0AA-444553540000), // 全球唯一标志符
]    // [ ] 表示属性
interface ICalculator : IUnknown   // interface 关键字表明接口定义的开始
{   
    HRESULT add( [in] long num1, [in] long num2, [out, retval] long result );
    HRESULT sub( [in] long subtrahend, [in] long minuend, [out, retval] long result ); 
};

接口的标识

逻辑名称与实质名称
两个开发人员可能选择同一个接口名字 ICalculator,然而有不同的成员函数,除了名字相同以外,两个接口是不兼容的。两个 COM 组件和各自的客户程序有可能在同一台机器上。冲突的可能性时刻存在。为了消除名字冲突,所有的接口在设计时分配一个二进制的名字,也就是实质名字,它使用 GUID 来标识接口。GUID 是一个 128 位长的数,能在概率意义上保证不重复。

GUID 用来命名接口时,它被称为接口 ID (Interface Identifier,IID),COM 的实现也使用 GUID 来标识,这时它被称为类 ID (Class Identifier,CLSID),用文本来表示,往往是以下的格式:

{CEBB3FBA-17F5-44c4-987C-631FAE5B80AC}  // 32 个 16 进制的数字

因为很少编译器支持 128 位整数,COM 定义了一个结构来表示 GUID 的 128 位值:

typedef struct _GUID {          
    DWORD Data1;
    WORD   Data2;
    WORD   Data3;
    BYTE  Data4[8];
} GUID;
typedef GUID IID; typedef GUID CLSID; // 为接口和实现类 ID 提供了别称。

COM 还提供了等价性的测试函数 IsEqualGUID 并为 GUID 引用类型重载了 == 和 != 操作符。

COM 数据类型

为了支持语言独立性和平*立性,COM IDL 提供了一组内置的数据类型,从这些数据类型到 C、C++、Java、VB 等语言之间可以建立一个映射。

  • COM 使用双字节字符(UniCode编码),用 OLECHAR(wchar_t)来表示。为了编写在 VB 或 C# 中使用的 COM 对象,应使用 BSTR 格式的字符串。BSTR 是带有长度前缀的 UniCode 编码字符串。COM 提供了一组函数用于 BSTR 的操作,如 SysAllocString、SysFreeString 等。
  • COM 与 IDL 支持联合类型(union)。COM 定义了一个通用的联合的类型 VARIANT,并且提供了一组函数操作 VARIANT,如 VariantInit 、VariantClear、VariantCopy 和 VariantChangeType 等。
  • IDL 支持指针类型,并使用 C 指针语法。
  • 函数的每一个参数必须指明是输入、输出或输入输出,以便编译器在生成的代码中进行列集和散集处理。
  • 几乎每个 COM 方法的返回值都是 HRESULT,它是一个 32 位的整数,可以向调用者的运行环境提供关于发生了什么类型的错误的信息,比如网络错误、服务器失败等等。HRESULT 分为 3 部分:严重程度位、操作码、信息码。MAKE_HRESULT 宏用来生成自定义的 HRESULT 值,SUCCEEDED 宏用来判断 HRESULT 值是否成功,FAILED 宏用来判断值是否失败。

MIDL 编译器

MIDL.exe 是 Win32SDK 提供的工具,实现从 IDL 到 C/C++ 的映射。它能编译 idl 文档以产生以下代码(以上面的 Calculator.idl 为例):

  • calculator.h  接口说明的头文件 (C/C++)
  • calculator_p.c 实现了接口的代理(proxy)和存根(stub)
  • calculator_i.c  定义了 IDL 中的 GUID、IID
  • dlldata.c    代理存根的入口函数以及其他数据结构(DllGetClassObject 等函数)
  • calculator.tlb  类型库文件,可以供 C#、Java 等编译器使用

IUnknown 接口

IUnknown 是一个接口,所有 COM 接口都继承于 IUnknown。

// IUnknown 的定义
interface IUnknown
{
    virtual HRESULT __stdcall QueryInterface( const IID& iid, void **ppv ) = 0;
    virtual ULONG __stdcall AddRef() = 0;
    virtual ULONG __stdcall Release() = 0;
}

COM 定义的每一个接口都必须从 IUnknown 继承过来,其原因在于 IUnknown 接口提供了两个非常重要的特性:生存周期控制接口查询。 客户程序只能通过接口与 COM 对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。

接口继承

接口的继承使得子类接口具有基类接口的功能。特别地,所有的接口都是从 IUnknown 接口派生而来,它们都具有接口的转换和引用计数的功能。接口本身不能多重继承。但是,这并不妨碍一个对象继承多个接口,如:

class FastString : public IFastString, public IPersistentObject
{
    ... ...
}

可以使用一个图来表示 COM 对象和它所支持的接口:
COM 接口

接口查询规则

按照 COM 规范,COM 对象可以支持多接口,而且对接口的获取是动态的,因此非常便于 COM 对象的升级和更新,这是体现 COM 生命力的地方。客户程序在运行时对 COM 对象的接口进行询问,如果它实现了该接口,则客户可以调用它的服务。在 COM 规范中,这是通过 IUnknown 的成员函数 QueryInterface 实现的。

HRESULT QueryInterface(const IID& iid, void **ppv);

QueryInterface 使用接口的 GUID 而不是字符串逻辑名称来区分接口。
当客户创建了 COM 对象之后,创建函数会给客户返回一个接口指针,由于所有的接口都派生自 IUnknown,它们都有 QueryInterface 成员函数,客户可以使用它来查询对象支持的其他接口。查询时,客户指定接口的 IID 号,查询函数把查询结果保存在接口指针 ppv 中。

QueryInterface 的实现要求(接口查询规则):

  1. 从同一对象的不同接口出发查询到的 IUnknown 接口完全相同
    也即得到相同的 IUnknown 子对象。这并不是总是成立的。 因为比如多重继承的关系,可能有多个 IUnknown 子对象。 而保持唯一性的目的是调用唯一的接口查询和引用计数,使得无论何时通过 IUnknown 接口开始的接口查询总是得到唯一的结果。
    而从不同的路径查询到的其他接口不必完全相同。这可以允许有的接口是动态地生成的,比如 tear-off 接口等。
  2. 对称性:A→B 成功,则 B→A 成功
    意味着客户不必关心先获得哪个接口指针。两种不同类型的接口指针可以以任意的次序获得。
    COM 接口
  3. 传递性: A→B,B→C,则 A→C
    意味着客户不必以任何特定的顺序来获得某个接口。如果任何两个接口之间不能直接转换,那么也不能通过第三方来完成。或者说,我们总是能够简单地从一个接口出发一步到位地到达其他的接口。
    COM 接口
  4. 自反性: A→A 总是成功的
    这实际是传递性的一个特例,对应于起点和终点相同的情形。
    COM 接口
    以上几条意味者所有的接口处于平等的地位(IUnknown 除外)。一个对象的所有接口构成一个双向连接图:
    COM 接口

COM 接口
EOF

上一篇:【RocketMQ】RocketMQ生产者和消费者端的幂等性怎么保证?


下一篇:Linq查询连接guid与varchar字段