西安站台票2016年6月:一句话 COM - 给 C++ 程序员的 COM 入门指南 - 漫漫求索路 - 51CTO...

来源:百度文库 编辑:九乡新闻网 时间:2024/04/25 08:28:36

[url]http://www.allaboutprogram.com[/url] 开了一个新的版面:类库讨论,希望能够对流行的类库进行探讨,不仅可以从中获得使用经验,也可以为自己今后设计类库指明方向。ATL(Active Template Library,活动模板库) 也是这个版面讨论的对象之一,它是微软开发的一套 COM(Component Object Model,组件对象模型) 支持库。通常,脱离所支持的对象而讨论类库意义不大,我就写一篇简单的文章介绍一下,对于想学 COM 的人来说,也是一块敲门砖,只是希望别用来砸我。(鉴于这个坛子上大家都对 C++ 很熟悉,所以我会拿 C++ 和 COM 做很多比较,也就是说,我预计的读者至少比较熟悉 C++。并且,虽然 COM 中对象和接口的区别与 C++ 中的含义不同,但我可能会在不引起混淆的情况下混用这两者。此外,我对 COM 的版本不做区分,也就是说,我用 COM 来称呼 COM,DCOM(Distributed COM) 和 COM+(从 Windows 2000 开始支持的 COM 的一个改进版本)。)

1. 二进制对象标准

a. C++ 的对象布局

大家都知道,在 C++ 里面,一个类可以拥有成员数据和成员函数,如果我有一个类和一个函数:
代码:
class MyClass
{
int number;
string name;
public:
MyClass(int, name);
string getName() const;
// ...
};

void print( const MyClass& obj )
{
cout<}
有经验的程序员都知道,如果这个类和函数是用 VC 编译出来的,那么你不要尝试在 Borland C++ Builder 里面使用它。别说 lib 文件格式不一样,就算一样,你能保证两个编译器下面的 string 定义一样?你能保证 VC 编译的时候的对齐方式和你目前的一样?你能保证 VC 里面使用的堆和 BCB 的一样?任何一个细微的差别,都可能导致问题,而且是 crash。所以,C++ 的对象标准不是二进制的,而是源代码级的。就连不同的 C++ 编译器都不遵循一个标准,更不要说和别的语言(VB,pascal)交互了。

b. 仅有 vtable 的类

在 C++ 的类中,有一种函数叫做虚拟函数。虚拟函数的调用是“动态绑定的”,也就是说,是在运行的时候才决定的。譬如说这段代码:
代码:
class MyInterface
{
virtual int getID() const = 0;
};
MyInterface* p = getSomeObject();
cout<getID()<这段代码中的 p->getID() 的调用,可能和下面的伪代码差不多:
function_pointer* vtbl = (function_pointer*)p;
p->vtbl[0];
也就是说,我们调用一个函数的时候,我们使用一个整数(虚函数表的下标)来唯一确定这个函数。并且,因为我们没有数据成员,所以这个类可能只有一个指针。这个指针的偏移量不用计算,就是 0;或者说,如果 vtbl 的布局是相同的,并且调用某个函数的调用规范也是相同的,那么我们可以在不同的编译器之间共享这个对象指针。要统一 vtbl 的布局和调用方式,比统一不同编译器类的布局,标准库的实现以及各种编译参数要简单多了。

c. COM

COM,全称为 Component Object Model,是 MS 提出的一个二进制的对象标准。它以来的就是我们在 b 中所说的相同的 vtbl 布局。任何一种可以通过间接指针调用函数的语言都可以实现或者使用 COM 对象,这些语言包括 C/C++,VB,Pascal,Java,...,甚至 DHTML。在 COM 中,类用户所看到的就是一个又一个的“接口”,也就是只有一个 vtbl 的类,它的每一个函数的调用方式是 __stdcall。

2. IUnknown - 生命期,发现新内容和版本

