COM原理与应用之COM的实现
1、COM的实现与操作系统平台密切相关
因为COM最初源于Microsoft Windows平台,所以COM实现部分(即COM库)很多地方直接用到了Windows系统的一些特性,比如系统注册表、动态连接库等等,但实际上 COM是一个与平台无关的组件软件模型。Windows上使用的COM标准只是COM的一个具体实现。
2、COM的实现方法
进程内组件(DLL ,in-process component)。
进程外组件(EXE ,out –of-process component)。
3、DLL程序的创建方法
(1)创建一个DLL工程
(2)创建DLL时,应该使用_stdcall调用习惯引出函数,并使用extern “C”说明符。这样能够保证与其他编译器和编程语言的兼容。
(3)按照传统的编程方法,编写一个DEF文件,用来描述DLL程序的模块信息,即列出所有引出函数,并给每个引出函数分配一个唯一的序号。在Win32平台上,可以不使用DEF文件,而直接在函数说明时使用_declspec(dllexport)说明符,如下:
extern “C” _declspec(dllexport) int _stdcall MyFunction();
4、客户程序操作DLL程序的三个系统函数
LoadLibrary,装载DLL模块函数
GetProcAddess,取引出函数地址的函数
FreeLibrary,释放DLL程序的函数
5、DLL的三点说明
(1)对于进程内组件,因为客户程序与DLL程序在同一个地址空间,所以,DLL程序不仅可以引出函数,也可以引出全局变量。
(2)VC++提供了使用工具DumpBin,通过/EXPORTS选项可以列出DLL程序中的所有被引出的信息。(实际运行中需要LINK.EXE和MSPDB60.DLL的支持)
(3)如果客户程序本身也是一个DLL程序,,则它一定要先被装入到进程空间中。
6、进程外组件与客户通信跨跃进程边界协同工作
(1)两个问题
一个进程如何调用另一个进程中的函数。
参数如何从一个进程被传递到另一个进程中。
(2)Windows平台上不同进程间通信的方法
动态数据交换(DDE)
命名管道(named pipe)
共享内存
(3)COM采用的进程间通信的方法
本地过程调用(LPC,local procedure call),用在同一机器的不同进程间的通信。
远过程调用(RPC),用于不同机器间的进程间通信。
跨进程通信是操作系统实现的重要部分,而且,在系统底层实现跨进程操作更为便利,因为在系统一级,它可以控制应用进程的资源分配,包括逻辑内存空间到物理内存的映射、CPU时间的调度等。从控制能力来讲,操作系统可以调用任何一个进程中的函数。
(4)应用程序调用其他进程中系统服务的过程
上图的调用过程中涉及到跨进程操作,实际上也就是用到LPC。应用A实际上调用的是系统模块DLL,其称为存根(stub)模块。
(5)客户程序和进程外组件之间的调用关系
进程外组件较进程内组件的效率要低,但在跨进程的调用中也为客户程序带来了安全性。在代理DLL和存根DLL之间的通信是通过列集和散集(对参数和返回值进行翻译和传递)操作来完成的。
如果只考虑客户程序和组件程序,那么事情就简化的多了,客户程序调用接口成员函数就好像是直接进行的,如上图虚线所示。按照这样简化的结构,进程外组件和进程内组件就有了一致的模型了,COM正是通过这种方式实现进程模型的透明特性的。如果组件程序运行在不同的机器上,则代理DLL和存根DLL就通过RPC方式进行网络上的过程调用,从而实现分布式组件对象模型,所有这些特性的实现可以完全建立在操作系统提供的LPC和RPC模块基础上,而不需要应用系统开发人员去考虑这些底层的细节技术问题。
(6)进程外组件的实现
除了实现组件程序外,还应给实现代理DLL和存根DLL两个程序模块,它们只与 COM接口有关,只负责接口成员函数调用过程中的中间处理工作。如果使用自定义的COM接口,则应该建立自己的DLL程序;如果使用COM预定义的标准接口或者OLE接口,则可以直接使用系统提供的DLL,COM库会为我们处理这些细节。
7、通过注册表管理COM对象
通常,组件对象的创建工作由COM来完成,COM库通过系统注册表(systrm registry)所提供的信息进行组件的创建工作。
8、COM组件注册信息
注册表结构与COM库是直接相关的,实现COM库时必须同时定义出注册表的结构。在Windows平台上,COM库所要求的注册信息都被放到其注册表中。RegEdit.exe可以用来编辑注册表。
COM组件信息在HKEY_CLASSES_ROOT的CLSID下。若是进程内组件,则组件的CLSID子键下包含了InprocServer32子键;若是进程外组件,则组件的CLSID子键下包含了LocalServer32子键,它们的缺省值为组件程序的全路径文件名。与CLSID同一层上,Interface子键给出了当前系统中一些COM接口的配置信息,TypeLib子键给出了当前系统中类型库的信息。代理DLL和存根DLL的信息分别保存在CLSID下的ProxyStubClsid或ProxyStubClsid32子键下。CLSID下的ProgID(program identifier ,程序标识符)子键记录了组件对象的类标识符,COM提供了两个API函数CLSIDFromProgID和ProgIDFromCLSID,用于在 128位正数值和类标识符之间转换。
COM提供了在注册表中对COM组件进行分类的机制,分类的原理很简单,如果COM组件支持同样的一组接口,则可以把它们分到统一类中,一个组件对象可以被分到多个类中。
类别信息也用一个GUID来描述,称为CATID。类别特性只是COM对象的部分特性,如果COM对象要加入到某个类别中,则它必须实现该类别指定的多有接口。在HKEY_CLASSES_ROOT键下有一个子键“Component Gategories”,包含了当前机器上所有的组件类别,其中列出了每个组件类别的CATID。用VC++提供的OleView.exe工具可以看到类别信息。
9、COM组件的注册操作
当组件程序被安装到机器上之后,必须通过注册才能使客户程序通过注册表使用它。进程内组件不能直接运行,所以必须通过其他进程调用才能获得控制,而进程外组件可以直接运行,可以在执行过程中完成自身的注册操作。Windows系统工具RegSer32.exe,可用于注册进程内组件,大需要进程内组件提供相应的入口函数DllRegisterServer和DllUnregisterServer。
10、客户程序如何使用组件程序
客户程序并不像第二章中那样直接调用组件程序的引出函数CreateObject,而是调用COM库的函数进行组件对象的创建工作,COM库的创建函数根据注册表的信息并调用组件程序的入口函数来创建组件对象。组件程序需要提供一个标准的入口函数DllGetObjectClass,用于提供本组件程序的组件信息。
11、类厂
类厂可称为“对象厂”,因为类厂是COM对象的生产基地,COM库通过类厂创建COM对象。对应每一个COM类,有一个类厂专门用于该COM类的对象创建操作。类厂本身也是一个COM对象,它支持一个特殊的接口:IClassFactory。
接口IClassFactory的成员函数CreateInstance用于创建对应的COM对象,LockServer用于控制组件的生存周期。
12、类厂的使用
因为类厂本身也是一个COM对象,用于其他COM对象的创建过程。它由引出函数DllGetObjectClass创建,DllGetObjectClass函数并不是COM库函数,而是由组件程序实现的引出函数。
COM库在接到对象创建的指令后,它要调用进程内组件的DllGetObjectClass函数,由该函数创建类厂对象,并返回类厂对象的接口指针,COM库或者客户一旦有了类厂的接口指针,它们就可以通过类厂接口IClassFactory的成员函数CreateInstance创建相应的 COM对象。
13、COM库与类厂的交互
在COM库中,有三个API函数可用于对象的创建,它们分别是CoGetClassObject、CoCreateInstance和 CoCreateInstanceEx。通常情况下,客户程序调用其中之一完成对象的创建,并返回对象的初始化接口指针。COM库与类厂也通过这三个函数进行交互。
COM对象是进程内组件对象:CoGetClassObject调用DLL模块的DllGetClassObject引出函数,把参数clsid、iid 和ppv传给DllGetClassObject函数,由DllGetClassObject创建类厂,并返回类厂对象接口指针。
COM对象是进程外组件对象:情形复杂得多。首先CoGetClassObject函数启动组件进程,然后一直等待,直到组件进程把它支持的COM类对象的类厂注册到COM中,于是,CoGetClassObject函数把COM中相应的类厂信息返回。因此,组件外进程被COM库启动时(带命令行参数 “/Embedding”),它必须把所支持的COM类的类厂对象通过CoRegisterClassObject函数注册到COM中,以便COM库创建 COM对象使用。当进程退出时,必须调用CoRevokeClassObject函数以便通知COM它所注册的类厂对象不再有效。组件程序调用 CoRegisterClassObject函数和CoRevokeClassObject函数必须配对,以保证COM信息的一致性。
关于三个创建对象的函数的选择:
(1) 如果创建远程对象或者希望一次获得对象的多个接口指针,则选用CoCreateInstanceEx函数。
(2) 如果希望获取类厂对象或者要调用类厂的某些成员函数,则选用CoGetClassObject函数,以便获得类厂对象,并对类厂对象进行操作。
(3) 在其他情况下,使用CoCreateInstance函数创建对象,这是最常用的方法。
14、CoGetClassObject与组件程序的交互过程的例子
用以说明在COM对象创建过程中,客户程序、COM库和进程内组件程序三者之间的顺序关系。
15、类厂对组件生存期的控制
类厂是一个COM对象,但通常只把它当作创建其他组件对象的手段,一般情况下,客户程序或者COM库只是在创建组件对象的时候才使用类厂对象的接口指针,创建完成后就把类厂对象丢弃掉。如果用户希望保留类厂的接口指针继续使用,则在类厂中引入锁计数来控制组件程序的生存周期。
16、COM库
COM库在整个COM对象体系中起了很重要的作用。COM除了定义了组件程序和客户程序交互的规范以外,它也提供了COM的实现部分即COM库,使得这些规范能够被真正地应用起来。并且,COM库也充当了组件程序和客户程序之间的桥梁,尤其是在组件对象的创建过程中,以及在对象管理、内存管理和一些标准化操作等方面起着重要的作用。
在客户程序和组件程序建立起协作关系之前,它们之间的通信只能靠COM库来传递,并且组件程序和客户程序都可能要用到COM库提供的各种服务。
17、COM库的初始化与卸载
COM库的初始化函数是CoInitialize,其参数pMalloc用于指定一个内存分配器,它是一个IMalloc指针接口,可由应用程序指定内存分配原则。一般将pMalloc设为NULL,由COM库将使用缺省提供的内存分配器。
一个进程对COM库只需要(也必须)进行一次初始化。在成功初始化并使用完COM库后,必须调用COM库的终止函数CoUninitialize。但有一个函数的调用不需要COM库的初始化与卸载,就是CoBuildVersion,该函数用于获取COM库版本。
18、COM库的内存管理
(1)内存使用的两种情况:一种是在客户程序和组件程序建立协作关系之前,靠COM库进行通信,COM需要申请内存,内存不一定是COM库本身使用,
也可能是它替组件程序申请并提交出来的;另一种情况是,因为客户程序通过接口指针对组件程序进行操作,虽然这种调用可能是直接进行的,但组件程序申请的内存有可能由客户程序来释放,所以,在COM库和组件程序、客户程序之间需要有统一的内存管理办法。
(2)COM库不仅提供了这样的内存管理器,还提供了内存管理器的标准,应用程序可以按照COM规范指定的标准建立自定义内存管理器,以取代COM库的缺省内存管理器。
(3)在COM库初始化成功之后,不管是使用缺省内存管理器还是使用自定义的内存管理器,应用程序都可以使用COM库进行内存分配或释放,为此,COM库提供了两种操作方法:
A、直接使用IMalloc接口指针。在客户程序或者组件程序中调用COM库函数CoGetMalloc,通过IMalloc接口指针和CoGetMalloc函数实现内存管理的统一。
B、COM库封装了三个API函数,可用于内存分配和释放:CoTaskMemAlloc、CoTaskMemRealloc、 CoTaskMemFree。这三个函数分别对应于IMalloc的三个成员函数:Alloc、Realloc和Free,参数的定义也完全一致。
(4)COM库的两种内存管理器:CoGetMalloc函数可用来获取COM库的内存管理器。一种是在初始化时指定的内存管理器或者其内部缺省的管理器(即作业管理器,task allocator),这种管理器在本进程内有效;另一种是跨进程的共享分配器,由OLE系统提供,它可在一个进程内分配内存并传给第二个进程,在第二个进程内使用此内存甚至释放掉此内存。
19、组件程序的装载和卸载
COM库对组件程序的装载和卸载进行控制。客户程序是在运行时刻与组件程序建立连接的,而且,一旦连接起来以后,客户程序和组件程序的通信是直接进行的,并不需要COM库的参与,但组件程序的装载是在客户创建第一个组件对象时进行的,组件程序的卸载是在最后一个组件对象被释放之后进行的,这两个动作并不由客户程序直接完成,而是在COM库中完成的。
进程内组件的卸载满足的两个条件:组件中对象数为0,类厂的锁计数器为0。客户程序调用CoFreeUnusedLibraries函数完成卸载工作。
进程外组件的卸载比较简单,因为组件程序运行在单独的进程中,一旦退出的条件满足,它只要从进程额主控函数返回即可。在Windows系统中,进程的主控函数为WinMain。类厂对象的引用计数无法控制进程的生存期,所以引入类厂对象的加锁和减锁操作。
20、COM库的常用函数
在COM库中还给出了许多标准接口的定义,例如IUnknown、IClassFactory、IMalloc。
21、HRESULT类型
在COM中大多数的函数以及一些接口成员函数的返回值类型均为HRESULT类型。HRESULT类型的返回值反映了函数调用过程中的一些情况,而且HRESULT类型定义也有一定的规范。
HRESULT并不是指向结果结构的句柄,而是一个32位整数,通常被定义为DWORD或long类型。HRESULT的32位被分成四个域:类别码(30-31)、自定义标志位(29)、操作码(16-28)、操作结果码(0-15)。
Win32 SDK的头文件WinError.h定义了Win32函数所有的可能返回结果,其中也包括了COM库函数以及OLE函数的返回值的宏定义。Win32 SDK提供的FormatMessage函数可根据结果值获得一个对应于该结果值的标准说明字符串,它也支持COM库函数的返回结果信息。
一般推荐在使用HRESULT类型作为引出函数或者作为接口成员函数的返回值时,尽量使用COM或者Win32提供的标准定义。
22、COM的实现是COM客户程序、COM库和COM组件程序三者之间通过COM制定的规范协同工作来完成的,三者之间形成了一个统一的整体。
23、第二章的字典组件程序是一个模拟组件程序,第三章的字典组件程序是真正的COM组件程序,这样,在运行客户程序之前必须先注册组件程序,命令行为:regsvr32.exe …\DictComp.dll 。文章出处:http://www.diybl.com/course/3_program/c++/cppxl/20081118/151936.html