最近在尝试用新的方式来写一个新的 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++ 链接库,也很方便。但这两者结合起来写应用,稳定性还有待进一步测试。
在调试 Visual Studio 2008 程序时,经常有一些动态链接库(即 dll 文件)需要加载到工程里,这样才能依赖第三方库进行程序调试。
这些动态链接库,往往都是测试版本或是开发中的版本,或者会有若干个版本;这个时候,如果直接把 dll 所在目录加到 PATH 里,则会有潜在冲突的危险;如果直接拷贝到 Visual Studio 的目录下,假如测试工程太多,每次有新版本的动态链接库更新时,你需要更新若干次,拷贝、粘贴苦不堪言。
在开发过程中,究竟怎样来让 Visual Studio 链接这些 lib 及 dll 文件会比较好呢?
总体上来说,有几种方法可以改变 Visual Studio 的环境变量设置:
这个方法最简单,也最直接,但是坏处是会影响全局的 PATH 设置,尤其是你包含着大量测试用的 dll 时。
通过 Visual Studio 菜单 ==> 工具 ==> 选项 ==> 项目和解决方案 ==> VC++目录,在下拉框里选择”可执行文件”,然后把 dll 所在路径添加进去。
这个方法也很简单,但是当你有若干个工程时,你每次更新 SDK 及其 dll 文件,你就要把所有的工程都更新,这个不符合文件唯一性的工程性准则。
在 Visual Studio ==> Project ==> Properties ==> Select Configuration ==> Configuration Properties ==> Debugging ==> Working directory 里填上 dll 所在目录,这样当在调试程序时,Visual Studio 会把当前工作目录切换到这个目录下,从而会自动读取本目录下的 dll 文件。
这个方法的优点很明显,简单!副作用也很明显,在你切换了当前工作目录后,你可能会找不到程序的配置文件,在程序里写的诸如”./config.ini”全部都找不到了;另外,你要把所有的 dll 都放到这个工作目录里,否则一样会提示说找不到 xxx.dll 的问题。
MSDN 上也有类似的介绍:How to: Set Environment Variables for Projects,方法很简单,在 “工程属性” ==> “调试” ==> “环境”里,添加类似如下所示的内容:
PATH=%PATH%;$(TargetDir)\DLLS
这样就可以把 $(TargetDir)\DLLS 临时添加到该工程所属的系统 PATH 里。
大家可以根据项目的实际情况,灵活选用以上方法。
注:本文撰写时参考了 StackOverflow 上的讨论话题:How do I set a path in visual studio?
在 Visual Studio 2008 MFC 工程中,利用 Berkeley DB 来构建数据存储引擎时,在编译 db.h 文件时出现编译错误,错误提示内容如下:
错误 3 error C2143: 语法错误 : 缺少"}"(在"("的前面) e:\water\berkeleydb\include\db.h 1226
微软的 MSDN 上有对 error C2143 的编译器错误进行解释,不过基本上没有太多可读性、可借鉴性,大意应该是一些宏定义、命名出错等。
最后,还是通过万能的 Google 大神找到了解答方法。错误的原因是 DB_TYPE, DB_UNKNOWN 类型已经在 MFC 系统头文件中被定义过,解决办法之一是在 db.h 中定义 DB_TYPE, DB_UNKNOWN 的语句之前加上如下语句即可:
#ifdef DB_UNKNOWN #undef DB_UNKNOWN #endif #ifdef DBTYPE #undef DBTYPE #else #define DBTYPE BDBTYPE #endif
看来,C 和 C++ 混在一块,命名、类型定义真是一个大问题。以后碰到类似的问题,也可以采用类似的解决办法。
参考资料:
在写客户端的时候,经常要传输一些文件,有一些服务器就是用 FTP 来搭建的,这个时候,如何用客户端来发起 FTP 网络连接呢?
如果是用 MFC 来发起 FTP 文件传输请求,和使用 MFC 来发起 HTTP 请求类似,非常简单。理论基础可以仔细阅读 MSDN 上的官方文档 Steps in a Typical FTP Client Application,浏忙绪绪在这里,就贴上我自己写的代码,与大家分享一下。
样例代码如下:
/* * 通过 ftp 方式来拿文件 * @param relativePath, 服务器保存该视频摘要文件夹、相对于 ftp 跟路径的相对路径 * @notice relativePath 应该是类似于 /test 这样相对于 ftp 根路径的相对路径 * 应该以 / 开头 * @param savePath, 从远程服务器下载文件后,在本地保存的文件夹路径 */ BOOL CGetRemoteFile::GetFtpfiles(CString relativePath, CString savePath) { //通过 http GET 协议来获取并保存文件 CString remotefile = L""; CString saveFilename = L""; CInternetSession session; session.SetOption(INTERNET_OPTION_CONNECT_TIMEOUT, 1000 * 20); session.SetOption(INTERNET_OPTION_CONNECT_BACKOFF, 1000); session.SetOption(INTERNET_OPTION_CONNECT_RETRIES, 1); /* 替换成相应的 FTP 用户名和密码 */ CFtpConnection* pFtp = session.GetFtpConnection( m_strServerIP, m_strUsername, m_strPassword, (INTERNET_PORT)m_iSeverPort); bool result = pFtp->SetCurrentDirectory(relativePath); if (!result) { AfxMessageBox(L"Can't get ftp file"); return false; } CFtpFileFind finder(pFtp); BOOL bFind = finder.FindFile( L"*", INTERNET_FLAG_RELOAD ); while (bFind) { bFind = finder.FindNextFile(); CString strPath = (LPCTSTR)finder.GetFileURL(); CString strFile = (LPCTSTR)finder.GetFileName(); CString ftpPath; ftpPath.Format(L"正在下载文件:%s/%s\n", strPath, strFile); TRACE(ftpPath); CInternetFile* pFile = pFtp->OpenFile(strFile);//注意,这里只需要传文件名 int len = pFile->GetLength(); char buf[2000]; int numread; CString filepath; filepath.Format(L"%s\\%s", savePath, strFile); TRACE(L"保存为文件%s",filepath); CFile myfile( filepath, CFile::modeCreate|CFile::modeWrite|CFile::typeBinary); while ((numread = pFile->Read(buf,sizeof(buf))) > 0) { strFile += buf; myfile.Write(buf, numread); } myfile.Close(); pFile->Close(); delete pFile; } session.Close(); return true; }
最近被一个问题折磨了好几天,VC++2008 编译出来的 Debug 版本程序,拷到目标机器上,没有办法运行。我用 VC++2008 编译的文件在自己电脑上可以运行,可一放到别人电脑上就显示程序配置有问题,试了几台电脑都这样,拿到另一台装了 VC++2008 的电脑上又正常了。以前用 VC++6.0 的时候没这么多事,这是怎么回事呢?
类似这种情况下,按理说应该是少了某个动态链接库,但是我确定第三方的动态链接库都拷贝到目标机器上了。
在目标机器上安装 Microsoft Visual C++ 2008 Redistributable Package (x86) 后,程序依然不能运行。但是如果我装给目标机器装上 VS2008 ,程序就可以顺利执行。
这个问题和 Debug 版本有关吗?还是 VS2008 的问题?编译成 Release 版本能不能解决这个问题呢?
我用静态编译的方式,编译出的 release 版本就更奇怪了,在我自己的电脑上运行,都提示如下错误:
"无法启动程序,因为计算机中丢失 MSVCP90.dll。尝试重新安装该程序以解决此问题。
这个库我怎么可能会没有?
上 MSDN 仔细找了找原因,出现类似上面的问题,有以下几点需要注意:
在寻找答案的过程中,也在 StackOverflow 里提问获得了帮助,原文是 Where is msvcp90d.dll supposed to come from?,笔记一下以备查阅。
如果需要检测程序依赖的动态链接库有哪些,可以使用 Dependency Walker 这个工具。
PS: 看来 Stackoverflow 里,问题的回答质量是相当高的,以后可以尝试多用。
如何对笔记进行索引是个大问题,尤其是像 Emacs Org 这种对搜索支持得不太好的编辑器而言,笔记的搜索是个特别烦恼的问题。我之前也讨论过《Emacs 笔记本全文搜索方法介绍》,如果你用的是 Windows7 操作系统,则可以用 Windows7 的索引机制,来方便地检索你的笔记目录,具体的方法如下:
像我,就把 Emacs org 笔记、Onenote 笔记全拉进去了,这样搜索起来一点都不吃亏。
不知道 Windows7 在进行库搜索的时候,是否还支持一些基本的正则表达式、语义搜索之类的操作,比如说”TRACE + MFC”等。
Windows7 索引常见问题可以参看微软的官方文档,很全很强大。
今天早上碰到一个很奇怪的事情,昨天明明还能在 Visual Studio 2008 里顺利编译的代码,今天编译的时候,就提示如下错误:
错误 171 错误的结果 -1073741819 (从"C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\mt.exe"返回)。 项目
用英文版的 Visual Studio 2008 应该是显示类似如下的错误信息:
Project : error PRJ0002 : Error result 31 returned from 'C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\mt.exe'.
这是怎么回事呢?原因我不是很清楚,但是可以用下面的方法来解决:
右键点击工程,选”属性”==>”配置属性”==>”链接器”==>”清单文件”==>”生成清单”==>设为”否”
英文版的 Visual Studio 2008 应该是如下路径:
Properties ==> Configuration Properties ==> Linker ==> Manifest File, set Generate Manifest to No.
知其然更要知其所以然,有谁知道原因的?
在使用有关 DirectShow 东西的时候,使用了头文件
#include <qedit.h>
结果,编译的时候提示如下错误:
错误 1 fatal error C1083: 无法打开包括文件:"dxtrans.h": No such file or directory c:\program files\microsoft sdks\windows\v6.0a\include\qedit.h 498
这个真是奇怪了,为啥微软自己 SDK 里的东西,都会出现找不到头文件的问题呢?
解决办法可以有两种:
(1) 从其它地方把 dxtrans.h 这个文件拷过来,例如从
Program Files\Windows Mobile 5.0 SDK R2\PocketPC\Include\Armv4i\dxtrans.h Program Files\Windows Mobile 5.0 SDK R2\Smartphone\Include\Armv4i\dxtrans.h D:\Program Files\Windows Mobile 6 SDK\PocketPC\Include\Armv4i D:\Program Files\Windows Mobile 6 SDK\Smartphone\Include\Armv4i
里拷 dxtrans.h 这个文件出来。
(2) 或者在引用 qedit.h 头文件的时候,加上这么几句:
#pragma include_alias( "dxtrans.h", "qedit.h" ) #define __IDxtCompositor_INTERFACE_DEFINED__ #define __IDxtAlphaSetter_INTERFACE_DEFINED__ #define __IDxtJpeg_INTERFACE_DEFINED__ #define __IDxtKey_INTERFACE_DEFINED__ #include "Qedit.h"
也可以。
微软的MSDN上还有对这个问题的讨论,看来真是微软自摆乌龙了。
不知道为什么,之前还能在 Visual Studio 2005 下编译得好好的工程,今天突然就碰到下面的问题:
Error 1 error MIDL2025 : syntax error : expecting ] or , near "annotation" C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\unknwn.idl 108 Error 2 error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\unknwn.idl 108
这个是 MIDL 编译器相关的问题,当你给新版本的 Windows SDK 头文件使用旧版本的 MIDL 编译器时,就会有这个问题。
解决的办法,就是把你新版本的 MIDL 编译器添加到 Visual Studio 的包含目录里。微软 Mike Wasson 的博客 MIDL error: ‘annotation’ 对此有着详细的解释,内容摘要如下:
A colleage recently got this error while compiling unknown.idl:
1>C:\Program Files\Microsoft SDKs\Windows\v6.0\Include\unknwn.idl(108) : error MIDL2025 : syntax error : expecting ] or , near "annotation"
1>C:\Program Files\Microsoft SDKs\Windows\v6.0\Include\unknwn.idl(108) : error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation
If you get this error, you are using a new version of the Windows headers with an older version of the MIDL compiler.
Fix:
1. Make sure you installed the MIDL compiler when you installed the latest Windows SDK. It should appear under "Program Files\Microsoft SDKs\Windows\v6.0\Bin".
Note: To install the MIDL compiler, you need to select Developer Tools / Windows Development Tools / Win32 Development Tools in the Windows SDK setup wizard. (At least, as of RC1.)
2. In Visual Studio, under Tools > Options > Projects and Solutions > VC++ Directories > Executable Files, add
x:\Program Files\Microsoft SDKs\Windows\v6.0\bin
to the top of the list.
在用 Visual Studio 2005 建立生成安装文件的工程时,因为应用程序的需要,在安装的时候要求 Windows Installer 把用户选择的安装路径写到注册表里。这个时候,有什么解决办法呢?
可以在生成安装文件的工程里,手动插入一个系统预留的属性值(即需要转义的字符串),在 Windows Installer 安装程序的时候,会自动把这些属性值替换掉。可以用 [] 或 {} 来把该属性值括起来。
例如,右键点击”安装工程名称 ==> 视图 ==> 编辑器 ==> 注册表”,这时,可以添加如下注册表项:
HKEY_CURRENT_USER|HKEY_LOCAL_MACHINE\Software\[Manufacturer]\
这样,在 Windows Installer 安装程序的时候, [Manufacturer] 就会自动转义为实际的值。当然,如果没有对应的系统预留属性值,则转义为空值( blank )。
系统预留的属性值有很多个,例如对于安装目录来说,是 [TARGETDIR] ,即 [TARGETDIR] 会根据安装时用户选择的安装路径赋值。当你在注册表里,需要填写程序安装路径的时候,就可以用这个属性值来占位。Windows Installer 在安装的时候,自然会把它替换为实际路径。安装后,会显示为类似于下面的路径:
C:\Program Files\TestApp\TestAppFirstPart\
如果你需要在注册表里指定相应的可执行文件或动态链接库,比如说,你要在注册表里插入下面的路径:
C:\Program Files\TestApp\TestAppFirstPart\test.dll
这时,你就应该在工程里把属性值设为:
[TARGETDIR]test.dll
注意 [TARGETDIR] 生成的目录会有最后一个 \ 号。
更多 Windows Installer 使用技巧可以看 Windows Installer (MSI) Tips and Notes。