我成了C#和C++程序员的翻译...

最近有点烦,客户要求我们用C#实现一个关于串口通讯的模块,这事儿本身没那么麻烦,只是他们给了数年前写的几千行的C++代码,要我们给改成C#版的... 我不爽,于是我说,用VC.NET吧(还不是VS2005不能用C++/CLI),Refactor后重新编译一下基本上就能给.NET用了(整个项目是基于.NET/C#的),我也干过这事儿,貌似不难。但是客户坚持不要额外的DLL,项目又是用C#写的,ILMerge工具也不能合并用VC.NET写的DLL,再加上市场部的哥们已经和客户说了,“我们什么软件都写得出来”,所以...

理论上是可行的

从头到尾看了他们的代码一遍之后,发现虽然是VC6的Project,但并没有用到多少MFC里面的类(用到的也就是CString之类的,无视),阿门,上帝保佑,阿弥陀佛,大部分都是在直接调用标准库和Windows API。所以我觉得最起码理论上可行,不就是P/Invoke嘛,把内存给我对齐就行了,所以说,“没问题”。

程序员都是过于乐观的一族,对于这个缺点,我好像特别严重。答应下来之后,放下电话,我怀着内疚的心情看了看项目组的反应,不由的心生胆怯。不久后PM走到跟前,语重心长地说,“Vista Xia(我的无敌的英文名),这个任务就交给你了。下一个Release的Dead Line就快到了,赶紧啊。”(我靠连个Assistant都没有)

这是一个用C#语言编写的C++项目

这事儿比较特别,完全违反了公司惯用的流程,没有需求文档,没有HL和DL的设计文档,也没有测试用例(待会儿再说为什么)。公司的SQA不知道管不管这事儿,要管的话项目估计又要从CMM5直接跌到CMM1了。总而言之,我只是写了个TODO List,就开始了:

  1. 在.h文件中找到所有的struct定义,先改成C#的。用Marshal.SizeOf得到其大小,初步进行测试:看看它和在C/C++程序中用sizeof得到的大小是否相等
  2. 对应所有C++的类,先写出所有C#的类的原型,包括成员字段和函数。所有函数里先放着throw new NotImplementedException()这么一句,省得影响编译
  3. 对应于C++类的析构函数,在C#用Dispose Pattern实现
  4. 找到所有用到的Windows API和其他Native API,在C#用static extern方法声明之,然后对不确定都否工作的那些(比如参数里头有n个*的那种)进行初步测试
  5. 在.h文件找到所有宏定义的常量,改成const的字段;而宏方法只能直接改成普通方法(C#没有inline关键字)。对于用宏改了名字的类型,专门记载到一个文档中,以后直接用文本替换(ctrl+h)搞定之
  6. 把.cpp文件的代码逐步copy到C#代码中,一个函数一个函数的改。基于C/C++标准库和语言本身特性的代码语句,一般都可在FCL和C#中找到对应的做法,这也包括指针操作;至于API的调用,前面已经准备好了
  7. 编译通过,测试一下

走火入魔

这个模块并不是要实现什么很复杂的功能,不过,仅仅是声明API和结构体的C#代码就一千多行了(还不算注释)。我想99%的.NET程序员都没做过这种事情 — 我认为愿意写这种代码的人一定有受虐倾向,除我之外

在Visual Studio,习惯了把所有代码写完,然后按F5就知道能不能跑了。然而我在这里忽略了一点,就是调试这个程序还需要相关硬件设备的配合(好像是客户开发的一些设备),需要连上我的机器安装,调试,才可运行。可客户在遥远的广东,公司里也不大可能有这种设备的,看来麻烦了

客户那边的程序员看起来比我还要乐观,他似乎觉得只要程序写对了就应该能跑,不需要调试的。那么除了让代码能通过编译,我能做的就是再review几遍代码而已了。他大概不知道,在C#作P/Invoke时,编译器能做的工作是很有限的:

  • 它不能检查API声明是否有效;也就是说,是否和API原型兼容
  • 托管struct被Marshal到本机struct的时候能否对齐内存,只有在运行时才会抛出一个毫无提示作用的异常(比如会莫名其妙的抛出空引用异常)
  • 有些托管struct,看起来很对,编译也能通过,但在运行时不给Marshal,我在前面的blog曾有提到。这是.NET 1.x的bug,2.0已经修好了
  • 对于指针类型的函数参数,在C#有好多好多种声明的方法,比如StringBuilder、string、byte[]、int、uint、IntPtr、ref xxx、out xxx,等等。只要保证Marshal时是一个32位的值就可以了(对于32位系统)。这视乎具体情况而定,编译器不管这个细节(事实上编译器也管不了这个),要程序员自己处理。有时候会有点复杂,比如数组的元素又是子数组时,你只能选择IntPtr或者IntPtr[],并且需要手工copy内存
  • .NET的字符串在内存中总是以Unicode方式编码的,但对于API函数需要string的编码,有时取决于Windows版本,有时需要显式设定,有时又需要显式转换。同样,编译器也管不了这个细节
  • 和C/C++编译器一样,这里也不会有越界检查和内存泄露等方面的保证

如履薄冰

且不说这些问题,就算编译器能帮上忙,对于这几千行几乎全部基于P/Invoke的代码,不测试就成功运行,也太NB了一点了吧。不过,这算是我的疏忽,早知本地无法测试(问题是客户也不早说),大概就不会答应这么做了,sigh。

不管怎么说,在反复review好几遍之后,我向VSS签入了代码,然后听天由命了(不过,我后来又给客户发了个mail说了一下risk,打个预防针先)。看来到客户现场调试已不可避免,嗯,不管了

怎么说P/Invoke呢

毫无疑问,这是一个非常重要的技术,事实上,FCL本身也大量应用了P/Invoke,它是CLR的几种基础技术之一。但在我看来,这并不代表微软推荐你也把这项技术作为你的应用程序的基础。适当使用就可以了,比如偶尔需要调用一两个API的时候。而像我手上的这个东西,显然应该用VC.NET的(现在的C++/CLI),实现起来要容易得多,也可靠得多。

BTW,
不知道是去年还是前年,有人居然用Javascript实现了Web上的《星际争霸》,着实很牛,但你不觉得它也属于走火入魔之列吗?现在流行用牛刀杀鸡,但不要用杀鸡刀宰牛啊!