a. 对象生命期
OO 系统中,对象的所有权和生命期是一个永恒的话题:这是因为,在复杂的 OO 系统中,对象之间的关系非常复杂,我们既不希望在引用一个对象的时候发现它已经不存在了,也不希望一个无用的对象继续在内存中占用系统的资源。C++ 提出了 auto_ptr,shared_ptr,...,这一切都为了方便管理对象的生命期。在没有 Garbage Collection 的系统中,常用的管理方式之一就是引用计数。当你需要使用一个对象的时候,把它的引用计数加一,使用完了,把它的引用计数减一。对象可以在内部维护一个计数器,也可以忽略这些信息(对于静态/栈上的对象来说)也就是说,我们通过一个对象引用计数使得对于不同对象生命期的管理具有同样的接口。 IUnknown接口具有两个方法:
ULONG AddRef();
ULONG Release();
前者可以把接口的引用计数加一,后者可以把接口的引用计数减一(请注意,是接口的引用计数!这和对象的引用计数有区别)

同时使用引用计数还可以避免一个问题,就是创建形式不同的问 题。譬如说,如果你在一个 dll 里面有一个函数:string* getName();。你在 exe 里面调用这个函数,获得一个指针,你使用完了这个指针以后,你可能会 delete 它,因为在 dll 里面,这个指针是通过 new 创建的。可是这样很可能会崩溃,因为你的 dll 和你的 exe 可能是用不同的堆,这样,exe 的 delete 在自己的堆里面寻找这个指针,往往会失败。

b. 发现新内容

C++ 里面,如果我们拥有一个基类的指针,我们可以通过强制类型转换获得派生类的指针,从而获得更丰富的功能:
代码:
class Derived:public Base
{
public:
virtual void anotherCall();
};
Base* b = getBase();
Derived* d = (Derived*)b;
可是,通常这被认为是一个很危险的操作,因为如果你不能确认这 个 b 的确指向了一个 d,那么接下去对 d 的使用带来的很可能是程序崩溃。C++ 提供了一个机制,可以在类中加入类型信息,并且通过 dynamic_cast 来获得有效的指针。出于种种原因(早期编译器对于 dynamic_cast 支持不好,或者是类型信息的加入往往是全局的,开销很大,...)有些类库,譬如说 MFC,自己实现了类似的机制。这样的机制的实现,往往是基于在类的内部维护一张表,以知道“自己”是什么,和“自己”有关系的类是哪些,...;同时,提供给外界一个可以查询这些信息的接口。要做到这个,必须解决两个问题:我怎样获得这个公共的接口,以及我用什么方法来标识一个类。在标准 C++ 中,我们通过运算符 typeid 来获得一个 const type_info&。在 MFC 中,因为所有支持动态类型信息的类都从 CObject 及其子类继承,所以我们在 CObject 中提供这些方法。这样,第一个问题对它们而言都解决了。在标准 C++ 中,表示一个类的动态信息就是那个 type_info,你可以比较它,判断它,或者获取一个没有确定含义的名字,但是你不能用它来做更多事情了。在 MFC 中,使用一个字符串来标识一个类,你可以通过一个字符串来动态的获得它所表示的类的类型信息。由于使用简单的字符串非常容易发生冲突,COM 使用的是一个 128 位的随机整数 GUID ( Global Unique Identifier ),这样的随机整数被认为是不太可能发生冲突的。IUnknown 中的另一个方法就是:
HRESULT QueryInterface( REFIID iid,void** Interface );
这个 REFIID 就是对于一个接口的唯一标识,我们也成为接口ID。你对一个接口指针调用 QueryInterface,来询问另一个接口。如果它支持这个接口,会返回给你一个指针,否则会返回一个错误。

d. 一些约定

