首页 > 编程 > Delphi > 正文

《COM原理与应用》学习笔记-第一部分COM原理

2019-09-08 23:09:07
字体:
来源:转载
供稿:网友
                                                                                                                                                       
                       
Feed: 大富翁笔记
Title: 《COM 原理与应用》学习笔记
Author: fgs_fgs
Comments  
《COM 原理与应用》学习笔记 - 第一部分 COM原理

savetime2k@yahoo.com
http://savetime.delphibbs.com

开始时间:2004.1.30
最后修改:2004.2.1

本文排版格式为:
正文由窗口自动换行;所有代码以 80 字符为边界;中英文字符以空格符分隔。

(本文内容基本上是从《COM 原理与应用》书中摘录,版权由作者潘爱民所有,请勿在公共媒体使用)

目录
===============================================================================
⊙ 第一章 概述
COM 是什么
COM 对象与接口
COM 进程模型
COM 可重用性
⊙ 第二章 COM 对象模型
全局唯一标识符 GUID
COM 对象
COM 接口
接口描述语言 IDL
IUnknown 接口
COM 对象的接口原则
⊙ 第三章 COM 的实现
COM 组件注册信息
注册 COM 组件
类厂和 DllGetObjectClass 函数
CoGetClassObject 函数
CoCreateInstance / CoCreateInstanceEx 函数
COM 库的初始化
COM 库的内存管理
组件程序的装载和卸载
COM 库常用函数
HRESULT 类型
⊙ 第四章 COM 特性
可重用性:包容和聚合
进程透明性 (待学)
安全性 (待学)
多线程特性 (待学)
⊙ 第五章 用 Visual C++ 开发 COM 应用
Win32 SDK 提供的一些头文件的说明
与 COM 接口有关的一些宏
===============================================================================


正 文
===============================================================================
⊙ 第一章 概述
===============================================================================
COM 是什么
-------------------------------------------------------------------------------
COM 是由 Microsoft 提出的组件标准,它不仅定义了组件程序之间进行交互的标准,并且也提供了组件程序运行所需的环境。在 COM 标准中,一个组件程序也被称为一个模块,它可以是一个动态链接库,被称为进程内组件(in-process component);也可以是一个可执行程序(即 EXE 程序),被称作进程外组件(out-of-process component)。一个组件程序可以包含一个或多个组件对象,因为 COM 是以对象为基本单元的模型,所以在程序与程序之间进行通信时,通信的双方应该是组件对象,也叫做 COM 对象,而组件程序(或称作 COM 程序)是提供 COM 对象的代码载体。

COM 对象不同于一般面向对象语言(如 C++ 语言)中的对象概念,COM 对象是建立在二进制可执行代码级的基础上,而 C++ 等语言中的对象是建立在源代码级基础上的,因此 COM 对象是语言无关的。这一特性使用不同编程语言开发的组件对象进行交互成为可能。

-------------------------------------------------------------------------------
COM 对象与接口
-------------------------------------------------------------------------------
类似于 C++ 中对象的概念,对象是某个类(class)的一个实例;而类则是一组相关的数据和功能组合在一起的一个定义。使用对象的应用(或另一个对象)称为客户,有时也称为对象的用户。

接口是一组逻辑上相关的函数集合,其函数也被称为接口成员函数。按照习惯,接口名常是以“I”为前缀。对象通过接口成员函数为客户提供各种形式的服务。

在 COM 模型中,对象本身对于客户来说是不可见的,客户请求服务时,只能通过接口进行。每一个接口都由一个 128 位的全局唯一标识符(GUID,Global Unique Identifier)来标识。客户通过 GUID 来获得接口的指针,再通过接口指针,客户就可以调用其相应的成员函数。
与接口类似,每个组件也用一个 128 位 GUID 来标识,称为 CLSID(class identifer,类标识符或类 ID),用 CLSID 标识对象可以保证(概率意义上)在全球范围内的唯一性。实际上,客户成功地创建对象后,它得到的是一个指向对象某个接口的指针,因为 COM 对象至少实现一个接口(没有接口的 COM 对象是没有意义的),所以客户就可以调用该接口提供的所有服务。根据 COM 规范,一个 COM 对象如果实现了多个接口,则可以从某个接口得到该对象的任意其他接口。从这个过程我们也可以看出,客户与 COM 对象只通过接口打交道,对象对于客户来说只是一组接口。

-------------------------------------------------------------------------------
COM 进程模型
-------------------------------------------------------------------------------
COM 所提供的服务组件对象在实现时有两种进程模型:进程内对象和进程外对象。如果是进程内对象,则它在客户进程空间中运行;如果是进程外对象,则它运行在同机器上的另一个进程空间或者在远程机器的空间。

进程内服务程序:
服务程序被加载到客户的进程空间,在 Windows 环境下,通常服务程序的代码以动态连接库(DLL)的形式实现。

本地服务程序:
服务程序与客户程序运行在同一台机器上,服务程序是一个独立的应用程序,通常它是一个 EXE 文件。

