COM接口是COM规范中最重要的部分,COM规范的核心内容就是对接口的定义,甚至可以说“在COM中接口就是一切”。组件与组件之间、组件与客户之间都要通过接口进行交互。接口成员函数将负责为客户或其他组件提供服务。与标识COM对象的CLSID类似,每一个COM接口也使用一个GUID来进行标识,该标识也被称为IID(interface identifier,接口标识符)。
COM接口实际限定了组件与使用该组件的客户程序或其他组件所能进行的交互方式,任何一个具备相同接口的组件都可对此组件进行相对于其他组件透明的替换。只要接口不发生变化,就可以在不影响整个由组件构成的系统的情况下自由的更换组件。通常在程序设计阶段需要将接口设计的尽可能完美,以减少在开发阶段对COM接口的更改。尽管如此,在实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。与C++中对类的继承有些类似,对COM接口的发展也可以通过接口继承来实现。但是COM接口的继承只能是单继承而不允许从多个基接口进行派生,而且派生接口只是继承了对基接口成员函数的说明而没有继承其实现。
interface IX // IX接口 { virtual void __stdcall Func1() = 0; virtual void __stdcall Func2() = 0; }; interface IY // IY接口 { virtual void __stdcall Func3() = 0; virtual void __stdcall Func4() = 0; }; class CObjectA // 组件A { 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这两个抽象基类,并实现了其所定义的虚函数。图2为此组件具有的这两个接口的模型展示:
图2 接口模型
抽象基类本身由于没有实体函数与变量,所以并不分配内存。通常只是用来为派生类指定内存结构。只有在派生类实现此抽象基类时,指定的内存才会被分配。图3为此内存结构的示意:
图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接口指针。
|
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展示了通过类厂创建组件的过程:
图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()的调用并一直完成了类厂对象接口函数对组件的创建和类厂对象的释放,因此对于客户,类厂的全部使用