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 02-02-2012
Filed Under (技术) by waterlin

在 C/C++ 多线程编程下,如果不注意,采用普通变量传递参数值给线程会有一些误区,需要特别小心。

下面浏忙绪绪就举两个例子来说明一下。

char* 参数在多线程下出现的怪异现象

最近在用 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 的值。

结论:

  1. 传入 char* 后,一定要对 char 字符串拷贝一个副本,否则这个指针指向的内容很有可能被改变;
  2. 多用 C++ 的 string,少用 char 或是 char*,用值拷贝的方式比单纯传递一个指针要更安全;
  3. 最安全的做法,应该是动态分配一个空间,用来保存传递给线程的参数值,在线程结束后再销毁该值。

int 参数在多线程下被重复赋值的怪异现象

同样,如果传入参数是整型或是其它类型的时候,也会有上述类似的问题。拿 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 数组把需要传入到各个线程的参数缓存起来,尽量保证地址不一样。

结论

传入线程的参数,应该尽量采用动态分配内存的方式来生成。否则如果采用临时变量,则随着变量生命周期的消逝,该变量的指针,很有可能会变成一个毫无意义的指针(或是被新的值覆盖,或是被成为一个遗留数)。

采用动态分配的变量作为线程启动时的参数,在线程结束后再销毁这个动态分配的变量,则是一个安全法则。

(0) Comments    Read More   
Posted on 17-01-2012
Filed Under (技术) by waterlin

最近在写 Linux 程序的时候,碰到这样的问题:Log4Cxx 0.10.0 在 Linux 下退出程序时导致程序中断,即提示出现 segmentation fault 错误。

如果你用 gdb 调试,会提示如下信息:

Program received signal SIGSEGV, Segmentation fault.
0x02fa68f3 in apr_vformatter () from /usr/lib/libapr-1.so.0

原因是因为 Log4Cxx 在退出时有非法的资源释放。

有一个解决办法:可以在程序退出时,显示地关闭 Log4Cxx 对象。

I have the same problem if I use log4cxx.AsyncAppender.
I fixed the problem by calling log4cxx::LogManager::shutdown(); before the end of the process.
It's not very clean but it works.

在程序退出时使用语句

log4cxx::LogManager::shutdown();

来清理 log4cxx,而不是让它自己来清理。

注意: 需要在程序所有可能退出的地方,设置这一语句 ,否则依然会有 segmentation fault 的问题。

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

最近都在写一些跨 Windows 和 Linux 平台的 C++ 代码,略有心得,整理成文,备忘一下。

有关预编译指令

Visual Studio 会自动在源代码里添加

#pragma once

这个指令,在 Linux Gcc 编译器下应该怎么样处理类似的情况呢?

维基百科上有一个权威说法:


http://en.wikipedia.org/wiki/Pragma_once

所以,在编写跨平台的 C++ 代码时,最好使用下面这种方式来获得跨平台的特性:

#pragma once
#ifndef GRANDFATHER_H
#define GRANDFATHER_H

struct foo
{
    int member;
};

#endif /* GRANDFATHER_H */

有关链接库工程的跨平台

Windows 下使用 __declspec(dllexport) 来标明一个动态链接库的函数接口,而在 Linux 下,则完全没有这个必要。

这个时候,如果动态链接库代码需要跨平台,应该怎么处理呢?

你可以用如下宏来进行区分:

#ifdef WIN32
#define EXPORT_XX __declspec(dllexport)
#else
#define EXPORT_XX
#endif

源代码的编码格式

源代码的编码格式,最好统一用 GBK 或是 UTF-8,以避免不同编码器之间转换造成的乱码。

注意代码里的文件名大小写。

Windows 里 include 一个头文件,你的大小写可以随便写。但是在 Linux 上,你得小心了,clsssa.h 你不能写成

#include "ClassA.h"

因为在 Linux 上,文件及目录名是大小写敏感的。

一些类型名称

Windows 处理宽字符集与窄字符集采用的方法,是用宏定义来区分。比如说 TCHAR 在不同的工程下,属于不同的类型。

而在 Linux 下,则需要注意这些问题。为了让你的代码可移植性强。一定要少用 BOOL, TCHAR 这些类型,而要用原生态的 C/C++ 类型,比如说 bool, char 等。

