本文共 3678 字,大约阅读时间需要 12 分钟。
《COM原理与应用》笔记
从技术上讲,接口是包含了一组函数的数据结构,客户程序用一个指向接口数据结构的指针来调用接口成员函数。如图2.2所示,接口指针实际指向另一个指针,第二个指针指向一组函数,称为接口函数表,接口函数表中每一项为4个字节长的函数指针,每个函数指针与对象的具体实现连接起来。通过这种方式,客户程序只要获得接口指针,就可以调用对象的实际功能。
接口函数表通常称为虚函数表(virtual function table,简称vtable),指向vtable的指针为pVtable。
对于一个接口来说,它的虚函数表vtable是确定的,因此接口的成员函数个数是不变的,而且成员函数的先后顺序也是不变的;对于每个成员函数来说,其参数和返回值也是确定的。在一个接口的定义中,所有这些信息都必须在二进制一级确定,不管什么语言,只要能支持这样的内存结构描述,就可以定义接口。 用C语言描述字典接口 struct IDictionaryVtbl; struct IDictionary { IDictionaryVtbl *pVtbl; }; struct IDictionaryVtbl { BOOL (*Initialize)(IDictionary *this); BOOL (*LoadLibrary)(IDictionary *this, String); BOOL (*InsertWord)(IDictionary *this, String, String); void (*DeleteWord)(IDictionary *this, String); BOOL (*LookupWord)(IDictionary *this, String, String *); BOOL (*RestoreLibrary)(IDictionary *this, String); void (*FreeLibrary)(IDictionary *this); }; (1)每个接口成员函数的第一个参数为指向IDictionary的指针,这是因为接口本身并不独立使用,它必定存在于某个COM对象上,因此,该指针可以提供对象实例的属性信息,在被调用时,接口可以知道是对哪个COM对象在进行操作。所以,该this指针与C++类成员函数定义中隐藏的this指针非常类似。如果我们在一个应用系统中同时用到了两本字典,即存在两个字典对象,不同的字典对象其this指针不同。 (2)在接口成员函数中,字符串变量必须用Unicode字符指针,COM规范要求使用Unicode字符,而且COM库中提供的COM API函数也使用Unicode字符。所以,如果在组件程序内部用到了ANSI字符的话,应该对两种字符进行转换,操作系统或者C/C++编译库会提供这样的转换函数。 (3)不仅成员函数的参数类型是确定的,而且应该使用同样的函数调用方式。客户程序在调用成员函数之前,必须先把参数压入栈中,然后再进入成员函数,成员函数依次把参数从栈中取出,在函数返回之前或返回之后,必须恢复栈的当前位置,才能保证程序的正常运行。在Windows平台上有两种调用方式,分别为_cdecl和_stdcall(在有的编译器中称为pascal),采用_cdecl可以实现C语言中用到的函数可变参数的特性(例如printf函数),在这种调用方式下,由调用程序处理栈的恢复。由于大多数语言(除C/C++之外)都使用了_stdcall或pascal,而且大多数的系统API(支持可变参数的函数例外)也都使用这种调用方式,所以,COM规范也采用_stdcall和pascal,并且,所有的COM API函数也使用了_stdcall。当然,这不是绝对的,但必须保证调用方和被调用方使用一致的调用方式,如果接口成员函数使用了_cdecl,则C/C++之外的大多数语言就不能使用这样的接口,所以,除非要使用可变参数特性,否则就使用_stdcall。 (4)在C语言中,用这种结构只是描述了接口,并没有提供具体的实现,对于客户程序,它只需要这样的描述就可以调用COM对象的接口;而对于组件程序,基于这样的描述必须提供具体的实现过程,也就是说,如果一个COM对象实现了这个接口,则它所提供的接口指针IDictionary所指向的IDictionaryVtbl结构中,每个成员必须是有效的函数指针。 (5)从C语言的描述中我们可以看出,由于COM接口的这种二进制结构,只要一种编程语言能够支持“structure”或“record”类型,并且这种类型能够包含双重的指向函数指针表的成员,则它就可以支持接口的描述,从而可以用于编写COM组件或者使用COM组件。---------------------------------------- 分割线 ----------------------------------------
COM接口也采用了全局唯一标识符,它被称为接口标识符(IID,interface identifier)。例如:
extern "C" const IID IID_IUnknown = { 0x00000000, 0x0000, 0x0000, { 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 }}; 客户程序要使用接口,必须知道该接口的IID和接口提供的方法(接口成员函数)。---------------------------------------- 分割线 ----------------------------------------
COM接口结构中的vtable与class的vtable(类的虚函数表)完全一致,因此,用class描述COM接口是最方便的手段。
用C++类重新定义IDictionary: class IDictionary { virtual BOOL Initialize() = 0; virtual BOOL LoadLibrary(String) = 0; virtual BOOL InsertWord(String, String) = 0; virtual void DeleteWord(String) = 0; virtual BOOL LookupWord(String, String *) = 0; virtual BOOL RestoreLibrary(String) = 0; virtual void FreeLibrary() = 0; }; class定义中隐藏了虚函数表vtable,每个成员函数隐藏了第一个参数this指针,this指针指向类的实例。图2.3显示了类IDictionary的内存结构:类IDictionary使用了纯虚函数,因为接口只是一种描述,并不提供具体的实现过程。如果COM对象要实现接口IDictionary,则COM对象必须以某种方式把它自身与类IDictionary联系起来,然后将IDictionary的指针暴露给客户程序。
当客户程序获得了COM对象的接口指针pIDictionary之后,就可以调用接口的成员函数,例如: pIDictionary->LoadLibrary("Eng_Ch.dict"); 如果使用C语言的struct IDictionary,则应该这样: pIDictionary->pVtbl->LoadLibrary(pIDictionary, "Eng_Ch.dict"); 由C++语言class的特性可知上述两种调用完全等价。---------------------------------------- 分割线 ----------------------------------------
用IDL描述接口IDictionary
interface IDictionary { HRESULT Initialize(); HRESULT LoadLibrary([in]string); HRESULT InsertWord([in]string, [in]string); HRESULT DeleteWord([in]string); HRESULT LookupWord([in]string, [out]string *); HRESULT RestoreLibrary([in]string); HRESULT FreeLibrary(); };
转载地址:http://upsli.baihongyu.com/