- 任意一个 COM 接口都必须从 IUknown 接口派生
- 对任意一个接口 QueryInterface 的结果,必须支持自反性,传递性,可逆性和持久性。也就是说:
对一个 interface 询问它本身必须成功
如果你对类型为 A 的接口 a 询问接口类型 B 并且成功地返回了指向 B 的指针 b,那么你对 b 再询问 A,一定能成立。
如果你对类型为 A 的接口 a 询问接口类型 B 并且成功地返回了指向 B 的指针 b,同时从 b 询问到了接口 C 的指针 c,那么你必须能够成功的从 a 询问到接口 C 的指针。请注意,这个 C 的指针并不一定和前面那个 C 的指针相同。(也就是说,这个约定只保证询问成功,但是不保证返回的指针相同)
如果你对接口 a 询问接口 B 曾经成功过,那么以后的每次询问也将成功。
- 如果你要传一个接口给函数,那么应该是你保证这个接口在函数返回前的生命期有效。如果一个函数返回一个接口指针,那么必须在返回前 AddRef,同理,如果你从一个函数获得了一个接口指针,那么不需要再 AddRef 了,但是用完后,应该 Release。
- 特别的,根据上一条,你对一个接口调用了 QueryInterface 以后,这个接口返回前已经被 AddRef 了。
- 向同一个接口指针多次查询另一个接口,每次返回的指针不一定一样,但是,如果你查询的接口 ID 是 IID_IUnknown,那么每次返回的指针都相同。这种说法等价于,指向 IUnknown 的指针是一个 COM 对象的标识,也就是说,比较两个接口是否属于同一个对象的唯一通用方法是对比从中 Query 出来的 IUnknown 接口。此外,需要注意虽然每个 COM 接口都是从 IUnknown 派生的,但是你不能简单地通过把一个接口指针赋给一个 IUnknown 的指针来获得一个类标识,尽管这在 C++ 中是合法的。

- 引用计数是基于接口的,所以这段代码可能有问题
代码:
IInterface1* p1 = getObj();
IInterface2* p2;
p1->QueryInterface( IID_Interface2, (LPVOID*)&p2 );//假设成功
p1->Release();
p1->func();//这个时候,p1 可能无效!!虽然这个对象有效
p2->Release();
3. IDL

IDL 就是接口定义语言( Interface Definition Language )。前面我们说道,COM 中所有的功能都是通过接口来展现的,作为一个二进制标准,我们需要有一个通用的方法去描述接口。C++ 虽然强大,但是不适合做这件事,首先,它过于复杂,其次,它的很多数据类型别的语言不一定支持。我们需要有一个中立的语言来定义整个接口的数据交换情况(我们并不需要用它来定义接口的功能,因为这是依赖于具体实现的。)IDL 就是这种语言。你可以使用 IDL 来描述一个 COM 对象或者是 COM 接口的属性:这个函数接受什么参数,每个参数的流向,...。IDL 比 C++ 占优势的另一点是,它比较简单,可以用工具来处理,生成的二进制信息,可以用来完全描述你的接口的外部特性。

其实在别的 ORB 系统中,通常是把 IDL 作为接口的原始定义语言。譬如说,CORBA 中,你先用 IDL 描述接口,然后使用某些程序转换成一个 C++ 定义,并且在这个 C++ 定义上继续实现接口的功能,这称为从 IDL 到 C++ 的映射(mapping)。MS 的 MIDL 编译器也可以实现同样的功能,但是概念上不一样。在 COM 中,你脱离 IDL 直接用 C++ 实现一个接口,是合法行为,并且是最初的常用行为;在 CORBA 中,你自己写一个 C++ 映射,被认为是一种取巧行为(当然,熟悉的人可以这么做)。

关于 IDL 具体的定义和用法,请参见 MSDN。

4. 位置无关

我看过的 COM 书里面,总是把这部分内容放在较后解释。但是我倾向于将它提前,理由有两个:不难(对有经验的 C++ 程序员来说),有用(对理解后面的内容来说)。

a. 一个 C++ 模型

假设我有一个 C++ 类,大概是这样的:代码:
class Local
{
public:
virtual void encrypt( char* str ) = 0;//加密一个字符串
};
我的客户是这样使用它的:
代码:
Local* p = getGlobalEncryptor();
p->encrypt( password );
这段代码很简单,如果我的代码和客户代码在同一个模块里,几乎没有任何问题。可是,如果我们有一天需要把这个加密的方法统一的放到一个服务器上(加密代码放在客户机上,很难保证没有汇编高手去读懂代码的),那么这个程序就要修改了。我们当然希望这个程序修改的地方越少越好,通常的方法如下:
首先,我们另外写一个 exe,这个 exe 运行在服务器上,它 load 这个 dll,并且监听某个特定的 TCP 端口,如果有连接,它就先读进来 4 个字节的字符串长度,然后再读进整个字符串,然后调用 dll 中的函数,然后把返回结果,还是按照 4 个字节长度 + 内容的形式,通过这个连接发回去。
其次,我们在 getGlobalEncryptor 上面做一点手脚,我们返回一个“假的”对象,这个对象接收到一个 string 参数后,会把这个参数用前面所说的方法发送到 server 端的那个特定端口。这样一来,我们的客户代码不需要改变,也可以实现相同的功能。熟悉设计模式的人可能会想起,这是一个 proxy 模式。