有关 _tmain 等主函数问题。

默认情况下,Visual Studio 新建的 Win32 Console 工程,全是类似于这样的。

int _tmain(int argc, _TCHAR* argv[])
{
}

这些代码,移植到 Linux 下面,就需要我们自己做一部分工作。

如果你在 Visual Studio 用的是 Unicode 编码的,则对应的 Linux 代码应该是另外一套。我则是用宏裁剪了两个主函数出来。如果你有什么更好的方法,可以跟我分享。

注意一些库在 Linux 和 Windows 平台下的不同表现

虽然你的程序可能依赖大量的跨平台库,这些库号称是跨平台的,但是很有可能在不同的平台的表现,会有所不同。例如,log4cxx 在 Linux 和 Windows 下就会有字符集设置的差别

你在编写跨平台代码的时候,就应该特别小心这些库的细微差别,及时调整。

后记

以上是一些跨平台 C/C++ 代码编写的经验,备忘一下,也希望对大家有用。

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

最近在 Linux 下使用 log4cxx 库,使用的 log4cxx 版本为 0.10.0,结果无法显示中文日志信息。

这可怎么办呢?我不可能把中文日志全部一行一行替换为英文的,这可是一个非常傻B的举动。

经过研究,终于知道需要经过如下步骤才能让 log4cxx 在 Linux 下正常显示中文日志:

  1. 你可以先 locale 检查一下 Linux 终端环境是不是 zh_CN ;

    $ locale
    
  2. 如果你的是 en_US 之类的编码,则需要把 locale 设置为简体中文:
    $ export LC_ALL="zh_CN.UTF-8"
    

    如果你的系统提示说没有安装本字符集,则需要用命令进行安装:

    $ sudo apt-get install language-pack-zh-hans
    
  3. 在程序里设置应用程序的 locale 和终端一样:
    LoggerPtr logger;
    log4cxx::PropertyConfigurator::configure("./log4cxx.properties");
    logger= Logger::getLogger("test") ;
    logger->info(("Start logging"));
    setlocale(LC_ALL, "zh_CN.UTF-8");
    

    关键是最后这一句 setlocale,要设置得和终端一样,都是 zh_CN.UTF-8。

这样,你的程序就可以用 log4cxx0.10.0 输出中文日志信息了。

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

今天在 Ubuntu 下使用 OpenCV 来读取一个视频,碰到了下述问题:

  1. 自己手动安装的 OpenCV2.2.0 无法正确读取视频内容,用函数 cvCaptureFromAVI() 及 cvGetCaptureProperty() 均提示说失败:

    所有的测试视频在 Windows 下用 OpenCV2.2.0 是可以正常读取的,应该是 ffmpeg 安装不完全或是不正确。

  2. 从源里安装 OpenCV2.1 后,可以打开视频读取数据,可是在读取视频帧内容的时候,却提示如下错误:
    [swscaler @ 0x94b3e80]No accelerated colorspace conversion found from yuv420p to bgr24.
    

    这个问题应该是 OpenCV 在用 ffmpeg 解压 yuv420 数据时出错。

对于上述问题,可以采用下面的办法来解决:

  1. 重新安装 ffmpeg x264:Install and use the latest FFmpeg and x264
  2. 再重新安装 OpenCV,经测试,可以正确安装 OpenCV 2.3.1 版本,安装成功后,视频读写一切正常。

另外:如果是在 Ubuntu-Server 版下安装 OpenCV,在安装 libhighgui-dev 的时候,需要安装很多和桌面相关的依赖包。

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

想弄一个 Java 内嵌网页服务器 ( Embedded Web Server ),这样,在我的 Java 应用程序里,可以直接提供网页服务,而不用再给用户配置一个网页服务器。对于轻量级的本地网页服务,这样就足够了!

在 Java 应用里内嵌一个网页服务器,Jetty 是一个很好的选择。以下是我在摸索过程中,总结的基本步骤:

1. Jetty 环境的配置

首先从官方网站下载开发包,再解压到你喜欢的目录,Java 的东西就是方便呀!

注意,Jetty 还分好几种包,Jetty@eclipse、Jetty@codehaus 是比较常见的,内容应该差不多,应该只是镜像罢了。

