Posted on 03-02-2012
Filed Under (技术) by waterlin

最近在尝试用新的方式来写一个新的 Windows 客户端,当作练习。主要是用 C++ 把复杂的算法写成链接库,然后用 C# 做界面前端,从 C# 调用用 C++ 写好的非托管代码。

尝试通过这种方式解决两个问题:

  1. C# 的计算效率问题,虽然从各种资料来看,好像 C# 的托管代码对效率的影响并没有想象的夸张,但不管怎么样,有些东西用 C++ 写就是方便一些;
  2. 用 C++ 可以方便地链接现有的代码库、算法库。

其实,这种架构可以看成是一种 C/S 架构的简化版,但是省去了 Socket 通信这一层。

我采用的是最简单的 P/Invokes 的方式来实现 C# 调用 C++ 链接库,详细的教程,可以看一下 Using dumpbin.exe as an Aid for Declaring P/Invokes 这篇文章。

这里先简单说说两点和代码无关的问题

  1. 如果 C++ 链接库的计算很耗时,一定要在 C# 客户端里开一个线程来处理,否则容易造成死机,这和 MFC 之类的原理一样。
  2. 为了测试你从 C# 链接 C++ 链接库是否成功,可以用 C# 新建一个命令行工程专门测试,用这种方式来测试更加直接与有效。

再谈几点有关技术实现细节的问题,也是我折腾了很久的困惑之处

1. 有关 C++ 链接库 EntryPoint 的名称

我在刚开始从 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 依然会是原始的名称。

2. C# 程序运行时提示说找不到链接库

如果按上述方法编写好了代码,一运行 C# 程序却提示说:

未处理的"System.DllNotFoundException"类型的异常出现在 example.exe 中。

其他信息: 无法加载 DLL"cppexample.dll": 找不到指定的模块。 (异常来自 HRESULT:0x8007007E)。

这个时候,你需要找一找你的 dll 是否在可执行目录下,或是你写的 dll 是否依赖于其它第三方dll,一定要确保所有的 dll 都能顺利被找到。

3. 参数的映射办法

你的函数肯定有若干个参数,那这些参数应该和 C# 里的类型如何一一对应呢?在 C# 里,这种映射关系叫做 marshal

类型映射,需要仔细检查一下手册,比如说,const char* 就应该这样映射:

[MarshalAs(UnmanagedType.LPStr)]

Using C++ Interop 文章的未尾,有列出一大串的类型映射列表。

小结

初步用 C# 来写界面,感觉更方便、快速,起码比 MFC 来得简单、直接;从 C# 里直接调用 C++ 链接库,也很方便。但这两者结合起来写应用,稳定性还有待进一步测试。

(0) Comments    Read More   
Posted on 31-10-2011
Filed Under (技术) by waterlin

在调试 Visual Studio 2008 程序时,经常有一些动态链接库(即 dll 文件)需要加载到工程里,这样才能依赖第三方库进行程序调试。

这些动态链接库,往往都是测试版本或是开发中的版本,或者会有若干个版本;这个时候,如果直接把 dll 所在目录加到 PATH 里,则会有潜在冲突的危险;如果直接拷贝到 Visual Studio 的目录下,假如测试工程太多,每次有新版本的动态链接库更新时,你需要更新若干次,拷贝、粘贴苦不堪言。

在开发过程中,究竟怎样来让 Visual Studio 链接这些 lib 及 dll 文件会比较好呢?

总体上来说,有几种方法可以改变 Visual Studio 的环境变量设置:

  1. 直接添加到系统的 PATH 变量里

    这个方法最简单,也最直接,但是坏处是会影响全局的 PATH 设置,尤其是你包含着大量测试用的 dll 时。

  2. 在 Visual Studio 全局设置里,把 dll 所在目录添加到 PATH 里:

    通过 Visual Studio 菜单 ==> 工具 ==> 选项 ==> 项目和解决方案 ==> VC++目录,在下拉框里选择”可执行文件”,然后把 dll 所在路径添加进去。

  3. 直接把所有 dll 拷贝到 Visual Studio 工程目录下,或是拷贝到生成可执行文件的文件夹(默认情况下是 Debug 或 Release 目录)下:

    这个方法也很简单,但是当你有若干个工程时,你每次更新 SDK 及其 dll 文件,你就要把所有的工程都更新,这个不符合文件唯一性的工程性准则。

  4. 在调试程序时,让 Visual Studio 帮你切换当前工作目录到 dll 相应的目录下:

    在 Visual Studio ==> Project ==> Properties ==> Select Configuration ==> Configuration Properties ==> Debugging ==> Working directory 里填上 dll 所在目录,这样当在调试程序时,Visual Studio 会把当前工作目录切换到这个目录下,从而会自动读取本目录下的 dll 文件。

    这个方法的优点很明显,简单!副作用也很明显,在你切换了当前工作目录后,你可能会找不到程序的配置文件,在程序里写的诸如”./config.ini”全部都找不到了;另外,你要把所有的 dll 都放到这个工作目录里,否则一样会提示说找不到 xxx.dll 的问题。

  5. 最后一个方法,也是我认为最好的一个方法,在 Visual Studio 工程属性里把一个目录临时添加到 PATH 环境变量里:

    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?