远程服务程序:
服务程序运行在与客户不同的机器上,它既可以是一个 DLL 模块,也可以是一个 EXE 文件。如果远程服务程序是以 DLL 形式实现的话,则远程机器会创建一个代理进程。

虽然 COM 对象有不同的进程模型,但这种区别对于客户程序来说是透明的,因此客户程序在使用组件对象时可以不管这种区别的存在,只要遵照 COM 规范即可。然而,在实现 COM 对象时,还是应该慎重选择进程模型。进程内模型的优点是效率高,但组件不稳定会引起客户进程崩溃,因此组件可能会危及客户;(savetime 注:这里有点问题,如果组件不稳定,进程外模型也同样会出问题,可能是因为进程内组件和客户同处一个地址空间,出现冲突的可能性比较大?)进程外模型的优点是稳定性好,组件进程不会危及客户程序,一个组件进程可以为多个客户进程提供服务,但进程外组件开销大,而且调用效率相对低一点。

-------------------------------------------------------------------------------
COM 可重用性
-------------------------------------------------------------------------------
由于 COM 标准是建立在二进制代码级的,因此 COM 对象的可重用性与一般的面向对象语言如 C++ 中对象的重用过程不同。对于 COM 对象的客户程序来说,它只是通过接口使用对象提供的服务,它并不知道对象内部的实现过程,因此,组件对象的重用性可建立在组件对象的行为方式上,而不是具体实现上,这是建立重用的关键。COM 用两种机制实现对象的重用。我们假定有两个 COM 对象,对象1 希望能重用对象2 的功能,我们把对象1 称为外部对象,对象2 称为内部对象。

(1)包容方式。
对象1 包含了对象2,当对象1 需要用到对象2 的功能时,它可以简单地把实现交给对象2 来完成,虽然对象1 和对象2 支持同样的接口,但对象1 在实现接口时实际上调用了对象2 的实现。

(2)聚合方式。
对象1 只需简单地把对象2 的接口递交给客户即可,对象1 并没有实现对象2 的接口,但它把对象2 的接口也暴露给客户程序,而客户程序并不知道内部对象2 的存在。

===============================================================================
⊙ 第二章 COM 对象模型
===============================================================================
全局唯一标识符 GUID
-------------------------------------------------------------------------------
COM 规范采用了 128 位全局唯一标识符 GUID 来标识对象和接口,这是一个随机数,并不需要专门机构进行分配和管理。因为 GUID 是个随机数,所以并不绝对保证唯一性,但发生标识符相重的可能性非常小。从理论上讲,如果一台机器每秒产生 10000000 个 GUID,则可以保证(概率意义上)的 3240 年不重复)。

GUID 在 C/C++ 中可以用这样的结构来描述:

typedef struct _GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;

例:{64BF4372-1007-B0AA-444553540000} 可以如下定义一个 GUID:

extern "C" const GUID CLSID_MYSPELLCHECKER =
{ 0x54BF0093, 0x1048, 0x399D,
{ 0xB0, 0xA3, 0x45, 0x33, 0x43, 0x90, 0x47, 0x47} };

Visual C++ 提供了两个程序生成 GUID: UUIDGen.exe(命令行) 和 GUIDGen.exe(对话框)。COM 库提供了以下 API 函数可以产生 GUID:

HRESULT CoCreateGuid(GUID *pguid);

如果创建 GUID 成功,则函数返回 S_OK,并且 pguid 将指向所得的 GUID 值。

-------------------------------------------------------------------------------
COM 对象
-------------------------------------------------------------------------------
在 COM 规范中,并没有对 COM 对象进行严格的定义,但 COM 提供的是面向对象的组件模型,COM 组件提供给客户的是以对象形式封装起来的实体。客户程序与 COM 程序进行交互的实体是 COM 对象,它并不关心组件模型的名称和位置(即位置透明性),但它必须知道自己在与哪个 COM 对象进行交互。

-------------------------------------------------------------------------------
COM 接口
-------------------------------------------------------------------------------
从技术上讲,接口是包含了一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的所有信息,客户程序利用这些函数获得组件对象的服务。

通常我们把接口函数表称为虚函数表(vtable),指向 vtable 的指针为 pVtable。对于一个接口来说,它的虚函数表是确定的,因此接口的成员函数个数是不变的,而且成员函数的先后先后顺序也是不变的;对于每个成员函数来说,其参数和返回值也是确定的。在一个接口的定义中,所有这些信息都必须在二进制一级确定,不管什么语言,只要能支持这样的内存结构描述,就可以使用接口。

接口指针 ----> pVtable ----> 指针函数1 -> |----------|
m_Data1 指针函数2 -> | 对象实现 |
m_Data2 指针函数3 -> |----------|

每一个接口成员函数的第一个参数为指向对象实例的指针(=this),这是因为接口本身并不独立使用,它必须存在于某个 COM 对象上,因此该指针可以提供对象实例的属性信息,在被调用时,接口可以知道是对哪个 COM 对象在进行操作。

