1、COM对象
在客户程序与组件交互的过程中,COM组件将以COM对象形式封装的实体提供给客户程序。与C++等面向对象语言中类的概念类似,COM对象也包含其成员属性和成员方法,前者反应COM对象的状态,后者是对象提供给外界的接口。这种封装特性也是COM对象的基本特性。
1.1、COM对象的标识——CLSID
对于客户程序来讲,COM组件的位置是透明的,客户程序并不会直接访问COM组件,客户程序通过全局标识符对对象实现创建和初始化操作。对此,COM标准采用128为的GUID作为一个COM对象的标识符。而CLSID本质上也是GUID,区别在于CLSID专门用于标识COM对象。
1.2、对比COM对象与C++对象
COM对象同C++对象在概念上存在相似之处,区别也比较明显。如COM的封装和重用建立在二进制一级,C++对象的封装和重用建立在代码一级。
(1)封装性:
C++对象的调用者与对象本身通常处于同一模块中,调用者有可能直接获取对象的数据成员,因此在类的定义中可以对数据成员进行访问控制(定义成public/private/protected),私有成员只能在本类的内部访问,公有成员可以在任意位置访问。
由于COM对象的客户与对象本身可能分别处于不同的进程甚至不同的机器上,所以COM对象的内部数据成员完全封装在对象内部,客户程序无论如何不可能直接访问。客户程序只能通过接口成员函数访问,而且在访问过程中还应实现对COM对象数据成员读写进行有效性判断。
(2)重用性:
C++对象的重用性表现在源码一级,通过类继承的方式实现。由基类继承得到的派生类获取到基类的全部非私有成员,但是基类与派生类联系紧密,当基类修改之后派生类也需要重新编译或修改。在最终的可执行代码中,二者处于同一模块,重用性体现在程序模块之内。C++类和对象重用的最广泛应用时类库,如MFC库等。
COM对象以包容和聚合实现重用性。无论以哪一种方式,被重用的对象更新时,重用对象不需要重新编译或设置就可以自动使用新版本的被重用对象,二者的重用关系是动态的,可以完全独立。
2、COM接口
2.1、COM接口基本概念:
接口是包含了一组可供客户程序调用的函数的数据结构,客户程序通过接口可以调用COM对象的功能并获得其服务。从客户程序来看,接口所包含的这组成员函数就COM对象暴露出来的所有信息。
客户程序通过一个指向接口数据的指针调用接口成员函数。接口一般定义为一个抽象类的形式,该类只包含几个虚函数成员。指向接口的指针实际上指向该类的虚函数表,该虚函数表中每一项为4字节长度的函数指针,这个指针与对象的具体实现连接。通过这种方式,客户获得接口指针就可以调用对象的实际功能。对于一个接口,其虚函数表是确定的,其成员的个数和次序也是不变的,同样每个成员函数也设置为固定的参数和返回值。所有的接口信息都定义在二进制级,因此COM接口理论上兼容各种编程语言。
由于COM接口结构的虚函数表与C++中相应的概念完全一致,因此C++类最适于描述COM接口。实际上,COM接口多数情况下定义为一个C++抽象类,每一个接口方法都定义为该类的一个纯虚函数由派生类(通常也是COM对象的实际实现类)去实现。在COM对象的实现类中,除了实现接口类定义的方法以外,还包含一些数据成员。这些数据成员作为对象的属性反映了各个对象的不同之处。一个客户程序若采用了两个这样的COM对象,那么两个对象各自拥有其数据成员,共享从接口类中派生的方法成员。每一个实现的接口成员函数都暗含一个this指针作为参数,该指针指向的是COM对象实例自身,因此在虚函数表中可以直接访问COM类的数据成员。
2.2、COM接口的一些特点:
二进制特性:接口规范定义了二进制一级的标准,任何语言只要可以对接口的二进制结构进行表达就可以开发COM组件相关的应用。
接口不变性:COM组件的接口应当保持不变,客户程序和组件应按照既定的接口开发。
继承性/扩展性:接口以抽象类的形式定义,因此接口类也可以继承、派生。与C++的普通类相比,接口只包含成员函数的声明部分,因此接口的继承只是声明继承,而普通C++类的继承包括了声明继承和实现继承。另外,C++普通类可以支持多继承,接口只允许单继承。实际上,所有的COM接口都是从IUnknown继承而来。
多态性:只要对象实现了同样的接口,多态性可以使客户程序使用统一的方法处理不同对象。另外,同一个对象还可以实现多个接口,因此每个接口上都可以体现COM对象的多态性。
3、IUnknown接口
IUnknown接口是所有COM接口的基类,因为IUnknown接口提供了COM对象非常关键的两个特性:生存期控制和接口查询功能。为了实现这三个功能,IUnknown接口包含了三个成员函数:QueryInterface/AddRef/Release。QueryInterface用于查询当前COM对象的其他接口指针,AddRef和Release用于对引用计数进行操作,前者对引用计数增加1,后者使引用计数减少1。
3.1、引用计数:
关于引用计数的概念,在之前学习ios的相关内容时已有涉及,可以参考这里。通常情况下,引用计数设置为COM类的一个成员函数,意面设置在组件和接口层导致的引用计数“分辨率”过粗或过细的问题。当客户程序创建组件对象并获得第一个接口指针后,引用计数为1;客户程序将接口指针赋给其他变量时,调用AddRef,将引用计数+1;当一个接口指针使用完成之后,调用Release,将引用计数-1。更加具体的引用计数规则有:
①函数参数中包含接口指针:若接口指针用作函数的输入参数,指针在函数内部不会改变,则不需要调用AddRef和Release函数;若接口指针作为函数的输出参数,此时输出函数相当于函数的一个返回值,此时在被调用函数返回前需要调用AddRef使接口引用计数增加1;若接口指针作为输入-输出参数,此时需要判断该指针是否被修改,若被修改,需要在修改前Release,在修改后AddRef,如果不修改则不作操作。
②局部接口指针:在一个局部函数块中,接口指针一直有效,因此局部接口指针被赋值并调用接口成员函数不需要调用AddRef和Release。
③全局接口指针:任何一个函数都可以访问全局接口指针,所以将全局接口指针传入子函数钱应调用Addref,再返回后应调用release。
④C++成员变量为接口指针:同上。
⑤其他:一般性地,如果对一个接口指针变量赋值,若复制前接口指针变量未结束,则必须调用Release结束其使用;然后需要对赋值后的接口指针调用AddRef。
3.2、接口查询:
一个COM对象可以实现多个接口,客户程序可以在运行时对COM对象的所有接口。QueryInterface方法共有两个参数,第一个输入参数表示带查询接口的GUID,第二个输出参数为保存查询结果指针。函数可能有三种返回值,S_OK, E_NOINTERFACE, E_UNEXPECTED。只有第一种才表示成功,返回指定的接口,其余两种分别表示对象不支持指定的接口和发生了意外错误,输出参数为空。
3.3、COM对象的接口原则
①每个对象的IUnknown接口是唯一的,通过不同接口查询到的IUnknown接口应完全一致。
②接口对称性。每个接口查询自身总应该成功。
③接口自反性。所有接口查询成功方式的反向查询也一定成功。
④接口传递性。如果接口A可以查询到接口B,接口B可以查询到接口C,那么接口A也可以查询到接口C。
⑤时间无关性。查询成功与否在任何时间都是确定的。