返回首页 | 新的开始 »

关于 COM 的无责任评论

这是前段写在老的留言本上的帖子。<a href="http://www.codingnow.com/2004/board/view.php?paster=786&reply=0">http://www.codingnow.com/2004/board/view.php?paster=786&reply=0</a>

为什么有 COM ?我的理解是,MS 要给对象的二进制表达规范一种标准,好做到二进制模块的复用。到 COM 的诞生日为止,C++ 是最适合实现模块对象化的语言,直到现在 C++ 依旧是可以用面向对象的方式直接生成的本地码的最佳工具。可惜 C++ 的实现方案并没有标准,比如对象中数据的布局规则,函数调用的参数传递方式,返回值的处理方式,多继承的实现,等等。

MS 在设计 COM 时采取最小原则,需要最少的统一的 C++ 实现方式,那就是虚表的结构。要求虚表必须放在对象物理地址的头四个字节,其它的都不要求了。所以我们看到的是,

1. COM 必须要求界面是一个纯虚类,也就是说不准把对象内部的变量暴露出来,不准有非虚的函数,这样,所有的函数入口地址信息都可以从虚表取到。2. 接口不准多继承,这样保证了接口虚表的唯一性。

但是,对象的实现可以是多继承多个接口的。为了解决这个问题,COM 在 IUNKNOWN 里要求每个对象都必须实现 QueryInterface 。这个函数其实就是为了解决不同的 C++ 编译器对多继承的实现不一致的情况设定的。

比如有 class A ; class B;class C : public A, public BA* a = &ObjectC;B* b = &ObjectC;这两条代码的实现很可能在不同的编译器上不一样, a 和 b 的值一般情况下也不等同。

同一个对象 C ,用 C 指针, B 指针,和 A 指针指向它的时候物理地址可以是不一样的。只有提供一个转换方法,也就是 COM 中要求的 QueryInterface,才可以统一正确的解决这个问题。

COM 要求 QueryInterface 的实现,就是给 COM 的对象提供了一种 C++ 中 dynamic_cast 的能力。正因如此,COM 要求 QueryInterface 有和dynamic_cast 同样的特性。即从 A QueryInterface 到 B 能够成功的话,B也可以 QueryInterface 回 A 。只要是同一个对象,对象中任意两个接口间都要求可以自由的转换。

现在我们来看为什么 IUnknown 中必须实现 AddRef 和 Release 。我们知道,C++ 的对象是不要求一定要保留一个自己的引用计数的。COM 为什么把这个作为一个必须的要求?

我认为,还是因为 COM 可以支持对象同时提供多个不相关接口所致。假设一个对象同时提供了一个阿猫接口,又提供了一个阿狗接口。这个对象可以看成是阿猫阿狗的聚合体,阿猫阿狗是同一层次没有从属关系的。当我们把这个对象分别交给两个东西去操作的时候,一旦使用完毕,我们无法决定是阿猫的部分决定释放自己,还是阿狗来释放,这是无法从程序逻辑上控制的。所以必须给对象加一个接口引用次数的机制。

以上,IUnknown 的三个必须实现的方法,即每个 COM 对象都必要的方法。的确是用来模拟 C++ 对象最精练的方式。但我认为,在实际运用中,这并不是最合理的结构。首先我们并不需要对象要严格的遵循 C++ 对象的实现。比如阿猫和阿狗的聚合体,一旦分离了阿猫和阿狗的接口,那么从阿猫中得到阿狗或者从阿狗中得到阿猫都是没有意义的。

其次,许多对象都是对单一接口的实现,而无须多继承多个接口。这种情况下,QueryInterface 几乎不被使用。转之,AddRef 和 Release 也非必要的需求。对接口的引用计数这里实际是对对象的引用次数的计数,而对对象的引用计数实际上是可以由引用者来计的,可以把引用量记在对象之外而不是之内。我们可以把工程中碰到的对象分成两类,一类是单件,永远都只有一份实例,外部保留住引用次数就可以保证最后安全的被销毁。而另一类,是从 Factory可以重复生产出多份的对象。后一种对象有开发人员显式的构造和销毁的过程,我们看这一过程,如果按 COM 的实现,构造的过程是由 Factory 完成的,而销毁的过程是由对象自己实现(调用 Release)。这不奇怪吗?按照我们普遍的设计原则,构造和销毁应该在同一层次上,由同一个东西来操作的。我现在在设计新的游戏引擎的底层架构,原本模仿 COM 来做的。最近越来越觉得有些问题,不吐不快。已经下决心重新设计一套更合适的方案了。

