[原] COM笔记 - 原理(基础)

发布时间:2012-12-14 17:24
分类名称:COM


COM对象和C++对象的区别

一个是面向二进制级别的, 一个是面向源码级别的

一个是平台无关的, 一个是平台相关的.

1. 封装性

COM对象完全被封装. COM对象和客户程序可能在不同的模块或不同的进程或不同的机器中. C++对象则往往在同一模块或同一进程中, 使用者有可能直接访问内部成员.

2. 可重用性
COM表现在包容和聚合, COM的多态通过接口体现, 面向二进制级。
C++多表现在继承和多态,源码级。

 

COM 对象

客户与COM组件程序交互的实体是COM对象,它并不关心组件模块的名称和位置,但它客户须知道自己在和哪儿个COM对象进行交互。

COM对象包括属性(also called 状态)和方法(also called 操作)。状态反映了对象的存在,也是区别于其它对象的要素;而对象提供的方法就是对象提供给外界的接口。

C++实现COM对象,很容易想到可以用class类来定义COM对象。所以,每个实例(C++)对应一个COM对象。如果使用C来实现,那对象就变成一个逻辑概念。(相对于客户来说,逻辑上说是COM对象,其实内COM部并没有自己认为的对象存在)。

一个COM中有很多对象存在,如何来寻找这些对象?使用GUID标识符号。为了和其它标识符相区分,将GUID命名为CLSID,在C++中,其实就是一个typedef。为什么使用GUID呢?很简单,在庞大的系统中,可能有人使用的标识和你使用的一样,使用GUID,基本上不可能一样。(一样的概率极其的低)。

 

COM 接口

一个COM组件也有很多接口,区分这些接口同样使用GUID,typedef为IID

客户 <====> 接口 <=====> 对象

客户 ===> 接口指针 ==> pVtable ==> vtable[pFunc1, pFunc2, pFunc3...] ==> [函数实现]

无论什么语言,只要能实现上面的这种内存结构(二进制级别),就可以定义接口。

定义接口需要注意几点:

  1. 每个接口的成员函数第一个参数总是接口本身的指针。(this指针)

    如果是C++的class实现,这个指针默认是隐藏的,所以无需实现。

  2. 字符串必须是Unicode(国际化)。
  3. 调用约定需要显示声明为_stdcall.

COM接口的描述不依赖任何一种编程语言,它使用IDL(Interface description language)语言来描述(其实还是使用了一种语言,不过这种语言只是用来描述,不是用来编程的)。

 

接口特点

  1. 二进制特性 支持COM的语言都能调用
  2. 接口不变性 接口需要稳固,总变不是件好事情
  3. 继承性(扩展性)一些接口最后有"2","EX",表明它是一个接口继承。
  4. 多态性

 

"接口之父" - IUnknown

平时,在c++定义一个全部都是虚函数的接口类,目的就是为了让继承它的子类去实现它定义的"规范"。你要不实现,编译器首先就不放过你。接口的作用就和那些通讯协议一样。只是个规范。

IUnknown规范了俩个重要的特性:生存期控制(引用计数)和接口查询

interface IUnknown

{

    HRESULT QueryInterface([in] REFIID iid, [out] void **ppv);

    ULONG   AddRef(void);

    ULONG   Release(void);

}

 

引用计数的实现

1. 组件级 太粗

2. 对象级 正好 (释放后通知组件)

3. 接口级 太细 (释放后通知对象)

 

使用引用计数规则

函数参数:

    - 输入参数        无需调用AddRef和Releae

    - 输出参数        函数返回之前对输出参数调用AddRef

    - 输入输出参数  修改之前调用Release,修改后调用AddRef。如果不修改,什么也不做。

局部接口指针变量  无需调用AddRef和Release

全局接口指针变量  传入函数之前AddRef,函数返回后,Release (应付多线/进程情况)

C++类成员变量     和全局变量类似

