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

走火入魔
这个模块并不是要实现什么很复杂的功能,不过,仅仅是声明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上的《星际争霸》,着实很牛,但你不觉得它也属于走火入魔之列吗?现在流行用牛刀杀鸡,但不要用杀鸡刀宰牛啊!
在我前面的这个post,曾谈到AsyncOperation类的一个令我着迷的功能,那就是它可以把一个方法提交到另一个线程上执行。它也是实现BackgroundWorker的关键组件。
However,前几天在一个Console程序再次试验这个类时,发现了一个不同的行为,就是.Post方法提交的delegate运行于哪个线程是不确定的,具体的说:
- 在Windows程序,如果AsyncOperation类是UI线程创建的,那么delegate将在UI线程执行(这个符合预期)
- 在Windows程序,如果AsyncOperation类是非UI线程创建的,那么delegate将在一个“随机”的来自线程池的Background线程上执行 — 而不一定是创建AsyncOperation实例的那个非UI线程
- 在Console程序,同第二条
也就是说,运行于其它线程的AsyncOperation.Post方法并不总是把delegate提交到AsyncOperation实例的创建者线程。对应到BackgroundWorker类的ReportProgress方法,同样如此。按照FCL的设计,应该是这样的。这是其一。
其二,假如说AsyncOperation和BackgroundWorker类专门针对Windows程序而设计,那么第2、3两条应该归为我的应用程序的设计问题。但是,有人在这里提到,即便是Windows程序(满足上面的第一条),也不能保证提交的delegate一定在UI线程执行!
这里不是讨论线程并发冲突的问题,而是BackgroundWorker和AsyncOperation类是否能真正简化Windows程序中多线程的应用。这里提到的问题,我自己暂时还没碰到。但如果属实,那我估计会放弃AsyncOperation和BackgroundWorker类,因为这相对于原先的猜测:“这个类只是一个线程的简单包装,用多线程模拟了异步调用而已”,其结果没有什么两样。
我不反对这两个类的使用,只是在ReportProgress时,也要用Control.Invoke来更新UI控件,防止RPWT
。
更深的原因是,AsyncOperation.Post方法的实现是未知的,我曾经想用Reflector找出答案,如上个post所述,未果。我不知道它究竟把delegate提交到了哪里。按照MSDN的说法:“Invokes a delegate on the thread or context appropriate for the application model.” — 轻描淡写。
在.NET 1.x的Threading ≈ Thread + ThreadPool + lock(syncRoot) + WaitHandle + Control.Invoke,我觉得基本上就是这套东西了;而在.NET 2.0,现在看来AsyncOperation.Post()这种新奇的做法最好还是慎用~
----- update on 4/4/2006
看来我的担心变成了现实,这里有人重现了同样的错误。
昨天在尝试使用System.ComponentModel.BackgroundWorker时,发现这个类的行为和我预料的大不一样,可以说是惊喜。原来以为这个类只是一个线程的简单包装,用多线程模拟了异步调用而已;但是看下面的这段代码:
Thread.CurrentThread.Name = "Main Thread";
backgroundWorker1.RunWorkerAsync();
...
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int i = 0;
while (i++ < 100)
{
backgroundWorker1.ReportProgress(i);
Thread.Sleep(50);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.Text = e.ProgressPercentage + "% - " + Thread.CurrentThread.Name;
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.Text = "DONE - " + Thread.CurrentThread.Name;
}
毫无疑问,_DoWork方法是运行在另一个不同线程之上的(很容易验证这一点,这也符合BackgroundWorker的设计),而这个方法又调用了backgroundWorker1.ReportProgress方法,触发了ProgressChanged事件。在通常的异步实现,_ProgressChanged方法应该运行于事件触发者相同的线程中;但在这里,它运行于主线程(名为Main Thread的线程)。_RunWorkerCompleted方法也是一样。
在我看来,这个行为非常特别,实际上它也非常有用。这样_ProgressChanged方法体中操作UI控件的代码都无需使用Control.Invoke包装了,让程序的编写大为简化。而我真正感兴趣的是这个类究竟是怎么实现的,我用Reflector打开它的源码之后,原来关键在于它用到了一个名为AsyncOperation的类(System.ComponentModel.AsyncOperation)。
AsyncOperation类有个Post方法,可以用来把一个委托(作为方法指针/列表)提交给另一个线程执行。继续反编译下去,又查到了System.Threading.SynchronizationContext类。不过具体怎么实现是无从得知了,因为追踪到最后,停在了一个[MethodImpl(MethodImplOptions.InternalCall)]的方法
,它由CLR本身实现。(我个人猜测,其中很可能利用了Windows API:Get/SetThreadContext,和结构体CONTEXT,改掉了线程上下文。)
退一步说,它怎么实现的并不是那么重要,重要的是我们可以用这个AsyncOperation类实现自己的BackgroundWorker
。这里是我写的和上面代码基本等价的实现:
AsyncOperation asyncOperation;
SendOrPostCallback progressReporter;
Thread workerThread;
public MainForm()
{
InitializeComponent();
asyncOperation = AsyncOperationManager.CreateOperation(null);
progressReporter = new SendOrPostCallback(ReportProgress);
workerThread = new Thread(new ThreadStart(WorkOnWorkerThread));
}
private void MainForm_Load(object sender, EventArgs e)
{
Thread.CurrentThread.Name = "Main Thread";
workerThread.Name = "Worker Thread";
workerThread.IsBackground = true;
workerThread.Start();
}
void ReportProgress(object obj)
{
this.Text = obj.ToString() + "% - " + Thread.CurrentThread.Name;
}
void WorkOnWorkerThread()
{
int i = 0;
while (i++ < 100)
{
asyncOperation.Post(progressReporter, i);
Thread.Sleep(50);
}
}