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






4.75/5 (5投票s)
2001 年 9 月 4 日
8分钟阅读

120040

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 文件中实现。它是一个您需要继承的单一类,再加上您需要声明的几个宏。
- 您的接口将是“双接口”(派生自
IDispatchImpl
)。向导默认会为您提供此功能。 - 您的 ATL 头文件将包含
multidisp.h
- 您的 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>
- 在您的类定义中,使用
DECLARE_MULTI_DISPATCH()
宏 - 为了方便(和清晰),请为您的
IDispatchImpl
派生类创建typedef
。typedef IDispatchImpl<IFoozle, &IID_IFoozle, &LIBID_FOOZLELib> dispBase1;
- 创建
MULTI_DISPATCH_MAP
。它的风格类似于 COM 接口映射,如下所示:BEGIN_MULTI_DISPATCH_MAP(CFoozle) MULTI_DISPATCH_ENTRY(dispBase1) MULTI_DISPATCH_ENTRY(dispBase2) END_MULTI_DISPATCH_MAP()
就是这样!这种方法的一个优点是,您可以选择要由类实现的双接口中的一部分或全部,从而控制对脚本编写者的可见性。
实现细节
当我开始研究如何解决这个问题时,我查看了 ATL 如何实现 IDispatchImpl
。我很快意识到 IDispatch
的两个最重要的函数——GetIdsOfNames
和 Invoke
——被委托给了一个 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
类,它重写了 GetIdsOfNames
和 Invoke
。我在一些宏定义中隐藏了一个 _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::GetIDsOfNames
和 IDispatch::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::GetIDsOfNames
和 IDispatch::Invoke
的调用会一起进行。Invoke
会紧随 GetIDsOfNames
的调用之后。当对象是动态创建的时,这确实是真的,但当它们存在于 object 标签中时则不是。在这种情况下,IE 在页面加载时会预先调用所有方法的 GetIDsIfNames
并将其 DISPIDs
缓存起来。显然,我简单的 true-false 切换算法在这里不起作用。幸运的是,有一个简单但巧妙的小 hack,我以前读到过,叫做 DISPID
编码。
DISPID 编码
Richard Grimes 博士在《Professional ATL Programming》中讨论了这项技术。本质上,它归结为将一个 DISPID
值放入 DISPID
的 LOWORD
。然后使用 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 个接口:IFoozle
和 IBaz
。每个接口都有 2 个方法,它们只是显示一个消息框,表明它们已被调用。包含 2 个 HTML 测试页面:testit.html
和 testit2.html
。第一个页面动态地测试组件,第二个页面将其作为页面中的嵌入对象进行测试。请随意编译并运行测试。然后注释掉我的扩展代码。请注意,只有默认接口可见。评论?
如果您有任何评论或发现任何问题,请随时与我联系!希望这对您有帮助!