一、进程内组件和进程外组件
前面说过,COM组件支持进程内组件和进程外组件两种方式。进程内组件的优势就是快、方便,但是如果组件里面有哪个地方崩溃了也会导致主进程的崩溃。进程外组件虽然比较慢,但是很稳定。如果组件某个地方崩溃了,最多就是没有结果,不会导致主进程的崩溃(比如,看到了浏览器插件崩溃了,没见到浏览器崩溃吧)。用户在使用COM组件的时候,实际上是感觉不出来进程内组件和进程外组件的区别的。用户在创建COM对象的时候都可以使用一样的方式,这个在本文的后面会详细介绍。客户程序与进程内组件和进程外组件的通信方式实际上差别是相当大的,但是COM库的实现保证了客户程序创建COM对象的进程透明特性。下面来说说进程内组件和进程外组件的使用方式:
1.进程内组件的方式:
客户程序和组件程序是处于同一个进程中的,由于在同一进程中,所以客户程序和组件程序是共享资源的,客户程序可以直接调用接口指针的成员函数。进程内组件程序也被成为进程内服务程序,由于进程内组件的执行效率非常高,所以进程内组件也有非常广泛的应用。进程内组件主要是以动态链接库(DLL)的形式存在(其实有没有其他方式我也不是很清楚~)。在客户程序运行时,客户程序将它所需的DLL的文件映像映射到调用进程的地址空间中(详见《Windows核心编程(第五版)》动态链接库的部分)。所以,DLL也是独立存在的,客户程序A可以使用这个DLL,客户程序B也可以使用这个DLL。但是在调用DLL中的函数之前,客户程序需要知道一些关于这个DLL的一些信息,这样客户程序才能正常调用DLL中的函数。这个信息也就是函数导出信息。这些导出信息的生成有很多方式,可以使用define XXX extern "C" __declspec(dllexport)的方式来生成(这种方式详见《Windows核心编程(第五版)》),但是COM组件一般是不会使用这种方式的。还有一种是写def文件,明确指出需要导出的函数(这种方式可以参见MSDN以及网上的一些介绍,《Windows核心编程(第四版)》中也有介绍,这种方式也很简单)。总的来说,使用进程内组件主要就是使用DLL装载的方式来实现对组件的使用。
2.进程外组件的方式:
这种方式中,客户程序和组件程序不在一个进程中。组件程序会单独跑一个进程,这样就不能和客户程序共享资源了。在Windows平台中,一般使用EXE程序来实现进程外组件。由于客户程序和组件程序处在不同的进程中,肯定会出现跨进程调用函数和传递数据的问题。这时就需要使用进程通信的方式了,关于进程通信,其实我也不懂,但是Windows中的进程通信方式很多。这里我也只了解了一下COM组件的进程间通信。COM采用了本地过程调用(LPC)和远过程调用(RPC)的方式进行进程间的通信。其中LPC用于本地机器的进程间通信,而RPC则用于远程计算机的进程间通信。一个程序如果需要调用其他进程的系统服务,虽然感觉上调用的是一个系统函数而已,这其中实际上也是使用了进程间通信。系统进程一直都在进行,所以要调用系统服务需要使用进程间通信。客户程序调用的实际上是系统的DLL模块,这个DLL一般叫作存根DLL。然后这个存根DLL再和系统进程进行通信,调用系统的服务。COM的进程外组件也是通过类似的方式来进行调用的。只不过调用的是一个代理DLL,而COM组件程序则使用的是存根DLL。然后代理DLL和存根DLL来实现进程间的通信。过程如下图所示:
二、COM对象的管理
COM组件要供给其他客户程序的使用。要使用这些COM组件就得有这些COM组件的信息才能调用COM库的函数来使用这些COM组件。在Windows操作系统中,COM组件的这些信息都保存在注册表中的。这些信息主要有GUID和文件的保存目录。一般来说,组件的创建工作都是由COM库来完成的,但是COM库的很多工作都是依赖于注册表的。在注册表的根节点下有HKEY_CLASSES_ROOT、HKEY_CURRNET_USER、HKEY_LOCAL_MACHINE、HKEY_USERS、HKEY_CURRENT_CONFIG、HKEY_DYN_DATA这6个子键。COM信息一般都保存在HKEY_CLASSES_ROOT的CLSID子键下的。大家可以去看一看,CLSID子键下有很多的数字键,那其实就是传说中的GUID。如果组件是进程外组件,这些子键下会保存代理DLL和存根DLL的信息,这个子键叫做ProxyStubClsid或者ProxyStubClsid32。如果组件是进程内组件的话,有一个子键会叫做InprocServer32。组件对象除了可以用CLSID来标识以外,也可以使用字符串对组件进行命名,这样的名字信息叫做ProgID,这个信息也是以子键保存下来的。如果想要让自己的组件能够被其他程序使用,一定得在注册表中添加组件的信息。这里就不多讲这个东西了,什么时候专门写一篇来写COM组件的注册表信息和其修改。
三、谁来创建COM对象——类厂
讲了这么多也没有说COM对象到底实际上是如何建立的。客户程序通过调用COM库函数来创建COM对象,COM库则通过调用类厂的接口函数来创建COM对象。类厂的代码定义如下:
1 IClassFactory : public IUnknown 2 { 3 public: 4 virtual HRESULT STDMETHODCALLTYPE CreateInstance( IUnknown *pUnkOuter, const IID &iid, void **ppv) = 0; 5 6 virtual HRESULT STDMETHODCALLTYPE LockServer( BOOL fLock) = 0; 7 };类厂本身实际上也是一个COM对象,所以类厂也会继承自IUnknown。一般来说,类厂不需要创建一个GUID来标识类厂。类厂中有个接口函数CreateInstance,这个CreateInstance就是COM库用来创建COM对象的函数。每个类厂只针对特定的COM对象,所以CreateInstance非常清楚自己需要创建什么COM对象。其中pUnknownOuter用于对象类被聚合的情况,一般情况下填NULL就可以了。第二个参数iid则为对象创建完成后客户端程序应该得到的初始接口IID。最后一个参数ppv则保存的是接口指针。另一个接口函数LockServer则用于控制COM组件的生命周期。如果一个客户程序希望在创建了COM对象以后继续使用类厂在以后创建COM对象,那么就应该调用LockServer函数来将组件锁顶起来,不让释放。这样主要是防止用户保存了类厂的接口指针,而接口指针在创建完成以后就被释放,以后如果再用这个接口指针创COM对象则会发生内存访问的错误。所以需要将组件锁起来,当不再继续使用的时候再释放掉。
四、谁来建立类厂
COM对象是由类厂建立的,但是类厂又是谁来建立的了。非常明显,类厂不可能是由类厂的类厂来建立的,类厂是由COM库直接来建立的。所有类厂的接口函数原型都是一模一样的,所以COM库可以调用所有的类厂接口函数。所以COM库可以直接调用类厂的接口函数。这样,客户端程序就可以通过调用COM库提供的函数来间接调用类厂的接口函数来创建COM对象了。
客户端程序调用CoCreateInstance等函数并传入COM对象的GUID(也就是CLSID)等数据,然后CoCreateInstance函数会调用DLL文件中的DllGetClassObject函数,然后该函数根据传入的CLSID来创建指定COM对象对应的类厂,并返回类厂的接口指针和接口的GUID(也就是IID),这里一般返回的是IClassFactory的IID,IID_IClassFactory。然后再由COM库调用类厂提供的接口函数来创建COM对象并返回COM对象的接口指针。