在接口成员函数中,字符串变量必须用 Unicode 字符指针,COM 规范要求使用 Unicode 字符,而且 COM 库中提供的 COM API 函数也使用 Unicode 字符。所以如果在组件程序内部使用到了 ANSI 字符的话,则应该进行两种字符表达的转换。当然,在即建立组件程序又建立客户程序的情况下,可以使用自己定义的参数类型,只要它们与 COM 所能识别的参数类型兼容。

Visual C++ 提供两种字符串的转换:
namespace _com_util {
BSTR ConvertStringToBSTR(const char *pSrc) throw(_com_error);
BSTR ConvertBSTRToString(BSTR pSrc) throw(_com_error);
}

BSTR 是双字节宽度字符串,它是最常用的自动化数据类型。

-------------------------------------------------------------------------------
接口描述语言 IDL
-------------------------------------------------------------------------------
COM 规范在采用 OSF 的 DCE 规范描述远程调用接口 IDL (interface description language,接口描述语言)的基础上,进行扩展形成了 COM 接口的描述语言。接口描述语言提供了一种不依赖于任何语言的接口的描述方法,因此,它可以成为组件程序和客户程序之间的共同语言。

COM 规范使用的 IDL 接口描述语言不仅可用于定义 COM 接口,同时还定义了一些常用的数据类型,也可以描述自定义的数据结构,对于接口成员函数,我们可以定义每个参数的类型、输入输出特性,甚至支持可变长度的数组的描述。IDL 支持指针类型,与 C/C++ 很类似。例如:

interface IDictionary
{
HRESULT Initialize()
HRESULT LoadLibrary([in] string);
HRESULT InsertWord([in] string, [in] string);
HRESULT DeleteWord([in] string);
HRESULT LookupWord([in] string, [out] string *);
HRESULT RestoreLibrary([in] string);
HRESULT FreeLibrary();
}

Microsoft Visual C++ 提供了 MIDL 工具,可以把 IDL 接口描述文件编译成 C/C++ 兼容的接口描述头文件(.h)。

-------------------------------------------------------------------------------
IUnknown 接口
-------------------------------------------------------------------------------
IUnknown 的 IDL 定义:

interface IUnknown
{
HRESULT QueryInterface([in] REFIID iid, [out] void **ppv);
ULONG AddRef(void);
ULONG Release(void);
}

IUnkown 的 C++ 定义:

class IUnknown
{
virutal HRESULT _stdcall QueryInterface(const IID& iid, void **ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virutal ULONG _stdcall Release() = 0;
}

-------------------------------------------------------------------------------
COM 对象的接口原则
-------------------------------------------------------------------------------
COM 规范对 QueryInterface 函数设置了以下规则:

1. 对于同一个对象的不同接口指针,查询得到的 IUnknown 接口必须完全相同。也就是说,每个对象的 IUnknown 接口指针是唯一的。因此,对两个接口指针,我们可以通过判断其查询到的 IUnknown 接口是否相等来判断它们是否指向同一个对象。

2. 接口自反性。对一个接口查询其自身总应该成功,比如:
pIDictionary->QueryInterface(IID_Dictionary, ...) 应该返回 S_OK。

3. 接口对称性。如果从一个接口指针查询到另一个接口指针,则从第二个接口指针再回到第一个接口指针必定成功,比如:
pIDictionary->QueryInterface(IID_SpellCheck, (void **)&pISpellCheck);
如果查找成功的话,则再从 pISpellCheck 查回 IID_Dictionary 接口肯定成功。

4. 接口传递性。如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,则从第三个接口指针一定可以查询到第一个接口指针。

5. 接口查询时间无关性。如果在某一个时刻可以查询到某一个接口指针,则以后任何时间再查询同样的接口指针,一定可以查询成功。

总之,不管我们从哪个接口出发,我们总可以到达任何一个接口,而且我们也总可以回到最初的那个接口。

===============================================================================
⊙ 第三章 COM 的实现
===============================================================================
COM 组件注册信息
-------------------------------------------------------------------------------
当前机器上所有组件的信息 HKEY_CLASS_ROOT/CLSID
进程内组件 HKEY_CLASS_ROOT/CLSID/guid/InprocServer32
进程外组件 HKEY_CLASS_ROOT/CLSID/guid/LocalServer32
组件所属类别(CATID) HKEY_CLASS_ROOT/CLSID/guid/Implemented Categories

COM 接口的配置信息 HKEY_CLASS_ROOT/Interface
代理 DLL/存根 DLL HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid
HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid32

类型库的信息 HKEY_CLASS_ROOT/TypeLib

字符串命名 ProgID HKEY_CLASS_ROOT/ (例如 "COMCTL.TreeCtrl")
组件 GUID HKEY_CLASS_ROOT/COMTRL.TreeControl/CLSID
缺省版本号 HKEY_CLASS_ROOT/COMTRL.TreeControl/CurVer
(例如 CurVer = "COMTRL.TreeCtrl.1", 那么
HKEY_CLASS_ROOT/COMTRL.TreeControl.1 也存在)

当前机器所有组件类别 HKEY_CLASS_ROOT/Component Categories

COM 提供两个 API 函数 CLSIDFromProgID 和 ProgIDFromCLSID 转换 ProgID 和 CLSID。

如果 COM 组件支持同样一组接口,则可以把它们分到同一类中,一个组件可以被分到多个类中。比如所有的自动化对象都支持 IDispatch 接口,则可以把它们归成一类“Automation Objects”。类别信息也用一个 GUID 来描述,称为 CATID。组件类别最主要的用处在于客户可以快速发现机器上的特定类型的组件对象,否则的话,就必须检查所有的组件对象,并把组件对象装入到内存中实例化,然后依次询问是否实现了必要的接口,现在使用了组件类别,就可以节省查询过程。

-------------------------------------------------------------------------------
注册 COM 组件
-------------------------------------------------------------------------------
RegSrv32.exe 用于注册一个进程内组件,它调用 DLL 的 DllRegisterServer 和 DllUnregisterServer 函数完成组件程序的注册和注销操作。如果操作成功返回 TRUE,否则返回 FALSE。

对于进程外组件程序,情形稍有不同,因为它自身是个可执行程序,而且它也不能提供入口函数供其他程序使用。因此,COM 规范中规定,支持自注册的进程外组件必须支持两个命令行参数 /RegServer 和 /UnregServer,以便完成注册和注销操作。命令行参数大小写无关,而且 “/” 可以用 “-” 替代。如果操作成功,程序返回 0,否则,返回非 0 表示失败。

-------------------------------------------------------------------------------
类厂和 DllGetObjectClass 函数
-------------------------------------------------------------------------------
类厂(class factory)是 COM 对象的生产基地,COM 库通过类厂创建 COM 对象;对应每一个 COM 类,有一个类厂专门用于该 COM 类的对象创建操作。类厂本身也是一个 COM 对象,它支持一个特殊的接口 IClassFactory:

class IClassFactory : public IUnknown
{
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter,
const IID& iid, void **ppv) = 0;
virtual HRESULT _stdcall LockServer(BOOL bLock) = 0;
}

