COM接口与COM组件
COM接口是COM规范中最重要的部分,COM规范的核心内容就是对接口的定义,甚至可以说“在COM中接口就是一切”。组件与组件之间、组件与客户之间都要通过接口进行交互。接口成员函数将负责为客户或其他组件提供服务。与标识COM对象的CLSID类似,每一个COM接口也使用一个GUID来进行标识,该标识也被称为IID(interface identifier,接口标识符)。
COM接口实际限定了组件与使用该组件的客户程序或其他组件所能进行的交互方式,任何一个具备相同接口的组件都可对此组件进行相对于其他组件透明的替换。只要接口不发生变化,就可以在不影响整个由组件构成的系统的情况下自由的更换组件。通常在程序设计阶段需要将接口设计的尽可能完美,以减少在开发阶段对COM接口的更改。尽管如此,在实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。与C++中对类的继承有些类似,对COM接口的发展也可以通过接口继承来实现。但是COM接口的继承只能是单继承而不允许从多个基接口进行派生,而且派生接口只是继承了对基接口成员函数的说明而没有继承其实现。
interface IX { virtual void _stdcall fun1() = 0; virtual void _stdcall fun2() = 0; } interface IY { virtual void _stdcall fun3() = 0; virtual void _stdcall fun4() = 0; } class ObjectA :public IX,IY { public: // 抽象基类IX的实现 virtual void Func1() {cout<<"Func1"<<endl;}; virtual void Func2() {cout<<"Func2"<<endl;}; // 抽象基类IY的实现 virtual void Func3() {cout<<"Func3"<<endl;}; virtual void Func4() {cout<<"Func4"<<endl;}; };
对于接口,通常是采用抽象基类来定义,并利用类的多重继承来实现该组件。例如,在上面这段代码中,IX和IY是用于实现接口的抽象基类。所谓的抽象基类是只包含一个或多个虚函数声明而未包括虚函数的具体实现的类。抽象基类不能被实例化,而只能用作基类使用,并要求其派生类完成其所有虚函数的实现。在上面这段代码中,CObjectA组件即继承了IX和IY这两个抽象基类,并实现了其所定义的虚函数.
接口模型
抽象基类本身由于没有实体函数与变量,所以并不分配内存。通常只是用来为派生类指定内存结构。只有在派生类实现此抽象基类时,指定的内存才会被分配。图3为此内存结构的示意:
抽象基类定义的内存结构示意
图中vtable为虚拟函数表,能够为实例数据的提供一个方便保存的位置,并能够在同一类的多个实例间共享。在每个实例的内存映射中均包含一个指向该类的vtable表的指针pVtable。pVtable指针存放于所有数据成员之前,由于每个虚函数在vtable表中有唯一的索引,编译器只需根据索引从vtable表中找到函数地址即可。也就是说,客户只要获取得到了接口指针,就可以使用此COM对象的实际功能。
由抽象基类指定的内存结构是符合COM规范的,因此抽象基类IX可以认为是一个COM接口,但这还不是一个严格意义上的COM接口。对于一个真正意义上的COM接口,在设计时应遵循以下几个规则:
1) 接口必须直接或间接地从IUnknown继承。
2) 接口必须具有唯一的标识(IID)。
3) 一旦分配和公布了IID,有关接口定义的任何因素都不能被改变。
4) 接口成员函数应具有HRESULT类型的返回值。
5) 接口成员函数的字符串参数应采用Unicode类型。
这几条规则中,最基本的是第一条,如果一个对象没有至少实现一个最小程度为IUnknown的接口,那么该对象也就不是一个严格的COM对象。IUnknown接口是COM的核心接口,从上述规则可以得知,任何一个COM接口都必须从IUnknown接口继承。客户在组件之间的通信是通过接口来实现的。组件可以不提供其他接口,但是必须提供IUnknown接口以使客户能够对组件其他接口进行查询。
IUnknown接口提供有成员函数QueryInterface()、AddRef()和Release(),分别用于查询组件中的其他接口和进行生存期控制。由于任何COM接口都是从IUnknown接口派生,因此在所有COM接口虚拟函数表中保存的前三个成员函数指针一定是指向QueryInterface()、AddRef()和Release()的指针。这样,任何一个COM接口都可以被当作IUnknown接口来处理。在创建组件时,客户可以通过CreateInstance()函数得到IUnknown接口指针。
COM规范允许使用多接口,QueryInterface()成员函数可以用来查询组件是否支持某个特定的接口。如果支持,QueryInterface()将返回此接口的指针。其第一个参数为一个IID结构,指出了客户所要查询的接口,查询到的接口指针将存放在ppv所指向的变量中。函数的成功执行与否将返回S_OK或E_NOINTERFACE。但是,在使用时不能简单的将QueryInterface()返回值与其进行比较,而应使用SUCCEEDED或FAILED宏。例如:
IUnknow* pI = CreateInstance();
IX* pIX = NULL;
HRESULT hResult = pI->QueryInterface(IID_IX, (void**)&pIX);
if (SUCCEEDED(hResult))
pIX->Func1();
由于QueryInterface()过于灵活,为避免由此引发的冲突在COM规范中定义了QueryInterface()所有实现都必须遵循的一些规则:
1) 过同一对象各个接口指针所查询得到的IUnknown接口指针必须是指向同一个IUnknown接口的。即,IUnknow接口的唯一性。
2) 如果某接口曾经被成功查询过,那么此后任何时间对该接口的查询也必定会成功。即,接口与查询时间的无关性。
3) 对于已经获取到的接口仍可对其进行再次查询,并且必定会成功。即,接口的自反性。
4) 客户能够从任何接口查询到另外一个接口,而且能够返回到起始接口。即,接口的对称性。
5) 如果能够从某接口获取到某特定接口,那么从任意接口都可以得到此接口。即,接口的传递性。
IUnknown接口的另两个成员函数AddRef()和Release()对对象的生存期进行了控制。每个COM对象都记录有一个引用计数,该引用计数表示了当前引用了此COM对象的有效指针的个数。AddRef()和Release()实现的即是这种引用计数的内存管理技术:引用计数初始为0,客户每得到一个指向此对象的接口指针即通过AddRef()将引用计数加1;在每用完此接口指针后,调用Release()函数将引用计数减1。如果引用计数减到0,则从内存卸载掉此COM对象。关于引用计数的使用,在COM规范中也设置了以下几条简单的规则:
1) 任何能够返回接口指针的函数(如QreryInterface()、CreateInstance()等)在返回接口指针之前,必须用相应的指针调用AddRef()函数。
2) 在使用完任何一个接口后,应及时调用该接口的Release()函数。
3) 在进行接口指针赋值操作后,应调用AddRef()函数。
COM组件的创建可以通过CoCreateInstance()函数来完成,函数原型为:
HRESULT __stdcall CoCreateInstace(
const CLSID& clsid,
IUnknown* pIUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void** ppv);
函数参数clsid是要创建组件的CLSID,pIUnknownOuter用于聚合组件,如果不使用可以设置为NULL。参数dwClsContext则限定了所创建组件的执行上下文。最后两个参数iid和ppv则分别为要使用接口的IID和返回得到的接口指针。在使用时只需将CLSID、IID等作为参数传入即可创建相应的组件并从输出参数ppv得到所请求接口的指针。如果函数是直接创建组件的,那么在函数返回时组件将创建完毕,这样客户将无法对组件的创建过程进行任何干预,灵活性太差。因此,CoCreateInstance()在函数内部实现中通过调用CoGetClassObject()函数先创建一种专门用来创建组件的组件来解决此问题。这种用途的组件被称为类厂(class factory)。
类厂所支持的用以创建组件的接口是IClassFactory,该接口从IUnknown派生,并具有两个自己的接口成员函数CreateInstance()和LockServer()。这两个成员函数分别用于创建COM组件对象和控制组件的生存期。下面先给出CreateInstance()的函数声明:
HRESULT __stdcall CreateInstance(IUnknown* pIUnknownOuter, const IID& iid, void** ppv);
可以看出,这个用于创建组件对象的CreateInstance()函数并未包含一个用来接受CLSID的参数,显然该函数将只能创建同某个CLSID相应的组件。对于一个类厂,由于只能通过CreateInstance()函数去创建组件,因此只能创建与某个特定CLSID相应的组件。
创建类厂的CoGetClassObject()函数将接收一个CLSID作为参数并返回指向类厂对象IClassFactory接口的指针。客户将可以通过此指针来创建所需要的组件并返回某接口的指针。通过此指针,客户将可以直接调用新创建的COM对象接口的成员函数,从而获得COM对象的所有服务。
在用CoGetClassObject()创建类厂对象时,如果COM对象是进程内组件(组件与客户处于同一进程地址空间,通常多以DLL形式存在),CoGetClassObject()将调用DLL模块的DllGetClassObject()引出函数并把clsid、iid和ppv等参数传递进去以创建类厂,并返回类厂对象的接口指针。
如果COM对象是进程外组件(拥有独立的进程地址空间,通常多以EXE形式存在),则CoGetClassObject()将要首先启动组件进程,并一直等待到组件进程通过CoRegisterClassObject()函数将类厂注册到COM后,才会返回COM中相应的类厂信息。一旦组件进程退出,此注册的类厂对象也就不再有效,需调用CoRevokeClassObject()函数予以通知。图4展示了通过类厂创建组件的过程:
客户程序对COM组件的调用主要分对进程内组件调用和进程外调用两种情况。在具体过程上却并没有什么太大的区别。为了能够使用COM库提供的API函数,首先要用CoInitialize()初始化COM库。
虽然通过CLSID和ProgID都可以标识一个组件,但ProgID显然要比CLSID更易于理解和使用,因此通常很少直接使用CLSID,而是通过使用CLSIDFromProgID(),根据ProgID得到组件的CLSID。进而以此返回的CLSID作为参数去调用CoGetClassObject()以创建类厂对象并返回类厂接口指针。通过该指针调用类厂对象的CreateInstance()接口成员函数,执行结果将创建与CLSID相应的组件对象并返回IUnknown接口指针。通过此接口的QueryInterface()成员函数将能够进一步获过程将是隐含进行的,使用更为简单。
取组件的其他接口指针,从而使用组件提供的各种服务。
最后,通过Release()函数释放接口指针。如果使用的进程内组件,在调用CoUninitialize()函数释放COM库资源之前,应首先调用CoFreeUnusedLibraries()将其从内存卸载。由于在CoCreateInstance()函数内部实现了对CoGetClassObject()的调用并一直完成了类厂对象接口函数对组件的创建和类厂对象的释放