(0) Comments    Read More   
Posted on 05-09-2011
Filed Under (技术) by waterlin

在 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++ 混在一块,命名、类型定义真是一个大问题。以后碰到类似的问题,也可以采用类似的解决办法。

参考资料:

  1. 解决在vs2008的mfc工程中编译BerkeleyDB出错问题
  2. VS2005中的MFC程序使用BerkeleyDB
  3. MSDN Visual Studio 2010 Compiler Error C2143
(0) Comments    Read More   
Posted on 22-08-2011
Filed Under (技术) by waterlin

在写客户端的时候,经常要传输一些文件,有一些服务器就是用 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;
}
(0) Comments    Read More   

最近被一个问题折磨了好几天,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 仔细找了找原因,出现类似上面的问题,有以下几点需要注意:

  1. 按道理来说,编译成 release 版本后,只要在目标机器上安装相应版本的 vc_redist 就可以了;
  2. 对于 VS2008 版本,光把编译生成的可执行文件及 Dll 拷贝到目标机器上是不行的,要加上 manifest 文件;
  3. 动态链接的程序,需要在文件目录里复制 MFC90.dll, MSVCR90.dll 和 manifest 文件,不加上这个 manifest 是运行不了滴。

在寻找答案的过程中,也在 StackOverflow 里提问获得了帮助,原文是 Where is msvcp90d.dll supposed to come from?,笔记一下以备查阅。

如果需要检测程序依赖的动态链接库有哪些,可以使用 Dependency Walker 这个工具。

PS: 看来 Stackoverflow 里,问题的回答质量是相当高的,以后可以尝试多用。

(0) Comments    Read More   
Posted on 14-05-2011
Filed Under (技术) by waterlin

如何对笔记进行索引是个大问题,尤其是像 Emacs Org 这种对搜索支持得不太好的编辑器而言,笔记的搜索是个特别烦恼的问题。我之前也讨论过《Emacs 笔记本全文搜索方法介绍》,如果你用的是 Windows7 操作系统,则可以用 Windows7 的索引机制,来方便地检索你的笔记目录,具体的方法如下:

  1. 在某个文件夹上,点右键,选”包含到库中”即可把当前目录添加到某个索引库里;

    像我,就把 Emacs org 笔记、Onenote 笔记全拉进去了,这样搜索起来一点都不吃亏。

  2. 在”包含到库中”里,如果你要新建一个库来组织某一主题的文档,点击”新建库即可”。
  3. 以后你需要搜索你的笔记时,只要在这个库里进行搜索就可以得到你想要的东西,在文件浏览器的左边,会有一个”库”的文件夹类别,点它进行任何操作即可。

不知道 Windows7 在进行库搜索的时候,是否还支持一些基本的正则表达式、语义搜索之类的操作,比如说”TRACE + MFC”等。

Windows7 索引常见问题可以参看微软的官方文档,很全很强大。

(1) Comment    Read More   
Posted on 04-04-2011
Filed Under (技术) by waterlin

今天早上碰到一个很奇怪的事情,昨天明明还能在 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.

知其然更要知其所以然,有谁知道原因的?

(0) Comments    Read More   
Posted on 04-09-2010
Filed Under (技术) by waterlin

在使用有关 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上还有对这个问题的讨论,看来真是微软自摆乌龙了。

(0) Comments    Read More   
Posted on 02-09-2010
Filed Under (技术) by waterlin

不知道为什么,之前还能在 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.
(0) Comments    Read More   
Posted on 25-08-2010
Filed Under (技术) by waterlin

在用 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

(0) Comments    Read More