这种方式来获得“位置”无关性的关键在于,我们使用了两个额外的对象,一个在客户端“模拟”所需要的接口,一个在服务端使用客户端传来的信息调用真正的接口。至于究竟通过什么方式来传递这个调用信息,并不是很重要。如果情况简单点,也就是说,我们的实现代码和客户代码在同一台机器上的两个进程内,那么我们可以用别的方式来完成这个通信,譬如说 Windows 的消息;我们甚至可以使用命名管道,NetBIOS 甚至文件来实现这个信息交换。

b. COM 中的 proxy 和 stub

COM 的目标是跨越进程,跨越网络的分布式对象系统,那么自然不能把对象和它的用户限制在同一个进程空间里面。所以 COM 提供了一套机制,称为 proxy/stub,我们把client 这边的这个模拟对象称为 proxy,把服务器那边的那个处理程序叫做 stub。你可以自己实现这两个程序,但是更直接的方法是,如果我们的所有的函数的参数和返回值都可以被 COM 基本服务所识别,它可以根据这些信息自动创建这套 proxy。这些信息可以通过用 MIDL 来编译你的 IDL 文件来获得。MIDL 可以创建这些 proxy/stub的源代码,你通过编译得到相应的 dll;也可以直接创建一个二进制的类型描述文件:type library,COM 运行时可以读取这个文件来生成 proxy/stub。COM 中,通过 proxy 和 stub 来连接远程对象的机制称为调度(marshal)。

如果你自己实现 marshal 代码,你可以在自己的对象上实现 IMarshal 接口。当远程创建对象时,stub 代码会向你的对象询问这个接口,并且对每一个需要的接口调用 MarshalInterface 方法,这个方法会传入一个 IStream 接口指针,你可以把和接口相关的数据(譬如说,服务器的名字,监听的端口,...)写入这个流,COM 服务会把这个流的内容传递给客户机;stub 代码还会通过调用 GetUnmarshalClass 来获得一个 CLSID,这个 CLSID 会被用来在本地创建一个 COM 对象,并且这个 COM 对象必须实现 IMarshal 接口。那个流的内容将被传给这个本地接口来创建一个 proxy 接口。

由于 dll 本身不能独立执行,因此 COM+ 提供了一个服务,叫做 surrogate,这是一个可执行文件,它可以 load 你的 dll,并且使用其中的对象。这样,你不仅可以访问在远程机器上的 dll 中实现的对象,你还可以把本地的 dll 放到代理进程中执行来获得更好的安全性。

注意:
- COM 基本接口的 marshal 支持都已经实现了,你不必提供自己的实现。
- COM 不仅在实现远程无关性中使用到 proxy/stub:在实现线程无关性的时候也使用了 proxy/stub,这在后面讲到 COM 线程模式的时候会详细讨论。
- proxy/stub 代码是基于接口的,而不是基于对象的。
- 需要远程执行的 COM 方法的返回值通常需要是 HRESULT(事实上推荐任意 COM 方法使用这个作为返回值)。因为一旦客户机和服务机之间的通信出现问题的时候,代理对象可以通过返回一些标识远程服务失败的 HRESULT 来告诉你这个事实,否则,假设你的返回值是 VOID,那么 proxy 对象没有任何方法可以通知你远程服务器出现的问题。

5. 创建或找到对象