CreateInstance 成员函数用于创建对应的 COM 对象。第一个参数 pUnknownOuter 用于对象类被聚合的情形,一般设置为 NULL;第二个参数 iid 是对象创建完成后客户应该得到的初始接口 IID;第三个参数 ppv 存放返回的接口指针。

LockServer 成员函数用于控制组件的生存周期。

类厂对象是由 DLL 引出函数 DllGetClassObject 创建的:

HRESULT DllGetClassObject(const CLSID& clsid, const IID& iid, (void **)ppv);

DllGetClassObject 函数的第一个参数为待创建对象的 CLSID。因为一个组件可能实现了多个 COM 对象类,所以在 DllGetClassObject 函数的参数中有必要指定 CLSID,以便创建正确的 class factory。另两个参数 iid 和 ppv 分别指于指定接口 IID 和存放类厂接口指针。

COM 库在接到对象创建的指令后,它要调用进程内组件的 DllGetClassObject 函数,由该函数创建类厂对象,并返回类厂对象的接口指针。COM 库或客户一旦拥有类厂的接口指针,它们就可以通过 IClassFactory 的成员函数 CreateInstance 创建相应的 COM 对象。

-------------------------------------------------------------------------------
CoGetClassObject 函数
-------------------------------------------------------------------------------
在 COM 库中,有三个 API 可用于对象的创建,它们分别是 CoGetClassObject、CoCreateInstnace 和 CoCreateInstanceEx。通常情况下,客户程序调用其中之一完成对象的创建,并返回对象的初始接口指针。COM 库与类厂也通过这三个函数进行交互。

HRESULT CoGetClassObject(const CLSID& clsid, DWORD dwClsContext,
COSERVERINFO *pServerInfo, const IID& iid, (void **)ppv);

CoGetClassObject 函数先找到由 clsid 指定的 COM 类的类厂,然后连接到类厂对象,如果需要的话,CoGetClassObject 函数装入组件代码。如果是进程内组件对象,则 CoGetClassObject 调用 DLL 模块的 DllGetClassObject 引出函数,把参数 clsid、iid 和 ppv 传给 DllGetClassObject 函数,并返回类厂对象的接口指针。通常情况下 iid 为 IClassFactory 的标识符 IID_IClassFactory。如果类厂对象还支持其它可用于创建操作的接口,也可以使用其它的接口标识符。例如,可请求 IClassFactory2 接口,以便在创建时,验证用户的许可证情况。IClassFactory2 接口是对 IClassFactory 的扩展,它加强了组件创建的安全性。

参数 dwClsContext 指定组件类别,可以指定为进程内组件、进程外组件或者进程内控制对象(类似于进程外组件的代理对象,主要用于 OLE 技术)。参数 iid 和 ppv 分别对应于 DllGetClassObject 的参数,用于指定接口 IID 和存放类对象的接口指针。参数 pServerInfo 用于创建远程对象时指定服务器信息,在创建进程内组件对象或者本地进程外组件时,设置 NULL。

