1、126,十三. 自动化对象,概念澄清:类型库与自动化接口Idispatch完全独立自动化对象与自动化控制完全独立命名的混乱类型库 IDispatch接口自动化对象的实现使用自动化对象客户晚绑定DISPID绑定早绑定、自动化对象编程实践,226,1类型库,COM不仅追求C+编译器的中立,而且追求语言的独立性. 因此它使用IDL语言来描述接口. 然后在IDL到具体的语言之间建立映射.但是一些数据类型在有些语言中难以表达。比如复杂的结构类型,指针类型,函数指针等等在一些弱类型的高级语言中比如Java, Visual Basic等等并没有得到支持. IDL到这些语言的映射不能顺利地进行. 客户通过接口
2、调用对象时,在编译时刻需要接口的准确的描述, 这个描述正是来自于MIDL对IDL编译后产生的头文件, 而Java, VB等无法使用这种基于C/C+的头文件. COM的语言无关性受到很多的限制。因此, MS使用类型库来解决这个问题. 类型库文件是一个二进制文件, 后缀为.tlb.用MIDL工具编译idl文件可以产生类型库文件,在实际的开发过程中不一定要手工使用MIDL工具,IDE对其进行了集成. 编译完成以后,我们可以选择把它随组件库一起分发. 类型库以机器可读的方式描述了组件与外界交互的必要信息. 如COM对象的CLSID, 它支持的接口的IID,接口的成员函数的签名等等. 本质上它等价于描述
3、接口的C/C+头文件.,326,一个类型库可以包含多个COM对象,这些COM对象可以实现多个接口,而且一般而言实现了IDispatch接口(不是必须).为了标识这些类型库,也使用GUID来作为它的唯一标识LIBID.并且也在注册表中注册,注册位置是HKEY-CLASSES_ROOTTypeLib,注册内容主要指明类型库所描述的对象的载体(dll文件等)的位置.VB, Java等语言的开发者不需要直接面对类型库. 相反,它是由编译器环境(VB虚拟机,Java虚拟机)来解释它. 这样它使得开发者在开发期能够浏览接口的相关信息. (以VB为例,通过Reference添加对类型库的引用后,使用Obje
4、ct Browser就可以查看COM接口了, 另一个工具OLE/COM Object Viewer使用更加方便). 而开发人员只需要使用宿主语言简单的语法,非常方便地使用COM. (烦心事交给编译器的开发者去吧! 我们看到,如果不是使用COM,而是以一般的库函数的形式,在VB这样的高端应用中使用起来就没有这么简便(对最终开发者而言). 每一样复杂的技术,在使用者的舒适的背后,是底层开发者的艰辛)当然,如果愿意,C+编译器也可以利用类型库. Visual CIDE中的ClassWizard和CBuilderIDE,DElphi中的importType Library命令都可以读入组件的类型库,并
5、利用其中的信息产生C代码。客户程序利用这些代码可以使用COM组件。,426,并不是只有IDE的开发者才知道怎样解析类型库. 为了操作类型库,Windows提供了一些API(LoadTypeLib 和LoadRegTypeLib等)和COM接口(ITypeLib和ITypeInfo等).LoadTypeLib可以根据指定的文件名装载类型库,并返回ITypeLib接口.使用LoadRegTypeLib可以根据类型库的LIBID查找注册表,找到类型库文件,返回ITypeLib接口.ITypeLib接口代表了类型库本身.使用其GetTypeInfoofGuid根据接口的IID或者使用GetTypeIn
6、fo根据接口在类型库中的索引号可以返回ITypeInfo接口.ITypeInfo接口则代表了接口的全部信息.包括有哪些方法,方法的签名等等. 如果接口是IDispatch接口,则还可以使用GetIDsofNames函数来根据方法的名字得到其分发ID,并使用Invoke函数通过方法的分发ID来执行这个方法.因此,为了在编译时刻了解接口的信息, 客户程序要么得到COM组件的IDL文件(使用头类型定义头文件,在代码中通知编译器接口的类型,如C+), 要么得到它的类型库文件(代码中没有准确的信息,由IDE环境从类型库中读取接口类型信息,如VB), 才能顺利地构造客户应用程序,从而使用COM对象.,52
7、6,无论是通过头文件,还是通过类型库,我们在开发客户程序时都有关于接口的先验知识.这些先验信息帮助我们顺利地编译客户程序.这种方式我们有时称为静态调用,或者早绑定(early binding).但是,还存在这样的情况,有的语言在开发过程中并没有经过编译阶段,而是直接以源代码的形式被配置发布. 在运行时才被解释运行.比如以HTML为基础的脚本语言.(VBScript,JavaScript等).它们在浏览器或Web服务器的环境中执行. 脚本代码以纯文本的形式嵌入在HTML文件中. 为了丰富脚本的功能,它们也可以创建COM对象,执行特殊的功能,比如访问数据库等等. 比如: var obj = new
8、 ActiveXObject(“LuBenjie.AutoObj); alert(obj.Hello(); 在脚本引擎中,目前还不能使用类型库或其他的先验知识来描述接口的信息.这意味着对象自身要帮助脚本解释器,将文本形式的脚本代码翻译为有意义的方法调用. 这种方式我们称为动态调用,或者晚绑定(late binding).为了支持晚绑定,COM定义了一个接口,用来表达这种翻译机制,这个接口就是IDispatch.分发接口有时称为自动化接口,实现了此接口的对象称为自动化对象.自动化接口的定义如下:,2 IDispatch接口,626,class IDispatch:public IUnknownp
9、ublic: HRESULT GetTypeInfoCount( unsigned int FAR* pctinfo );/如果对象提供类型支持,则返回1,否则0. 客户在获取类型信息之前先使用此函数进行判断.HRESULT GetTypeInfo( unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo ); / 一般给iTInfo赋值0, 返回指向对象类型信息的ITypeInfo接口指针, 通过ITypeInfo接口可以访问该自动化接口的所有类型信息.HRESULT GetIDsOfNames( REFIID riid, OL
10、ECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ); / 返回指定名字的方法或属性的分发ID. IDispatch使用分发ID管理接口的属性和方法. rgszNames 指定属性或方法的名字, rgDispId返回其分发IDHRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCE
11、PINFO FAR* pExcepInfo, unsigned int FAR* puArgErr ); ;/是命令的翻译器。客户程序通过invoke函数访问方法或属性。客户给定分发ID dispIdMember 、输入参数pDispParams 。invoke返回输出参数pDispParams .自动化对象所有的方法和属性的调用都通过invoke函数来实现。使得运行时刻动态绑定属性和方法并进行参数类型检查成为可能.,726,当一个脚本引擎首次尝试访问一个对象时,它使用QueryInterface向对象请求IDispatch接口.如果请求失败,则不能使用此对象.如果成功,则继续调用GetIDs
12、ofName方法,得到方法或属性的分发ID号.通过此ID号,调用Invoke方法,就可以调用想要调用的方法.分发接口与普通接口的区别在于,接口的逻辑功能是如何被调用的.普通的COM接口是以该方法的静态的先验知识为基础.而分发接口是以该方法的预期的文字表示为基础.如果调用者正确地猜测出方法的原型,那么此调用可以被顺利地分发,否则不能.假设有一个自动化对象CMath,它只实现了分发接口,进行加减乘除的工作.这些具体的工作由内部函数来完成.并没有向外界提供接口.这些计算功能由Invoke函数根据分发ID来调用特定的函数.uuid(C2895C1F-020E-4C1F-8A65-F59094DFBD9
13、7)dispinterface DMath /dispinterface 关键字说明这是一个分发接口. properties: methods: id(0) long Add(long Op1,long Op2); /0,1,2,3分别是分发ID id(1) long Substract(long Op1,long Op2); id(2) long Multiply(long Op1,long Op2); id(3) long Divide(long Op1,long Op2);此对象的虚表及其分发表示意图如下:,826,自动化对象的虚表和分发表.,926,自动化对象可以只实现分发接口:cla
14、ss CMath:public IDispatchpublic: /来自IUnknown的三个函数virtual HRESULT _stdcall QueryInterface() ; virtual ULONG_stdcall AddRef() ; virtual ULONG_stdcall Release() ;/ 来自IDispatch的四个函数 HRESULT GetTypeInfoCount( );HRESULT GetTypeInfo( ); HRESULT GetIDsOfNames(); HRESULT Invoke( ); ;/此COM对象只能通过分发接口给外界提供服务.虽然
15、这样做显得别扭,有舍近求远之嫌, 但是,原理上是可行的.,1026,更常用地,我们把具体的计算功能也作为接口直接暴露出去,我们从IDispatch派生一个接口IMath. object,uuid(2756E11C-A606-482F-969C-14153E1D1609),dual/说明是一个双接口interface IMath: IDispatch properties: methods: id(0) HRESULT Add /0,1,2,3分别是分发ID (in long Op1,in long Op2,out,retval long* pResult); id(1) HRESULT Sub
16、stract (in long Op1,in long Op2,out,retval long* pResult); id(2) HRESULT Multiply (in long Op1,in long Op2,out,retval long* pResult); id(3) HRESULT Divide (in long Op1,in long Op2,out,retval long* pResult); 自动化对象实现双接口:,1126,class CMath:public IMathpublic: /来自IUnknown的三个函数virtual HRESULT _stdcall Que
17、ryInterface() ; virtual ULONG_stdcall AddRef() ; virtual ULONG_stdcall Release() ;/ 来自IDispatch的四个函数 HRESULT GetTypeInfoCount( );HRESULT GetTypeInfo( ); HRESULT GetIDsOfNames(); HRESULT Invoke( ); / 来自IMath的三个函数HRESULT Add(long Op1, long Op2, long* pResult); HRESULT Substract(long Op1, long Op2, lon
18、g* pResult); HRESULT Multiply(long Op1, long Op2, long* pResult); HRESULT Divide(long Op1, long Op2, long* pResult); ;/此COM对象同时通过分发接口给外界提供分发调用服务;通过IMath接口直接通过虚表来提供普通的服务.实现双接口的自动化对象的虚表和分发表:,1226,实现双接口的自动化对象的虚表和分发表,1326,3 自动化接口的实现,分发接口的四个函数从功能上来说分为两组:GetTypeInfoCount与GetTypeInfo函数表示对类型库的支持. 通常客户并不需要从分
19、发接口的这两个函数中来访问类型库.如果愿意,客户可以借助IDE生成封装类,或者直接使用操作类型库也可以. 但如果真要实现它,那么:提供类型库文件 (MIDL编译器对IDL编译的结果)GetTypeInfoCount返回1, 否则返回0;GetTypeInfo 使用LoadTypeLib得到ITypeLib接口.然后得到 ITypeInfo接口.一旦客户得到ITypeInfo接口指针就可以完全地了解接口的类型及其所支持的属性和方法。GetIDsOfNames和Invoke完成函数的分发调用. GetIDsOfNames有两种实现方法:1.由自动化对象自己实现。它当然知道自己所有的方法和属性的分发
20、ID。使用switch case或者如果数目太多的话,使用表格进行查表.,1426,HRESULT GetIDsOfNames( REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ) / 假设cNames=1,即一回只查一个名字. char * str=OLE2T(rgszzNames0); if (strcmp(“Add”,str,3)=0) rgDispId0=0; /加法返回0 else if (strcmp(“Substract”,str,8)=0
21、) rgDispId0=1; /减法返回1 else if 2.如果实现了GetTypeInfo,那么直接从其中得到ITypeInfo指针,然后使用这个指针的GetIDsOfNames方法即可.(绕了一大圈,但是也可行).HRESULT GetIDsOfNames() ITypeInfo * pITI; GetTypeInfo( ,1526,Invoke函数的实现。 1。可以根据分发ID,逐个分支处理,可以使用内部函数,或者,如果是双接口,分支内部直接使用IMath接口的功能函数.HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lc
22、id, WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr ); switch (dispIdMember) case 0: /作加法, 直接实现,或者调用内部函数. case1: /减法 2。使用类型信息指针。 如果实现了GetTypeInfo,那么直接从其中得到ITypeInfo指针,然后使用这个指针的Invoke方法即可.(也绕了一大圈,但是也可行).HRESULT Invoke()ITypeInfo
23、 * pITI; GetTypeInfo( ,1626,4.使用自动化对象.,对于自动化对象的使用,根据其实现接口和对类型库的支持程度不同, 有不同的使用方法:只实现了分发接口,没有提供类型库.只能使用晚绑定.实现了分发接口,提供了类型库,当然可以使用晚绑定,也可以使用DISPID绑定(早绑定的一种,为了区分起见就命名为DISPID绑定).实现了双接口,提供了类型库, 那么可以使用晚绑定,DISPID绑定和早绑定.晚绑定-DISPID绑定-早绑定 性能越来越高. 灵活性越来越低.,1726,4.1 晚绑定,晚绑定. 一般的COM对象都只能使用早绑定.但是自动化对象可以使用晚绑定.是重要特色之一
24、.开发阶段不进行类型检查,运行时决定组件的功能. 代价昂贵,速度最慢. 灵活性最高. 服务器接口发生变化(比如说分发ID变了) ,客户程序不用重现编译.1.使用VB Dim obj as Object Set obj=CreateObject(“MathLib.Math”) /动态创建. obj.Add(10,20) /结果为30 Set obj=Nothing /释放对象 注意,在编译时刻并没有进行类型检查, obj.Add(10,20) 纯属猜测! 如果方法的名字不符合或者参数不符合,都将引起运行时错误. 2.使用C+. 使用C+ 我们能更清楚地看到分发调用的过程.(虽然晚绑定一般是针对V
25、B这样的语言的),1826,首先看函数调用调用的参数类型typedef struct tagDISPPARAMS VARIANTARG *rgvarg; /参数数组,类型为VARIANT,大小为cArgsDISPID *rgdispidNamedArgs;/命名参数的ID数组.UINT cArgs; /参数的个数UINT cNamedArgs; /命名参数的个数 见MSDN文档 DISPPARAMS; 其中VARIANT是一个结构体,结构体中包含巨大的Union和一个指示实际类型的域vt.见MSDN文档. 在使用晚绑定时,只能使用VARIANT所支持的数据类型.客户的调用代码:IDispatc
26、h *pD;HRESULT hr=CoCreateInstance(CLSID_Math, NULL, CLSCTX_SERVER, IID_IDispatch, /加法字符串对应的分发ID存在此, 下面先找到它,1926,pD-GetIDsofNames(IID_NULL, lpOleStr, 1,LOCAL_SYSTEM_DEFAULT, /释放接口,2026,注意以上计算过程,我们只是使用了分发接口,我们猜测了加法的名字和参数.我们事先没有使用到自动化对象的任何信息.不需要包含接口声明的头文件. 编译时刻没有进行任何类型检查. 如果猜测失误将引起运行时错误.,2126,4.2 DISPI
27、D绑定,如果提供类型库,那么就可以在编译时进行类型检查.VB中使用Reference导入类型库.我们就可以象VB中固有的数据类型一样使用COM对象.编译器将根据组件中的类型信息检查代码中的语法和参数类型. VB为方法和属性缓存一个DISPID. 避免在运行时刻去查询方法或属性的分发ID. 以上措施,可以避免出错,提高性能. 组件的接口改变时,要重新编译客户程序.Dim obj as New MathLib.Math obj.Add(10,20) /返回30 不是猜测的! 如果不符合,则编译会出错! 这是类型库起的作用.下面看C+中如何使用DISPID绑定MFC提供了COleDispatchDr
28、iver类,可以用来使用DISPID绑定来访问自动化对象:,2226,COleDispatchDriver类是MFC提供的封装类,它通过自动化对象的类型库把原自动化对象的方法和属性的分发ID硬性地记录下来, 把原来的方法和属性在封装类中进行封装. 使得用户避免复杂的invoke参数序列, COleDispatchDriver 有一个数据成员m_lpDispatch,它包含了对应组件的IDispatch接口指针。COleDispatchDriver提供了几个成员函数包括InvokeHelper GetProperty SetProperty,这三个函数通过m_lpDispatch调用invoke
29、函数。COleDispatchDriver的其他成员管理IDispatch接口指针,CreateDispatch根据CLSID创建自动化对象,并把IDispatch接口指针赋给m_lpDispatch成员。AttachDispatch使得当前的COleDispatchDriver与某个自动化对象联系起来。DetachDispatch则取消这种联系。,2326,两种使用方式:根据组件的类型库生成COleDispatchDriver的派生类。从ClassWizard对话框的Add Class中选取From a type library,指定类型库文件,IDE为我们生成COleDispatchDri
30、ver的派生类的派生类。针对原自动化对象的属性和方法分别生成此派生类的函数。这些函数在实现时调用COleDispatchDriver的SetProperty,GetProperty和InvokerHelper函数。如果我们已经得到了自动化对象的IDispatch指针,(如果没有,当然可以调用CreateDispatch等方法.)使用AttachDispatch把自动化对象与COleDispatchDriver对象联系起来通过SetProperty、GetProperty访问对象的属性,通过InvokerHelper访问对象的方法。,2426,以第一种方法为例,使用IDE的添加类向导from t
31、ype library.选择类型库,则产生以下类:class IOMath:public COleDispatchDriverpublic: long Add(long Op1,long Op2); long Substract(long Op1,long Op2); long Multiply(long Op1,long Op2); long Divide(long Op1,long Op2);long IOMath: Add(long Op1,long Op2)static BYTE params=VTS_I4 VTS_I4;long result;InvokeHelper(0x1, DISPATCH_METHOD, VT_I4, ,2526,4.3早绑定,如果实现了双接口,又有类型库的支持.那么就可以使用早绑定.实际上这就是一般的COM对象的使用方式.即直接使用虚表来调用接口的方法.而没有使用GetIDsofName和Invoke函数.在VB中使用Reference引进类型库后.代码与前一种方法一样. Dim obj as New MathLib.Math obj.Add(10,20) 而C+语言则是按照普通的COM接口一样,不用理会分发接口即可.,2626,5自动化对象编程实践,MFC的支持ATL的支持 见 第五章,第十一章.以及其他文档,