所谓 OO 系统,当然离不开创建对象。我们通常在 C++ 中怎么创建对象的呢?下面是一些常用的方法:
代码:
class Derived:public Base
{
virtual Derived* clone() const;
};
Base* b1 = new Derived();//显式的创建对象
Base* b2 = b1->clone();//通过一个成员函数,返回一个对象的指针(可能是创建的,也可能是别的方式得来的)
Base* b3 = createObject("base");//通过一个全局函数,返回一个对象的指针,可能需要提供一些信息。
显式的创建对象需要你知道类的标识,调用函数创建对象,则可能 不需要提供标识。COM 中,我们通常难以使用创建对象这个概念,因为我们通常关心的是:我怎么获得一个对象的第一个接口指针,因为获得了这个指针后,我可以通过 QueryInterface 来获得别的感兴趣的接口。COM 并不提供一个类似于 new 的方法来创建对象,通常我们通过工厂对象来显式创建一个新的对象(有一点必须注意,虽然这里我们称之为“创建对象”,但是实际上你获得的是一个有效的接口。虽然大多数时候你使用 IClassFactory 的确是有对象被创建了,但是还有些情况下,并没有对象被创建。)。
什么是对象工厂,对象工厂广义上指一个类,这个类的一个或几个方法可以根据你的需要,创建对象(或者返回给你有效的对象指针,以下我们不再刻意区分这点);狭义上说,在 COM 世界里面,我们通常指一个实现了 IClassFactory 接口的对象。通常我们通过调用一个函数来获得对象工厂的指针:CoGetClassObject。它的原型如下:
STDAPI CoGetClassObject( REFCLSID rclsid, DWORD dwClsContext, COSERVERINFO * pServerInfo, REFIID riid, LPVOID * ppv );
rclsid 和前面所说的接口 ID 一样,是一个 GUID,在这里被称为类 ID ( Class ID )。在这里使用一个 GUID 的原因和接口标识使用 GUID 一样,也是因为字符串的名字比较容易冲突。dwClsContext 和 pServerInfo 目前我们先不操心。riid 是一个接口 id,因为你创建的对象,必须以某个接口指针的形式返回给你,通常我们使用 IID_IFactory 来获得一个对象工厂。ppv 自然就是那个创建出来的指针了。
为什么这个函数叫 CoGetClassObject,而不叫 CoGetClassFactory 呢?ClassObject 在 OO 领域中,特指一个类的静态部分。譬如说,如果你在一个 C++ 类中,有一些静态方法,那么你可以把这些静态方法和静态数据放到一个独立的对象里面,这个对象就是这个类的类对象。COM 中,我们不能对接口给出静态方法,所以就使用 ClassObject。IClassFactory 是一个 ClassObject 通常会实现的接口,但是 ClassObject 可以不支持这个接口。当然,通常我们实现自己的 ClassObject 的时候,会实现(甚至只实现) IClassFactory 接口,因为这是 COM 基本服务支持的标准类对象之一。
你得到了指向 IClassFactory 的指针后,可以调用它的 CreateInstance 来创建一个你所要的对象。
在 COM 中,提供了很多别的方法来获得一个接口指针,譬如说,调用某个全局函数,例如 CoGetMalloc;或者是调用一个接口指针的方法获得另一个接口指针,典型的就是 QueryInterface。其中最值得一提的是 Moniker。(是否展开?)

COM 服务通过注册表中的信息来获得 CLSID 到对象实现位置的有关信息。当你把一个 CLSID 传给 CoGetClassObject 的时候,COM 服务会去 HKEY_CLASSES_ROOT\CLSID 下面查找相应的键,如果这个键定义了 InprocServer32 子键,那表示这个对象被定义在一个 dll 中,这个键的缺省值是这个 dll 的完整文件名;如果定义的是 LocalServer32,则表示这个对象被定义在一个 exe 中。COM 中的每一个类还有一个“易读”的名字,称为 ProgID。你可以在 HKEY_CLASSES_ROOT 下面发现很多类似于 xxxxx.yyyyy.n 的键,这些键有一个 CLSID 子键唯一表示了相对应的 CLSID。你可以使用 CLSIDFromProgID 和 ProgIDFromCLSID 来完成两者之间的转换。

当 COM 服务装载了一个 dll 后,它会调用 dll 所导出的 DllGetClassObject 来获得一个 CLSID 所对应的类对象。当 COM 服务装载一个 exe 时,它会使用 /embedding 参数运行这个 exe,这个 exe 应该调用 CoRegisterClassObject 来把注册 ClassObject,当然这种情况下客户最后获得的 IClassFactory 指针往往是被 marshal 过的。

