1 / 26

十一. 自动化对象

十一. 自动化对象. 概念澄清:就概念而言,类型库与自动化接口 IDispatch 没有任何关系,但是在应用上却有着极其密切的联系.为了准确把握自动化对象,先要准确地理解它们的概念. 当然, 自动化对象与自动化控制更是没有任何关系,碰巧使用了同一个词而已 …… 类型库 IDispatch 接口 自动化对象的实现 使用自动化对象客户 晚绑定 DISPID 绑定 早绑定、 自动化对象编程实践. 1类型库. COM 不仅追求 C++ 编译器的中立,而且追求语言的独立性. 因此它使用 IDL 语言来描述接口. 然后在 IDL 到具体的语言之间建立映射.

mika
Download Presentation

十一. 自动化对象

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 十一. 自动化对象 • 概念澄清:就概念而言,类型库与自动化接口IDispatch没有任何关系,但是在应用上却有着极其密切的联系.为了准确把握自动化对象,先要准确地理解它们的概念. 当然, 自动化对象与自动化控制更是没有任何关系,碰巧使用了同一个词而已…… • 类型库 • IDispatch接口 • 自动化对象的实现 • 使用自动化对象客户 • 晚绑定 • DISPID绑定 • 早绑定、 • 自动化对象编程实践

  2. 1类型库 • COM不仅追求C++编译器的中立,而且追求语言的独立性. 因此它使用IDL语言来描述接口. 然后在IDL到具体的语言之间建立映射. • 但是一些数据类型在有些语言中难以表达。比如复杂的结构类型,指针类型,函数指针等等在一些弱类型的高级语言中比如Java, Visual Basic等等并没有得到支持. IDL到这些语言的映射不能顺利地进行. 客户通过接口调用对象时,在编译时刻需要接口的准确的描述, 这个描述正是来自于MIDL对IDL编译后产生的头文件, 而Java, VB等无法使用这种基于C/C++的头文件. COM的语言无关性受到很多的限制。 • 因此, MS使用类型库来解决这个问题. 类型库文件是一个二进制文件, 后缀为.tlb.用MIDL工具编译idl文件可以产生类型库文件,在实际的开发过程中不一定要手工使用MIDL工具,IDE对其进行了集成. 编译完成以后,我们可以选择把它随组件库一起分发. 类型库以机器可读的方式描述了组件与外界交互的必要信息. 如COM对象的CLSID, 它支持的接口的IID,接口的成员函数的签名等等. 本质上它等价于描述接口的C/C++头文件.

  3. 一个类型库可以包含多个COM对象,这些COM对象可以实现多个接口,而且一般而言实现了IDispatch接口(不是必须).为了标识这些类型库,也使用GUID来作为它的唯一标识LIBID.并且也在注册表中注册,注册位置是HKEY-CLASSES_ROOT\TypeLib,注册内容主要指明类型库所描述的对象的载体(dll文件等)的位置.一个类型库可以包含多个COM对象,这些COM对象可以实现多个接口,而且一般而言实现了IDispatch接口(不是必须).为了标识这些类型库,也使用GUID来作为它的唯一标识LIBID.并且也在注册表中注册,注册位置是HKEY-CLASSES_ROOT\TypeLib,注册内容主要指明类型库所描述的对象的载体(dll文件等)的位置. • VB, Java等语言的开发者不需要直接面对类型库. 相反,它是由编译器环境(VB虚拟机,Java虚拟机)来解释它. 这样它使得开发者在开发期能够浏览接口的相关信息. (以VB为例,通过Reference添加对类型库的引用后,使用Object Browser就可以查看COM接口了, 另一个工具OLE/COM Object Viewer使用更加方便). 而开发人员只需要使用宿主语言简单的语法,非常方便地使用COM. (烦心事交给编译器的开发者去吧! 我们看到,如果不是使用COM,而是以一般的库函数的形式,在VB这样的高端应用中使用起来就没有这么简便(对最终开发者而言). 每一样复杂的技术,在使用者的舒适的背后,是底层开发者的艰辛) • 当然,如果愿意,C++编译器也可以利用类型库. Visual C++IDE中的ClassWizard和C++BuilderIDE,DElphi中的importType Library命令都可以读入组件的类型库,并利用其中的信息产生C++代码。客户程序利用这些代码可以使用COM组件。

  4. 并不是只有IDE的开发者才知道怎样解析类型库. 为了操作类型库,Windows提供了一些API(LoadTypeLib 和LoadRegTypeLib等)和COM接口(ITypeLib和ITypeInfo等). • LoadTypeLib可以根据指定的文件名装载类型库,并返回ITypeLib接口. • 使用LoadRegTypeLib可以根据类型库的LIBID查找注册表,找到类型库文件,返回ITypeLib接口. • ITypeLib接口代表了类型库本身.使用其GetTypeInfoofGuid根据接口的IID或者使用GetTypeInfo根据接口在类型库中的索引号可以返回ITypeInfo接口. • ITypeInfo接口则代表了接口的全部信息.包括有哪些方法,方法的签名等等. 如果接口是IDispatch接口,则还可以使用GetIDsofNames函数来根据方法的名字得到其分发ID,并使用Invoke函数通过方法的分发ID来执行这个方法. • 因此,为了在编译时刻了解接口的信息, 客户程序要么得到COM组件的IDL文件(使用头类型定义头文件,在代码中通知编译器接口的类型,如C++), 要么得到它的类型库文件(代码中没有准确的信息,由IDE环境从类型库中读取接口类型信息,如VB), 才能顺利地构造客户应用程序,从而使用COM对象.

  5. 2 IDispatch接口 • 无论是通过头文件,还是通过类型库,我们在开发客户程序时都有关于接口的先验知识.这些先验信息帮助我们顺利地编译客户程序.这种方式我们有时称为静态调用,或者早绑定(early binding). • 但是,还存在这样的情况,有的语言在开发过程中并没有经过编译阶段,而是直接以源代码的形式被配置发布. 在运行时才被解释运行.比如以HTML为基础的脚本语言.(VBScript,JavaScript等).它们在浏览器或Web服务器的环境中执行. 脚本代码以纯文本的形式嵌入在HTML文件中. 为了丰富脚本的功能,它们也可以创建COM对象,执行特殊的功能,比如访问数据库等等. 比如: var obj = new ActiveXObject(“LuBenjie.AutoObj"); alert(obj.Hello()); 在脚本引擎中,目前还不能使用类型库或其他的先验知识来描述接口的信息.这意味着对象自身要帮助脚本解释器,将文本形式的脚本代码翻译为有意义的方法调用. 这种方式我们称为动态调用,或者晚绑定(late binding). • 为了支持晚绑定,COM定义了一个接口,用来表达这种翻译机制,这个接口就是IDispatch.分发接口有时称为自动化接口,实现了此接口的对象称为自动化对象. • 自动化接口的定义如下:

  6. class IDispatch:public IUnkown {public: 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, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ); // 返回指定名字的方法或属性的分发ID. IDispatch使用分发ID管理接口的属性和方法. rgszNames 指定属性或方法的名字, rgDispId返回其分发ID HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr ); }; //是命令的翻译器。客户程序通过invoke函数访问方法或属性。客户给定分发ID dispIdMember 、输入参数pDispParams 。invoke返回输出参数pDispParams .自动化对象所有的方法和属性的调用都通过invoke函数来 实现。使得运行时刻动态绑定属性和方法并进行参数类型检查成为可能.

  7. 当一个脚本引擎首次尝试访问一个对象时,它使用QueryInterface向对象请求IDispatch接口.如果请求失败,则不能使用此对象.如果成功,则继续调用GetIDsofName方法,得到方法或属性的分发ID号.通过此ID号,调用Invoke方法,就可以调用想要调用的方法.当一个脚本引擎首次尝试访问一个对象时,它使用QueryInterface向对象请求IDispatch接口.如果请求失败,则不能使用此对象.如果成功,则继续调用GetIDsofName方法,得到方法或属性的分发ID号.通过此ID号,调用Invoke方法,就可以调用想要调用的方法. • 分发接口与普通接口的区别在于,接口的逻辑功能是如何被调用的.普通的COM接口是以该方法的静态的先验知识为基础.而分发接口是以该方法的预期的文字表示为基础.如果调用者正确地猜测出方法的原型,那么此调用可以被顺利地分发,否则不能. • 假设有一个自动化对象CMath,它只实现了分发接口,进行加减乘除的工作.这些具体的工作由内部函数来完成.并没有向外界提供接口.这些计算功能由Invoke函数根据分发ID来调用特定的函数. [uuid(C2895C1F-020E-4C1F-8A65-F59094DFBD97)] 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); } • 此对象的虚表及其分发表示意图如下:

  8. 自动化对象可以只实现分发接口: class CMath:public IDispatch {…… public: //来自IUnknown的三个函数 virtual HRESULT __stdcall QueryInterface(……) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // 来自IDispatch的三个函数 HRESULT GetTypeInfoCount( …… ); HRESULT GetTypeInfo( …… ); HRESULT GetIDsOfNames(……); HRESULT Invoke( …… ); };//此COM对象只能通过分发接口给外界提供服务.虽然这样做显得别扭,有舍近求远之嫌, 但是,原理上是可行的.

  9. QueryInterface 接口指针 pVtable IUnknown m_pData AddRef Release GetTypeInfoCount GetTypeInfo IDispatch GetIDsofNames Invoke Add 0 Substract 1 Multiply 2 Divide 3 组件的实际业务功能 分发表 • 自动化对象的虚表和分发表.

  10. 更常用地,我们把具体的计算功能也作为接口直接暴露出去,我们从IDispatch派生一个接口IMath.更常用地,我们把具体的计算功能也作为接口直接暴露出去,我们从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 Substract ([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); }

  11. 自动化对象实现双接口: class CMath:public IMath {…… public: //来自IUnknown的三个函数 virtual HRESULT __stdcall QueryInterface(……) ; 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, long* pResult); HRESULT Multiply(long Op1, long Op2, long* pResult); HRESULT Divide(long Op1, long Op2, long* pResult); };//此COM对象同时通过分发接口给外界提供分发调用服务;通过IMath接口直接通过虚表来提供普通的服务.

  12. QueryInterface 接口指针 pVtable IUnknown m_pData AddRef Release GetTypeInfoCount GetTypeInfo IDispatch GetIDsofNames Invoke Add 0 Substract 1 IMath Multiply 2 Divide 3 分发表 实现双接口的自动化对象的虚表和分发表

  13. 3 自动化接口的实现 • 分发接口的四个函数从功能上来说分为两组: • GetTypeInfoCount与GetTypeInfo函数表示对类型库的支持. 通常客户并不需要从分发接口的这两个函数中来访问类型库.如果愿意,客户可以借助IDE生成封装类,或者直接使用操作类型库也可以. 但如果真要实现它,那么: • 提供类型库文件 (MIDL编译器对IDL编译的结果) • GetTypeInfoCount返回1, 否则返回0; • GetTypeInfo 使用LoadTypeLib得到ITypeLib接口.然后得到 ITypeInfo接口.一旦客户得到ITypeInfo接口指针就可以完全地了解接口的类型及其所支持的属性和方法。 • GetIDsOfNames和Invoke完成函数的分发调用. GetIDsOfNames有两种实现方法: 1.由自动化对象自己实现。它当然知道自己所有的方法和属性的分发ID。使用switch case或者如果数目太多的话,使用表格进行查表.

  14. HRESULT GetIDsOfNames( REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ) { // 假设cNames==1,即一回只查一个名字. char * str=OLE2T(rgszzNames[0]); if (strcmp(“Add”,str,3)==0) rgDispId[0]=0; //加法返回0 else if (strcmp(“Substract”,str,8)==0) rgDispId[0]=1; //减法返回1 else if … } 2.如果实现了GetTypeInfo,那么直接从其中得到ITypeInfo指针,然后使用这个指针的GetIDsOfNames方法即可.(绕了一大圈,但是也可行). HRESULT GetIDsOfNames(……) { ITypeInfo * pITI; GetTypeInfo( … &pITI); pITI->GetIDsofNames(……); pITI->Release(); }

  15. Invoke函数的实现。 1。可以根据分发ID,逐个分支处理,可以使用内部函数,或者,如果是双接口,分支内部直接使用IMath接口的功能函数. HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, 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 * pITI; GetTypeInfo( … &pITI); pITI->Invoke(……); pITI->Release();}

  16. 4.使用自动化对象. • 对于自动化对象的使用,根据其实现接口和对类型库的支持程度不同, 有不同的使用方法: • 只实现了分发接口,没有提供类型库.只能使用晚绑定. • 实现了分发接口,提供了类型库,当然可以使用晚绑定,也可以使用DISPID绑定(早绑定的一种,为了区分起见就命名为DISPID绑定). • 实现了双接口,提供了类型库, 那么可以使用晚绑定,DISPID绑定和早绑定.. • 晚绑定->DISPID绑定->早绑定 性能越来越高. 灵活性越来越低.

  17. 4.1 晚绑定 • 晚绑定. 一般的COM对象都只能使用早绑定.但是自动化对象可以使用晚绑定.是重要特色之一.开发阶段不进行类型检查,运行时决定组件的功能. 代价昂贵,速度最慢. 灵活性最高. 服务器接口发生变化(比如说分发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++ 我们能更清楚地看到分发调用的过程.(虽然晚绑定一般是针对VB这样的语言的)

  18. 首先看函数调用调用的参数类型 typedef struct tagDISPPARAMS { VARIANTARG *rgvarg; //参数数组,类型为VARIANT,大小为cArgs DISPID *rgdispidNamedArgs;//命名参数的ID数组. UINT cArgs; //参数的个数 UINT cNamedArgs; //命名参数的个数 见MSDN文档 } DISPPARAMS; 其中VARIANT是一个结构体,结构体中包含巨大的Union和一个指示实际类型的域vt.见MSDN文档. 在使用晚绑定时,只能使用VARIANT所支持的数据类型. • 客户的调用代码: IDispatch *pD; HRESULT hr=CoCreateInstance(CLSID_Math, NULL, CLSCTX_SERVER, IID_IDispatch, &pD) //创建自动化对象,返回自动化接口 LPOLESTR lpOleStr=L”Add”; //加法,注意只是一个字符串 DISPATCH dispid; //加法字符串对应的分发ID存在此, 下面先找到它

  19. pD->GetIDsofNames(IID_NULL, lpOleStr, 1,LOCAL_SYSTEM_DEFAULT, &dispid); //得到加法的分发ID DISPPARAMS dms; //准备作加法的参数 memset(&dms,0,sizeof(DISPPARAMS)); dms.cArgs=2; //有两个参数 VARIANTTAG*pArg=new VARIANTTAG[dms.cArgs]; //动态分配内存 dms.rgvarg=pArg; memset(pArg,0,sizeof(VARIANT)*dms.cArgs); dms.rgvarg[0].vt=VT_I4; //第一个参数是长整数 dms. rgvarg[0].lVal=10; //值为10 dms.rgvarg[1].vt=VT_I4; //第二个参数也是长整数 dms. rgvarg[1].lVal=20; //值为20 VARIANTARG vaResult; //输出结果的参数 VariantInit(&vaResult); hr=pD->Invoke(dispid, IID_NULL, LOCAL_SYSTEM_DEFAULT, DISPATCH_METHOD,&dispparams,&vaResult,0,NULL); //使用invoke,根据分发ID进行计算.输入计算参数,提供返回参数 pD->Release(); //释放接口

  20. 注意以上计算过程,我们只是使用了分发接口,我们猜测了加法的名字和参数.我们事先没有使用到自动化对象的任何信息.不需要包含接口声明的头文件. 编译时刻没有进行任何类型检查. 如果猜测失误将引起运行时错误.

  21. 4.2 DISPID绑定 • 如果提供类型库,那么就可以在编译时进行类型检查. • VB中使用Reference导入类型库.我们就可以象VB中固有的数据类型一样使用COM对象.编译器将根据组件中的类型信息检查代码中的语法和参数类型. VB为方法和属性缓存一个DISPID. 避免在运行时刻去查询方法或属性的分发ID.以上措施,可以避免出错,提高性能. • 组件的接口改变时,要重新编译客户程序. Dim obj as New MathLib.Math obj.Add(10,20) //返回30 不是猜测的! 如果不符合,则编译会出错! 这是类型库起的作用. 下面看C++中如何使用DISPID绑定 MFC提供了COleDispatchDriver类,可以用来使用DISPID绑定来访问自动化对象:

  22. COleDispatchDriver类是MFC提供的封装类,它通过自动化对象的类型库把原自动化对象的方法和属性的分发ID硬性地记录下来, 把原来的方法和属性在封装类中进行封装. 使得用户避免复杂的invoke参数序列, COleDispatchDriver 有一个数据成员m_lpDispatch,它包含了对应组件的IDispatch接口指针。COleDispatchDriver提供了几个成员函数包括InvokeHelper GetProperty SetProperty,这三个函数通过m_lpDispatch调用invoke函数。 COleDispatchDriver的其他成员管理IDispatch接口指针,CreateDispatch根据CLSID创建自动化对象,并把IDispatch接口指针赋给m_lpDispatch成员。AttachDispatch使得当前的COleDispatchDriver与某个自动化对象联系起来。DetachDispatch则取消这种联系。

  23. 两种使用方式: • 根据组件的类型库生成COleDispatchDriver的派生类。从ClassWizard对话框的Add Class中选取From a type library,指定类型库文件,IDE为我们生成COleDispatchDriver的派生类的派生类。针对原自动化对象的属性和方法分别生成此派生类的函数。这些函数在实现时调用COleDispatchDriver的SetProperty,GetProperty和InvokerHelper函数。 • 如果我们已经得到了自动化对象的IDispatch指针,(如果没有,当然可以调用CreateDispatch等方法.)使用AttachDispatch把自动化对象与COleDispatchDriver对象联系起来通过SetProperty、GetProperty访问对象的属性,通过InvokerHelper访问对象的方法。

  24. 以第一种方法为例,使用IDE的添加类向导from type library.选择类型库,则产生以下类: class IOMath::public COleDispatchDriver {…… public: 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, &result, params, lOp1,lOp2);

  25. 4.3早绑定 • 如果实现了双接口,又有类型库的支持.那么就可以使用早绑定.实际上这就是一般的COM对象的使用方式.即直接使用虚表来调用接口的方法.而没有使用GetIDsofName和Invoke函数. • 在VB中使用Reference引进类型库后.代码与前一种方法一样. • Dim obj as New MathLib.Math obj.Add(10,20) 而C++语言则是按照普通的COM接口一样,不用理会分发接口即可.

  26. 5自动化对象编程实践 • MFC的支持 • ATL的支持 见<<原理>> 第五章,第十一章.以及其他文档

More Related