十一. 自动化对象

26
1 . 十十十十十 十十十十 : 十十十十十 , 十十十十十十十十十 IDispatch 十十十十十十 , 十十十十十十十十十十十 十十十十十 . 十十十十十十十十十十十 , 十十十十十十十十十十十十 . 十十 , 十十十十十十十十十十十 十十十十十十十十 , 十十十十十十 十十十十…… 1. 十十十 2. IDispatch 十十 3. 十十十十十十十十 4. 十十十十十十十十十 1. 十十十 2. DISPID 十十 3. 十十十5. 十十十十十十十十十

Upload: mika

Post on 17-Jan-2016

178 views

Category:

Documents


4 download

DESCRIPTION

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

TRANSCRIPT

Page 1: 十一.   自动化对象

1

十一 . 自动化对象 概念澄清 :就概念而言 ,类型库与自动化接口 IDispatch没有任何

关系 ,但是在应用上却有着极其密切的联系 .为了准确把握自动化对象 ,先要准确地理解它们的概念 . 当然 , 自动化对象与自动化控制更是没有任何关系 ,碰巧使用了同一个词而已……

1. 类型库2. IDispatch接口3. 自动化对象的实现4. 使用自动化对象客户

1. 晚绑定2. DISPID绑定3. 早绑定、

5. 自动化对象编程实践

Page 2: 十一.   自动化对象

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++头文件 .

Page 3: 十一.   自动化对象

3

一个类型库可以包含多个 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组件。

Page 4: 十一.   自动化对象

4

并不是只有 IDE的开发者才知道怎样解析类型库 . 为了操作类型库 ,Windows提供了一些 API(LoadTypeLib 和LoadRegTypeLib等 ) 和 COM接口 (ITypeLib 和 ITypeInfo等 ).

1. LoadTypeLib可以根据指定的文件名装载类型库 ,并返回ITypeLib接口 .

2. 使用 LoadRegTypeLib可以根据类型库的 LIBID查找注册表 ,找到类型库文件 ,返回 ITypeLib接口 .

3. ITypeLib接口代表了类型库本身 .使用其 GetTypeInfoofGuid根据接口的 IID或者使用 GetTypeInfo根据接口在类型库中的索引号可以返回 ITypeInfo接口 .

4. ITypeInfo接口则代表了接口的全部信息 .包括有哪些方法 ,方法的签名等等 . 如果接口是 IDispatch接口 ,则还可以使用GetIDsofNames函数来根据方法的名字得到其分发 ID,并使用Invoke函数通过方法的分发 ID来执行这个方法 .

因此 ,为了在编译时刻了解接口的信息 , 客户程序要么得到COM组件的 IDL文件 (使用头类型定义头文件 ,在代码中通知编译器接口的类型 , 如 C++), 要么得到它的类型库文件 (代码中没有准确的信息 , 由 IDE环境从类型库中读取接口类型信息 ,如 VB), 才能顺利地构造客户应用程序 , 从而使用 COM对象 .

Page 5: 十一.   自动化对象

5

无论是通过头文件 ,还是通过类型库 ,我们在开发客户程序时都有关于接口的先验知识 .这些先验信息帮助我们顺利地编译客户程序 .这种方式我们有时称为静态调用 ,或者早绑定 (early binding).

但是 ,还存在这样的情况 ,有的语言在开发过程中并没有经过编译阶段 ,而是直接以源代码的形式被配置发布 . 在运行时才被解释运行 .比如以HTML为基础的脚本语言 .(VBScript,JavaScript等 ).它们在浏览器或Web 服务器的环境中执行 . 脚本代码以纯文本的形式嵌入在 HTML文件中 . 为了丰富脚本的功能 ,它们也可以创建 COM对象 ,执行特殊的功能 ,比如访问数据库等等 . 比如 :

var obj = new ActiveXObject(“LuBenjie.AutoObj"); alert(obj.Hello());

在脚本引擎中 , 目前还不能使用类型库或其他的先验知识来描述接口的信息 .这意味着对象自身要帮助脚本解释器 ,将文本形式的脚本代码翻译为有意义的方法调用 . 这种方式我们称为动态调用 ,或者晚绑定 (late binding).

为了支持晚绑定 ,COM定义了一个接口 ,用来表达这种翻译机制 ,这个接口就是 IDispatch.分发接口有时称为自动化接口 ,实现了此接口的对象称为自动化对象 .

自动化接口的定义如下 :

2 IDispatch接口

Page 6: 十一.   自动化对象

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函数来

实现。使得运行时刻动态绑定属性和方法并进行参数类型检查成为可能 .

Page 7: 十一.   自动化对象

7

当一个脚本引擎首次尝试访问一个对象时 ,它使用 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);} 此对象的虚表及其分发表示意图如下 :

Page 8: 十一.   自动化对象

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对象只能通过分发接口给外界提供服务 . 虽然这样做显得别扭 ,有舍近求远之嫌 , 但是 , 原理上是可行的 .

Page 9: 十一.   自动化对象

9