6. 错误处理

任意系统中,返回值总是一个常用的处理错误的形式。COM 中很多函数的返回值是一个 HRESULT。COM 是一个跨平台,跨语言的架构,必然要求返回值的含义更多,并且可以方便用户自己定义。HRESULT 一共 32 位,最高位叫 severity 表示这次操作成功与否,接下来的两位保留,接下来的 13 位叫 facility 表示状态(不仅仅是错误)的产生方。最后的 16 位就是状态的代码。举个简单的例子,HRESULT_FROM_WIN32 是把一个 Win32 的错误值转成 HRESULT 的。所以,当这个值是 0 的时候,HRESULT 就是 0(最高位是 0,表示成功)。否则,它会把最高为设成 1,并且把 facility 设成 FACILITY_WIN32,然后把状态值设成(错误代码 & 0xffff)。
如果你要给自己的接口方法返回一些自己的错误信息,可以使用 FACILITY_ITF。ITF 表示 interface,也就是说是接口自定义的错误信息。别的 FACILITY 有 FACILITY_NULL,FACILITY_RPC,…

很多情况下,一个简单的返回值显得很苍白,譬如说,打开文件失 败是一个返回值,但是究竟哪个文件打开失败了呢?一个 32 位的整数不能表达这样的信息。C++ 中,我们可以使用一个异常类来给出更多的错误信息,COM 中类似的机制是 IErrorInfo。对客户方来说,它可以对一个接口查询 ISupportErrorInfo,并且调用 ISupportErrorInfo 的 InterfaceSupportsErrorInfo 方法来确定这个对象的某个接口是不是支持 IErrorInfo,如果一个接口支持 IErrorInfo,并且你调用它的方法返回了一个表示错误的 HRESULT,你就可以调用 GetErrorInfo 来获得一个 IErrorInfo,并且调用这个接口指针的方法获得详细的错误信息。对服务器方来说,它需要实现 ISupportErrorInfo,并且在发生错误的时候,调用 CreateErrorInfo 来创建一个 ICreateErrorInfo 接口指针,并且完成相应的设置工作。

7. 线程

多线程总会给你的程序带来额外的麻烦。Windows 作为一个多线程系统,自然没办法拒绝在 COM 中使用多线程。假设我们在 C++ 中使用一个别人提供的库函数:
int doSomeCriticalThing( int );
如果我们本身的程序是多线程的,并且我们每个线程中都要调用这个函数,那么该怎么办呢?首先当然是检查这个函数的文档,看它是不是线程安全的。如果它是线程安全的,那什么都不用考虑,直接调用。如果它不是线程安全的,那么我们就需要在调用它的时候作一些同步。譬如说,我们设置一个全局的临界区,每次调用前进入临界区,然后调用这个函数,返回后再退出临界区。当然,还有一个方法是利用 Windows 现存的消息机制,我创建一个消息线程,这个线程接收消息并且根据消息的参数来调用这个函数,并且将结果返回给调用方(当然也是通过消息)。而调用这个函数的调用方,则每次都把这个函数的参数通过消息发到那个线程(是不是有点像 marshal?)更极端的情况是,这个函数的实现者根本就不知道世界上有多线程这件事情,所以他写的函数,只能在程序的主线程里面运行。这个时候怎么办?只有把主线程变为消息线程,然后别的线程通过向主线程发送消息来间接调用这个函数。
我们再来看另一种情况,如果我们的程序是一个单线程的程序,而需要调用的函数支持多线程,那么是否就可以直接调用了呢?不总是!对于上面这个函数来说可以,但是对于下面这个函数来说,不一定。

typedef int (* Callback )( int );
int doSomeCriticalThingAndCallMeBack( int, Callback );
这个函数里面,我们必须传给它一个回调函数,我们的回调函数可能是线程不安全的,而这个函数既然是多线程的,它可能内部实现了一个线程池,并且把这个工作分派给别的线程执行,如果它在那个线程里面调用这个回调函数,很难保证不出现同步问题,所以也需要作上面的特殊处理。