如果 CoGetClassObject 函数创建的类厂对象位于进程外组件,则情形要复杂得多。首先 CoGetClassObject 函数启动组件进程,然后一直等待,直到组件进程把它支持的 COM 类对象的类厂注册到 COM 中。于是 CoGetClassObject 函数把 COM 中相应的类厂信息返回。因此,组件外进程被 COM 库启动时(带命令行参数“/Embedding”),它必须把所支持的 COM 类的类厂对象通过 CoRegisterClassObject 函数注册到 COM 中,以便 COM 库创建 COM 对象使用。当进程退出时,必须调用 CoRevokeClassObject 函数以便通知 COM 它所注册的类厂对象不再有效。组件程序调用 CoRegisterClassObject 函数和 CoRevokeClassObject 函数必须配对,以保证 COM 信息的一致性。

-------------------------------------------------------------------------------
CoCreateInstance / CoCreateInstanceEx 函数
-------------------------------------------------------------------------------
HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, (void **)ppv);

CoCreateInstance 是一个被包装过的辅助函数,在它的内部实际上也调用了 CoGetClassObject 函数。CoCreateInstance 的参数 clsid 和 dwClsContext 的含义与 CoGetClassObject 相应的参数一致,(CoCreateInstance 的 iid 和 ppv 参数与 CoGetClassObject 不同,一个是表示对象的接口信息,一个是表示类厂的接口信息)。参数 pUnknownOuter 与类厂接口的 CreateInstance 中对应的参数一致,主要用于对象被聚合的情况。CoCreateInstance 函数把通过类厂创建对象的过程封装起来,客户程序只要指定对象类的 CLSID 和待输出的接口指针及接口 ID,客户程序可以不与类厂打交道。CoCreateInstance 可以用下面的代码实现:

(savetime 注:下面代码中 ppv 指针的应用,好像应该是 void **)

HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, void *ppv)
{
IClassFactory *pCF;
HRESULT hr;

hr = CoGetClassObject(clsid, dwClsContext, NULL, IID_IClassFactory,
(void *) pCF);
if (FAILED(hr)) return hr;
hr = pCF->CreateInstance(pUnknownOuter, iid, (void *)ppv);
pFC->Release();
return hr;
}

从这段代码我们可以看出,CoCreateInstance 函数首先利用 CoGetClassObject 函数创建类厂对象,然后用得到的类厂对象的接口指针创建真正的 COM 对象,最后把类厂对象释放掉并返回,这样就把类厂屏蔽起来。

但是,用 CoCreateInstance 并不能创建远程机器上的对象,因为在调用 CoGetClassObject 时,把第三个用于指定服务器信息的参数设置为 NULL。如果要创建远程对象,可以使用 CoCreateInstance 的扩展函数 CoCreateInstanceEx:

HRESULT CoCreateInstanceEx(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, COSERVERINFO *pServerInfo, DWORD dwCount,
MULTI_QI *rgMultiQI);

前三个参数与 CoCreateInstance 一样,pServerInfo 与 CoGetClassOjbect 的参数一样,用于指定服务器信息,最后两个参数 dwCount 和 rgMultiQI 指定了一个结构数组,可以用于保存多个对象接口指针,其目的在于一次获得多个接口指针,以便减少客户程序与组件程序之间的频繁交互,这对于网络环境下的远程对象是很有意义的。

-------------------------------------------------------------------------------
COM 库的初始化
-------------------------------------------------------------------------------
调用 COM 库的函数之前,为了使函数有效,必须调用 COM 库的初始化函数:

HRESULT CoInitialize(IMalloc *pMalloc);

pMalloc 用于指定一个内存分配器,可由应用程序指定内存分配原则。一般情况下,我们直接把参数设为 NULL,则 COM 库将使用缺省提供的内存分配器。

返回值:S_OK 表示初始化成功
S_FALSE 表示初始化成功,但这次调用不是本进程中首次调用初始化函数
S_UNEXPECTED 表示初始化过程中发生了错误,应用程序不能使用 COM 库

通常,一个进程对 COM 库只进行一次初始化,而且,在同一个模块单元中对 COM 库进行多次初始化并没有意义。唯一不需要初始化 COM 库的函数是获取 COM 库版本的函数:

DWORD CoBuildVersion();

返回值:高 16 位 主版本号
低 16 位 次版本号

COM 程序在用完 COM 库服务之后,通常是在程序退出之前,一定要调用终止 COM 库服务函数,以便释放 COM 库所维护的资源:

void CoUninitialize(void);

注意:凡是调用 CoInitialize 函数返回 S_OK 的进程或程序模块一定要有对应的 CoUninitialize 函数调用,以保证 COM 库有效地利用资源。
(? 如果在一个模块中调用 CoInitialize 返回 S_OK,那么它调用 CoUnitialize 函数后,其它也在使用 COM 库的模块是否会出错误?还是 COM 库会自动检查有哪些模块在使用?)

