COM

来源:互联网 发布:世界地图软件哪个最好 编辑:程序博客网 时间:2024/04/28 08:14
COM的产生
 
一. 为什么要引入COM?
先给一个答案:COM的产生是为了解决OLE技术。当然今天OLE已经变成了COM的一个成功应用例,就好比牛顿当初发明微积分是为了解决物理运动问题,但是今天物理运动问题只是微积分的一个应用而已——以上不是我瞎猜的,是那本InsideCOM的书上说的。
但是这个答案虽然明确,总还有不干不净的尾巴。比如说什么是OLE?为什么要有OLE?OLE有什么难点以至于要引入COM?等等等等,没完没了。所以我宁愿跳过OLE的存在而直接告诉自己,COM的产生只是为了更好的解决重用性问题。
 
我猜测,重用的终极目标应该是任何功能在系统中都有而且只有一份二进制代码,任何程序都是由这些二进制代码组合而成的。为了实现这个目标,重用的单位必须足够的小,这样才不会产生功能上的重复;重用的单位必须足够的独立,这样才能够适应广泛的应用;重用的单位必须有一个标准的管理策略,这样才能被用户程序顺利找到;重用的单位之间必须有标准来规定通信方法,这样所有的小单位才能过无缝的结合成一个整体。
 
如果对应过来,COM思想就正好满足了这些要求——(其实是废话,因为上面的要求就是我根据COM的特性倒猜回去了啊-_-||)。
 
 
二. COM是怎样设计出来的?
我猜测,COM的思想其实是从OO开始,一点点增加出来的。
(1)既然所有的程序说到底都是一段段二进制执行码,那么COM的标准直接针对二进制码也就理所当然了。这样带来的好处显而易见,就是语言的无关性。
(2)OO中的类和对象思想已经提供了一种高内聚低耦合的实现,因此COM也顺理成章的借鉴了这一切。COM中重用的单位是COM对象,其二进制代码需要满足一定的标准。而这个标准来自于Windows平台下,经VC(GCC一样,其实)编译的,用C++写出来的类在实例化成对象后得到的二进制代码。所以为什么COM对象的开头都是一列指针,每个指针指向一列函数呢?因为C++就是这样实现虚函数和多重继承的。
(3)从COM对象逆推回去,就有了COM类的概念。因为开发者不一定用OO语言来实现COM对象,所以COM类不一定能和OO中的类扯上关系,它们的类似之处只在于抽象提取了COM对象的功能。当然,如果用C++来开发,则C++中的类和对象就都可以平移推到COM上了。
(4)C++中一个类可以有多个实例对象,COM也一样。C++通过名字来标识类,利用操作符new和构造函数来获取内存空间和初始化内存内容,得到实例化的对象。COM类用一个全局唯一的ID(GUID)来标识,称为CLSID,COM利用类厂(ClassFactory)来得到实例化的COM对象。
(5)系统用公共空间保存所有可被重用的COM类的CLSID和其具体位置的对应(在Windows下是保存在注册表中),这样所有的用户只要知道CLSID,都能顺利找到COM类。然后COM类可以利用类厂生成COM对象。COM类和类厂的实现代码可以在DLL中,也可以在EXE中,可以在本地,也可以在网络的另一端。
(6)不管如何,使用COM对象的最终目的还是为了用里面的函数。COM对象要实现多个接口(Interface),每个接口都包含一组函数,也用一个GUID来标识,称为IID。接口的二进制代码很简单,就是一个函数指针数组,因此只要知道接口指针就能顺利调用接口的每一个函数。COM统一了一个标准的接口,每个COM类都必须有的。其中一个函数QueryInterface()就可以根据IID来得到接口指针。
 
这样一来,当需要重用某COM对象时,顺序可以被简单写成:
用户根据CLSID找到可重用的COM类所在的位置 → 通过类厂得到COM对象 → 用户根据IID来得到想调用的函数所在的接口→ 调用接口中的某函数指针。
 
