65.9K
CodeProject 正在变化。 阅读更多。
Home

CMultiDispatch - 自动化客户端的多个 IDispatch 接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (5投票s)

2001 年 9 月 4 日

8分钟阅读

viewsIcon

120040

downloadIcon

1593

一个 ATL 扩展,用于在单个对象上支持多个 IDispatch 接口,供脚本使用者可见

引言

COM 的一个基本思想是二进制兼容性。根据微软的说法,用任何语言编写的客户端都可以使用 COM 组件。这在某种程度上是真的。但还有一些需要了解的是,某些客户端环境需要组件提供更多的“支持”。客户端大致可以分为两大类,能力不等。第一类是以 C++ 以及(后来)VB 等语言为代表。这些语言完全了解 QueryInterface (QI) 并通常可以通过 v-table 或双接口的自定义部分访问组件支持的任何接口。第二类,能力更有限,保留给纯自动化客户端。今天,这意味着通常在 IE 中运行的 VB Script 和 JavaScript 执行引擎。这些客户端需要 OLE 自动化类型,但不幸的是,它们无法进行 QI。自动化环境每种对象只能访问一个 IDispatch 接口,称为默认接口。

假设您最初设计并实现了您的对象,以提供多个接口给 v-table 感知的客户端。您已经找到了“正确”的接口抽象,并且在对象上有意义时,您已费心使用 QI。现在您发现您的某些客户端将是脚本编写者,并且您需要让他们能够访问对象上的所有接口方法——就像您的 C++ 客户端一样。问题是,由于脚本环境无法进行 QI,它们每种 COM 标识(CLSID)只能“看到”一个默认的 IDispatch 接口。如果您想将对象的所有功能暴露给脚本世界,那么您的接口划分将是徒劳的。如果您可以为 C++ 和 VB 分割接口,但又想将它们作为一个大集合提供给脚本编写者,那该怎么办?您将拥有两全其美。您可以保留您优秀的设计,而无需创建额外的 hack 的 dispinterfaces。

这是我用于此目的的 ATL 扩展。(有关此问题的精彩讨论以及其他解决方案列表,请访问 Chris Sell 的网站: http://www.sellsbrothers.com/tools/multidisp/index.htm

使用 CMultiDispatch

我的解决方案实现为一个 ATL 扩展,它在一个 .h 文件中实现。它是一个您需要继承的单一类,再加上您需要声明的几个宏。

  1. 您的接口将是“双接口”(派生自 IDispatchImpl)。向导默认会为您提供此功能。
  2. 您的 ATL 头文件将包含 multidisp.h
  3. 您的 ATL 类将继承自 CMultiDispatch,并将自身作为模板参数传递——如下所示
        class ATL_NO_VTABLE CFoozle :
             public CComObjectRootEx<CComSingleThreadModel>,
             public IDispatchImpl<IFoozle, &IID_IFoozle, &LIBID_FOOZLELib>,
             public IDispatchImpl<IBaz, &IID_IBaz, &LIBID_FOOZLELib>,
             public CComCoClass<CFoozle, &CLSID_Foozle>,
             public CMultiDispatch<CFoozle> 
  4. 在您的类定义中,使用 DECLARE_MULTI_DISPATCH()
  5. 为了方便(和清晰),请为您的 IDispatchImpl 派生类创建 typedef
    typedef IDispatchImpl<IFoozle, &IID_IFoozle,  &LIBID_FOOZLELib> dispBase1;
  6. 创建 MULTI_DISPATCH_MAP。它的风格类似于 COM 接口映射,如下所示:
    BEGIN_MULTI_DISPATCH_MAP(CFoozle)
        MULTI_DISPATCH_ENTRY(dispBase1)
        MULTI_DISPATCH_ENTRY(dispBase2)
    END_MULTI_DISPATCH_MAP() 

就是这样!这种方法的一个优点是,您可以选择要由类实现的双接口中的一部分或全部,从而控制对脚本编写者的可见性。

实现细节

当我开始研究如何解决这个问题时,我查看了 ATL 如何实现 IDispatchImpl。我很快意识到 IDispatch 的两个最重要的函数——GetIdsOfNamesInvoke——被委托给了一个 CComTypeInfoHolder 成员。事实证明,这个类提供了 ITypeInfo 接口的一个包装器。IDispatch 方法可以通过委托给这个类型库特定的接口来实现。虽然有趣,但主要问题仍然存在:我如何让单个对象向自动化客户端提供多个基于 IDispatch 的接口?可以肯定的是,有一点很清楚:需要有一个对自动化客户端可见的单一 IDispatch 实现。无论如何,这个单一的 IDispatch 实现者都需要将调用转发到多重继承链中的适当的 IDispatchImpl。查看 CComTypeInfoHolder::Invoke,我看到第一个参数是一个 IDispatch 指针。这被证明是关键:在 ATL 的 IDispatchImpl::Invoke 中;它只是一个从 this 指针进行的简单类型转换。所以就是它了。我所要做的就是找到 v-table 中正确的 IDispatchImpl 部分,并将其传递给 CComTypeInfoHolder::Invoke 方法。

这时我已经触及了我的 C++ 知识的边界。我完全不确定如何获取对象布局的正确偏移量,以便在多重继承链中正确识别 IDispatchImpl(s)。幸运的是,我不需要这样做——ATL 开发者已经为我做了!通过搜索“offsetof”,我在 ATLDEF.H 中找到了 offsetofclass 宏。这看起来正是我所需要的。

第一个 hack

开始时,我编写了一个 CMultiDispatch 类,它重写了 GetIdsOfNamesInvoke。我在一些宏定义中隐藏了一个 _TIH_ENTRY 结构的静态数组。继承链中的每个 IDispatchImpl 最多有一个 _TIH_ENTRY 结构。每个静态条目都有从 IDispatchImpl 继承的静态 CComTypeInfoHolder,一个布尔标志,表示 GetIdsOfNames 是否刚刚被调用,以及一个 DWORD,表示从派生类到特定 IDispatchImpl 的偏移量。静态数组通过宏表示如下:

    typedef IDispatchImpl<IFoozle, &IID_IFoozle,  &LIBID_FOOZLELib> dispBase1;
    typedef IDispatchImpl<IBaz, &IID_IBaz,  &LIBID_FOOZLELib> dispBase2;

    BEGIN_MULTI_DISPATCH_MAP(CFoozle)
        MULTI_DISPATCH_ENTRY(dispBase1)
        MULTI_DISPATCH_ENTRY(dispBase2)
    END_MULTI_DISPATCH_MAP() 

这会展开为:

    static struct _TIH_ENTRY* GetTypeInfoHolder()
    {
        static struct _TIH_ENTRY pDispEntries[] = {
            &dispBase1::_tih, false, offsetofclass(dispBase1, CAbundantFeast) },
            &dispBase2::_tih, false, offsetofclass(dispBase2, CAbundantFeast) },
            NULL, false, 0UL }
        };
        return(pDispEntries);
    }