ps. 谢谢孟岩提供了一篇参考文章 <a href="http://www.relisoft.com/win32/olerant.html">http://www.relisoft.com/win32/olerant.html</a> 前段时间看过,却没仔细考量。昨天一夜未眠,仔细考虑引擎结构的问题时,又回忆起那些内容,方觉得又有进一步的理解。

Comments

资源的释放上还是需要主动一点,所谓“有借有还,再借不难”,借和还都是主动的行为

COM的设计是精巧的,也是非常成功的,是微软组件化技术的基础。mozilla的XPCOM也采用了这种思想。
正如c有c的规则,c++有c++的规则。

COM有COM的规则。COM是一种规范,和语言层面的东西没有什么可比性。至于说到OO,那是程序员的事情,COM不懂OO。
从面向对象的观点,比如继承,虚拟表这些,或者从程序语言特性来考虑COM是不合适的,这些都是实现的问题了。需要从组件的观点来分析。

AddRef, Release和垃圾收集不一定要扯上关系。AddRef的本意是告诉组件某客户要使用这个接口,Release是告诉组件某客户不再使用这个接口了。至于组件要干什么,完全是组件自己的事情。

QueryInterface的作用不是用来“猜测”是否有别的接口的,如果理解还停留在这里,说明还没有领会IUNKOWN的含义。
QueryInterface是用来“拿”某个使用者已知的接口的。GetInterface可能恰当些。

接口是一组行为的集合,一个组件可以支持多组行为。这样设计是很恰当的。飞机可以支持IFly和ILanding两个接口,作为组件使用者的飞行员在驾驶过程中可以使用IFly进行飞行,也使用ILanding进行降落。飞行员当然是知道这两个接口的,而且知道每个接口的细节,他如果还要去“猜测”,事情就不对了。

当然微软在这个简单规范的基础上还提供了很多。在很多方面帮助组件开发者: 如资源的分配,组件的复用,线程模型问题(对一般的开发员,开发多线程程序是很有挑战的,COM里几个选项就搞好了),进程间的通讯等等。

简化的例子:
task A(){
VMSG vMsg(cmdid);
DispMsg(vMsg);
}

Class Model1{
virtual OnMsg(VMSG *pvMsg){
switch(pvMsg->msg){
case cmdid:
DoFoo(pvMsg);
}
}
}

我“发明”了一种“软总线”思想,把各模块独立出来。命令以msg前缀,结果以mev为前缀,即结果/事件。各模块可“安装”到总线上,也“拔下”。http://community.csdn.net/Expert/topic/5429/5429487.xml?temp=.1593592

个人认为在资源的释放上还是需要主动一点,所谓“有借有还,再借不难”,借和还都是主动的行为。我想,如果操作系统有思想,它的心里也许会更加好受一些吧,不然,操作系统成了要债的了,真的可怜。

IUnknown的三个方法其实是高级语言必要的语言特性的最简单实现。他不是一个C++对象,而是一个高级语言(比如VB)的对象。

AddRef和Release是垃圾收集。QueryInterface则是dynamic_cast的语言无关版本。

而类型转换对于高级语言来说其实是一种不好的特性。一个接口之应该具有接口自己的能力,而不应该可以“猜测”是否有别的接口。所以QueryInterface不是必要的。

而AddRef和Release只是对象生命周期管理的一种方式,不是唯一的方式,这种方式和C/C++要求构造和销毁应该在同一层次上的原则是不同的。AddRef/Release其实是基于垃圾收集的生命周期管理,而且还是一种会导致循环引用的,不完善的垃圾收集。

Post a comment

非这个主题相关的留言请到:留言本