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

假行僧0life

假行僧0life

2016-02-19 15:14

图老师设计创意栏目是一个分享最好最实用的教程的社区,我们拥有最用心的各种教程,今天就给大家分享《COM 原理与应用》学习笔记 - 第一部分 COM原理的教程,热爱PS的朋友们快点看过来吧!

  ⊙ 第一章 概述

  ===================================================

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

(本文来源于图老师网站,更多请访问https://m.tulaoshi.com/bianchengyuyan/)

  进程内服务程序:

  服务程序被加载到客户的进程空间,在 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,

(本文来源于图老师网站,更多请访问https://m.tulaoshi.com/bianchengyuyan/)

  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 库

展开更多 50%)
分享

猜你喜欢

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

编程语言 网络编程
《COM 原理与应用》学习笔记 - 第一部分 COM原理

AJAX开发简略 (第一部分)

Web开发
AJAX开发简略 (第一部分)

s8lol主宰符文怎么配

英雄联盟 网络游戏
s8lol主宰符文怎么配

经典搜索案例1001 第一部分:基础篇

电脑网络
经典搜索案例1001 第一部分:基础篇

一个 C++ 日期类(第一部分)

C语言教程 C语言函数
一个 C++ 日期类(第一部分)

lol偷钱流符文搭配推荐

英雄联盟 网络游戏
lol偷钱流符文搭配推荐

Shared Source CLI Essentials第一章第一部分

电脑网络
Shared Source CLI Essentials第一章第一部分

JavaScript进阶教程(第四课第一部分)

Web开发
JavaScript进阶教程(第四课第一部分)

lolAD刺客新符文搭配推荐

英雄联盟
lolAD刺客新符文搭配推荐

Delphi中为TreeView添加单选和复选框

Delphi中为TreeView添加单选和复选框

如何学好Delphi

如何学好Delphi
下拉加载更多内容 ↓