Com Introduction
(wang hailong)
1.神话
我两年前学习使用COM,现在想起那段经历,还有些困惑不解。
我不明白,人们为什么要把一些很明了的事情,弄得玄之又璇,而对一些真正的杰出特性却避而不谈?我写本文的目的,是希望对COM感兴趣的开发人员,不再重蹈我的覆辙,不再被一些说法和资料误导。
刚开始接触到COM的概念,我虚心求教,从各方面得知了如下的COM神话:
(1)COM是位置无关的。你不用知道COM组件放在哪里。
(2)COM是二进制标准,语言无关。你可以用多种编程语言开发COM组件,调用COM组件。
等等。太神奇了。简直象魔术一般。
我这个一头雾水的初学者,对神秘的COM充满了敬仰,却又无从下手。
后来,我只好停止学习这些概念,直接从MSDN入手。我运行所有的COM例子,阅读MSDN资料。不由得惊叹,COM的构思之巧妙,但这些巧妙之处,却鲜有人提及。而那些被传得沸沸扬扬的神话,有很多故弄玄虚的成分。
(1)COM是位置无关的。你不用知道COM组件放在哪里。
是的,你不用知道COM组件放在哪里。但是你需要知道一个COM组件的ID(一个保证独一无二的数字串),这个组件ID存放在windows注册表里,里面记载着这个COM组件的位置。当你调用这个组件的时候,你需要把COM组件ID作为参数,获取这个组件。Windows系统根据这个组件ID,查找注册表,找到组件的位置,启动或者返回对应的组件。
等一下,问题不是这么简单。COM组件需要从组件工厂创建,所以,你获得的组件ID是组件工厂的ID,你先创建一个组件工厂,然后,通过这个组件工厂,创建你所需要的组件。(注意,你通过“工厂接口”使用组件工厂。)
组件工厂根据什么来创建你需要的组件呢?通过接口ID。接口ID也是一个保证独一无二的数字串,这个接口ID对应一个接口的定义。你的头文件必须包含这个接口的定义。
你把接口ID传给组件工厂,组件工厂返回一个组件接口给你。你通过这个接口,调用组件的功能。这就是,“你的头文件必须包含这个接口的定义”的原因。
我们来看,我们不需要位置信息,那我们需要什么信息?组件ID,接口ID,接口定义,(当然,还有“工厂接口”的定义)。
这种复杂的调用是一个优点?还是一个缺点?显然是一个缺点,却被粉饰成一个神话般的优点。这种方法,是为了把部署信息,尽量集中到注册表里面。你只要把组件正确注册,客户程序就可以正确运行。单纯为了部署的目的,这种代价是否值得?
如果只是为了部署的目的,我觉得,这种代价不值得。我们看看Java和.Net工程里面各种配置文件的使用,就知道,有很多更灵活有效的部署方法。
COM的构思远远超过了部署的目的,下一章会讲到。
(2)COM是二进制标准,语言无关。你可以用多种编程语言开发COM组件,调用COM组件。
COM确实是二进制标准,核心是一个VTable(虚表),这个“虚表”的概念和C++类的虚函数表的概念一样。所以,用C++实现COM,非常自然。
前面提到,你如果想使用组件,你必须先知道这个组件的接口定义,你的头文件必须包含这个接口的定义。假如我们是用C++实现COM,工程里面会包括一个C++头文件,头文件里面包括了接口的定义。如果客户端也使用C++,只要把这个C++头文件拷贝给用户就行了,用户把头文件包括在C++工程里面,就可以使用这个接口了。
如果你不用C++呢?那也好办,我不是把提供C++头文件给你了吗?里面已经包含了接口的定义。你先研究一下,这个接口的虚表是怎么构成的,然后,你用你的语言,照样写一个头文件,只要遵守我的虚表结构就行了。
:-) 只是一个玩笑。
我还有个折衷的办法,您用过CORBA吗?CORBA使用IDL进行接口定义,您可以用工具把IDL翻译成各种语言,C++,Java,等等。我也用MIDL写一套接口定义,您也可以用工具把MIDL翻译成各种语言,C++,VB,Delphi等等。什么?您没有这种工具?那么您写一个这样的工具就行了,也不是很难,只不过是生成一个虚表结构。
好了,我们有MIDL作为公用的交流语言,现在问题都解决了。您还有什么问题吗?哦,您需要头文件。您用什么语言?C++?好,我把C++头文件发给您。还有那位朋友,您使用什么语言?您用其它的语言,好,我把MIDL头文件发给您。您找到一个转换工具,把MIDL转换成您的语言就行了。
哎呀,这么多人来索要头文件,太麻烦了。我还有个方法。我把这些接口定义信息,和组件一起放在注册表里吧。我不是给了你组件ID吗?你到注册表里查到这个组件,里面会有一个叫做“类型库”的东西。好了,现在您可以从“类型库”导出您的头文件了。什么,您问我怎么用“类型库”?对不起,我忘了告诉您,您得通过ITypeInfo接口使用它。
您嫌麻烦?一点都不麻烦,您看,VC,VB,Delphi都提供了自动导出组件接口定义的向导。这些事情都不用您亲自动手。
现在,我们有了一套完整的解决方案。COM是二进制标准,语言无关。
您还有问题,VB不支持指针,怎么调用虚表VTable?您为什么要用VB调用COM呢?好吧,我再多做一步工作,让我的COM组件支持IDispathch接口,IDispathch接口叫做自动化接口。IDispathch接口会查表,你把函数名交给IDispathch接口,IDispathch接口会自动把请求派发,进行处理。C++的用户也不用着急,我还保留以前的虚表VTable。现在,我给我的组件接口命名为“双接口”dual interface.
终于松了一口气,现在,我们有了一套完整的解决方案。COM是二进制标准,语言无关。
2. “群件”—— powered by queryInterface
“群件”是Lotus莲花公司的概念,我这里借用一下,表达我对COM组件的赞叹。
COM组件的核心思想就是“二进制接口”——VTable。
我们来看IUnknow接口的三个方法。
addRef和release两个方法,管理组件引用计数,我对这两个方法,持保留态度(negative opinion :-)。
第三个方法,queryInterface,才是整个COM组件思想的精华体现。
通过queryInterface,你可以获取另一个接口,另一种组件服务。
为什么我把COM组件称为“群件”,原因就在于此。每个COM组件都支持一组接口,一“群”接口,一组功能服务,而不是单一的功能。
可能会有人说,这也太绝对了吧,有的时候,人们只需要一个简单功能的组件。我要说,最简单的COM组件,也要实现两个接口——其中一个是IUnkonw,另一个是自己定义的接口。(我这里有些抬杠了。:-)
queryInterface如此出色。我在EJB和CORBA中,都看不到类似的东西。也许有类似的情况,比如,组件的一个方法返回另一个组件。但远远做不到COM组件这种自然的程度。COM组件天生就支持一“群”接口。
下面我们来分析queryInterface的工作原理。
为了说明白这个问题,我先引入一些java和Design Pattern的概念。
Java对象能够实现多个接口。外部程序在使用Java对象的时候,有时候,就要判断Java对象是否支持某一个接口。比如,当外部程序要比较两个Java对象的大小的时候,就需要判断这些Java对象是否支持Comparable接口。诸如这样的语句if(obj instanceof Comparable)。我们来看,这个语句多么象queryInterface。queryInterface也是用来查询组件是否支持某个接口的。
我们在进行类设计的时候,不希望只提供一个很“宽”的接口,包含所有操作。总是试图为不同的调用者分配不同的“角色”,即,提供一组比较“窄”的接口。为不同要求,不同权限的调用者,返回不同的接口。在Design Pattern中,这称为Adapter Pattern。
COM组件正是基于Adapter Pattern创建的。首先,你获得IUnknow接口,你需要某个服务的时候,才从IUnknow接口出发,去查找特定的接口。
通过上面的分析,我们可以看到,Java对象和COM组件很相像。但两者的实现机制却大不相同,Java对象根据Java语言的特性实现,所有的接口实现都包含同一个类中。又有人要说了,Java对象也可以包含一些其它对象,把一些操作交给这些对象。我这里说的是,这同一个类必须显示地声明,实现所有的接口。比如
class MyObject implements Comparable, Serializable, Interface1, …, interface n.
只有这样,obj instanceof Comparable,obj instanceof Interface1这样的语句才能返回真。
COM组件没有这样的限制。COM组件只返回二进制的VTable。COM组件本身可以不实现这些VTable,可以返回其它组件的VTable。不同的接口,可以在同一个组件中实现,也可以在不同的组件中实现。这也就是COM重用的“聚合”和“包含”等概念。
而且,COM组件的生存状态也很灵活,可以存在于客户程序的进程空间,称为进程内组件,也可以存在于独立于客户程序的进程空间,称为进程间组件。
当一个COM组件(我们称这个组件为A)的queryInterface方法返回其它组件(我们称这个组件为B)时,我们来看,现在,A组件的地位很像B组件的组件工厂,通过A组件创建B组件。我们现在可以明白,为什么不直接创建组件,而要采用一种不直观的方法,通过另一个组件——工厂组件来创建组件。这样,组件创建的概念就统一起来了——组件通过工厂组件创建。
我不知道,这是一种巧合,还是一种设计上的深思熟虑。不管怎样,COM组件的设计构思给了我深深的震撼,真是巧夺天工。
注意,由于queryInterface的自反性要求,实际上的处理要复杂一些。queryInterface的自反性:组件A的queryInterface返回组件B的接口,那么,调用这个组件B的queryInterface,也能够查到组件A的接口。还有甚者,如果组件B的queryInterface返回组件C的接口,组件C的queryInterface,也能够查到组件A的接口。
想到queryInterface的种种好处,这些复杂程度,还是可以忍受的。:-)
这还不是COM的全部,只是COM的基础原理。更精彩的是建立在COM基础的一系列技术,Structured Storage,OLE Document,Automation,ActiveX等等。每一种技术都非常出色,构思之巧妙,令人赞叹不已。COM标准给我们带来的这么多麻烦,似乎都可以忍受了。
强烈建议使用这些建立在COM基础上的技术,至少尝试体会其中的思想,会有很多启发。从头开始,定义开发自己的基础COM组件?:-) 我不知道。限于眼界,我还从来没有看到过任何出色的基础COM组件,至少没有IStorage,IDispatch等微软自己定义的接口出色。
微软很多杰出的应用程序都是COM组件。Office系列,Word, Excel, PowerPoint, IE等等。其他的应用程序厂家,以前提供自己的插件标准,现在提供自己的COM组件。