我偶尔会觉得有必要把COM的互操作和PInvoke结合起来。在某些场景中,用PInvoke的声明和方法会更容易编码一些。在这些场景中包括进COM对象,并且在签名上加上合适的Marshal标记也是合法的。
最简单的完成这些场景的方法是有本地的签名只暴露IUnkown实例。在托管代码这边,用一个对象声明并且标记上MarshalAs(UnmanagedType.IUnknown)。例如:
[DllImport("SomeDll.dll")]
[return: MarshalAs(UnmanagedType.IUnknown)]
public static extern object GetSomeComObject();
有一条需要记住,在这种场景中怎么处理ref这个关键字。在任何情况下,如果一个COM对象被当作来自于PInvoke的签名,CLR会假定他会去调用IUKnown::Release()。 相对的本地代码必须考虑这种情况,适当的对这个对象AddRef()。
这已经包括了任何的场景,像上面的代码,COM对象返回的值是function [1].
一言以蔽之:不会,任何情况都不回被截短。
我和同事前几天聊到一个有趣的案例,它涉及到IntPtr, Pinvoke以及64bit整体概念的准确性。 最终这场讨论把我们引向对IntPtr处理long类型的构造函数。令我吃惊的是,这个构造函数的是这么写的:
问题在于long类型的值被肆意地截短成一个int值。这将直接导致丢失一切越过4G的内存地址 (换言之,没有64bit的寻址)。这个确确实实放在眼前的大漏洞,使我想到这是不是有可能是个反编译器(disassembler)的bug。于是我用了.NET Reflector的IL模式。
L_0000: ldarg.0
L_0001: ldarg.1
L_0002: conv.ovf.i4
L_0003: conv.i
L_0004: stfld void* System.IntPtr::m_value
L_0009: ret
这段IL证实了long值确确实实是被截短了(还在开始处做了一个溢出检测)。但是且慢,mscorlib.dll是个与处理器运行模式紧密相关的特殊的DLL,这一切有可能仅仅是32位操作系统惹得祸。于是我切换到一台64bit的机器,重新打开Reflector,令我气恼的是面对我的依旧是惊人相同的代码。
几分钟后我想到打开任务管理器(task manager)看看,竟然看到reflector是个运行在WoW64的进程。这意味着reflector还是调用了32位版本的mscorlib.dll。顺利成章地,我用ildasm打开了64位的mscorlib,同时看到在64位模式,内存地址将不再被截短。
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: conv.u
IL_0003: stfld void* System.IntPtr::m_value
IL_0008: ret
conv.u 是一个非托管的unsigned int的惯用方法,在64位平台上,conv.u将是一个unsigned 8 字节数值 (详见:OpCodes.Conv_U)
概括一下,对于开发人员来说,IntPtr(long)的在不同的平台上做着完全正确的事,实现因平台不同而异。在32位操作系统上,如果一个非4GB的内存地址被当做参数传入的话,实现代码将(正确地)抛出一个异常。在64位的世界,实现代码将按照程序员的意愿返回一个正确的地址,不去截短任何值。