COM 里面,每一个对象都属于一个套间(Apartment),每一个套间可以由零个,一个或多个线程组成。COM 支持四种套间模式:STA(Single Threaded Apartment,一个线程),MTA(Multiple Threaded Apartment,多个线程),NA(Neutral Apartment,0 个线程),Main STA。STA 就是我们前面说的单线程并且通过消息调度的方式,MTA 就是多线程方式,Neutral Apartment 类似于我们前面说的临界区方式,Main STA 是指那种必须在主线程运行的 STA。我们可以对某个对象做标记,来告诉系统它支持什么方式的多线程,COM 支持五种标记方式:STA,Main STA,MTA,Both 和 NA,其中 Both 意味着一个对象既可以生存在 STA 中,也可以生存在 MTA 中。每一个线程在调用 CoInitializeEx 的时候,根据参数的不同,会进入某个套间。一个进程可以有多个 STA,但是只能有一个 MTA。不同的套间之间的对象调用,可能需要 marshal。

8. 数据存储和传递



9. persist

对象系统的一个难点就是持久化。几乎每一个 OO 的 framework 都会实现自己的持久化机制。我们在 C++ 中,通常采用某些特定的类函数来完成持久化,譬如说 operator << 和 operator >>,但是在多根体系中,给出一个全局有效的持久化机制通常很困难。COM 通过一系列的 IPersist* 机制(IPersistStream,IPersistFile,IPersistStorage,IPersistStream)实现持久化机制。当你获得一个接口后,你可以根据需要对它查询 IPersist* 中的某个,如果查询成功,就可以调用相应的功能来实现持久化数据的写入或读出。通过结合上面的 IStorage 和 IStream 接口,我们可以实现高效且优雅的持久化系统。

10. 重用

C++ 中,说到重用往往是继承和组合。这些重用方法,也可以使用在 COM 程序中,前提是你使用 C++ 开发,并且可以访问所有的源代码。我们把这种类型的重用称为源代码重用。在 COM 中我们还可以使用一些二进制的重用方式:包含(containment/delegation)和聚合(aggregation)。

所谓包含,非常容易理解,就是在你的对象内部包含了指向另一个 对象的接口指针,你通过调用那个接口的方法,来实现自身部分或者全部的功能。这个和 C++ 中的组合几乎一样,区别在于,C++ 中的组合可以把另一个对象完全的包含在自身对象的内部,而 COM 中你只能在内部包含另一个接口的指针。

聚合是一个比较有趣的技术,因为它可以完全使用另一个对象的实 现来取代本身需要实现的某个接口。假设我们要实现的对象为 Outer,我们需要实现的接口是 IPrint,而一个名为 Inner 的对象正巧有这个接口,并且可以直接为我们使用。为了能够做到这点,我们需要以下的实现:首先,在我们创建 Outer 的时候,我们调用 CoCreateInstance 创建一个 Inner 对象,并且把这个对象的指针保留在数据成员中;其次,在我们的 QueryInterface 中,每当客户需要 IPrint 接口的时候,我们把这个请求传递给 Inner 对象的 QueryInterface。乍一看,用户可以通过这种方式很方便的获得重用,但是这违反了 COM 的几个基本原则。首先,虽然我可以从 Outer 对象获得了一个 IPrint 的指针,但是如果我对这个 IPrint 查询某个 Outer 对象实现的接口,它会返回一个错误,因为 Inner 对象并没有实现这个接口。并且,如果我对这个 IPrint 接口查询 IUnknown,它返回的指针和我直接在 Outer 对象的某个接口上查询出的 IUnknown 指针不同,这样,违反了 COM 对对象的标记原则。要解决这个问题的关键点在于,我们必须让 Inner 知道它正在被 Outer 聚合,COM 中已经为我们提供了这个机制。CoCreateInstance 以及 IClassFactory 的 CreateInstance 方法,都有一个参数叫做 IUnknownOuter。当 Outer 在创建 Inner 时,对这个参数传入非 NULL 的值(自己的 IUnknown 指针),Inner 就知道它将被这个 IUnknownOuter 所聚合。这个时候,Inner 需要把这个 IUnknownOuter 指针保存下来,然后把自己真正的 IUnknown 指针返回给 Outer,并且以后每次外界对 IPrint 接口调用 AddRef/Release/QueryInterface 的时候,简单的把这些请求传递给 IUnknownOuter(如果你熟悉传统戏曲的话,应该知道这就是两个对象一起唱的一出双簧),这样一来,可以保证 QueryInterface 的传递性,可逆性以及 IUnknown 对对象的唯一标识。对于 Outer 来说,它会保留那个创建时候返回的 IUnknownInner,并且把对 IPrint 接口的查询传递给它。实现聚合的时候,需要注意以下几点:
- Inner 获得 Outer 的 IUnknown 指针后,不要对它进行 AddRef,否则这两个对象会循环引用,以至于永远也不能释放
- Outer 创建 Inner 的时候,只能要求返回 IUnknown 指针,否则,创建失败。因为如果你获得的是别的接口的指针,你将不能实现我们上面所说的功能
- Outer 的 QueryInterface 不能把所有的查询都无条件的转给 IUnknownInner,而只能传递所需要的接口。否则,Inner 可能会提供给 Outer 的客户一些难以预料的接口。