最后,IDispatch::GetIDsOfNamesIDispatch::Invoke 的声明和实现是通过 DECLARE_MULTI_DISPATCH 宏完成的。这些方法被简单地转发到 CMultiDispatch 的相同实现。这处理了脚本编写者可见的唯一 IDispatch。有关详细信息,请参阅头文件。

通常,对 IDispatch::Invoke 的调用会 precedes 对 IDispatch::GetIDsOfNames 的调用。CMultiDispatch::GetIDsOfNames 只是遍历静态数组,调用 GetIDsOfNames 直到成功(因此有一个限制:每个方法名必须是唯一的)。然后将 DISPID 返回给客户端,并设置结构的布尔条目为 true。CMultiDispatch::Invoke 的工作方式类似,它遍历数组,查找设置为 true 的布尔标志。该标志被重置,并且 Invoke 调用被委托给具有适当偏移量的 CComTypeInfoHolder

    HRESULT hr = pEntry->ptih->Invoke((IDispatch*)(((DWORD)pT)+pEntry->offset),
                                       dispidMember, riid, lcid,
                                       wFlags, pdispparams, pvarResult,
                                       pexcepinfo, puArgErr);

问题

这似乎工作得相当好。我可以在脚本中创建对象并像这样使用所有公开的方法:

    obj = new ActiveXObject("MultiDispTest.Foozle");
    obj.SomeFn();

但是,在 object 标签中存在的同一个对象将惨败。很长一段时间,我以为是 IE 的某种限制。终于,在 Tabor 先生的洞察和调试的帮助下,我能够看到问题不在于 IE,而在于我的算法。我假设对 IDispatch::GetIDsOfNamesIDispatch::Invoke 的调用会一起进行。Invoke 会紧随 GetIDsOfNames 的调用之后。当对象是动态创建的时,这确实是真的,但当它们存在于 object 标签中时则不是。在这种情况下,IE 在页面加载时会预先调用所有方法的 GetIDsIfNames 并将其 DISPIDs 缓存起来。显然,我简单的 true-false 切换算法在这里不起作用。幸运的是,有一个简单但巧妙的小 hack,我以前读到过,叫做 DISPID 编码。

DISPID 编码

Richard Grimes 博士在《Professional ATL Programming》中讨论了这项技术。本质上,它归结为将一个 DISPID 值放入 DISPIDLOWORD。然后使用 HIWORD 进行编码。当调用 GetIDsOfNames 时,会在 DISPID 上设置一个 HIWORD 位,并以这种方式返回给客户端。当客户端调用 Invoke 时,会传入带有编码位的 DISPID。然后使用编码位进行查找,在实际调用 ITypeInfo::Invoke 之前将其掩码掉。这一切都非常巧妙——尽管它确实要求所有 DISPIDs 小于或等于 16 位(65,535)能容纳的大小。请注意,编码位是根据静态数组中的位置构建的。(有关详细信息,请参见代码。)一旦进行了此更改,我发现我可以将我的对象嵌入到标签中,并随意访问我的方法。

已知限制

如前所述,此实现存在一些已知的、细小的限制。首先,所有双接口中的每个方法名都必须是唯一的。在实践中,这也不是什么大问题,因为 C++ 编译器会大声抱怨如果它们不唯一的话。其次,DISPIDs 必须使用 32 位整数的低字。同样,这也不是太大的限制,因为保持 DISPIDs 小于或等于 65,535 是很容易的。最后,您的接口应该实现为派生自 IDispatchImpl 的双接口。严格来说,唯一真正的要求是一个静态的 CComTypeInfoHolder 变量,名为 _tih。有关详细信息,请参阅 ATL 的 IDispatchImpl

示例

我包含了一个简单的 COM 组件,它实现了 2 个接口:IFoozleIBaz。每个接口都有 2 个方法,它们只是显示一个消息框,表明它们已被调用。包含 2 个 HTML 测试页面:testit.htmltestit2.html。第一个页面动态地测试组件,第二个页面将其作为页面中的嵌入对象进行测试。请随意编译并运行测试。然后注释掉我的扩展代码。请注意,只有默认接口可见。

评论?

如果您有任何评论或发现任何问题,请随时与我联系!希望这对您有帮助!

© . All rights reserved.