这几条能够解决一部分重用性的问题,但是光有这些还远远不够,要完备的解决还有一些其它更细的东西。


COM的接口
 
一. 接口概念的出现
承接COM的目的,现在需要将重用的COM对象相互关联在一起,那么有什么好方法呢?
(1)OO中使用public成员来让外界和内部对象进行数据交互。COM中更进一步,只能使用public的成员函数。因为直接访问对象内存不利于实行低耦合的模型,所以数据的交互都应该使用函数调用。
(2)一个COM对象可以提供多个函数供外部调用,这是很自然的。
(3}类似于OO中的多态,一个函数可以被多个COM对象实现,这样调用方可以方便统一的实现所需的功能。
 
在C++中,我们用public成员函数来提供对外接口,用虚函数来实现多态。因此,对COM的要求,实际上就是需要一个虚基类,其定义了一组函数,然后COM类来继承这个基类,从而也拥有了这组函数。而当COM类要有多个供外界调用的函数时,可以把这些函数分别定义在一些虚基类中,然后再用多重继承的方法使COM类拥有这些函数。
尽管对每个函数都去定义一个虚基类也毫无不可,但很多时候这样分散并不利于管理这些函数。因此常常把一组功能有关联的函数合并在一个虚基类里面。
这个只拥有一组虚函数的基类就是COM中的接口,其目的是定义COM对象被访问的方法。每个接口都被一个GUID标识,称为IID。
 
二. 接口的本质
如果把C++描述转化成二进制代码的话,就会发现接口本质上就是vtable,位于COM对象的开头,指向一组函数指针。那么为什么是接口而不是单个函数被GUID表识,从而能够被准确定位呢?
还是参考C++编写的COM对象在内存中的二进制表示吧。此时其开头是一列指针,分别对应于多重继承而来的各个接口,然后每个指针指向一个函数指针数组,就是对应于各个接口的成员函数。因此,接口和对象在内存中是平级的!C++实现多重继承时只不过是罗列了多个虚函数表,然后调用函数时再根据具体使用的指针类型,给指向对象的指针加上某个偏移量得到该类型对应的虚函数表,再找到具体的函数。显然指针在接口这一层时可以很自如的通过偏移得到其它的接口指针以及对象指针——而一旦得到具体函数指针后,就很难回头了。
 
COM规定了一个函数QueryInterface(),用来得到接口的指针。并把QueryInterface()放入接口类IUnknown中,而且规定所有的接口都要从IUnknown继承,换句话说,所有的接口都要实现IUnknown类定义的那几个函数。QueryInterface()的引入可以让调用方在使用COM对象时,能够在COM对象提供的接口之间自由的来回切换。当然,正如上面所述,其本质只是指向接口的指针做了偏移而已。
 
三. 接口的使用方法
首先,所有COM对象的接口都继承自IUnknown,而IUnknown中是有QueryInterface()函数的。
再次,COM对象的开头就是第一个接口的vtable,所以指向COM对象的指针同时也是指向第一个接口的指针。并且,由于接口都是继承自IUnknown,因此这个指针也一定是指向IUnknown的指针。这样一来的话就能够顺利调用QueryInterface(),得到某个接口的指针了。
然后,不管任何时候,只要有某接口的指针,就可以接着用QueryInterface()来得到该COM对象拥有的其它接口指针。
 
类厂以及COM对象的构造
 
一. 类和对象
在OO语言中操作是基于对象进行的,而对象的抽象归纳就是类。但是这只是分析阶段的思考,当真正编程的时候顺序是倒过来的,先定义类,然后再在程序中将类具体实例化成为对象。以C++为例,先定义好一个类CA,然后在程序中用newCA()的方法就可以得到一个指向CA的对象。不过,我们能通过这样简单的操作来生成对象,其实是因为C++编译器帮我们做了很多工作:分配内存,建立虚函数表,初始化数据,调用构造函数,等等。即便是自己重载new操作符,也要或多或少的借助编译器的智能来实现所有步骤。
但是,编译器无法智能生成C++对象,它唯一的依据就是就是C++类,因此从OO思想上说,类是对象的抽象,但如果在编程中具体的说,类只是生成对象的准则而已。
 
