COM对象与C++对象的区别

 

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<<obj.getName()<<endl;
}

有经验的程序员都知道,如果这个类和函数是用 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<<p->getID()<<endl;

这段代码中的 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++ 映射,被认为是一种取巧行为(当然,熟悉的人可以这么做)。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值