11. 注册信息

12. C++ 和 COM

有些 C++ 程序员觉得 COM 的某些机制和 C++ 格格不入,并把这个作为 COM 不好的理由之一。常见的抱怨有:我用惯异常了,COM 里面的错误处理不象异常那样可以跳跃多个层次;C++ 的继承多方便,我在 COM 里面实现重用,还要自己写聚合代码;...。其实使用 C++ 来实现或使用 COM 对象的时候,遇到这样的问题,应该是我们检讨自己 C++ 水平,或者检讨 C++ 这个语言(如果某个功能,没有任何人可以很简单的实现的时候)。因为 COM 是一个技术,是一个标准,C++ 是一个通用语言,如果我们都不能够通过正确的使用 C++ 的机制和 OO 的思想来对 COM 做出一个完善的描述,那是 C++ 语言的不足。其实使用 C++ 来创建/使用 COM 对象,非常的方便。譬如说,你可以写一个 COM 的 HRESULT 和 IErrorInfo 到 C++ 的异常的转换函数,你也可以写一个函数使用 try/catch 到的异常来设置 IErrorInfo;你可以实现一套 C++ 类来简单的创建支持聚合的对象;你也可以实现 COM 接口的 smart ptr 来保证释放了每一个接口;...。事实上,Visual C++ 提供的 ATL 以及一些 COM 支持类,实现了大部分这样的功能,我们可以通过创建/扩展/使用这样的类库来简化所遇到的 COM 开发工作。

13. 结语

我在向有些 C++ 程序员介绍 COM 的时候,他们总是问我一句话:这些功能不难,为什么不自己实现?其实这个问题我在自己看 COM 的时候也经常地问自己,尤其是当你看到 MTS,Object Pool,DCOM 以及 LCE 等所谓的 COM 的“高级功能”的时候,你会觉得每一个功能,其背后的实现和想法都很基本。对于这个问题,我给自己的回答是:既然微软提供了一套高效,成熟,无错(相对你自己的实现来说)的基本服务,为什么我们不使用它呢?C++ 程序员和使用别的语言的程序员比起来,往往缺乏安全感,通常我们如果不知道一个东西背后的具体实现究竟是怎样的,我们可能就不会去使用它。所以对大多数 C++ 程序员来说,我们在使用一个新的类库之前都会做一件事情:阅读源代码,这样,当我们使用这个类库的功能的时候,我们可以知道我们写的每一句话究竟做了些什么。在此我不对这种习惯做任何评价(我也是 C++ 程序员之一 ),那么我就希望这篇文章可以让大家对 COM 基本服务究竟在做什么,有一个大概的了解。

其实,我觉得 COM 分为两部分,一部分是 COM 的标准,另一部分是 COM 的基本服务,这两部分相辅相成,给实现 OO 系统的人带来的很多好处。我遇到过不少高手觉得 COM 不值一提,也遇到过不少初学者,觉得 COM 已经过时。无论如何,COM 作为一个 OO 的基本系统,理解它的思想对于设计自己的 OO 系统还是非常有意义的。

走马观花,未能详尽之处,欢迎另行讨论。如果有熟悉 COM 的朋友觉得我遗漏了重要的知识点,请告诉我,我尽量在下一版中加入。