340 likes | 494 Views
第二章 COM 对象和接口. 这一章主要详细介绍 COM 规范,尤其是 COM 对接口使用的约定。 2.1 COM 对象 在 COM 规范中,并没有对 COM 对象进行严格的定义,但 COM 提供的是面向对象的组件模型, COM 组件提供给客户的是以对象形式封装起来的实体。客户程序与 COM 组件程序进行交互的实体是 COM 对象,它并不关心组件模块的名称和位置(即位置透明性),但它必须知道自己 COM 在于哪个对象进行交互。. COM 对象和接口. 2.1.1 COM 对象的标识--- CLSID
E N D
第二章 COM对象和接口 • 这一章主要详细介绍COM规范,尤其是COM对接口使用的约定。 • 2.1 COM对象 在COM规范中,并没有对COM对象进行严格的定义,但COM提供的是面向对象的组件模型,COM组件提供给客户的是以对象形式封装起来的实体。客户程序与COM组件程序进行交互的实体是COM对象,它并不关心组件模块的名称和位置(即位置透明性),但它必须知道自己COM在于哪个对象进行交互。
COM对象和接口 • 2.1.1 COM对象的标识---CLSID COM的位置对客户程序来说是透明的,因为客户程序并不直接去访问COM组件,客户程序通过一个全局标识符进行对象的创建和初始化。 从可读性来考虑,用字符串是最简单的,可是这样却不能保证组件对象的唯一性。 也可采用IP地址标识法,用一个32位整数来建立这个全局标识符,可保证唯一,可是必须有一个专门的权威机构为COM组件分配整数标识符,对Internet是合理的,可对于COM则不可取。 COM规范采用了128为全局唯一标识符GUID, {54BF6567-1007-11D1-B0AA-444553540000}
COM对象和接口 • 在C/C++语言中可以用如下的结构来描述 typedef strut_GUID{ DWORD Data1; DWORD Data2; DWORD Data3; BTYE Data4[8]; } GUID; 这样的话前面的GUID可定义为: extern “C” const GUID CLSID_MYSPELLCHECKER= {0x54bf6567,0x1007,ox11d1, {0xb0,0xaa,0x44,0x45,0x53,0x54,0x00,0x00}}; 空间时间值
COM对象和接口 • COM规范使用GUID来标识COM对象的思想源于OSF(Open Software Foundation,开放式软件基金会)采用的UUID(Universally Unique Identifier),UUID被定义为DCE(Distributed Computing Environment,分布式计算环境)的一部分,主要用于标识RPC(remote procedure call,远程过程调用)通信的双方。 • 手工来构造128位GUID或者编写程序来产生GUID比较麻烦。为此,Microsoft Visual C++提供了两个工具来实现:UUIDGen.exe或GUIDGen.exe,前者是一个命令行程序,后者是一个基于对话框的应用程序。 • COM库提供了API函数来产生GUID: HRESULT CoCreateGuid(GUID * pguid);
COM对象和接口 • 2.1.2 COM对象与C++对象的不比较 1. 封装特性 数据封装是两者都有的特性,但形式不同。 在COM对象中,数据是完全封装在对象内部的,外部不可能直接访问对象的数据属性,因为COM对象和客户程序可能在不同的模块中甚至在不同的进程中或不同的机器上,因此,客户不能直接访问COM对象的属性。
COM对象和接口 • 2. 可重用性 COM对象的可重用性表现在COM对象的包容和聚合上,一个对象可以完全使用另一个对象的所有功能。 C++对象的可重用性表现在C++类的继承性上,派生类可以调用其父类的非私有成员函数。
COM对象和接口 • C++对象还有一个多态性。C++的多态性体现了C++语言用来描述事务的高度抽象的特征,在C++中对象的多态性需要通过其虚函数才能体现;COM对象也有多态性,这种多态性是通过COM对象所具有的接口来表现的。 • 2.2 COM接口 COM对象的客户与对象之间通过接口进行交互,所以组件之间接口的定义很重要,它也是COM规范的核心。 • 2.2.1 从API到COM接口 假如要实现一个字处理应用系统,它需要一个查字典的功能,按照组件程序设计的方法,把查字典的功能放到一个组件程序中实现最好,如果以后字典程序的查找算法或字典库改变了,只要接口没有变,新的程序仍能够运行。
COM对象和接口 • 为了把应用系统和组件程序连接起来,又能使它们协同工作,就是先定义一组查字典的函数。 在字典组件程序中,定义的API函数如下: BOOL EXPORT Initialize(); BOOL EXPORT LoadLibrary(char *); BOOL EXPORT InsertWord(char *,char *); BOOL EXPORT DeleteWord(char *); BOOL EXPORT LookupWord(char *,char **); BOOL EXPORT RestoreLibrary(char *); void EXPORT FreeLibrary(); 应用A或组件A 应用B或组件B 字典组件
COM对象和接口 • 平面型API接口层可以很好地把两个程序连接起来,但存在一下问题: 1. 当API函数非常多时,使用会非常不方便,需要对函数进行组织。 2. API函数需要标准化,按照统一的调用方式进行处理,以适应不同的语言编程实现。 COM定义了一套完整的接口规范,不仅可以弥补API作为组件接口的不足,还充分发挥了组件对象的优势,并实现了组件的多态性。 2.2.2 接口定义和标识 接口是包含一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能。接口定义了一组成员函数,这组成员函数是组件对象暴露出来的所有信息,客户程序利用这些函数获得组件对象的服务。
COM对象和接口 • 接口函数表通常被称为虚函数表(virtual funtion table,简称vtable),指向vtable的指针为pVtable。 • 对于一个接口来说,它的vtable是确定的,因此接口成员函数个数是不变的,而且成员函数的先后顺序也是不变的;对其参数和返回值也是确定。在一个接口的定义中,所有这些信息都必须在二进制一级确定,不管什么语言,只要能支持这样的内存结构描述,就可以定义接口。 • struct IDictionaryVtbl; • struct IDictionary;{ • IDictionaryVtbl *pVtbl;}; • stuct IDictionaryVtbl{ BOOL (* Initialize)(IDictionary * this); BOOL (* LoadLibrary)(IDictionary * this,String); void (* FreeLibrary)(Idictionary* this);};
COM对象和接口 • 以上定义需要说明的几点如下: 1. 每一个接口成员函数的第一个参数为指向Idictionary的指针, 2. 在接口成员函数中,字符串变量必须用Unicode字符指针,COM规范要求使用Unicode字符,而且COM库中的API函数也使用Unicode字符。如果用ANSI字符,则必须进行转化。 3.不仅成员函数的参数名是确定,而且应该使用相同的调用习惯。客户程序在调用成员函数之前,必须把参数压到栈中,然后再进入成员函数中,成员函数依次把参数从栈中取出,在函数返回之前或返回之后,必须恢复栈的位置。在windows中,有两种调用习惯,分别为_cdecl和_stdcall(或pascal)。
COM对象和接口 • 4. 在C语言中,用这种结构只是描述了接口,并没有提供具体的 实现,对于客户程序,它只需要这样描述,就可以调用COM对象的接口;而对于组件程序,还必须提供具体的实现过程。 • 5. 从C语言的描述可以看出,由于COM接口的这种二进制结构,只要编程语言能够支持“structure”或“record”类型,并且这种类型能够包含双重的指向函数指针表的成员,就可用来编写COM组件和使用它。 • 类似于COM对象的表示方法,COM接口也采用了全局标识符IID(interface identifier)。例如: extern “C” const IID IID_IUnkouwn= {0x54bf6567,0x1007,ox11d1, {0xb0,0xaa,0x44,0x45,0x53,0x54,0x00,0x00}};
COM对象和接口 • 2.2.3 用C++语言定义jiek COM接口结构中vtable与C++中类的vtable(类的虚函数表)完全一致,因此,用class描述COM接口是最方便的。 用C++定义字典接口如下: • class IDictionaryl{ BOOL Initialize)()=0; BOOL LoadLibrary(String)=0; BOOL InsertWord(String,String)=0; void FreeLibrary()=0;}; 因为class定义中隐藏了vtable,并且,每个成员函数隐藏了一个参数this指针,this指针指向类的实例。
pVtable vtable 对象实现 指针函数1 接口指针 指针 指针函数2 指针函数3 ………… 接口结构 vtable IDictionary BOOL Initialize(this *); . . . . . . void FreeLibrary(this *); this pVtable C++中类的内存结构 COM对象和接口 • COM接口结构和C++中类的内存结构完全一致。
COM对象和接口 • 接口只是一种描述,如果COM对象要实现IDictionary接口,则COM对象必须以某种方式把它自身与IDictionary联系起来,然后把IDictionary的指针暴露给客户程序,于是客户程序就可以调用该对象的字典功能。 • 当客户端获得某个字典对象的接口指针品品pIDictionary之后,她就可以调用该接口的成员函数。例如: • pIDictionary->LoadLibrary(“Eng_ch.dict”); • 如果使用C语言的struct IDictionary,则对接口成员函数的调用应如下: pIDictionary->pVtbl->LoadLibrary(“pIDictionary ,Eng_ch.dict”); 上述两种调用完全相同。
COM对象和接口 • 2.2.4 接口描述语言IDL COM规范在采用OSF的DCE规范的描述远程调用接口IDL的基础上,进行扩展形成了COM接口的描述语言。接口描述语言提供了一种不依赖于任何语言的接口描述方法,因此,它可以成为组件程序和客户程序之间的共同语言。 其中不仅定义了COM接口,还定义了一些常用的数据类型,也可以描述自定义的数据结构,对于接口成员函数,可以指定每个参数的类型、输入输出特性,甚至支持变长度的数组描述。例如: • interface IDictionaryl{ HRESULT Initialize)(); HRESULT LoadLibrary([in] String); HRESULT FreeLibrary();}; 在VC中提供了MIDL工具,可以把IDL接口描述文件编译为C/C++兼容的接口描述头文件(.h)。
COM对象和接口 • 2.2.5 接口的内存模型 COM对象往往有自己的属性数据,这些属性数据反映了对象的状态,正是通过这些属性数据,才反映了对象的不同。例如,字典对象有一个字典数据表m_pData成员和字典文件名m_DictFilename作为其基本的属性数据。用C++语言实现如下: class Cdictionary:public Idictionary { public: ………… private: struct DictWord * m_pData; char * m_DictFilename[128]; ………… };
vtable CDIctionary类中虚函数的具体实现 Intialize 客户使用的 接口指针 pIDictionary pVtable LoadLibray m_pData InsertWord m_DictFilename ………… 接口IDictionary与字典对象属性之间的结构关系 COM对象和接口 • 按照类Cdictionary的定义,则接口IDictionary和字典对象的内存结构将变为:
vtable CDIctionary类中虚函数的具体实现 Intialize 客户使用的 接口指针 pIDictionary1 pIDictionary2 pVtable LoadLibray m_pData InsertWord m_DictFilename ………… pVtable m_pData m_DictFilename 多个字典对象与接口IDictionary之间的结构关系 COM对象和接口 • 如果一个客户使用了两个字典对象,则两个字典对象公用了成员函数,但数据属性不能公用,根据C++的编译原理,内存结构如下:
vtable CDIctionary类中虚函数的具体实现 Intialize 客户使用的 接口指针 pIDictionary1 pIDictionary2 pVtable LoadLibray m_pData InsertWord m_DictFilename ………… vtable 另一个字典对象类中虚函数的具体实现 pVtable Intialize 字典数据 LoadLibray ………… InsertWord ………… 不同方法实现的两个字典对象与接口IDictionary之间的结构关系 COM对象和接口 • 如果第二个字典组件对象没采用CDictionary类的结构来实现其字典功能,但也实现了Cdictionary接口,则此时结构如下:
COM对象和接口 • 在以上给出的三个模型图中,每个接口成员函数都包含一个this指针,通过this指针,接口成员函数可以访问到字典对象的属性数据。按照Ciictionary的定义方法,该this指针就是指向Ciictionary类的对象,因此在虚函数中可以直接访问Ciictionary的数据成员。 • 并非一定要采取这种机制来定义接口,也可采用其它方法来定义,只要接口成员函数中的this指针(接口指针)与对象数据能建立确定的连接,在进口成员函数中可以访问到对象数据即可。例如,VC的MFC库和ATL(active template library,活动模板库)模板库分别采用了不同的机制来提供对COM接口的支持。
COM对象和接口 • 2.2.6 接口的一些特性 1. 二进制特性 2. 接口不变性 3. 继承性 接口的继承与类的继承不同。 a. 类继承不仅是说明继承,也是实现继承,既派生类可以继承基类的函数实现,而接口继承只是说明继承,即派生的接口只继承基接口的成员函数说明,并没有继承基接口的实现,因为接口定义布包含函数 实现部分。 b. 类继承允许多重继承,一个派生类可以有多个基类,但接口只允许单继承,不允许多重继承。
COM对象和接口 • 4. 多态性---运行过程的多态性 COM的多态性体现在接口上,多态性使得客户程序可以用统一的 方法处理不同的对象,甚至是不同类型的对象,只要它们实现了同样的接口。如果几个不同的COM对象实现了同一个接口,则客户程序可以用同样的代码调用这个COM对象。 COM规范允许一个对象实现多个接口,因此,COM对象的多态性可以在每个接口上得到体现。正是由于COM的多态性,才可以用COM规范建立插件系统,应用程序可以用通用的方法处理每个插件。
COM对象和接口 • 2.3 IUnknown接口 COM定义的每个接口都必须从IUnknown继承过来, 主要是因为IUnknown接口提供可两个非常重要的特性:生存控制和接口查询。 生存控制: 接口查询:
COM对象和接口 • IUnknown接口的定义(IDL): interface IUnknown { HRESULT QueryInterface([in], REFIID iid,[out] void ** ppv); ULONG AddRef(void); ULONG Release(void);} 为了便于理解和对照,给出IUnknown接口的C++定义形式: class IUnknown { public: virtual HRESULT _stdcall QueryInterface(const IID& iid,void ** ppv)=0; virtual ULONG _stdcall AddRef()=0; virtual ULONG _stdcall Release()=0;} QueryInterface函数用来查询其它接口指针, AddRef和Release函数用来对引用计数进行操作。
COM对象和接口 • 2.3.1 引用计数 1. COM对象只实现了一个接口,例如为ISomeInterface。 因为ISomeInterface接口继承与IUnknown接口,因此 ISomeInterface接口中的成员函数包含Iunknown的三个函数。一个使用该COM对象的客户程序通过某种途径调用获得了该接口的接口指针pSomeInterface,并且,客户程序在许多逻辑模块中都用到了该COM对象,从而在客户程序的很多地方都保持了对该接口指针的引用,比如说有三个地方分别用pSomeInterface1, pSomeInterface2,pSomeInterface3指向该接口的指针。 在客户程序的这三个模块中,它可以调用接口成员函数获得接口所提供的服务,如果它一直需要该接口所提供的服务,那它就需要控制该接口对象使它一直保持在内存中;如果不再需要,就应该通知接口不再需要服务了。由于每个模块并不知道其它模块是否在使用COM对象,只知道自己还用没用。而对COM对象来说,只要有任一一个模块还在用它,它就必须驻留在内存中,不能释放。
COM对象和接口 • COM采用了“引用计数”技术来解决内存的管理问题,COM对象通过引用计数来决定是否继续生存下去。每个COM对象都记录了一个称为“引用计数”的数值,该数还有表示有多少个有效的对象在引用这个COM对象。其中主要是通过对这个“引用计数”进行加1和减1来对COM对象的生存进行控制的。客户的到了一个指向该对象的指针时就加1,不用后就减1。当为0时,从内存中释放该CON对象。对接口进行复制时(调用拷贝构造函数),也加1。IUnknown接口成员函数AddRef和Release分别完成引用计数的加1和减1。 • 2. 如果一个COM对象实现了多个接口,则可以采用同样的技术,只要引用计数不为0,就表明该COM对象的客户还在使用它。 • 通过引用计数COM对象的客户程序可以通过接口指针很好地控制对象的生存期。
COM对象和接口 • 2.3.2 实现引用计数 按照COM规范,一个COM组件可以实现多个COM对象,每个COM对象又可支持多个COM接口。因此可有一下选择: 在COM组件一级实现引用计数。 在COM对象一级实现引用计数。 在COM接口一级实现引用计数。 1. 设置一个针对整个组件全局的引用计数。在实现组件时,我们用一个全局整数变量记录引用计数,当组件被初始化装入内存时,该计数为0;对象被创建时,计数值开始增加,在整个组件被使用过程中,计数值一直保持大于0,当组件中地对象都被用完后,计数值应该减回到0,于是组件模块就可以从内存中卸出。 这种引用计数可以控制组件模块的生存周期,但控制不了COM对象的生存与否。如果一个组件在运行过程中产生了两个COM对象,不管是同类还是不同类,当某个对象减1时,由于引用是全局的,是在全局引用计数中减1,因此还不能判断是否这个对象已经不再用了,必须等到所有对象释放完后,也就是这个引用计数为0时,才可进行释放。这样资源的利用效率就降低了。称之为“计数分辨率太粗”
COM对象和接口 • 2. 为每个COM对象设置一个引用计数。当COM对象被创建时,计数值开始从0增加,只要对象还在被客户程序使用,则这个计数值就大于0;当不用时,就减回0,然后就可以释放掉。 这样可以有效的管理多个对象的组件程序,但每个对象被释放掉之后,它必须通知组件程序,组件程序发现没有对象存在时,在就可把组件模块从内存中进行卸掉。因此,组件程序应该保持一份有效对象的记录,可以用一个全局的对象计数值来控制组件的生存周期。当对象释放时减1,为0时释放。 3. 为每个接口设置一个引用计数。因为客户通过接口指针与组件对象进行通信,所以为每个接口设置引用计数可以跟踪客户对COM对象的使用情况。在对对象进行调用过程中,并非调用所有的接口,这时与那些不用的接口相关的资源就可以不被占用。 这样做对调试组件程序和分析客户程序的使用情况非常有帮助,可以有效的管理每个接口的使用情况。可是在减到0时,接口就要通知对象,对象要判断是否所有接口都减到0,如果是,对象释放,然后对象又要通知组件,组件又要去判断,如果所有对象减到0,组件就被释放。这有点“计数分辨太细”。
COM对象和接口 • 在对象一级实现引用计数可选择全局变量;在对象一级实现引用计数可通过成员变量来实现。在接口一级实现引用计数,可通过为对象实现的每个接口设置一个类成员变量作为引用计数变量。综合来看,在对象一级实现引用计数以控制对象和组件的生存周期比较合理。 • CDictionary的成员函数AddRef和Release就实现了在对象一级的引用计数,这两个函数以及这个类的构造函数如下: CDictionary:: CDictionary(){ m_ref=0; //……} ULONG CDictionary:: AddRef(){ m_ref++; return(ULONG) m_ref} ULONG CDictionary:: Release(){ m_ref--; if(m_ref==0){ delete this; return 0;} Return(ULONG) m_ref;}
COM对象和接口 • 按照引用计数的原理,可以制定最基本的客户控制规则: 1. 客户创建了组件对象并获得了第一个接口指针后,引用计数应该是1。 2. 在客户程序中,当把接口指针赋给其它变量时,应该调用AddRef,使引用计数加1。 3.在客户程序中,当一个接口指针被使用完之后时,应该调用Release,使引用计数减1。按照以上规则,可给出客户代码如下(伪C++代码): //产生一个新的字典对象 IDictionary * p IDictionary=CreateObject(……); if (pIDictionary==NULL) return; //如果成功,引用计数为1 Bool retValue= pIDictionary->LoadLibrary(“1.dict”); if(retValue==FALSE){ pIDictionary->Release(); return;}
COM对象和接口 …… IDictionary * pIDictionaryForword= pIDictionary; pIDictionaryForWord->AddRef(); pIDictionaryForWord->InsertWord(“……”,”……”); pIDictionaryForWord->DeleteWord(“……”); pIDictionaryForWord->Release(); …… pIDictionary->Release(); //最后释放字典对象 2.3.3 使用引用计数规则 下面是一个完整的引用计数规则,这将使客户模块之间写作使用组件对象更趋于一般化,其中分不同场合使用或传递接口指针变量进行分类,给出相应规则如下: 1. 函数的参数中使用接口指针变量。分in、out和inout,分处理如下: a.输入参数。此参数值在调用过程中不会变,多数语言中,此参数为传值参数或为常数。由于此参数由调用函数控制,因此在被调用函数执行过程中,接口指针一直保持有效。所以,在被调用函数中,不必调用AddRef和Release函数。
COM对象和接口 • b.输出参数。此参数在被调用过程中进行赋值,而且被调用 函数并没有用到函数初始化传进来的值,此参数相当于函数的一个返回值。输出参数相当于在被调用函数中生成了一个新的接口指针变量,因此,在被调用函数返回之前,对此参数应该调用AddRef使接口引用计数加1。 • c.输入、输出参数。对这种参数采用的规则是:在参数被修改前,对原来传进来的接口指针调用Release以使引用计数减1,在参数被修改之后,对新的接口指针变量调用AddRef,以标记对新的接口指针的引用。如果在函数执行过程中,参数没有被修改,则类似于in,即不加1,又不减1。 • 2. 局部接口指针变量。如果在一个局部函数块中,一个局部接口指针变量被赋予值并调用了接口成员函数,则对该局部接口指针变量可以不调用AddRef和Release,因为在这个局部函数块中,接口指针总是有效的。 • 3.全局接口指针变量。因为任何一个函数都可以访问全局接口指针变量,所以在把全局接口指针作为输入参数传给某个函数之前,应该调用AddRef以保证在函数中可以使用该接口指针变量,因为它是全局变量,其它的函数有可能回调用Release函数。在函数返回之后应调用Release。
COM对象和接口 • 4. C++中类成员变量为接口指针变量。因为对于类的作用域来讲,成员变量相当于全局变量,在类的所有成员函数中都可以访问此变量,因此规则3也适用于类成员变量的情形。 • 5. 当以上情况都不适合时,可使用以下一般性规则: a.在顺序执行过程中,如果要对一个接口指针变量赋值,赋值后 就要调用AddRef,并且,在赋值前此接口指针还没有结束,则赋值前必须对它调用Release以便先结束它的使用。 b.如果要结束一个接口指针变量,以后不再用到它了,则调用Release函数。 在某些特殊条件下,可以省略对AddRef和Release的调用。 使用引用计数主要可以防止两方面的问题:当使用一个接口指针变量时,发现它所指向的COM对象已经不存在了。;当使用完了COM对象后,对象并不被清除。对于前者,可能忘了AddRef,导致出错;对于后者,可能忘了Release,导致资源不释放。