注释:引用计数使用很繁琐,稍有不慎就会出错,因此平常使用一些工具类,例如CComPtr,CComQIPtr之类的智能指针。

自动AddRef和Release。

 

接口查询

函数返回值:S_OK,E_NOINTERFACE,E_UNEXCEPTED

 

查询接口原则

  1. 同一个对象的不同接口,查询到的IUnKnown接口必须完全相同。

    也可以利用这点来判断俩个接口是否属于同一个对象。

  2. 对称性,查询自身总是成功的。
  3. 自反性,第一个接口查询到另一个接口,则从第二个接口必定能查到第一个接口。
  4. 传递性,第一个接口查询到第二个,第二个到第三个,从第三个必定能查询到第一个。
  5. 时间无关,在某时刻能够查询到,在以后任何时候都可以再次查询到。

 

接口实现

多继承(ATL使用),或内嵌类(MFC使用),多继承不要使用虚拟继承,他会破坏虚表的正常信息。(为什么?去查《深度探索 C++对象模型》,而且编译器不同,对虚表的调整结构也不尽相同)

 

GUID

typedef struct _GUID

{

DWORD Data1;        // 随机数

WORD   Data2;        // 和时间相关

WORD   Data3;        // 和时间相关

BYTE     Data4[8];    // 和网卡MAC相关

} GUID;

可以看到GUID是以空间+时间的方式防止随机值重复。

 

COM对象的标识 CLSID

COM接口标识 IID

 注释:为何使用俩个GUID分别标识接口和对象?一个接口可以有多个实现。通过IID查询到接口,获得接口支持的方法信息,然后选择其中一个实例化的对象来操作。所以调用CoCreateInstance会需要俩个GUID参数,一个是接口的,一个是对象的。

   

extern "C" const GUID CLSID_MYSPELLCHECKER = {0xFAEAE6B7, 0x67BE, 0x42a4, {0xA3, 0x18, 0x32, 0x56, 0x78, 0x1E, 0x94, 0x5A}};

GUID 乃随机数, 虽然不能保证唯一, 不过重复的几率很低.

GUID值可以由网络适配器来计算出随机值, 以及利用时间值来计算.

俩个工具来构造GUID: UUIDGen.exe(console) 和 GUIDGen.exe.

一个函数来生成: HRESULT CoCreateGuid(GUID *pguid);

   

接口描述语言: IDL(Interface Description Language)。

VC中,编译完IDL, 会生成TLB文件, xxx.h, xxx_i.c, 代理/存根源程序(dlldata.c、xxx_p.c、xxxps.def、xxxps.mak).

   

进程外通讯利用LPC和RPC机制

   

注册表

+ HKEY_CLASSES_ROOT/CLSID/{GUIDs ... }

  - InprocServer32(进程内组件)

  - LocalServer32(进程外组件)

  - TypeLib

  - Implemented Categories (对应HKEY_CLASSES_ROOT/ Component Categories 中的分类)

+ Interface

  - ProxyStubClsid

  - ProxyStubClsid32

+ <ProgID>

  - CLSID

  - CurVer

 

CATID COM组件分类

HKEY_CLASSES_ROOT/ Component Categories

 

注册COM    

进程内:regsvr32.exe (调用DllRegisterServer, DllUnRegisterServer)

进程外:/RegServer /UnRegServer

   

导出函数

DllRegisterServer

DLLUnregisterServer

DLLGetClassObject     不要直接调用,CoGetClassObject会调用CoLoadLibrary,接着就调用此函数。

DLLCanUnloadNow

   

类厂

class IClassFactory : public IUnknown

{

       virtual HRESULT __stdcall CreateInstance(IUnknown *pUnknownOuter, const IID &idd, void *ppv) = 0;

       virtual HRESULT __stdcall LockServer(BOOL bLock) = 0;

};

COM规定,每一个COM对象应该有一个对应的类厂对象。(这种类厂在设计模式中,应该就是"类厂方法"吧)。

