June 2006

给.Net程序员的PInvoke Tips [2]: Are Strings Immutable?

早在Java到来之际,程序员们都已逐渐接受并乐于接受String的这一特性:immutable。从C/C++转到.Net/C#的程序员们,在最初可能非常不适应把char[]和string分开对待,但是一旦习惯了就会觉得非常方便,尤其是直接以+进行连接,以及支持switch…case等。

这个时候问题来了,string真的是immutable的吗?

cbrumme的blog上给出了一个例子:

using System;
using System.Runtime.InteropServices;
 
public class Class1
{
    static void Main(string[] args)
    {
        String computerName = "strings are always immutable";
        String otherString = "strings are always immutable";
 
        int len = computerName.Length;
        GetComputerName(computerName, ref len);
 
        Console.WriteLine(otherString);
    }
 
    [DllImport("kernel32", CharSet=CharSet.Unicode)]
    static extern bool GetComputerName(
        [MarshalAs (UnmanagedType.LPWStr)] string name,
        ref int len);
}

该程序的执行结果也许正在你的预料之中,输出的是类似MYCOMPUTERNAMElways immutable之类的字符串,也就是说原字符串的前面一部分被计算机名覆盖掉了。

对上面的程序,我们可以做出如下分析:
1,computerName和otherString 的文本相同,因此由于编译器的Interning的结果,二者其实指向同一个字符串,用Object.ReferenceEquals()可以验证其相等。
2,红色部分标出的Marshal指令,使得该string被marshal为一个unmanaged pointer(LPWSTR)传递给了GetComputerName函数;
3,GetComputerName函数直接改写了computerName指向的缓冲区,string的immutable特性即被破坏。

由此我们可以看到,在与Unamanaged代码进行交互操作时必须额外小心,因为从某种意义上来说Unmanaged代码权限更大,破坏力也就更大,也就更容易引起意想不到的问题。

那么,上面那段使用GetComputerName的代码中,对该函数的包装要如何改进呢?

首先,在使用一个API之前应该注意其各个参数的in, out性质,例如关于GetComputerName,MSDN上有如下一段:

BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize);
lpBuffer : [out] Pointer to a buffer that receives a null-terminated string containing the computer name or the cluster virtual server name. The buffer size should be large enough to contain MAX_COMPUTERNAME_LENGTH + 1 characters.

很显然,lpBuffer应该是用来输出的缓冲区,因此不应该用string,而是用byte[],StringBuilder之类的类型与之对应;即便一定要用String,也绝对不能Marshal为LPWSTR/LPTSTR,而是Marshal为VBByRefStr,以确保Managed代码侧string的immutable性质。

此外,使用.Net中unsafe代码也可以打破String的immutable,这和使用Unmanaged代码的本质是相同的。

Throw an Exception Which is NOT an Exception

在C#中,用什么语句可以捕获所有的异常?——对于这个问题,很多人可能很习惯地用写出下面的程序:

try {
    //Some code
}
catch (System.Exception ex) {
    System.Console.Write("Error!");
}

这条语句的捕获对象是“System.Exception”。由于.Net中所有的异常必须直接或间接继承自这个类,因此理论上讲似乎是的确可以捕获所有异常的。但是仔细想想,真的没有办法抛出一个不是继承自System.Exception的对象吗?如果答案是可以的,那么显然这种异常就无法由上面的语句捕获。

抛出不是异常的异常……这种想起来都觉得不可能的事情,可能吗?事实上,关于这个问题.Net 1.x 和 2.0有一些细微差别,下面分开考虑。

■在.Net 1.x当中,抛出不继承自System.Exception的对象是可能的

例如,在C++当中,我们可以用throw "Error!"这样的语句,将一个字符串作为“异常”抛出;在IL当中,我们甚至可以用下面的方式,将任何对象作为异常抛出:

.assembly ThrowerLib { } .class public Thrower {
    .method static public void Start( ) {
        ldstr "Oops"
        throw
        ret
    }
}

因此,在.Net 1.x当中,经常使用下面这种最保险的方式:

