最近在尝试用新的方式来写一个新的 Windows 客户端,当作练习。主要是用 C++ 把复杂的算法写成链接库,然后用 C# 做界面前端,从 C# 调用用 C++ 写好的非托管代码。
尝试通过这种方式解决两个问题:
其实,这种架构可以看成是一种 C/S 架构的简化版,但是省去了 Socket 通信这一层。
我采用的是最简单的 P/Invokes 的方式来实现 C# 调用 C++ 链接库,详细的教程,可以看一下 Using dumpbin.exe as an Aid for Declaring P/Invokes 这篇文章。
我在刚开始从 C# 里链接 C++ 链接库的 API 时,想当然地以为就是函数名称。但是这样操作无论如何也调用不成功,需要在 C++ 链接库里添加一个 extern 关键字,否则链接库编译出来的 API 名称,是混淆过的,不方便你在 C# 里作为 EntryPoint 来书写。
比如说,有如下 C++ 链接库的 API 函数(建议链接库给外面调用的 API 最好用 C 风格来实现,方便减少头文件依赖关系):
extern "C" __declspec(dllexport) bool Function1(const char* param1, const char* param2, const char* param3);
翻译成 C# 函数则如下:
[DllImport("Example.dll", EntryPoint = "Function1", ExactSpelling = false] [return: MarshalAs(UnmanagedType.U1)] public static extern bool Function1([MarshalAs(UnmanagedType.LPStr)] String param1, [MarshalAs(UnmanagedType.LPStr)] String param2, [MarshalAs(UnmanagedType.LPStr)] String param3);
这里注意,找 EntryPoint 一定要准确,否则不容易找到。为了明确地找到 API 函数的 EntryPoint 名称,可以使用 Dumpbin.exe 工具。
dumpbin.exe 工具默认在以下目录:
C:\Program Files\Microsoft Visual Studio 9.0\VC\bin
如果从这个目录里运行 dumpbin.exe 会提示找不到动态链接库 mspdb80.dll 的错误,可以把 dumpbin.exe 拷贝到目录
C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE
,并从这个目录下运行 dumpbin.exe 来解决这个问题。
运行命令
dumpbin.exe /EXPORTS dllname.dll
后,你会看到很多 ? @ 混合在一起的名称,所以,为了使你在 C# 里的代码可读性比较强,需要改造这些名称。在链接库里,我们可以通过用 extern 关键字来标明,这样生成的链接库 EntryPoint 依然会是原始的名称。
如果按上述方法编写好了代码,一运行 C# 程序却提示说:
未处理的"System.DllNotFoundException"类型的异常出现在 example.exe 中。 其他信息: 无法加载 DLL"cppexample.dll": 找不到指定的模块。 (异常来自 HRESULT:0x8007007E)。
这个时候,你需要找一找你的 dll 是否在可执行目录下,或是你写的 dll 是否依赖于其它第三方dll,一定要确保所有的 dll 都能顺利被找到。
你的函数肯定有若干个参数,那这些参数应该和 C# 里的类型如何一一对应呢?在 C# 里,这种映射关系叫做 marshal。
类型映射,需要仔细检查一下手册,比如说,const char* 就应该这样映射:
[MarshalAs(UnmanagedType.LPStr)]
Using C++ Interop 文章的未尾,有列出一大串的类型映射列表。
初步用 C# 来写界面,感觉更方便、快速,起码比 MFC 来得简单、直接;从 C# 里直接调用 C++ 链接库,也很方便。但这两者结合起来写应用,稳定性还有待进一步测试。
在 C/C++ 多线程编程下,如果不注意,采用普通变量传递参数值给线程会有一些误区,需要特别小心。
下面浏忙绪绪就举两个例子来说明一下。
最近在用 Boost 库写多线程程序时,需要启动若干个线程,这些线程分别处理不同的事情,线程会获取一个字符串参数,用来标识内容。在编写代码的时候,出现了一个很怪异的现象,例子代码如下:
const int tNum = 4;//并发线程数 vector<boost::thread*> tBox; for (int i=0; i < tNum; i++ ) { char strThread[20]; sprintf(strThread, "thread%d",i); //strThread 字符串是传入的 const char* 类型 boost::thread* thread_0 = new boost::thread( StreamProcesser, param1, param2, strThread); tBox.push_back(thread_0); boost::xtime xt; boost::xtime_get(&xt, boost::TIME_UTC); xt.sec += 2; boost::thread::sleep(xt); } for (int i=0; i < tNum; i++ ) { boost::thread* thread_0 = tBox[i]; thread_0->join(); delete thread_0; }
在线程函数启动参数中,有一个参数是 const char* 类型。如果我在线程中,没有先对 char 字符串拷贝一个副本,则当4个线程都跑起来后,再去读取这个参数,很有可能会读到同一个字符串。
这是因为编译器把 strThread 的地址编码为同一个内存地址造成的,所以,所有的线程读取的都是最后一次设置 strThread 的值。
结论:
同样,如果传入参数是整型或是其它类型的时候,也会有上述类似的问题。拿 Win32 的 CreateThread 函数来说,同样需要保证传入的参数不被修改,例如下面的代码就非常危险:
DWORD WINAPI CloseThreadFun( LPVOID param) { int* pHandle = (int*)param; const int handle = *pHandle; //打印句柄 printf("get handle is %d", handle); return 0; } int _tmain(int argc, _TCHAR* argv[]) { const int THREAD_NUM = 4; HANDLE* lphandle = new HANDLE[THREAD_NUM]; for (int j = 0; j < THREAD_NUM; j++) { HANDLE hthread; hthread = CreateThread(NULL, 0, CloseThreadFun, (LPVOID)&j, 0, NULL); lphandle[j] = hthread; } WaitForMultipleObjects(THREAD_NUM, lphandle, TRUE, INFINITE); delete [] lphandle; return 0; }
在调用 CloseThreadFun 来启动一个线程后,j 的值很有可能已经被修改掉了:线程启动总是需要时间的,而参数指针指向地址的内容,很有可能在此期间被修改了。比如,上面的代码,运行后,打印的内容如下:
get handle is 3 get handle is 4 get handle is 4 get handle is 4
这就说明了 j 值被重复修改后,会导致线程参数不对的现象。
解决办法:用一个 int 数组把需要传入到各个线程的参数缓存起来,尽量保证地址不一样。
传入线程的参数,应该尽量采用动态分配内存的方式来生成。否则如果采用临时变量,则随着变量生命周期的消逝,该变量的指针,很有可能会变成一个毫无意义的指针(或是被新的值覆盖,或是被成为一个遗留数)。
采用动态分配的变量作为线程启动时的参数,在线程结束后再销毁这个动态分配的变量,则是一个安全法则。