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 。