260 likes | 423 Views
十一. 自动化对象. 概念澄清:就概念而言,类型库与自动化接口 IDispatch 没有任何关系,但是在应用上却有着极其密切的联系.为了准确把握自动化对象,先要准确地理解它们的概念. 当然, 自动化对象与自动化控制更是没有任何关系,碰巧使用了同一个词而已 …… 类型库 IDispatch 接口 自动化对象的实现 使用自动化对象客户 晚绑定 DISPID 绑定 早绑定、 自动化对象编程实践. 1类型库. COM 不仅追求 C++ 编译器的中立,而且追求语言的独立性. 因此它使用 IDL 语言来描述接口. 然后在 IDL 到具体的语言之间建立映射.
E N D
十一. 自动化对象 • 概念澄清:就概念而言,类型库与自动化接口IDispatch没有任何关系,但是在应用上却有着极其密切的联系.为了准确把握自动化对象,先要准确地理解它们的概念. 当然, 自动化对象与自动化控制更是没有任何关系,碰巧使用了同一个词而已…… • 类型库 • IDispatch接口 • 自动化对象的实现 • 使用自动化对象客户 • 晚绑定 • DISPID绑定 • 早绑定、 • 自动化对象编程实践
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++头文件.
一个类型库可以包含多个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组件。
并不是只有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对象.
2 IDispatch接口 • 无论是通过头文件,还是通过类型库,我们在开发客户程序时都有关于接口的先验知识.这些先验信息帮助我们顺利地编译客户程序.这种方式我们有时称为静态调用,或者早绑定(early binding). • 但是,还存在这样的情况,有的语言在开发过程中并没有经过编译阶段,而是直接以源代码的形式被配置发布. 在运行时才被解释运行.比如以HTML为基础的脚本语言.(VBScript,JavaScript等).它们在浏览器或Web服务器的环境中执行. 脚本代码以纯文本的形式嵌入在HTML文件中. 为了丰富脚本的功能,它们也可以创建COM对象,执行特殊的功能,比如访问数据库等等. 比如: var obj = new ActiveXObject(“LuBenjie.AutoObj"); alert(obj.Hello()); 在脚本引擎中,目前还不能使用类型库或其他的先验知识来描述接口的信息.这意味着对象自身要帮助脚本解释器,将文本形式的脚本代码翻译为有意义的方法调用. 这种方式我们称为动态调用,或者晚绑定(late binding). • 为了支持晚绑定,COM定义了一个接口,用来表达这种翻译机制,这个接口就是IDispatch.分发接口有时称为自动化接口,实现了此接口的对象称为自动化对象. • 自动化接口的定义如下:
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函数来 实现。使得运行时刻动态绑定属性和方法并进行参数类型检查成为可能.
当一个脚本引擎首次尝试访问一个对象时,它使用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); } • 此对象的虚表及其分发表示意图如下:
自动化对象可以只实现分发接口: 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对象只能通过分发接口给外界提供服务.虽然这样做显得别扭,有舍近求远之嫌, 但是,原理上是可行的.
QueryInterface 接口指针 pVtable IUnknown m_pData AddRef Release GetTypeInfoCount GetTypeInfo IDispatch GetIDsofNames Invoke Add 0 Substract 1 Multiply 2 Divide 3 组件的实际业务功能 分发表 • 自动化对象的虚表和分发表.
更常用地,我们把具体的计算功能也作为接口直接暴露出去,我们从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); }
自动化对象实现双接口: 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接口直接通过虚表来提供普通的服务.
QueryInterface 接口指针 pVtable IUnknown m_pData AddRef Release GetTypeInfoCount GetTypeInfo IDispatch GetIDsofNames Invoke Add 0 Substract 1 IMath Multiply 2 Divide 3 分发表 实现双接口的自动化对象的虚表和分发表
3 自动化接口的实现 • 分发接口的四个函数从功能上来说分为两组: • GetTypeInfoCount与GetTypeInfo函数表示对类型库的支持. 通常客户并不需要从分发接口的这两个函数中来访问类型库.如果愿意,客户可以借助IDE生成封装类,或者直接使用操作类型库也可以. 但如果真要实现它,那么: • 提供类型库文件 (MIDL编译器对IDL编译的结果) • GetTypeInfoCount返回1, 否则返回0; • GetTypeInfo 使用LoadTypeLib得到ITypeLib接口.然后得到 ITypeInfo接口.一旦客户得到ITypeInfo接口指针就可以完全地了解接口的类型及其所支持的属性和方法。 • GetIDsOfNames和Invoke完成函数的分发调用. GetIDsOfNames有两种实现方法: 1.由自动化对象自己实现。它当然知道自己所有的方法和属性的分发ID。使用switch case或者如果数目太多的话,使用表格进行查表.
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(); }
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();}
4.使用自动化对象. • 对于自动化对象的使用,根据其实现接口和对类型库的支持程度不同, 有不同的使用方法: • 只实现了分发接口,没有提供类型库.只能使用晚绑定. • 实现了分发接口,提供了类型库,当然可以使用晚绑定,也可以使用DISPID绑定(早绑定的一种,为了区分起见就命名为DISPID绑定). • 实现了双接口,提供了类型库, 那么可以使用晚绑定,DISPID绑定和早绑定.. • 晚绑定->DISPID绑定->早绑定 性能越来越高. 灵活性越来越低.
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这样的语言的)
首先看函数调用调用的参数类型 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存在此, 下面先找到它
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(); //释放接口
注意以上计算过程,我们只是使用了分发接口,我们猜测了加法的名字和参数.我们事先没有使用到自动化对象的任何信息.不需要包含接口声明的头文件. 编译时刻没有进行任何类型检查. 如果猜测失误将引起运行时错误.
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绑定来访问自动化对象:
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则取消这种联系。
两种使用方式: • 根据组件的类型库生成COleDispatchDriver的派生类。从ClassWizard对话框的Add Class中选取From a type library,指定类型库文件,IDE为我们生成COleDispatchDriver的派生类的派生类。针对原自动化对象的属性和方法分别生成此派生类的函数。这些函数在实现时调用COleDispatchDriver的SetProperty,GetProperty和InvokerHelper函数。 • 如果我们已经得到了自动化对象的IDispatch指针,(如果没有,当然可以调用CreateDispatch等方法.)使用AttachDispatch把自动化对象与COleDispatchDriver对象联系起来通过SetProperty、GetProperty访问对象的属性,通过InvokerHelper访问对象的方法。
以第一种方法为例,使用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);
4.3早绑定 • 如果实现了双接口,又有类型库的支持.那么就可以使用早绑定.实际上这就是一般的COM对象的使用方式.即直接使用虚表来调用接口的方法.而没有使用GetIDsofName和Invoke函数. • 在VB中使用Reference引进类型库后.代码与前一种方法一样. • Dim obj as New MathLib.Math obj.Add(10,20) 而C++语言则是按照普通的COM接口一样,不用理会分发接口即可.
5自动化对象编程实践 • MFC的支持 • ATL的支持 见<<原理>> 第五章,第十一章.以及其他文档