COM对象跟C++对象比,它只提供接口给外部调用,具有完全严格的封装性;再者C++只是源码层上的重用,COM则在二进制层上建立了统一规范,把接口定义(IDL语言描述,可用工具转换成C/C++头文件或其他语言支持的格式)与接口实现分离,真正地实现了与编程语言无关的程序重用。COM的具体实现可以是DLL、OCX,也可以是EXE程序。COM的调用可以在进程内、进程间或网络主机间,无论以何种方式对于客户端来说都是透明的。
COM相关的一些概念:
CLSID:COM对象标识,即128位的GUID(从概率上基本可以保证唯一性)。
ProgID:COM组件标识符,字符串表示,与 CLSID有对应关系,函数CLSIDFromProgID、ProgIDFromCLSID用于互相转换。
IID:接口标识,也是GUID,接口定义表现为一组函数的数据结构定义,这组函数表称为虚函数表,每个接口有都一个虚函数表指针。
入口函数:HRESULT DllGetClassObject(const CLSID &clsid,const IID &iid,void **ppv),一般用来创建类厂对象。
卸载函数:HRESULT DllCanUnloadNow()。
接口的继承只继承函数说明,每个接口都从IUnknown接口派生,直接的或间接的。IUnknown接口定义(C++):
class IUnknown {
public:
// 查询COM对象的其他接口指针
// S_OK成功、E_NOINTERFACE找不到接口(ppv返回NULL)、E_UNEXPECTED发生意外(ppv返回NULL)
virtual HRESULT _stdcall QueryInterface(const IID &iid,void **ppv) = 0;
// 引用计数操作
virtual ULONG _stdcall AddRef() = 0;
virtual ULONG _stdcall Release() = 0;
}
每个COM对象类都有一个类厂接口,C++定义:
class IClassFactory:public IUnknown {
public:
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter,const IID &iid, void **ppv) = 0;
virtual HRESULT _stdcall LockServer(BOOL bLock) = 0;
}
COM注册表信息:
HEKY_CLASSES_ROOT->CLSID下,对于进程内组件,有子键InprocServer32,缺省字符串为文件路径,进程外组件对应的是LocalServer32子键。如果有ProgID子键,说明此COM具有字符串别称,另有HEKY_CLASSES_ROOT->CLSID->别称->CLSID项,能通过别称找到对应CLSID后获取COM组件信息。
COM注册:
RegSvr32 文件,调用导出函数DllRegisterServer来完成注册信息。
RegSvr32 /u 文件,调用导出函数DllUnregisterServer来移除注册信息。
CoGetClassObject与CoCreateInstance:
HRESULT CoGetClassObject(const CLSID &clsid,DWORD dwClsContext,COSERVERINFO *pServerInfo,const IID &iid,void **ppv);
HRESULT CoCreateInstance(const CLSID &clsid,IUnknown *pUnknownOuter,DWORD dwClsContext,const IID &iid,void **ppv);
COM组件的创建一般不由客户程序直接调用导出函数创建,通常是利用COM库完成组件初始化工具,一般的过程如下:
1.CoInitialize(NULL)初始化COM库。返回S_OK成功,S_FALSE之前已初始化过,E_UNEXPECTED初始化错误。
2.CoCreateInstance(),CoCreateInstance函数先传入类厂接口标识调用CoGetClassObject函数,CoGetClassObject内部又会调用导出函数DllGetClassObject创建类厂对象,得到返回的类厂接口指针,接着再调用类厂接口的CreateInstance函数创建COM对象,最后释放类厂对象。
3.CoFreeUnusedLibraries()卸载COM组件,函数会检测当前进程中的所有组件,当发现某个组件导出函数DllCanUnloadNow返回TRUE时卸载该组件。
4.CoUninitialize()释放COM库。
COM库提供的内存管理接口IMalloc:
// IID_IMaloc:{00000002-0000-0000-c000-000000000046}
class IMalloc:public IUnknown {
virtual void* Alloc(ULONG cb) = 0;
virtual void* Realloc(void *pv,ULONG cb) = 0;
virtual void Free(void *pv) = 0;
virtual ULONG GetSize(void *pv) = 0;
// pv是否由内存管理接口所分配
virtual int DidAlloc(void *pv) = 0;
// 使堆内存尽可能减小
virtual void HeapMinimize() = 0;
}
获取IMalloc指针:CoGetMalloc(MEMCTX_TASK,IMalloc **ppMalloc),使用完成后应调用pMalloc->Release()函数释放。
COM库封装好的内存操作函数:CoTaskMemAlloc(ULONG cb),CoTaskMemFree(void *pv),CoTaskMemRealloc(void *pv,ULONG cb)。
包容与聚合:
COM的重用有包容与聚合这2种模型,包容是指COM在重用另一个组件接口时,本组件只是另一个组件接口的代理存根,该组件只不过转发调用了已有的接口实现,但是对客户端来说是透明的,客户端并不知道组件的嵌套关系。
聚合就比较麻烦一点,首先在聚合的外部对象中,只有聚合内部对象接口的定义而没有具体实现(连代理存根都没有),外部对象初始化过程中创建内部对象并保存其IUnknown指针。在客户端查询接口时,外部对象的QueryInterface方法根据IID返回本对象接口或内部对象接口(使用保存的内部IUnknown查询)。根据COM规范,一个COM对象的IUnknown接口值必须是唯一的且可以通过任何一个接口查询到所有的接口,所以要实现聚合内部对象也要作一些配合。内部对象在创建时会判断CoCreateInstance第二个参数IUnknown*的值,如果是NULL则表明未聚合状态创建,否则此参数是外部对象的IUnknown指针表明聚合状态下创建,内部对象QueryInterface处理中如果是聚合状态则转发调用外部对象的QueryInterface(使用保存的外部IUnknown指针)。另外对引用、释放计数也要做到合理对应,避免递归析构等情况。