作者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年12月10日
1. COM概述
COM即组件对象模型,是一种已组建为发布单元的对象模型,使各软件组件可以用一种统一的方式进行交互。COM既规定了组件之间进行交互的规范,也提供了实现交互的环境,可以作为不同语言协作开发的一种标准。
1.1 面向对象的组件模型 —— COM
-
组件的产生
计算机软件发展的早期,一个app往往是一个单独的应用程序,从软件模型角度来考虑,进化的方法就是把一个庞大的应用程序分成多个模块,每个模块保持一定的功能独立性。协同工作时通过相互之间的接口完成实际任务,这样的模块称为组件。
-
面向对象的组件
组件之间的接口是组件软件的关键,需要遵守统一的标准。如果不考虑与其他软件通信,则可以使用自定义的接口标准,否则需要使用一些公用的标准,COM就是Windows系统上这样一个组建标准。
COM不仅提供了组件之间接口标准,还引入了面向对象的思想:组件模块为COM对象提供活动的空间,COM对象以接口的方式提供服务。
-
组件、对象、接口
Windows下一个COM组件可以是dll或exe,一个组件程序可以包含多个COM对象,每个COM对象可以实现多个接口。普通程序调用组件的功能时先创建一个COM对象,或获取一个COM对象,然后通过该对象实现的COM接口调用提供的服务。
1.2 COM结构
COM标准包括规范和实现两部分,规范部分定义了组件之间通信的机制,实现部分就是COM库,为COM规范的具体实现提供了一些核心服务。
1.2.1 对象与接口
-
COM对象
COM对象类似于C++中对象的概念,是某个类的实例,而类则是一组相关的数据和功能组合在一起的一个定义。接口是一组逻辑上相关的函数集合,对象通过接口成员函数为客户提供服务。
-
COM接口
COM模型中对象本身对客户是不可见的,客户只能通过接口请求服务。每一个接口都由一个128位GUID标识,客户通过GUI获得接口的指针,通过接口指针调用响应的成员函数。
-
COM接口指针
客户如何标识COM对象呢?与接口类似,每个对象也用128位GUID来标识,称为CLSID。只要系统中含有这类COM对象的信息,并包括COM对象所在的模块文件dll/exe以及COM对象在代码中的入口点,客户程序就可以由CLSID来创建COM对象。创建成功后客户得到一个指向对象某个接口的指针,就可以调用该对象所有接口提供的服务了。
COM对象本身可以是有状态的也可以是无状态的,对用户透明。
1.2.2 C/S模式
C/S模式的优点是稳定性好,但COM不仅仅是一种简单的C/S模式,客户也可以反过来提供服务。
1.2.3 COM库
COM本身除了规范之外还有实现的部分,包括一些核心的OS级代码。这些库以dll形式存在,包括:
- 提供少量API实现C/S端COM应用的创建过程
- COM通过注册表查找本地服务,progname与CLSID的转换
- 提供一种标准的内存控制方法
COM库在OS层实现,一个OS只有一个COM库实现,保证所有组件按照统一的方式进行交互操作,使我们在编写COM应用时不用编写COM通信相关的大量基础代码。
1.3 COM特性
- 语言无关性:二进制层次的对象,C++/java等都可以调用
- 进程透明性:不需要关心dll/exe/远程等多种形式
- 可重用性:包容、聚合
2. COM对象与接口
2.1 COM对象
2.1.1 COM对象的标识 —— CLSID
COM组件的位置对客户来说是透明的,因为客户不直接访问COM组件,而是通过一个CLSID进行对象的创建和初始化。CLSID实际上就是GUID,只不过是专门用于COM的GUID。VS提供了两个工具来随机生成GUID:uuidgen.exe/guidgen.exe. COM库提供以下API:
HRESULT CoCreateGuid(GUID* pguid);
2.1.2 COM对象 vs C++对象
-
封装特性:
COM对象中数据是完全封装在对象内部的,外部不可能直接访问对象的数据属性;C++使用者可以直接访问对象中的public数据成员。
-
可重用性:
COM对象A要使用COM对象B的功能,可以通过包容和聚合实现,不需要重新编译;C++则表现在源码一级上,需要重新编译。
2.2 COM接口
2.2.1 从API到COM接口
假如我们要实现一个字处理应用程序,需要一个查字典的功能:
BOOL EXPORT Initialize();
BOOL EXPORT LoadLibrary(char*);
BOOL EXPORT InsertWord(char*, char*);
BOOL EXPORT DeleteWord(char*);
BOOL EXPORT LookupWord(char*, char**);
BOOL EXPORT RestoreLibrary(char*);
BOOL EXPORT FreeLibrary();
2.2.2 接口定义与标识
接口指针又指向另一个叫做vptr的指针,vptr再指向vtable。对一个接口来说,vtable是确定的、接口的成员函数个数是确定的、先后顺序是确定的、每个函数的参数和返回值也是确定的,只要在二进制一级确定这些信息,任何语言都可以实现。
C++中的定义如下:
class IDictionary
{
public :
virtual BOOL __stdcall Initialize() = 0;
virtual BOOL __stdcall LoadLibrary(String) = 0;
virtual BOOL __stdcall InsertWord(String, String) = 0;
virtual void __stdcall DeleteWord(String) = 0;
virtual BOOL __stdcall LookupWord(String, String *) = 0;
virtual BOOL __stdcall RestoreLibrary(String) = 0;
virtual void __stdcall FreeLibrary() = 0;
};
2.2.3 描述语言IDL
使用MIDL工具能把IDL转化成C/C++的头文件。
interface IDictionary
{
public :
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();
};
2.2.4 接口的内存模型
我们实现刚刚的接口类,并添加属性:
class CDictionary : public IDictionary
{
public :
CDictionary();
~CDictionary();
// IDictionary member function
virtual BOOL __stdcall Initialize();
virtual BOOL __stdcall LoadLibrary(String);
virtual BOOL __stdcall InsertWord(String, String);
virtual void __stdcall DeleteWord(String);
virtual BOOL __stdcall LookupWord(String, String *);
virtual BOOL __stdcall RestoreLibrary(String);
virtual void __stdcall FreeLibrary();
private :
struct DictWord **m_Data;
char *m_DictFilename[128];
};
如果两个CDictionary对象:
如果一个使用CDictionary的实现,另一个不是CDictionary的实现,那么:
2.3 IUnknown 接口
COM规范规定每一个接口都必须从IUnKnown继承,因为IUnknown提供了两个重要特性:生存期控制和接口查询:
class IMyUnknown
{
public:
virtual HRESULT __stdcall QueryInterface(const IID& iid, void **ppv) = 0 ;
virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
};
2.3.1 引用计数
这部分与一般的智能指针原理一样,不再详细介绍。需要注意的是一般采用在COM对象一级使用引用计数,因此需要为每个COM对象实现IUnKnown的AddRef和Release方法:
ULONG CDictionary::AddRef()
{
m_Ref ++;
return (ULONG) m_Ref;
}
ULONG CDictionary::Release()
{
m_Ref --;
if (m_Ref == 0 ) {
delete this;
return 0;
}
return (ULONG) m_Ref;
}
2.3.2 接口查询
一个COM对象可以实现多个接口,客户程序可以在运行时刻对COM对象的接口进行查询,如果对象实现了该接口则提供服务,否则拒绝服务。这一切是通过QueryInterface
实现的。
输入参数iid
为接口标识符IID
,输出参数ppv
为查询得到的结果的接口指针,NULL
表示没有实现该接口。下面是使用方式:
BOOL __stdcall CreateObject(const CLSID& clsid, const IID& iid, void **ppv)
{
if (clsid == CLSID_Dictionary ) {
CDictionary *pObject = new CDictionary;
HRESULT result = pObject->QueryInterface(iid, ppv);
return (result == S_OK) ? TRUE : FALSE;
}
return FALSE;
}
2.3.3 接口查询的原则
- 同一个对象的不同接口指针,查询得到的IUnknown接口必须相同
- 接口的自反性:查询自己总能成功
- 接口的对称性:A->B成功则B->A一定成功
- 接口的传递性:A->B B->C C->D 那么D->A一定成功
- 接口查询时间无关性
2.3.4 实现多个接口时的内存模型
【注意】不能使用虚继承,因为一定要保证两个接口对应的IUnKnown是不同的。
2.3.4 QueryInterface
函数实现
HRESULT CDictionary::QueryInterface(const IID& iid, void **ppv)
{
if ( iid == IID_IUnknown ) {
*ppv = (IDictionary *) this ;
((IDictionary *)(*ppv))->AddRef() ;
} else if ( iid == IID_Dictionary ) {
*ppv = (IDictionary *) this ;
((IDictionary *)(*ppv))->AddRef() ;
} else if ( iid == IID_SpellCheck ) {
*ppv = (ISpellCheck *) this ;
((ISpellCheck *)(*ppv))->AddRef() ;
} else {
*ppv = NULL;
return E_NOINTERFACE ;
}
return S_OK;
}
【注意】在第一个if中没有把this直接转换成IUnKnown指针,因为CDictory有两个IUnKnown节点,会存在二义性。因此先转成实现的接口中任意一个,然后输出时会二次转换成IUnKnown。
对应的CDictionary的COM服务器与客户端程序,猛击此处下载
- 如果这篇文章对您有帮助,请到CSDN博客留言;
- 转载请注明:来自[雨润的技术博客]http://(blog.csdn.net/sunyurun) http://blog.csdn.net/sunyurun