对COM中的概念而言,恰恰和分析的顺序一致而和C++的顺序相反。COM首先根据C++对象在内存中的二进制格式定义了COM对象,然后再把生成COM对象的准则抽象出来成了COM类。因为COM并不一定要用OO实现,所以COM类和OO中的类其实是有差别的……尽管大多数时候我们是用C++来实现,此时就会发现两者本质上是一样的。
 
二. 类厂概念的引入
类厂(ClassFactory)这个名词其实有点迷惑性,因为这个东西实际上应该叫对象工厂。类厂也是一个普通的COM对象,它有一个特殊的接口IClassFactory,这个接口的一个函数CreateInstance()能够生成COM对象,并返回其需要的接口。
如果把C++中的概念平移过来,就会发现类厂的作用本质上就是那个被C++编译器隐藏了的new。在COM中没有类定义,自然也没有new,要想生成COM对象,只能靠COM类的规范。类厂就实现了从COM类规范到COM对象的过程。
 
当用C++实现COM的时候,往往在类厂也就是new出来一个对象,然后做一个QueryInterface()得到接口指针。表面上看,中间多了类厂这么一层有点多此一举,实际上这里隐含了根据抽象的COM类在内存中生成COM对象的步骤,绝非可有可无的。
 
三. 类厂的返回值
在前面说过COM中以COM对象为单位实行重用,COM对象通过接口和外界交互,COM对象的接口之间可以通过偏移来实现跳转。并且,从二进制上看,指向COM对象的指针就是指向COM对象继承的第一个接口的指针。所以,在COM中并不需要一个指向COM对象的指针,而只需要指向该COM对象的某一个接口的指针。因此类厂最后是返回COM对象的一个接口指针来告诉用户,这个COM对象已经生成了。当然,这个接口指针的表识(IID)需要用户提供。


COM对象的调用
 
一. EXE和DLL
不管COM对象有多少花哨的东西,其本质只是一段内存,有数据有代码而已。因此当一个COM对象被使用的时候,是谁,并且怎样把这些数据和代码载入内存呢?
首先,最简单的就是调用者和COM对象在一起,这时候函数调用就是普通的调用,没有任何使用上的障碍。然而这样一来显然无法把COM对象独立出来,自然无法做到重用。因此COM对象首先应该孤立于任何调用者,单独被保存在系统中。这时候作为可执行程序,它可以是DLL,也可以是EXE。
COM对象作为DLL被使用时,调用者会先用LoadLibrary()把DLL整个装入自己的进程空间,然后获取里面类厂的地址,再通过调用类厂的接口函数来生成COM对象。此时COM对象是在用户进程空间内生成的,对用户而言能够更加方便快速的使用COM对象提供的功能。
另外一种方式是COM对象作为EXE被使用。此时调用者会先给当前进程创立一个子进程来运行该EXE,然后通过进程之间的通信来依次调用类厂的接口函数,COM对象的接口函数,等等。
 
这两者区别仅仅在于调用者和COM对象是否存在于同一个进程空间而已。
 
二. COM库
不管是DLL还是EXE,首先是找到这些文件,依据只有我们需要用的COM的CLSID。如前面所述,Windows系统中把COM类的CLSID和其保代码保存在的文件,以及其它一些相关信息都纪录在注册表中。同时,Windows还提供了一系列API函数,可以自由的通过一项信息搜索出其它需要的所有信息。
当能够把CLSID和具体的DLL或EXE文件对号入座后,下一步就是将DLL文件载入进程空间,或者执行EXE文件于另一个进程空间。两者的本质依然是先获取类厂的接口函数指针,然后利用类厂来生成COM对象。
 
