让人混淆的UnmanagedType.LPStruct列集指令(翻译)
原始文献地址:
http://blogs.msdn.com/adam_nathan/archive/2003/04/23/56635.aspx
在列集有多种非托管表现形式的托管数据类型时,MarshalAsAttribute属性可以改变列集(Marshaling)行为。一般来说,使用MarshalAsAttribute的难点是如何在它的构造函数里面选择正确的UnmanagedType枚举值。我看到过的最经常被误用的UnmanagedType枚举值应该是LPStruct了。我不责怪这些使用错误的程序员,因为它的名字就是极其误导用户。我可以肯定地说如果不是出于向后兼容的考虑的话,我们早就将它的名字改掉了。
作为一个为程序员提供技术支持的人,我想在这里澄清(LPStruct)这个列集指令的意思,这样你可以正确使用它,或者就是(最好)避免使用它。
UnmangedType.LPStruct只能够支持一个特殊的例子:将值类型System.Guid列集成非托管结构体GUID的指针。换句话说,这个指令告诉.NET的列集器,在将System.Guid从托管代码列集到非托管一端时,增加一层额外的指针封装,而在将GUID结构从非托管一端列集到托管一端时,去掉这层额外的指针封装。
你可以通过将下面的C#接口定义导出到类型库的时候看到上面描述的行为:
public interface IUseGuids { void ByValGuid(System.Guid g); void ByRefGuid(ref System.Guid g); void ByValGuidWithLPStruct( [MarshalAs(UnmanagedType.LPStruct)] System.Guid g); void ByRefGuidWithLPStruct( [MarshalAs(UnmanagedType.LPStruct)] ref System.Guid g); } |
结果是(使用OLEVIEW.EXE查看输出的类型库文件):
interface IUseGuids : IDispatch { [id(0x60020000)] HRESULT ByValGuid([in] GUID g); [id(0x60020001)] HRESULT ByRefGuid([in, out] GUID* g); [id(0x60020002)] HRESULT ByValGuidWithLPStruct([in, out] GUID* g); [id(0x60020003)] HRESULT ByRefGuidWithLPStruct([in, out] GUID** g); }; |
请注意导出类型库程序(tlbexp.exe)是一个很好的工具,它知道托管代码里面的参数、类成员和返回值是如何列集的,因为它创建的(函数和接口)原型要完全匹配CLR列集器的行为的。即使是在生成P/Invoke函数声明的时候,如果你不知道如何确定参数的正确类型,你可以根据这个技巧来将函数暂时放到一个接口定义里面(删掉”static”和”extern”等关键字),然后运行tlbexp.exe来生成正确的函数原型。
在什么时候你需要用到UnmanagedType.LPStruct?当你要调用接受一个[in]GUID *的参数的非托管API,例如CoCreateInstanceEx时,(你需要用到这个列集指令),MSDN对CoCreateInstanceEx的定义是:
HRESULT CoCreateInstanceEx( REFCLSID rclsid, IUnknown* punkOuter, DWORD dwClsCtx, COSERVERINFO* pServerInfo, ULONG cmq, MULTI_QI* pResults ); |
而第一个参数的说明是:
rclsid
[in] CLSID of the object to be created.
因此,使用UnmanagedType.LPStruct,你可以定义一个象下面这样的C# P/Invoke函数原型:
[DllImport("ole32.dll", PreserveSig=false)]
static extern void CoCreateInstanceEx(
[MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
[MarshalAs(UnmanagedType.IUnknown)] object punkOuter,
uint dwClsCtx,
[In] ref COSERVERINFO pServerInfo,
uint cmq,
[In, Out] MULTI_QI [] pResults
);
而不是得到下面这个原型:
[DllImport("ole32.dll", PreserveSig=false)]
static extern void CoCreateInstanceEx(
ref Guid rclsid,
[MarshalAs(UnmanagedType.IUnknown)] object punkOuter,
uint dwClsCtx,
[In] ref COSERVERINFO pServerInfo,
uint cmq,
[In, Out] MULTI_QI [] pResults
);
前者我们可以以这种方式调用:
Guid clsid = ...;
...
CoCreateInstanceEx(clsid, null, CLSCTX_REMOTE_SERVER, ref serverInfo, 1, results);
这种方法对于一些仁兄来说,要比使用下面的方法调用后者稍微简洁一些:
Guid clsid = ...;
...
CoCreateInstanceEx(ref clsid, null, CLSCTX_REMOTE_SERVER, ref serverInfo, 1, results);
特别是因为将Guid参数以by-ref的形式传递的方式会让调用方相信这个Guid实例有可能被改变—虽然实际上不会被改变。
当然啦,LPStruct的技巧只能对GUID *的参数有效,这是因为在非托管一方对GUID的修改都不会反映到以按值传递方式传递过来的System.Guid实例上。而使用[in]GUID *作为非托管API的输入参数是一个非常普遍的模式,即使这些API都不会修改GUID实例,从调用方传递GUID指针的方式还是比把整个结构放到堆栈上的方式效率高的多。
那么上面这一个故事的精髓在哪呢?你应该避免使用UnmanagedType.LPStruct,它带来的麻烦远比带来的那么一点小方便要多得多。