发布时间: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...] ==> [函数实现]
无论什么语言,只要能实现上面的这种内存结构(二进制级别),就可以定义接口。
定义接口需要注意几点:
如果是C++的class实现,这个指针默认是隐藏的,所以无需实现。
COM接口的描述不依赖任何一种编程语言,它使用IDL(Interface description language)语言来描述(其实还是使用了一种语言,不过这种语言只是用来描述,不是用来编程的)。
接口特点
"接口之父" - 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
查询接口原则
也可以利用这点来判断俩个接口是否属于同一个对象。
接口实现
多继承(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
存根/代理
代理/存根作用:
客户端调用起来看起来就像调用一个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;
};
代码略去指针检查:
DWORD length = MAX_LENGTH;
IMalloc *pIMalloc;
HRESULT hr = CoGetMalloc(MEMCTX_TASK, &pIMalloc);
psz = pIMalloc->Alloc(length);
pIMalloc->Release(); // Notice here
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 |
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)