** 将 dotNET 组件暴露给 COM **
小气的神
2002-4-23
Article Type: In-Depth
难度等级: 6/9
版本: 2.32
COM 和 dotNET 之间的话题似乎总也讲不完,只要 COM 还存在而你又使用着 dotNET ,那么它们之间总有些有趣的事让你遇到。这丝毫不像小说中男女主人公相遇那么经典浪漫,反而像一个 Debug 和查错的过程,你必须很清楚问题是出在 COM 这边还是 dotNET 这边,事实上,有时你会忽略一个比较重要的因素:人的因素-我们每个人都会犯错 :)
dotNET 体系中的交互性调用可以分为两层面:一个层面是关于原始的平台调用;一个是关于 COM 对象的调用 (似乎 dotNET 之前 Microsoft 划分 DLL 是按 COM 来的,除了 COM 之外的就是基于 win32s 的 C/C++DLL 了, dotNET 之后 Microsoft 划分 DLL 是按 dotNET 来了,除了 managed DLL 剩下就是非 managed DLL ),对于第一个层面的调用, Microsoft 建议使用 P/Invoke ,事实上它能工作的很好,但这也是单方向的,即 dotNET ( CLR )调用平台的 DLL ( win32s );反之相反的方向上,在原始的 DLL 中调用 dotNET 组件的问题, Microsoft 还没有太迫切的需求, Widnows 平台之外的开发商似乎对这个问题更感兴趣一些(他们没有 COM )。对于 P/invoke ,在经历了 VJ++ 之后 Microsoft 利用他在编译器上的丰厚经验把这个问题解决得很好,开发人员利用属性告诉编译器如何如何转换这些参数,因为调用最后是发生在堆栈上的(无论如何堆栈是共享的),所以只要保证调用时堆栈上的类型翻译和转换是正确的也就无所谓 managed 还是非 managed 的。 Don Box 的文章曾公布过一份 ** Isomorphic and Nonisomorphic Types ** 的名单上面列举了 dotNET 数据类型和平台 DLL 中的类型比较。几乎 Win32s DLL 中所有的类型和 dotNET 现有的类型都是 Isomorphic 的,这也就意味着这些类型调用如果发生在调用和被调用者的共享堆栈上,是不用进行转换的。剩下的是一些和字符( WCHAR 和 LPST )、 interface 、 BSTR 、 VARIANT 、 SAFEARRAY 相关的少数类型(看得出都和 COM 有关,也终于有些明白 Struct 在 C# 以及整个 CLR 中的原因了: Struct 也是 Isomorphic 的, Microsoft 的那些设计者们真的是深谋远虑 haha )
剩下的就是与 COM 的交互性了,实际上问题已经演变到一个 dotNET 组件的引用如何转变成 COM 的引用或是相反的问题。这涉及到对象的引用计数和生命周期,远远不是堆栈能够解决的了。 Microsoft 给出了一个明确的方案:以 MSCOREE.dll 为界,当一个 CLR 对象的引用越过 MSCOREE 的边界(那么之外一定是 COM 的天空)所以一个保证这个 CLR 对象可以被 COM 调用的代理自动建立( COM-callable wrapper );同样一个 COM 对象的应用进入 MSCOREE 的边界(那么里面一定是 dotNET 的天空),所以也有一个保证这个 COM 对象可以被 dotNET 调用的代理自动建立( Runtime-callable wrapper )。这种代理 / 存根的游戏 COM 时代就有而且丰富多彩,现在也一样,只不过大家以 MSCOREE 为界,双方对于对象的引用计数、生命周期、事件、序列化、同步等等技术各有各的实现,事实上 COM 对象没有被 dotNET 接管和控制, dotNET 对象也没有被 COM 接管和控制,大家通过代理交换数据以及暴露接口和功能。往回看看这似乎又回到了第一个层面,现在的调用成为 P/Invoke 的一个超集了(地点从共享堆栈转移到了代理和存根之间)
上面是所谓的观念了,但事实上实际应用和操作中我们根本看到不到什么边界、什么代理 / 存根。而对于 dotNET 调用 COM 组件的情况更容易造成假相: VS.NET IDE 几乎封装了一切。但反方向的过程让你会对老式的开发环境产生恐惧(好的办法是 Microsoft 针对目前的 VS98 再发布一个 SP7 haha )。下面我们会到一些比较典型的问题比如如何把一个 dotNET 的组件暴露给 VB6 的环境使用。相对于第一种 P/Invoke 我认为没有太多的内容可以讲述,这个方面唯一的技巧是你的观察力和实践能力,收集专家或高手们 P/Invoke 声明是最有效的,实践是检验这些函数声明是否正确的标准,最主要是这些 Win’s 的 API 太多太多了;其间我们会看到一些有关的技巧和信息,这些都是关于 COM 和 dotNET 交互的,我认为这些是我们应用 dotNET 过程中更多会遇到的问题,特别是迁移应用和切换开发环境时一定会遇到的。
第一个问题,就是如何向 COM 展示 dotNET 的组件,这多是我们希望以前的 VB , ASP 或其他的语言能够直接使用 dotNET 组件和其带来的好处。这里必须抱怨一些 dotNET Beta2 的作品或文章,如果今天你还是按照上面所写的去做,往往会听到这样的疑问:为什么我写的 dotNET 组件在 VB 的对象浏览器中看不到一个方法?
往往他们看到的不是下面这样一副图,而是左边有类名,但右边没有任何成员函数可以调用。

附带文件中的 COMVisible 项目中,你会看到整个的代码过程,这里我们只是讲述一些容易忽视的地方,下面是一个可能的 Checklist :
1. 是否引用了 System.Runtime.InteropServices 和 System.Reflection
2. 组件是否强名,如果没有最好先用 sn - k 将组件强名。
3. 希望暴露的 Class 和 Interface 是否都已经使用了 ProgId 属性。
4. 希望暴露的 Class 和 Interface 是否都已经使用了 GuidAttribute 属性和产生了固定的 GUID 。
5. 是否使用了 ClassInterfaceAttribute 属性和设置了正确的值。
6. 是否在 VS.NET 进行正确的设置或是用手工的方式进行注册了 。
7. 是否已经产生了后绑定需要的 Tlb 文件,如果已有,查看一下它的内容是否符合你的需要。
对于 1 至 4 点我的建议是最好一定都有,尽管有些它们不是必须的;对于 5 点是最重要也是最容易忽视的。
ClassInterfaceAttribute 有三种类型参数:
** AutoDispatch **
|
指示 Class 仅仅支持后绑定的 COM 消费者,当 COM 消费者请求时会自动产生一个 dispinterface ,但这个接口的类型信息将不在 Tlbexp 产生的类型库中进行描述。这是 ClassInterfaceAttribute 缺省的选项。
---|---
** AutoDual **
|
指示将自动的产生一个双接口类并且暴露给 COM ,类型库信息中将包括类接口的信息,但这个选项不是一个建议的选项。 Microsoft 警告你要小心版本兼容问题。
** None **
|
指示不为类产生任何类接口。这是一个建议的选项。
如果这样的描述不是很清楚,看到下面我分别用这三个参数产生的 Tlb 文件描述也许你就清楚了。
** AutoDual ** 参数产生的类型库描述:
coclass dotNETClass {
[default] interface _dotNETClass;
interface _Object;
};
[
odl,
uuid(1EA7C0BF-0562-3D1E-A6C3-89CF63F040F6),
hidden,
dual,
nonextensible,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, COMVisible.dotNETClass)
]
interface _dotNETClass : IDispatch {
[id(00000000), propget,
custom(54FC8F55-38DE-4703-9C4E-250351302B1C, 1)]
HRESULT ToString([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT Equals(
[in] VARIANT obj,
[out, retval] VARIANT_BOOL* pRetVal);
[id(0x60020002)]
HRESULT GetHashCode([out, retval] long* pRetVal);
[id(0x60020003)]
HRESULT GetType([out, retval] _Type** pRetVal);
[id(0x60020004)]
HRESULT Add(
[in] long x,
[in] long y,
[out, retval] long* pRetVal);
[id(0x60020005)]
HRESULT AddEx(
[in] long x,
[in] long y,
[in] long z,
[out, retval] long* pRetVal);
[id(0x60020006)]
HRESULT Hello([out, retval] BSTR* pRetVal);
};
特别:
本文原创, CSDN 署名首发,所有文字和图片版权所有。未经授权请勿传播、转载或改编。
如果有问题或建议,请发电子邮件给 [email protected]