try
{
    //Some code
}
catch(System.Exception ex)
{
    System.Console.WriteLine("System.Exception error: " + ex.Message);
}
catch
{
    System.Console.WriteLine("Non System.Exception based error.);
}

其中第二个catch的效果就是,拦截“不是Exception及其子类的异常”。

■在.Net 2.0中,默认所有异常都必须继承自System.Exception;亦可和1.x相同方式运作

在.Net2.0当中,为了确保跨语言的兼容性(在C#下无法throw一个字符串,在C++下则可以),CLR会自动将不是继承自System.Exception的异常包裹在RuntimeWrappedException对象中——例如,在C++中throw一个字符串,该字符串将被包裹起来,实际上抛出的则是一个RuntimeWrappedException。这样的结果就是,所有语言抛出的异常将都是System.Exception的子类了。

但是同时,为了保证和1.x版本的兼容性,.Net 2.0提供了RuntimeCompatibilityAttribute类,指定CLR不要对异常进行包装:

[assembly:System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows = false)]

 

 附:测试用程序

  1. [assembly: System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows = false)]
  2.  
  3. namespace ThrowerExample
  4. {
  5.     class ThrowerHarness
  6.     {
  7.         static void Main(string[] args)
  8.         {
  9.             try
  10.             {
  11.                 Thrower.ThrowException();
  12.             }
  13.             catch (System.Exception ex)
  14.             {
  15.                 System.Console.WriteLine("System.Exception error: " + ex.Message);
  16.             }
  17.             catch
  18.             {
  19.                 System.Console.WriteLine("Non System.Exception based error.");
  20.             }
  21.  
  22.             try
  23.             {
  24.                 Thrower.ThrowString();
  25.             }
  26.             catch (System.Exception ex)
  27.             {
  28.                 System.Console.WriteLine("System.Exception error: " + ex.Message);
  29.             }
  30.             catch
  31.             {
  32.                 System.Console.WriteLine("Non System.Exception based error.");
  33.             }
  34.         }
  35.     }
  36. }

执行结果是,第一个异常将被catch(System.Exception ex){}捕获;第二个异常由于不是System.Exception的子类,将落到catch{}中。

但是如果把第一行的属性去掉,编译时将出现下面的警告:

warning CS1058: A previous catch clause already catches all exceptions. All non-exceptions thrown will be wrapped in a System.Runtime.CompilerServices.RuntimeWrappedException

执行的话,两个异常都将被catch(System.Exception ex){}捕获,看消息可知第二个异常的类型为RuntimeWrappedException。

给.Net程序员的PInvoke Tips [1]: String is Sometimes an Integer

习惯了.Net编程,尤其是以前几乎没有用过Basic,Pascal,C/C++等“较古老”的语言的程序员,遇到PInvoke,尤其是COM interoperation的时候,往往是一头雾水不得要领。相信在在将来,一方面是从C#,.Net以及Java开始学习编程的人会越来越多,一方面整个Windows也逐渐往Managed平台迁移,懂得如何直接同Win32API打交道的程序员就会越来越少(当然绝对数量肯定还是很多的,至少比用DDK写驱动程序的多吧……)

但是,对于.Net程序员来说,虽然不用直接使用C++调用API,但是PInvoke的知识还是必不可少的,对于Handle,Unmanaged Thread,COM等等最基本的东西仍然是必修课。

前一段写程序时遇到这么一个PInvoke的实际例子:

某个C#写的WindowsForm中,要对资源DLL进行读取(Resource DLL,Win32格式的只含有资源的DLL)。其中要用到这么一个API函数:

BOOL EnumResourceTypes(
    HMODULE hModule,
    ENUMRESTYPEPROC lpEnumFunc,
    LONG_PTR lParam
);

其中,ENUMRESTYPEPROC,其定义为:

BOOL CALLBACK EnumResTypeProc(
    HMODULE hModule,
    LPTSTR lpszType,
    LONG_PTR lParam
);

其中的LPTSTR,在C#里应该怎么写呢?我当时想也没想,既然是LPTSTR (Long Pointer to String),那肯定就是字符串了。于是就有了:

public delegate bool EnumResTypeProc(IntPtr hModule, string strType, IntPtr param);

结果,这段代码在执行的时候出错了——用Marshal.GetLastWin32Error()查看,发现是ERROR_NOACCESS(998)。说明,程序访问了禁止访问的地址空间。经测试,其他几个参数:hModule是正确无误的DLL库的Handle,最后的参数也被设置为NULL,那么原因只能是这个strType了。

再回过头来仔细想想……该参数的类型是LPTSTR,在Unicode环境下也即LPWSTR,也即WCHAR * ——没错啊,不就是字符串么?

但是我们仔细看一下MSDN的说明,就会发现,Windows提供了一些标准的资源类型;例如RT_ICON,RT_STRING,RT_VERSION,……仔细看这些类型的定义就会发现它们都是数字。

查阅了相关资料才明白,lpszType这个参数,在值小于某个数值(例如256)的情况下应该被当作数字来处理;而在大于这个数值时,则作为指针,其值是内存中的地址值,也即应该将其当作指针处理。这种情况下,显然就不能用.Net的String类型来一概而论。正确的做法是,使用IntPtr,如果判断其值不是在256以下,再将其Marshal为指向字符串的指针,获取字符串。

结论:在.Net中,数据类型是“强类型”的,不同类型之间无法被强制转换,因此保证了类型安全;但是在C/C++等语言为基础的Win32中,类型却是可以任意变换的,各种Object“一切皆指针”,你可以把一个int* 当作char*,可以把HGDI当作HMODULE……没有人来保证这种变换的安全性,但是也正因此带来了灵活性。Windows的各种数据类型当中,很多类型都是名称不同,但是实际上的数据结构是相同的(例如关于Handle,就有HWND,HMENU,HINSTANCE,HHOOK等等许多种)。相反,同一个数据类型,时而代表数值,时而代表指针的例子也不少见。因此在处理Win32API的时候,必须特别注意这些特点。