HRESULT DllGetClassObject(const CLSID &clsid, const IID &iid, void **ppv); 来获得创建的类厂。

   

创建对象三个API

1. CoGetClassObject(const CLSID &clsid, DWORD dwClsContext,

COSERVERINFO *pServerInfo,

const IID &iid, void **ppv);

 

进程内组件调用流程

User -> CoGetClassObject -> DllGetClassObject -> Create instance of IClassFactory

 

2. CoCreateInstance

CoGetClassObject 包装, 然后接着调用厂类的CreateInstance, 创建COM对象和返回对应的接口.

只适用创建本地组件对象.

进程内组件调用流程

User -> CoGetClassObject -> DllGetClassObject -> IClassFactory::CreateInstance -> Create Object and return the pointer of the specific interface 

 

3. CoCreateInstanceEx 用于创建多个接口. 及其远端Instance

 

 

 

进程外组件

CoRegisterClassObject

CoRevokeClassObject

   

存根/代理

 

代理/存根作用:

  1. LPC/RPC调用
  2. 对参数和返回值进行翻译和传递
    1. 参数列集(marshaling)
    2. 参数散集(unmarshaling)

 

客户端调用起来看起来就像调用一个DLL,如同虚线画的那样。

   

 

类厂实现

1.       继承接口IClassFactory

2.       对生存期的控制(LockServer)

   

COM库

1.       初始化CoInitialize(IMalloc *pMalloc); (调用CoBuildVersion()函数可以不初始化)

2.       终止COM服务CoUninitialize();

   

适用COM库分配内存

class IMalloc : public IUnkown

{

    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;

};

  1. 直接使用IMalloc指针. CoGetMalloc

    代码略去指针检查:

    DWORD length = MAX_LENGTH;

    IMalloc *pIMalloc;

    HRESULT hr = CoGetMalloc(MEMCTX_TASK, &pIMalloc);

    psz = pIMalloc->Alloc(length);

    pIMalloc->Release(); // Notice here

  2. 适用封装好的API 函数.

void *CoTaskMemAlloc(ULONG cb);

void CoTaskMemFree(void *pv);

void CoTaskMemRealloc(void *pv, ULONG cb);

 

 常用函数:

类别

函数

功能

初始化函数

CoBuildVersion

CoInitialize

CoUninitialize

CoFreeUnusedLibraries

获取COM版本号

COM库初始化

COM库服务终止

释放进程中不在使用的组件

 GUID相关函数

IsEqualGUID

IsEqualIID

IsEqualCLSID

CLSIDFromProgID

StringFromCLSID

IIDFromString

StringFromIID

StringFromGUID2

判断GUID是否相等

判断IID是否相等

判断CLSID是否相等

ProgID -> CLSID

CLSID -> String

String -> IID

IID -> String

GUID -> String

对象创建函数

CoGetClassObject

CoCreateInstance

CoCreateInstanceEX

 

CoRegisterClassObject

 

CoRevokeClassObject

CoDisconnectObject

获取类厂对象

创建COM对象

同时,可指定多个接口和远程对象

登记一个对象,以便其它应用可以连接到该对象

取消登记操作

断开连接

 内存管理

CoTaskMemAlloc
CoTaskMemRealloc
CoTaskMemFree
CoGetMalloc

 

 

 

COM 内部调用过程

1 进程内组件协作

客户程序

COM库

组件程序(DLL)

CLSID clsid

IClassFactory *pClf;

IUknown *pUnknown;

CoInitialize(NULL);

CLSIDFromProgID("Dictinary.Object", &clsid);

  

  

  

COM 在注册表中查找CLSID

  

CoGetClassObject(clsid, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory,

(void **)&pClf);

  

  

  

COM库在内存中查找clsid组件 如果DictComp.dll还没装入内存,那就从注册表中获取组件程序全路径名.然后CoLoadLibrary(…), 接着调用DLL的DLLGetClassObject

  

  

  