-------------------------------------------------------------------------------
COM 库的内存管理
-------------------------------------------------------------------------------
由于 COM 组件程序和客户程序是通过二进制级标准建立连接的,所以在 COM 应用程序中凡是涉及客户、COM 库和组件三者之间内存交互(分配和释放不在同一个模块中)的操作必须使用一致的内存管理器。COM 提供的内存管理标准,实际上是一个 IMalloc 接口:

// IID_IMalloc: {00000002-0000-0000-C000-000000000046}
class IMalloc: public IUnknown
{
void * Alloc(ULONG cb) = 0;
void * Realloc(void *pv, ULONG cb) = 0;
void Free(void *pv) = 0;
ULONG GetSize(void *pv) = 0; // 返回分配的内存大小
int DidAlloc(void *pv) = 0; // 确定内存指针是否由该内存管理器分配
void HeapMinimize() = 0; // 使堆内存尽可能减少,把没用到的内存还给
// 操作系统,用于性能优化
}

获得 IMalloc 接口指针:

HRESULT CoGetMalloc(DWORD dwMemContext, IMalloc **ppMalloc);

CoGetMalloc 函数的第一个参数 dwMemContext 用于指定内存管理器的类型。COM 库中包含两种内存管理器,一种就是在初始化时指定的内存管理器或者其内部缺省的管理器,也称为作业管理器(task allocator),这种管理器在本进程内有效,要获取该管理器,在 dwMemContext 参数中指定为 MEMCTX_TASK;另一种是跨进程的共享分配器,由 OLE 系统提供,要获取这种管理器,dwMemContext 参数中指定为 MEMCTX_SHARED,使用共享管理器的便利是,可以在一个进程内分配内存并传给第二个进程,在第二个进程内使用此内存甚至释放掉此内存。

只要函数的返回值为 S_OK,则 ppMalloc 就指向了 COM 库的内存管理器接口指针,可以使用它进行内存操作,使用完毕后,应该调用 Release 成员函数释放控制权。

COM 库封装了三个 API 函数,可用于内存分配和释放:

void * CoTaskMemAlloc(ULONG cb);
void CoTaskFree(void *pv);
void CoTaskMemRealloc(void *pv, ULONG cb);

这三个函数分配对应于 IMalloc 的三个成员函数:Alloc、Realloc 和 Free。

例:COM 程序如何从 CLSID 值找到相应的 ProgID 值:

WCHAR *pwProgID;
char pszProgID[128];
hResult = ::ProgIDFromCLSID(CLSID_Dictionary, &pwProgID);
if (hResult != S_OK) {
...
}
wcstombs(pszProgID, pwProgID, 128);
CoTaskMemFree(pwProgID); // 注意:必须释放内存

在调用 COM 函数 ProgIDFromCLSID 返回之后,因为 COM 库为输出变量 pwProgID 分配了内存空间,所以应用程序在用完 pwProgID 变量之后,一定要调用 CoTaskMemFree 函数释放内存。该例子说明了在 COM 库中分配内存,而在调用程序中释放内存的一种情况。COM 库中其他一些函数也有类似的特性,尤其是一些包含不定长度输出参数的函数。
-------------------------------------------------------------------------------
组件程序的装载和卸载
-------------------------------------------------------------------------------
进程内组件的装载:

客户程序调用COM 库的 CoCreateInstance 或 CoGetClassObject 函数创建 COM 对象,在 CoGetClassObject 函数中,COM 库根据系统注册表中的信息,找到类标识符 CLSID 对应的组件程序(DLL 文件)的全路径,然后调用 LoadLibrary(实际上是 CoLoadLibrary)函数,并调用组件程序的 DllGetClassObject 引出函数。DllGetClassObject 函数创建相应的类厂对象,并返回类厂对象的 IClassFactory 接口。至此 CoGetClassObject 函数的任务完成,然后客户程序或者 CoCreateInstance 函数继续调用类厂对象的 CreateInstance 成员函数,由它负责 COM 对象的创建工作。

CoCreateInstance
|-CoGetClassObject
|-Get CLSID -> DLLfile path
|-CoLoadLibrary
|-DLLfile.DllGetClassObject
|-return IClassFactory
|-IClassFactory.CreateInstnace

进程外组件的装载:

在 COM 库的 CoGetClassObject 函数中,当它发现组件程序是 EXE 文件(由注册表组件对象信息中的 LocalServer 或 LocalServer32 值指定)时,COM 库创建一个进程启动组件程序,并带上“/Embedding”命令行参数,然后等待组件程序;而组件程序在启动后,当它检查到“/Embedding”命令行参数后,就会创建类厂对象,然后调用 CoRegisterClassObject 函数把类厂对象注册到 COM 中。当 COM 库检查到组件对象的类厂之后,CoGetClassObject 函数就把类厂对象返回。由于类厂与客户程序运行在不同的进程中,所以客户程序得到的是类厂的代理对象。一旦客户程序或 COM 库得到了类厂对象,它就可以完成组件对象的创建工作。
进程内对象和进程外对象的不同创建过程仅仅影响了 CoGetClassObject 函数的实现过程,对于客户程序来说是完全透明的。

