想像一个人学习开车的过程可能是这样的:首先他要懂得一些汽车构造的基本知识,然后要学会一些基本的动作(如插拔钥匙、点火、油门、离合、刹车、转向等)。有了这些技能后,基本上可以断定他可以把车开起来了。可仅是单纯的能把车开起来,并不表示已经成为了一个合格的驾驶员。因此他除了基本技能外,还必须掌握大量交通法规和汽车维护保养知识。之后要成为一个优秀的驾驶员,他所欠缺的就是丰富的实践经验了。
COM的学习过程也是如此。首先我们需要通晓COM最核心最基础的理念(能把车开起来的技能),然后要了解COM世界的诸多规则(交通法规的学习),最后就是实践经验的培养。
接下来我们按照这个思路,逐步的进入COM的世界。
COM的原理
COM的原理是什么?简单讲,就是利用C++的基类指针调用由派生类实现的虚函数。
在C++中按照COM的思路进行开发,需要对我们惯常的设计习惯做一些调整,无需过多担心,这种调整在不涉及具体的“交通规则”时,是很简单方便的,并且不需要除了C++语言外的知识。
首先要明确抽象基类的概念。
在C++语言中,如果一个类中含有一个纯虚函数,那么这个类就自动被归并到抽象类范畴。这个类本身是不可以实例化的,除非在它的派生类中显式实现了相关的纯虚函数,否则它的派生类也是不可以实例化的。我们对此做更严格的限制:
a 一个抽象基类不能包含数据成员
b 一个抽象基类不能包含非纯虚函数
c 一个抽象基类不能派生自非抽象基类
其次要将抽象基类应用到程序中。
我们需要按照功能分划出不同的抽象基类,每个基类声明出该类型功能需要提供的通用的函数原型。实现具体功能的类要继承自相应的抽象基类,并完成对抽象基类中所声明的函数原型的实现。
最后是将按照逻辑功能分类的模块,以可分发重用的形式生产出来,也就是动态链接库的理解和应用。
抽象基类在COM中被称为“接口”。
在COM中有3个概念是必须在现在就理清的。那就是库,类和接口。库可以理解为动态链接库本身,通常库是可分发的最小单位。一个库包含一个或多个类,这个“类”不同于C++语言里类的概念,它指的是一个或多个逻辑上有关联的接口的集合。接口正如我们前面所讲的抽象基类,它定义了一组方法的原型。
COM自身制定了一堆的预定义接口,实现者必须实现其中一个或多个。正是如此繁多的接口,使COM看起来晦涩难懂。我们挑要紧的来讲,那就是IUnknown接口和IClassFactory接口。
IUnknown接口定义了3个方法的原型:
1) AddRef : 引用计数增量操作
2) Release : 引用计数减量操作,如果引用计数为零,销毁当前对象。
3) QueryInterface : 从当前接口获得同一类对象中其它的接口指针。
IClassFactory接口定义了2个方法原型:
1) CreateInstance : 创建一个类实例,并返回一个用户指定的接口指针。
2) LockServer : 确保本服务的实现者驻留在内存中,在多次调用CreateInstance时可以获得最好的性能。
IUnknown接口是所有接口的祖先,除了IUnknown接口外的所有接口,都必须直接或间接派生自IUnknown。这个接口提供了引用计数原型以控制接口的生命期。
COM标准要求每个类(A)应该有一个实现了IClassFactory接口的类(B),由这个实现IClassFactory的类(B)负责创建类(A)的实例。那么这个类(B)又由谁创建呢?COM规定每个COM可分发对象,也就是DLL,必须实现一个导出函数DllGetClassObject,由此函数创建相应的类(B)对象实例。
除此之外的可选预定义接口还有很多,比如IDispatch、IMoniker、IPersist、IConnectionPoint…..刚开始学习COM的时候就陷入这些接口陷阱是很悲惨的,所以我们不妨先绕开。
接下来我们用一个例子来简单实践一下。为了不涉及前面尚未提到知识,这个例子并非完全按照COM所要求的标准创建,只是为了理解COM开发的一些基本特征。
/* IUnknown 接口 */
class IUnknown
{
public:
virtual unsigned int AddRef(void) = 0;
virtual unsigned int Release(void) = 0;
virtual bool QueryInterface( const char* InterfaceName,
void ** ppInterface ) = 0;
};
/* IClassFactory 接口*/
class IClassFactory : public IUnknown
{
public:
virtual bool CreateInstance(const char* InterfaceName, void ** ppInterface ) = 0;
virtual bool LockServer( BOOL bLock ) = 0;
};
/* 用户定义接口*/
class IEquipment : public IUnknown
{
public:
virtual bool TurnOn(void) = 0;
virtual bool TurnOff(void) = 0;
};
/* 用户接口实现A */
class CTV : public IEquipment
{
private:
unsigned int m_ref;
public:
CTV(void):m_ref(0){};
protected:
virtual ~CTV(void){};
public:
/* 继承自IUnknown */
virtual unsigned int AddRef(void)
{
return ++m_ref;
}
virtual unsigned int Release(void)
{
unsigned int uirtn = --m_ref;
if ( 0 == m_ref )
{
delete this;
}
return uirtn;
}
virtual bool QueryInterface( const char* InterfaceName,
void ** ppInterface )
{
if ( 0 == strcmp("IUnknown", InterfaceName) )
{
*ppInterface = dynamic_cast<IUnknown * >(this);
((IUnknown*)(*ppInterface))->AddRef();
}
else if ( 0 == strcmp( "IEquipment", InterfaceName )
{
*ppInterface = dynamic_cast<IEquipment * >(this);
((IEquipment*)(*ppInterface))->AddRef();
}
else
{
*ppInterface = NULL;
}
return NULL != *ppInterface;
}
/* 继承自IEquipment */
virtual bool TurnOn(void)
{
//...Turn on TV
return true;
}
virtual bool TurnOff(void)
{
//...Turn off TV
return true;
}
};
/* 类厂实现 */
class CclassFactoryObj : public IClassFactory
{
public:
virtual bool CreateInstance(const char* InterfaceName, void ** ppInterface )
{
*ppInterface = NULL;
CTV * _ptv = new CTV();
If( NULL != _ptv )
{
_ptv->AddRef();
if ( _ptv->QueryInterface( InterfaceName, ppInterface ) )
{
return true;
}
else
{
_ptv->Release();
_ptv = NULL;
}
}
return false;
}
virtual bool LockServer( BOOL bLock )
{
/* 不支持此操作,简单返回false */
return false;
}
/* 派生自IUnknown函数的实现,略*/
…
};
为了利用类厂创建一个CTV的实例,我们提供一个单独的函数:
bool DllGetClassObject( const char * InterfaceName, void ** ppInterface )
{
bool brtn = false;
CClassFactoryObj * pobj = new CClassFactoryObj();
If ( NULL != pobj )
{
pobj->AddRef();
brtn = pobj->CreateInstance( InterfaceName, ppInterface );
pobj->Release();
pobj = NULL;
}
return brtn;
}
客户程序可以这样使用:
……
IEquipment * pIEquipment = NULL;
if (DllGetClassObject (“Iequipment”, & pIEquipment) )
{
pIEquipment->TurnOn();
pIEquipment->TurnOff();
pIEquipment->Release();
pIEquipment = NULL;
}
……
上述就是COM的基本思想。接下来该介绍具体的“交通规则”了。
COM的规则
1. GUID
GUID是一个128位的数,用来全球唯一地标识一个库、一个类以及每一个接口。它的概念有些类似网卡的MAC地址。
VC附带的工具GUIDGEN.exe 可以生成GUID,这个工具位于“VC6安装目录/Common/Tools”目录下。
在前面提到的QueryInterface函数中,应该用接口的GUID来代替直接用字符串指定的接口名。
相对于COM中的类和接口,它们的GUID通常被称为CLSID 和IID。
2. 返回值
HRESULT 是标准的COM接口函数返回值,是一个32位的整数。各位含义如下:
最高位:严重程度位,指示了操作成功还是失败。
29~30位:保留。
16~28位:操作码,指示了HRESULT对应于什么技术。
0~15位:信息码,在给定的严重程度和相应的技术情况下精确的结果值。
有两个宏可以简化对 HRESULT相关信息的检查:
HRESULT hr;
…
SUCCEEDED( hr ); // 如果为真,操作成功
FAILED(hr ); // 如果为真,操作失败
3. 类厂与对象创建
客户程序并不需要显式的装入DLL并调用DllGetClassObject以创建类厂。COM底层的机制会自动的完成这一切。COM提供了3个API函数用于对象的创建,它们的原型如下:
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 );
HRESULT CoCreateInstanceEx( const CLSID & clsid,
IUnknown * pUnknownOuter,
DWORD dwClsContext,
COSERVERINFO * pServerInfo,
DWORD dwCount,
MULTI_QI * rgMultiQI );
clsid :
将要创建的对象的CLSID。
dwClsContext :
指定组件的类别,在现阶段可以固定指定为CLSCTX_INPROC_SERVER,意思是进程内组件。
pServerInfo :
分布式系统使用,现阶段指定为NULL。
iid :
创建类对象后想获得的第一个接口的IID。
ppv :
返回iid所指定的接口指针。
pUnknownOuter:
用于聚合,如果不涉及聚合,设置为NULL。
CoCreateInstanceEx 可以在创建后返回多个接口的指针。
4. 聚合与包容
聚合和包容是COM的两种重用模型。
聚合有些像C++语言里面的公共继承机制。派生类继承父类后,也就拥有了父类所有的功能。
包容类似C++语言里面的成员变量机制。比如一个实现某些功能的类X作为类A的一个成员变量,然后类A要实现一系列的函数,把客户的调用映射那个类X型的成员变量的相应函数上。
应用包容模型是非常简单的,只是重新定义调用映射的函数工作量比较大。
聚合模型的实现就困难得多,被聚合的COM类必须实现“一真一假”两套IUnknown接口,一套(“真”)在对象被创建时返回给创建者;另一套(“假”)把所有对自己的QueryInterace调用回传给创建者。由创建者决定返回哪个接口。
5. 连接点
COM模型决定了由客户调用COM组件的功能是很方便的,但是如果需要COM组件与客户程序进行交互,也就是实现COM组件与客户程序间的双向通信,就需要引进连接点。 连接点其实就是由客户程序实现的接口,然后客户程序把这个接口交给COM组件;COM组件在需要主动和客户程序通信的时候,使用这个接口提供的函数。有能力接受客户程序提供的连接点的COM组件,被称为可连接对象。
可连接对象必须实现IConnectionPointContainer接口,该接口定义如下:
class IconnectionPointContainer :public IUnknown
{
public:
virtual HRESULT EnumConnectionPoints( IEnumConnectionPoints ** ) = 0;
virtual HRESULT FindConnectionPoint( const IID * , IConnectionPoint **) = 0;
};
IEnumConnectionPoints称为枚举器接口,它可以枚举此可连接对象所支持的所有连接点接口。
COM最根本的原理大致如此。未涉及的部分还包括列集-散集操作原理、分布式COM(DCOM )原理等。由于COM涉及面实在太广,无法提供一个比较周全的介绍。本文只对入门者必须了解的部分做了概要的讲解。