创建类厂对象 CDictionaryFactory并返回IClassFactory接口.

  

COM库返回IClassFactory给用户

  

pClf->CreateInstance(NULL, IID_IUnknown,

(void **)&pUnknown);

  

  

  

  

类厂对象的CreateInstance函数被调用(通过组件的vtable直接被客户调用). 用new操作符构造字典组件对象new CDictionary; 返回IUnknown接口指针.

客户使用字典组件, 通过其他接口进行各种操作……

 

pClf->Release();

pUnknown->Release();

  

  

  

  

组件对象的Release被调用

{

delete this;

return 0;

}

CoFreeUnusedLibraries()

  

  

  

COM库调用字典组件的引出函数DllCanUnloadNow

  

  

  

DllCanUnloadNow函数中:

return TRUE;

else

return FALSE;

  

if (字典组件

DllCanUnloadUnloadNow函数返回TRUE)

   CoFreeLibrary(…);

  

CoUninitialize()

  

  

  

COM 库释放资源

  

客户程序退出

  

  

   

进程外组件协作

客户程序

COM库

组件程序(DLL)

CLSID clsid

IClassFactory *pClf;

IUknown *pUnknown;

CoInitialize(NULL);

CLSIDFromProgID("Dictinary.Object", &clsid);

  

  

  

COM 在注册表中查找CLSID

  

CoGetClassObject(clsid, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory,

(void **)&pClf);

  

  

  

COM库在内存中查找clsid组件 如果 组件.exe 还没启动或者COM需要另一个实例, 那就从注册表中获取EXE组件名,创建组件进程. 等待创建完成.

  

  

  

调用CoInitialize

创建组件支持的各种类厂对象, 调用COM函数注册所有类厂对象; CoRegisterClassObject

  

等COM注册完, COM库返回IClassFactory给用户

  

pClf->CreateInstance(NULL, IID_IUnknown,

(void **)&pUnknown);

  

  

  

  

类厂对象的CreateInstance函数被调用(通过组件的vtable直接被客户调用). 用new操作符构造字典组件对象new CDictionary; 返回IUnknown接口指针.

客户使用字典组件, 通过其他接口进行各种操作……

pClf->Release();

pUnknown->Release();

  

  

  

  

组件对象的Release被调用

{

delete this;

return 0;

}

CoUninitialize()

  

  

  

COM库对所有该客户没用成功释放的对象调用Release函数

  

  

  

组件程序退出

  

COM 库释放资源

  

客户程序退出

  

  

   

总结: 实现及调用COM流程

"组件"端实现流程

1. 定义接口

2. 实现接口, 实现类厂

3. 实现必要的导出函数(俩类: 注册表相关, 厂对象相关):

extern "C" HRESULT __stdcall DllGetClassObject 

说明: 得到创建厂类对象;

   

extern "C" HRESULT __stdcall DllCanUnloadNow(void)

说明: 当前对象和厂对象为0是, retrn TRUE;

   

extern "C" HRESULT __stdcall DllRegisterServer()

说明: 调用 regsvr32 *.dll 时候调用, 以便注册控件到注册表中.

   

DllUnregisterServer

说明: 调用 regsvr32 *.dll /u 时候调用, 以便删除控件在注册表中的信息.

 

注释:若使用MFC或者ATL,通过向导生成的代码中,导出函数和类厂已经自动实现完,不需要添加多余代码。就专注具体实现吧。而且引用计数的维护工作也无需管理。

   

"客户"端使用流程

初始化COM库 CoInitialize

创建COM实例, 创建的方式有:

CoGetClassObject

CoCreateInstance

CoCreateInstanceEx

查询接(QueryInterface)

使用接口指针

释放对象资源(CoFreeLibrary, CoFreeUnusedLibraries)

释放COM(CoUninitialize)