CoGetClassObject
|-LocalServer/LocalServer32
|-Execute EXE /Embedding
|-Create class factory
|-CoRegisterClassObject ( class factory )
|-return class factory (proxy)

进程内组件的卸载:

只有当组件程序满足了两个条件时,它才能被卸载,这两个条件是:组件中对象数为 0,类厂的锁计数为 0。满足这两个条件时,DllCanUnloadNow 引出函数返回 TRUE。COM 提供了一个函数 CoFreeUnusedLibraries,它会检测当前进程中的所有组件程序,当发现某个组件程序的 DllCanUnloadNow 函数返回 TRUE 时,就调用 FreeLibrary 函数(实际上是 CoFreeLibrary 函数)把该组件从程序从内存中卸出。
该由谁来调用 CoFreeUnusedLibraries 函数呢?因为在组件程序执行过程中,它不可能把自己从内存中卸出,所以这个任务应该由客户来完成。客户程序随时都可以调用 CoFreeUnusedLibraries 函数完成卸出工作,但通常的做法是,在程序的空闲处理过程中调用 CoFreeUnusedLibraries 函数,这样做既可以避免程序中处处考虑对 CoFreeUnusedLibraries 函数的调用,又可以使不再使用的组件程序得到及时清除,提高资源的利用率,COM 规范也推荐这种做法。

进程外组件的卸载:

进程外组件的卸载比较简单,因为组件程序运行在单独的进程中,一旦其退出的条件满足,它只要从进程的主控函数返回即可。在 Windows 系统中,进程的主控函数为 WinMain。
前面曾经说过,在组件程序启动运行时,它调用 CoRegisterClassObject 函数,把类厂对象注册到 COM 中,注册之后,类厂对象的引用计数始终大于 0,因此单凭类厂对象的引用计数无法控制进程的生存期,这也是引入类厂对象的加锁和减锁操作的原因。进程外组件的载条件与 DllCanUnloadNow 中的判断类似,也需要判断 COM 对象是否还存在、以及判断是否锁计数器为 0,只有当条件满足了,进程的主函数才可以退出。
从原则上讲,进程外组件程序的卸载就是这么简单,但实际上情况可能复杂一些,因为有些组件程序在运行过程中可以创建自己的对象,或者包含用户界面的程序在运行过程中,用户手工关闭了进程,那么进程对这些动作的处理要复杂一些。例如,组件程序在运行过程中,用户又打开了一个文件并进行操作,那么即使原先创建的对象被释放了,而且锁计数器也为 0,进程也不能退出,它必须继续为用户服务,就像是用户打开的进程一样。对这种程序,可以增加一个“用户控制”标记 flag,如果 flag 为 FALSE,则可以按简单的方法直接退出程序即可;如果 flag 为 TRUE,则表明用户参与了控制,组件进程不能马上退出,但应该调用 CoRevokeClassObject 函数以便与 CoRegisterClassObject 调用相响呼应,把进程留给用户继续进行。
如果组件程序在运行过程中,用户要关闭进程,而此时并不满足进程退出条件,那么进程可以采取两种办法:第一种方法,把应用隐藏起来,并把 flag 标记设置为 FALSE,然后组件程序继续运行直到卸载条件满足为止;另一种办法是,调用 CoDisconnectObject 函数,强迫脱离对象与客户之间的关系,并强行终止进程,这种方法比较粗暴,不提倡采用,但不得已时可以也使用,以保证系统完成一些高优先级的操作。

-------------------------------------------------------------------------------
COM 库常用函数
-------------------------------------------------------------------------------
初始化函数 CoBuildVersion 获得 COM 库的版本号
CoInitialize COM 库初始化
CoUninitialize COM 库功能服务终止
CoFreeUnusedLibraries 释放进程中所有不再使用的组件程序
GUID 相关函数 IsEqualGUID 判断两个 GUID 是否相等
IsEqualIID 判断两个 IID 是否相等
IsEqualCLSID 判断两个 CLSID 是否相等 (*为什么要3个函数)
CLSIDFromProgID 字符串组件标识转换为 CLSID 形式
StringFromCLSID CLSID 形式标识转化为字符串形式
IIDFromString 字符串转换为 IID 形式
StringFromIID IID 形式转换为字符串
StringFromGUID2 GUID 形式转换为字符串(*为什么有 2)
对象创建函数 CoGetClassObject 获取类厂对象
CoCreateInstance 创建 COM 对象
CoCreateInstanceEx 创建 COM 对象,可指定多个接口或远程对象
CoRegisterClassObject 登记一个对象,使其它应用程序可以连接到它
CoRevokeClassObject 取消对象的登记
CoDisconnectObject 断开其它应用与对象的连接
内存管理函数 CoTaskMemAlloc 内存分配函数
CoTaskMemRealloc 内存重新分配函数
CoTaskMemFree 内存释放函数
CoGetMalloc 获取 COM 库内存管理器接口