自动化对象的虚表和分发表 .

pVtable QueryInterface

AddRef

Release

m_pData

GetTypeInfoCount

GetTypeInfo

GetIDsofNamesInvoke

接口指针

Add

Substract

MultiplyDivide

0

1

23

分发表组件的实际业务功能

IUnknown

IDispatch

Page 10: 十一.   自动化对象

10

更常用地 ,我们把具体的计算功能也作为接口直接暴露出去 ,我们从 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); }

Page 11: 十一.   自动化对象

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接口直接通过虚表来提供普通的服务 .

Page 12: 十一.   自动化对象

12

实现双接口的自动化对象的虚表和分发表

pVtable QueryInterface

AddRef

Release

m_pData

GetTypeInfoCount

GetTypeInfo

GetIDsofNamesInvoke

接口指针

Add

Substract

MultiplyDivide

0

1

23

分发表

IUnknown

IDispatch

IMath

Page 13: 十一.   自动化对象

13

3 自动化接口的实现 分发接口的四个函数从功能上来说分为两组 :

1. GetTypeInfoCount 与 GetTypeInfo函数表示对类型库的支持 . 通常客户并不需要从分发接口的这两个函数中来访问类型库 .如果愿

意 ,客户可以借助 IDE生成封装类 ,或者直接使用操作类型库也可以 . 但如果真要实现它 , 那么 :

1. 提供类型库文件 (MIDL编译器对 IDL编译的结果 )

2. GetTypeInfoCount返回 1, 否则返回 0;

3. GetTypeInfo 使用 LoadTypeLib得到 ITypeLib接口 .然后得到 ITypeInfo接口 .一旦客户得到 ITypeInfo接口指针就可以完全地了解接口的类型及其所支持的属性和方法。

2. GetIDsOfNames 和 Invoke完成函数的分发调用 .

GetIDsOfNames有两种实现方法 :

1.由自动化对象自己实现。它当然知道自己所有的方法和属性的分发 ID。使用 switch case或者如果数目太多的话 ,使用表格进行查表 .

Page 14: 十一.   自动化对象

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();

}

Page 15: 十一.   自动化对象

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();}

Page 16: 十一.   自动化对象

16

4.使用自动化对象 .

对于自动化对象的使用 ,根据其实现接口和对类型库的支持程度不同 , 有不同的使用方法 :

1. 只实现了分发接口 ,没有提供类型库 .只能使用晚绑定 .

2. 实现了分发接口 ,提供了类型库 ,当然可以使用晚绑定 ,也可以使用 DISPID绑定 (早绑定的一种 ,为了区分起见就命名为DISPID绑定 ).

3. 实现了双接口 ,提供了类型库 , 那么可以使用晚绑定 ,DISPID绑定和早绑定 ..

晚绑定 ->DISPID绑定 ->早绑定 性能越来越高 . 灵活性越来越低 .

Page 17: 十一.   自动化对象

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这样的语言的 )

Page 18: 十一.   自动化对象

18

首先看函数调用调用的参数类型typedef struct tagDISPPARAMS {VARIANTARG *rgvarg; // 参数数组 ,类型为 VARIANT, 大小为 cArgsDISPID *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 存在此 , 下面先找到它

Page 19: 十一.   自动化对象

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(); //释放接口

Page 20: 十一.   自动化对象

20

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

Page 21: 十一.   自动化对象

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绑定来

访问自动化对象 :

Page 22: 十一.   自动化对象

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则取消这种联系。

Page 23: 十一.   自动化对象

23

两种使用方式:1. 根据组件的类型库生成 COleDispatchDriver的派生类。从

ClassWizard对话框的 Add Class中选取 From a type library,指定类型库文件, IDE为我们生成COleDispatchDriver的派生类的派生类。针对原自动化对象的属性和方法分别生成此派生类的函数。这些函数在实现时调用 COleDispatchDriver 的 SetProperty , GetProperty 和InvokerHelper函数。

2. 如果我们已经得到了自动化对象的 IDispatch指针,(如果没有,当然可以调用 CreateDispatch等方法 . )使用AttachDispatch把自动化对象与 COleDispatchDriver对象联系起来通过 SetProperty 、 GetProperty 访问对象的属性,通过 InvokerHelper 访问对象的方法。

Page 24: 十一.   自动化对象

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);

Page 25: 十一.   自动化对象

25

4.3早绑定

如果实现了双接口 , 又有类型库的支持 . 那么就可以使用早绑定 .实际上这就是一般的 COM对象的使用方式 . 即直接使用虚表来调用接口的方法 .而没有使用 GetIDsofName 和 Invoke函数 .

在 VB中使用 Reference引进类型库后 .代码与前一种方法一样 . Dim obj as New MathLib.Math

obj.Add(10,20)

而 C++语言则是按照普通的 COM接口一样 ,不用理会分发接口即可 .

Page 26: 十一.   自动化对象

26

5自动化对象编程实践

1. MFC的支持2. ATL的支持

见 << 原理 >> 第五章 , 第十一章 .以及其他文档