问题是,在上面几步中,并没有什么需要特别处理的地方。因此Windows直接提供了一组API来完成这里的需求,用来让代码看上去简洁明了。比如说,用CoInitialize()来初始化COM库,用CoCreateInstance()来通过类厂生成COM对象,等等。这些函数其实就是COM的库函数。
 
三. IPC/LPC/RPC和Proxy/Stub
调用方和COM对象之间,存在几种不同的情况:(1)在同一进程空间(2)在一台机器的不同进程空间(3)在不同的机器上。这几种就分别对应了Inner/Local/Remote三种ProcessCall。
调用时,(1)是最简单的,(2)要复杂一些,(3)是最麻烦的。
 
但是,在Windows下,系统提供了一些功能,能够协助用户来实现进程之间的调用,这就是Proxy和Stub的概念。通信在Proxy和Stub之间进行,这部分是COM库来实现的。用户和Proxy之间的通信,以及Stub和COM对象之间的调用,都是各自进程中的IPC。因此,通过Windows帮忙,LPC/RPC对于用户来说等于没有,这里只有进程内的调用Proxy而已。


包容和聚合
 
一. COM组件的升级/扩展需求
在一开始的分析中,已经假设了COM模型中目标就是单个组件的重用性达到最佳。但是,单个组件发布后,功能就已经固定下来了。随着用户的需求不断增加,很可能现有组件将无法完成好用户的所有需求,因此必须对现有组件进行升级。
然而,对既有组件的升级其实并不是一件简单的事情。首先最初的编写者和后来的升级者不一定是同一个人,他们擅长的编程语言也不一定一样,而且更糟糕的是原有的源代码可能无法看到或者写的非常晦涩。如果做过程序维护的话就应该理解,要想看明白别人的程序是一件多么痛苦的事情。
因此,对COM而言,希望做到的就是,尽可能多的利用原来的二进制代码,增加新的功能。也因此,提出了2种实现的方法。但是,不管怎么实现,最初的重用方针就决定了在升级版的COM对象CExt中一定要有一个原有版本的COM对象COri。CExt创建的时候COri也创建,CExt卸载的时候COri也卸载。花招在于CExt如何使用COri的接口,换句话说,当有一个CExt的接口后,怎样才能使用COri的接口呢?
 
二. 包容
包容是一种很自然的思想。就是在CExt中逐一定义COri的接口,其函数的实现就是调用COri的相应接口的函数。换句话说,CExt定义了所有的接口,然后当这些接口是COri原来就有的时候,其内容变成了一个简单的调用。
 
三. 聚合
聚合的思想比较简单,但实现有一些复杂。聚合中并不重新定义一遍COri的接口,而是在实现QueryInterface()的时候,直接返回CExt内部的COri的相应接口指针。换句话说,如果接口是COri中的话,CExt的QueryInterface()函数将直接调用COri的QueryInterface()函数。
这里最大的问题在于,从CExt到COri没有问题,但是如何能够从COri的接口返回到CExt的接口?它们之间可并不是同一层的。
解决的办法是,从一开始COri的接口就要做一些准备工作。首先,COri中必须有一个指针来表明这个对象是单独使用,还是作为CExt的一部分试用。如果是前者,这个指针为NULL,如果是后者,这个指针指向CExt的头,也就是CExt的IUnknown接口。然后,COri的QueryInterface()必须能够判断该指针是否为空,从而在接口查询时能够在必要的时候转而调用CExt的QueryInterface()。
为了使用方便,可以把原来的IUnknown接口以及接口中的函数用另外的名字标记,比如INondelegatingUnknown(书上用这个名字)什么的。当COri不是在被聚合使用的时候,IUnknown接口的函数就去直接调用INondelegatingUnknown接口的函数,实行相应的功能。当COri是被聚合使用的坏死后,IUnknown接口的函数就去调用保存的外部聚合对象CExt的IUnknown接口的函数。
 