-------------------------------------------------------------------------------
HRESULT 类型
-------------------------------------------------------------------------------
大多数 COM 函数以及一些接口成员函数的返回值类型均为 HRESULT 类型。HRESULT 类型的返回值反映了函数中的一些情况,其类型定义规范如下:

31 30 29 28 16 15 0
|-----|--|------------------------|-----------------------------------|

类别码 (30-31) 反映函数调用结果:
00 调用成功
01 包含一些信息
10 警告
11 错误
自定义标记(29) 反映结果是否为自定义标识,1 为是,0 则不是;
操作码 (16-28) 标识结果操作来源,在 Windows 平台上,其定义如下:
#define FACILITY_WINDOWS 8
#define FACILITY_STORAGE 3
#define FACILITY_RPC 1
#define FACILITY_SSPI 9
#define FACILITY_WIN32 7
#define FACILITY_CONTROL 10
#define FACILITY_NULL 0
#define FACILITY_INTERNET 12
#define FACILITY_ITF 4
#define FACILITY_DISPATCH 2
#define FACILITY_CERT 11
操作结果码(0-15) 反映操作的状态,WinError.h 定义了 Win32 函数所有可能返回结果。
以下是一些经常用到的返回值和宏定义:
S_OK 函数执行成功,其值为 0 (注意,其值与 TRUE 相反)
S_FALSE 函数执行成功,其值为 1
S_FAIL 函数执行失败,失败原因不确定
E_OUTOFMEMORY 函数执行失败,失败原因为内存分配不成功
E_NOTIMPL 函数执行失败,成员函数没有被实现
E_NOTINTERFACE 函数执行失败,组件没有实现指定的接口

不能简单地把返回值与 S_OK 和 S_FALSE 比较,而要用 SECCEEDED 和 FAILED 宏进行判断。

===============================================================================
⊙ 第四章 COM 特性
===============================================================================
可重用性:包容和聚合
-------------------------------------------------------------------------------
包容模型:

组件对象在接口的实现代码中执行自身创建的另一个组件对象的接口函数(客户/服务器模型)。这个对象同时实现了两个(或更多)接口的代码。

聚合模型:

组件对象在接口的查询代码中把接口传递给自已创建的另一个对象的接口查询函数,而不实现该接口的代码。另一个对象必须实现聚合模型(也就是说,它知道自己正在被另一个组件对象聚合),以便 QueryInterface 函数能够正常运作。

在组件对象被聚合的情况下,当客户请求它所不支持的接口或者请求 IUnknown 接口时,它必须把控制交给外部对象,由外部对象决定客户程序的请求结果。
聚合模型体现了组件软件真正意义上的重用。

聚合模型实现的关键在 CoCreateInstance 函数和 IClassFactory 接口:

HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter,
DWORD dwClsContext, const IID& iid, (void **)ppv);

// class IClassFactory : public IUnknown
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter,
const IID& iid, void **ppv) = 0;

其中 pUnknownOuter 参数用于指定组件对象是否被聚合。如果 pUnknownOuter 参数为 NULL,说明组件对象正常使用,否则说明被聚合使用,pUnknownOuter 是外部组件对象的接口指针。

聚合模型下的被聚合对象的引用计数成员函数也要进行特别处理。在未被聚合的情况下,可以使用一般的引用计数方法。在被聚合时,由客户调用 AddRef/Release 函数时,必须转向外部组件对象的 AddRef/Release 方法。这时,外部组件对象要控制被聚合的对象必须采用其它的引用计数接口。

-------------------------------------------------------------------------------
进程透明性 (待学)
安全性(待学)
多线程特性(待学)
-------------------------------------------------------------------------------

===============================================================================
⊙ 第五章 用 Visual C++ 开发 COM 应用
===============================================================================
Win32 SDK 提供的一些头文件的说明
-------------------------------------------------------------------------------
Unknwn.h 标准接口 IUnknown 和 IClassFacatory 的 IID 及接口成员函数的定义
Wtypes.h 包含 COM 使用的数据结构的说明
Objidl.h 所有标准接口的定义,即可用于 C 语言风格的定义,也可用于 C++ 语言
Comdef.h 所有标准接口以及 COM 和 OLE 内部对象的 CLSID
ObjBase.h 所有的 COM API 函数的说明
Ole2.h 所有经过封装的 OLE 辅助函数

-------------------------------------------------------------------------------
与 COM 接口有关的一些宏
-------------------------------------------------------------------------------
DECLARE_INTERFACE(iface)
声明接口 iface,它不从其他的接口派生
DECLARE_INTERFACE_(iface, baseiface)
声明接口 iface,它从接口 baseiface 派生
STDMETHOD(method)
声明接口成员函数 method,函数返回类型为 HRESULT
STDMETHOD_(type, method)
声明接口成员函数 method,函数返回类型为 type

===============================================================================
⊙ 结 束
===============================================================================

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表

图片精选