二、IDispatch 接口和双接口    使用者要想调用普通的 COM 组件功能,必须要加载这个组件的类型库(Type library)文件 tlb(比如在 VC 中使用 #import)。然而,在脚本程序中,由于脚本是被解释执行的,所以无法使用加载类型库的方式进行预编译。那么脚本解释器如何使用 COM 组件那?这就是自动化(IDispatch)组件大显身手的地方了。IDispatch 接口需要实现4个函数,调用者只通过这4个函数,就能实现调用自动化组件中所有的函数。这4个函数功能如下:
 

HRESULT GetTypeInfoCount(
    [out] UINT * pctinfo)

组件中提供几个类型库?当然一般都是一个啦。

但如果你在一个组件中实现了多个 IDispatch 接口,那就不一定啦(注1)

HRESULT GetTypeInfo(
    [in] UINT iTInfo,
    [in] LCID lcid,
    [out] ITypeInfo ** ppTInfo)

调用者通过该函数取得他想要的类型库。

幸好,在 99% 的情况下,我们都不用关心这两个函数的实现,因为 MFC/ATL 都帮我们完成了默认的一个实现,如果是自己完成函数代码,甚至可以直接返回 E_NOTIMPL 表示没有实现。(注2)

HRESULT GetIDsOfNames(
    [in] REFIID riid,
    [in,size_is(cNames)] LPOLESTR * rgszNames,
     [in] UINT cNames,
    [in] LCID lcid,
    [out,size_is(cNames)] DISPID * rgDispId)

根据函数名称取得函数序号,为调用 Invoke() 做准备。

所谓函数序号,大家去观察双接口 IDL 文件和 MFC 的 ODL 文件,每一个函数和属性都会有[id(序号)....] 这样的描述。

HRESULT Invoke(
    [in] DISPID dispIdMember,
     [in] REFIID riid,
     [in] LCID lcid,
     [in] WORD wFlags,
     [in,out] DISPPARAMS * pDispParams,
     [out] VARIANT * pVarResult,
     [out] EXCEPINFO * pExcepInfo,
     [out] UINT * puArgErr)

根据序号,执行函数。

使用 MFC/ATL 写的组件程序,我们也不必关心这个函数的实现。如果是自己写代码,则该函数类似如下实现:

switch(dispIdMember)

{

    case 1: .....; break;

    case 2: .....; break;

    ....

}

其实,就是根据序号进行分支调用啦。(注3)

    从 Invoke() 函数的实现就可以看出,使用 IDispatch 接口的程序,其执行效率是比较低的。ATL 从效率出发,实现了一种叫“双接口(dual)”的接口模式。下面我们来看看,到底什么是双接口:



图一、双接口(dual) 结构示意图


    从上图中可以看出,所谓双接口,其实是在一个 VTAB 的虚函数表中容纳了三个接口(因为任何接口都是从 IUnknown 派生的,所以就不强调 IUnknown 了,叫做双接口)。我们如果从任意一个接口中调用 QueryInterface()得到另外的接口指针的话,其实,得到的指针地址都是同一个。双接口有什么好处那?答:好呀,多好呀,特别好呀......

 

使用方式

因为

所以

脚本语言使用组件

解释器只认识 IDispatch 接口

可以调用,但执行效率最低

编译型语言使用组件

它认识 IDispatch 接口

可以调用,执行效率比较低

编译型语言使用组件

它装载类型库后,就认识了 Ixxx 接口

可以直接调用 Ixxx 函数,效率最高啦

结论

双接口,既满足脚本语言的使用方便性,又满足编译型语言的使用高效性。
于是,我们写的所有的 COM 组件接口,都用双接口实现吗?
错!否!NO!
如果不是明确非要支持脚本的调用,则最好不要使用双接口,因为:

如果所有函数都放在一个双接口中,那么层次、结构、分类不清

如果使用多个双接口,则会产生其它问题(注4)

双接口、IDispatch接口只支持自动化的参数类型,使用受到限制,某些情况下很不方便喽

还有很多弊病呦,不过现在我想不起来喽......