四. 包容和聚合的比较
包容比较简单,适用面也大,所有的COM组件都可以被包容。但是包容实际上给CExt多定义了COri的接口函数,做的并不算很漂亮。而且由于这些虚函数都是后期绑定,在编译期无法像一般的函数调用已经直接地址转移节约时间,因此包容的效率比较差。
聚合是真正符合COM要求的重用,其中并没有重复的工作。但是聚合必须要原有的COM组件支持,比如说原有的COM组件有没有实现INondelegatingUnknown接口之类。


AddRef和Release
 
一. IUnknown接口
按照COM标准,所有的COM接口的前三个函数都必须是IUnknown接口的那三个函数:QueryInterface(),AddRef()和Release()。如果用C++表述的话,就是所有的COM接口都必须从IUnknown这个虚基类继承而来。
QueryInterface()的作用前面已经说过了,是根据IID查询当前COM对象是否有此接口,并返回接口指针。那么AddRef()和Release()呢?
按照字面的意思,AddRef()的意思就是说增加当此接口被引用的次数,而Release()则是释放。实际上也差不多就是这么回事……虽然Release()表面上看起来起一个SubRef()的名字能够更加和AddRef()匹配一点。
 
二. COM对象的创建过程和引用计数的需求
如果按照一般的思想,COM对象被创建后,大家自由使用就是了,为了什么非要引入AddRef()和Release()函数?其实这里涉及到的问题主要是COM对象的生存期问题。一个COM对象何时被谁创建?何时又被谁释放呢?
最自然的回答肯定是需要时创建以实现应用,不需要时释放以节约系统资源。但是这里实现就有很多问题:首先,按照前面所述,客户并不真正的了解COM对象,它只能提供CLSID来定位COM对象,提供IID来查询接口,然后能做的就是利用接口实现功能。在Windows的COM库中,用CoCreateInstance()函数来封装客户端的调用,然后CoCreateInstance()根据CLSID在注册表中找到实现该对象保存的文件,再根据调用方式的不同(进程内/进程外)将该文件装载入内存,创建类厂,然后用类厂的CreateInstance()接口创建COM对象并返回IID指定的接口。这一连串的工作分的很细,主要的目的就是用中间层,比如COM库函数和标准IClassFactory接口等隔开用户和具体COM对象,实现更好地封装。
既然如此,具体生成COM对象的并不是客户端而是COM组件中和COM对象对应的类厂对象。因此,释放或者说从内存中卸载COM对象的任务也不能是客户端完成。而在COM组件中,类厂只管生成,那么释放的任务就只能交给COM对象自己完成了。
 
所以,最后的要求就变成了COM对象自己需要知道什么时候能够释放自身,那么就需要有一个量来表示现在到底又多少用户在使用此COM对象,这就是引用计数了。
 
三. 引用计数的实现
实现引用计数的方法很简单,用一个全局的变量来保存计数,多一个引用时加一,少一个引用时减一。COM规定当创建COM对象时先把计数从0加到1,然后加加减减,直到计数变到0,说明已经没有用户使用该COM对象,那么这个就可以释放资源了。
由于客户端只能对接口操作,因此AddRef()和Release()需要保证能够在任何接口下都能调用,包括IUnknown。这样一来,这两个函数和QueryInterface()并列成为IUnknown的三个成员也就顺理成章了。

这里还有一些小问题。比如说是针对COM对象整体计数呢,还是针对各个接口计数?COM标准没有硬性规定,但是作为COM对象的使用者,客户端必须考虑到不同情况,所以必须是调用增加或减少引用的那个接口的AddRef()和Release()。

COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatch。