2. 在 Eclipse 里建立 Jetty 内嵌网页服务器 Hello World 程序

Java 开发中使用第三方库的时候,就需要把该库添加到工程里。Jetty 的库文件,全部在解压目录的 lib 目录下,比如说,在我这里就是

D:\eclipse\jetty-hightide-8.0.4.v20111024\lib

目录。在 Eclipse 里新建一个 Java 工程,同时,在工程上点右键,弹出的菜单里,选择”Properties”,打开工程属性对话框,进行如下操作:

在 “Java Build Path” ==> “Libraries” 选项卡里,选择 “Add External JARs” 按钮,然后切换到 Jetty 目录里的 lib 目录,把所有的 jar 包都添加到工程里。

现在,我们可以用最简单的例子来启动一个 Jetty 内嵌网页服务器,但是这个代码,只可以获得一个 404 错误。不过没有关系,这起码可以证明 Jetty Server is Ready!

相比于上面这个 404 错误的例子,相信你会更喜欢下面这个 HelloWorld 例子

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;

import java.io.IOException;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;

public class HelloWorld extends AbstractHandler
{
    public void handle(String target,
                       Request baseRequest,
                       HttpServletRequest request,
                       HttpServletResponse response)
        throws IOException, ServletException
    {
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        response.getWriter().println("<h1>Hello World</h1>");
    }

    public static void main(String[] args) throws Exception
    {
        Server server = new Server(8080);
        server.setHandler(new HelloWorld());

        server.start();
        server.join();
    }
}

3. 其它添加 Jetty 库文件的方法

如果需要在 Java 工程里进行 Embeded Server 开发,也可以把 Jetty 的库目录包含到 CLASSPATH 环境变量里。事实上,只要能找到下面几个文件就可以了:

servlet-api-3.0.jar
jetty-util-8.0.4.v20111024.jar
jetty-8.x.jar

手册上是这样写的,不过最后一个 jetty-8.x.jar 文件,我没有找到。

(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 09-10-2011
Filed Under (技术) by waterlin

Emacs Org Mode 里,默认情况下 _ 这种字符会被当成标记语言来进行转义。有的时候,如果你只是写写文章,这种默认的转义,会让你很不方便,尤其是你在写一篇介绍技术的文章,里面出现的变量名有很多的下划线。这时,你会说:”Oh, my god! 难道要我一个一个去标记不让 _ 转义么?”

如何设置让 Org Mode 在默认情况下,不转义 _ 字符呢?

单个 org 文件的解决办法

你可以在一个 org 文件的开头,用下面的设置来关闭这个功能。

#+OPTIONS: ^:nil

如果你需要更方便的设置,可以把上面这个改为

#+OPTIONS: ^:{}

这样,当你写

a_{b}

时,_ 后被 {} 括起来的内容就会被转义,而写 a_b 时,就按普通的方式来显示。

org sites 的解决办法

如果你是用 org sites 来写笔记,想让某个 site 的所有 org 文件不转义 _ 字符,则也可以直接在 org sites 配置文件里,配置下面这么一句,一了百了:

(setq org-export-with-sub-superscripts nil)

也可以在 org sites 里设置这个属性,只作用于某一个 site:

:sub-superscript nil

如果需要像上面一样,采用相对智能的 {} 方式,可以设置成

:sub-superscript {}

或是直接用 elisp 来设置全局的属性:

(setq org-export-with-sub-superscripts '{})

这样就会用 {} 来转义了。

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

最近折腾得够呛,天天写代码,都没时间更新博客了。

总结一下最近碰到的两个问题,写到中文 Wiki 里了,希望对大家有点帮助。

  1. OpenCV 在 Ubuntu 11.04 平台上的编译

    最近要迁移大把工作成果到 Linux 平台,即要在 Linux 下大量地折腾与 OpenCV 相关的代码。很久没有在 Linux 上折腾了,时过境迁,同样碰到了不少问题,写成日志《OpenCV 各种安装错误汇总》

  2. Berkeley DB 数据库的操作还是蛮有技巧的,如果需要把内存里结构比较复杂的数据写到 Berkeley DB 里,我当然拥有我的技巧,以把 IplImage 数据写入到 Berkeley DB 为例,介绍我对这一技术的理解。
(0) Comments    Read More