COM学习笔记(二)
COM对象的标识——CLSID
客户程序不直接去访问COM组件,而是通过一个GUID进行对象的创建和初始化工作。COM规范采用了128位全局唯一标识符GUID。一个GUID的例子如下:
{54BF6567-1007-11D1-B0AA-444553540000}
在C/C++语言中可以用这样的结构描述:
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
}GUID;
于是前面的GUID例子可以定义为:
{0x54bf6567,0x1007,0x11d1,{0xb0,0xaa,0x44,0x45,0x53,0x54,0x00,0x00}};
GUID的生成可以借助VS下的一个工具GUID生成器,或者利用COM库中的API函数CoCreateGuid,定义如下:
HRESULT CoCreateGuid(
__out GUID *pguid
);
如果函数创建GUID成功,则返回S_OK,并且pguid将指向所得到的GUID值。实际上CLSID是用来标识COM对象的GUID,CLSID在结构上与GUID一致。(GUID并不是专门用来定义COM对象的,它也用于定义其他实体的标识符)
COM对象的特点
COM对象与C++对象类似都具有封装性和可重用特性。可重用特性上篇已经提过,COM对象的可重用性是建立在二进制可执行文件的基础上的,而C++对象的可重用性只是建立在源码的基础上。另外,COM对象的实现过程中往往这两种重用性都会用到。
COM对象的数据成员的封装以组件模块为最终边界,对于对象的用户是完全不可见的,C++对象的封装特性只是语意上的封装,对与对象用户是可见的。
COM接口的定义和标识
上篇提到,COM对象的客户与对象之间通过接口进行交互,所以组件之间接口的定义至关重要。接口是包含一组函数的数据结构,通过这组数据结构客户代码可以调用组件对象的功能。客户程序用一个指向接口数据结构的指针来调用接口成员函数,实际上接口指针又指向另一个指针,这第二个指针指向一组函数,称为接口函数表,接口函数表中每一项为4字节长的函数指针,指向具体的实现代码。通常把接口函数称为虚函数表(vtable),指向vtable的指针为pvtable,上述关系如图2.1所示。
图2.1 接口结构
与COM对象标识方法一致,COM接口也采用了GUID,不过这里它就称为IID(interface identifier)。如果客户程序要使用一个COM对象的某个接口,则它必须知道该接口的IID和接口所能提供的方法(即接口成员函数)。一般我们采用C++语言来定义接口,如:
{
virtual BOOL MyFunc1() = 0;
virtual BOOL MyFunc2() = 0;
virtual void MyFunc3() = 0;
virtual BOOL MyFunc4() = 0;
};
C++的class定义中隐藏的this指针可以帮助我们找到pVtable。实际中,COM对象除了这些接口函数外,往往还含有自己的属性数据,这些属性数据反映了对象的状态信息以区别其他对象。然而this指针可以为我们提供COM对象的接口函数,但并不知道this指针如何与对象的状态信息关联起来,所以我们往往用一个自己定义的类来继承接口,如:
{
public:
CMyInterfaceClass();
~CMyInterfaceClass();
public:
virtual BOOL MyFunc1();
virtual BOOL MyFunc2();
virtual void MyFunc3();
virtual BOOL MyFunc4();
private:
BYTE* m_MyData;
char* m_MyName;
//other private information
};
这样可以通过每个接口成员函数所包含一个隐藏的this指针,就可以访问到对象的属性数据了。这样接口与对象之间的结构关系如图2.2所示。
图2.2 接口和对象之间的结构关系
如果一个客户程序使用了两个该对象,显然两个对象公用了成员函数,但属性数据是不能公用的。这时,多个对象与接口之间的结构关系如图2.3所示。
图2.3 多个对象与接口之间的结构关系
如果第二个对象并没有采用CMyInterfaceClass类的结构来实现类似的函数功能,而是另外实现了该接口,则此时多个对象和接口的内存结构关系如图2.4所示。
图2.4 不同方法实现的两个该对象与接口的结构关系
这种把接口指针与对象属性数据绑定在一起的方法并不是唯一的,也可以采用其他方法来实现接口,只要成员中的this指针(即接口指针)与对象数据能建立联系,在接口成员函数中可以访问到对象数据即可。在MFC和ATL中就分别采用了不同的机制来提供对COM接口的支持。