三、对象创建函数:
1、CoGetClassObject 获得对象的类厂。
2、CoCreateInstance 创建COM对象。CoCreateInstance其实是对COM对象创建过程的封装。其过程是这样的:a.客户程序调用CoCreateInstance函数 b.CoCreateInstance函数调用CoGetClassObject函数 c.客户程序调用CoInitialize初始化COM库,COM库开始运行。d.COM库找到组件程序DLL,并进入它。e.在组件DLL里调用DLLGetClassObject函数,这个函数用于创建类厂,创建完类厂后把类厂对象的指针返回给CoGetClassObject函数。f.CoGetClassObject函数把类厂指针返回给CoCreateInstance函数,然后它调用类厂对象创建函数。g.类厂创建COM对象。h.类厂把COM对象返回给CoCreateInstance函数,CoCreateInstance函数返回。i.客户程序直接调用COM对象。
3、CoCreateInstanceEx 创建COM对象,可指定多个接口或远程对象,这是为了避免客户程序与COM组件的频繁交互。这个函数用于组件外调用。
4、CoRegisterClassObject  注册一个类厂接口,类厂接口是组件程序一启动就创建好了的,无论客户程序是否调用。
5、CoRevokeClassObject 与CoRegisterClassObject配套使用。
6、CoDisconnectObject 断开其它应用程序与对象的连接。
四、内存管理函数:
1、CoTaskMemAlloc 内存分配函数.当客户程序调用COM组件的一个功能接口时,如果这个接口函数的参数有[out]的参数,并且这个参数不是整形或浮点形时就要调用这个函数来审请一块内存。
2、CoTaskMemRealloc 重新分配内存。
3、CoTaskMemFree 与CoTaskMemAlloc函数配套使用。
4、CoGetMalloc 获取COM库的内存管理器接口。

一、初始化函数:
1、CoBuildVersion 获取COM库的版本号。COM库也是在不断的升级的,这样会出现不同的版本号,当客户程序想要获取COM库的新功能和性能时必须要进行版本的检查,版本号分为主版本号和次版本号,主版本号放在返回值的高16位,次版本放在低16位。一般只要比较主版本号就可以了。
2、CoInitialize COM库的初始化。COM库只有初始化后才可以使用,CoInitialize有一个参数,一个IMalloc的内存接口器,用于COM库内存的分配管理工作,如果CoInitialize参数指定为NULL,则由系统提供默认的管理器。CoInitialize返回三种值:S_OK 初始化成功,S_FALSE COM库已经在线程当中初始化了,RPC_E_CHANGE_MODE 初始化失败。
3、CoUninitialize COM库功能服务终止。当调用CoUninitialize时,CoUninitialize所在的DLL也将终止服务退出内存。CoUninitialize与CoInitalize配套使用。
4、CoFreeUnusedLibraries 自动释放不再使用的DLL。注意,当调用CoFreeUnusedLibraries时,所调用的DLL并不是立即就退出的,而是要有一定的延迟。这是因为当有多个线程运行时,线程之间会因为争夺资源而产生某一线程暂停,CoFreeUnusedLibraries会误以为这是不再使用的线程,所以会去释放它,而实际它只是暂停而已。所以CoFreeUnusedLibraries一般会有十分钟的延迟时间。解决方法就是在代码里设置一个定时器,定时有调用CoFreeUnusedLibraries。
二、跟GUID有关的函数:
1、IsEqualGUID/IsEqualIID/IsEqualCLSID/三个函数分别是判断两个GUID/IID/CLSID是否相等。
2、CLSIDFromProgID 根据ProgID的值在注册表里找到对应的CLSID的值,ProgID是为了方便记忆CLSID而取一个字符串名字。
3、StringFromClSID 把CLISD的结构形式转换成字符串的形式。

typedef struct GUID
{
DWORD data1;//0x12345678
WORD data2; //0x1234
WORD data3; //0x5678
BYTE data4[8];//0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88
}GUID


COM组件的存在方式:DLL文件或者EXE文件;
COM组件包括COM对象;
COM对象包括COM接口;
COM对象之间通信方式COM接口;
COM接口为一组函数的集合;

GUID:全局唯一标识符,类似的ID有,CLSID,IID等,根据用途不同进行的命名。



0 0
原创粉丝点击