.NET的跨平台调用一例(PreserveSig)
这样一段代码:
.class public auto ansi Test extends [mscorlib]System.Object
{
.method public static pinvokeimpl("msvcrt.dll" cdecl)int32 sscanf(string, string, int32&) cil managed { }
.field public static int32 val
.method public static void Main() cil managed
{
.entrypoint
.locals init (int32 n)
ldstr "Enter a number"
call void [mscorlib]System.Console::WriteLine(string)
call string [mscorlib]System.Console::ReadLine()
ldstr "%d"
ldsflda int32 Test::val
call int32 sscanf(string, string, int32&)
stloc.0
ldloc.0
brfalse.s Error
// elide for clarity
}
}
基本功能是从控制台取得用户输入的数字,调用 C 的运行库函数 sscanf 将字符串形式的数字转换为数值,并根据 sscanf 的返回值判断字符串是否合法的数字字符串。
奇怪的是,sscanf 总是返回 0 !
查 MSDN,关于返回值,说明如下:
intsscanf(const char* buffer, const char* format [, argument]…);
[Thefunction] returns the number of fields successfully converted and assigned; thereturn value does not include fields that were read but not assigned. A returnvalue of 0 indicates that no fields were assigned.
该函数返回成功转换的字段数,返回值不包含已读取但没有赋值的字段。返回 0 表示没有字段被赋值。也就是说,转换不成功!
难道这段 IL 代码有问题?为了验证这个疑问,又写了一小段等价的 C# 代码:
usingSystem;
usingSystem.Runtime.InteropServices;
publicstatic class Test
{
[DllImport("msvcrt.dll", CharSet=CharSet.Ansi,CallingConvention=CallingConvention.Cdecl)]
public static extern int sscanf(string, string, out int nval);
public static void Main()
{
int n = 0;
int val = 0;
Console.WriteLine("Enter a number");
string line = Console.ReadLine();
n = sscanf(line, "%d", out val);
Console.WriteLine("n = " + n + ", val = " +val);
}
}
编译运行,n = 1,正确!奇怪!
将生成的 exe 程序反编译为 IL 代码,其中 sscanf 函数的声明如下:
.methodpublic hidebysig static pinvokeimpl("msvcrt.dll" ansi cdecl)
int32 sscanf(string sval,
string fmt,
[out] int32& nval) cilmanaged preservesig
{ }
将这个声明与上面的 sscanf 的声明对比一下,发现这里多了一个 preservesig 特性。修改上述 IL 代码的 sscanf 声明,增加 preservesig,编译运行,正确!
MSDN 对 DllImportAttribute.PreserveSig 字段说明如下:
Indicateswhether unmanaged methods that have HRESULT or retval return values aredirectly translated or whether HRESULT or rettval return values areautomatically converted to exceptions.
Set thePreserveSig field to true to directly translate unmanaged signatures withHRESULT or retval values; set it to false to automatically convert HRESULT orretval values to exceptions. By default, the PreserveSig field is true.
也就是说,对于有返回值(不管是HRESULT 还是其他返回值)的非托管代码,CLR对返回值的处理有两种方式:或者直接返回给调用者,或者将 HRESULT 或其他返回值转换为异常抛出(如果返回值不是 S_OK 的话)。到底取何种处理方式,取决于 PreserveSig 的设置。如果 PreserveSig 设置为 true,则直接返回给调用者(这种方式保留了非托管方法的签名,这也是 Preserve Signature 的本义)。如果 PreserveSig 设置为 false,则将返回值(HRESULT 或其他返回值)转换为异常抛出(如果返回值不等于 S_OK),此时,如果没有异常抛出,则调用者也得不到返回值(因为没有返回)!
这就是为什么上述的 IL 代码中的 sscanf 函数总是返回 0 的原因。C# 中的 DllImportAttribute.PreserveSig 默认为 true,而在 IL 代码中必须显式指定 preservesig。