零、前言:

  该篇博客的Title原计划是“在VC++中调用libmemcached的设计技巧”,可结果却事与原违,原因很简单,移植失败了。尽管结果如此,然而这3天的付出却是非常值得的,原因也很简单,收获非常大。事实上,我曾经在6月份的时候成功移植了当时的最新版本0.49,并写出了下面的博客:

  http://www.cnblogs.com/stephen-liu74/archive/2011/06/20/2084882.html

  这次移植的目标非常明确,就是基于上次的经验,对libmemcached进行基于C++的封装,以便其可以更好的集成到我的底层服务框架中,使我的程序在Windows平台也可以享受memcached服务器带来的性能优势。带着这份憧憬开始了我的艰苦移植历程。首先需要说明的是0.49和最新版0.53之间的差异是非常大的,这一点也让我始料不及,因此走了一些弯路,好在及时做出调整,才没有耽搁更多的时间去验证一些不可能的事情。下面是我在动手移植libmemcached之前的设计思路,移植过程中遇到的问题,以及移植失败后的经验总结。

一、最初的设计思路:

  为什么不在VC中直接调用编译后的libmemcached库呢?原因非常简单,我们无法直接调用。由于目标库(libmemcached)是在Mingw32环境下通过gcc编译的,而gcc在Windows下编译动态链接库时(DLL)并没有生成相应的lib文件,这样在VC中也就无法通过静态链接动态库的方式将libmemcached链接到调用程序中。这样我们只能利用Windows中提供的另一种方式,即通过LoadLibrary和GetProcAddress等Win32 API来动态加载该动态库,因为该方法不需要.lib文件。尽管该方式在技术上被认为是可能行的,然而libmemcached中存在大量的导出函数,以及这些函数所依赖的结构体(struct)。为了保证GetProcAddress返回的函数指针能够正常的被调用,如memcached_create等,我们不得不在当前工程中定义(typedef)大量的函数指针以及相关的结构体。鉴于之前的移植经验,由于这些结构体嵌套了大量的内部子结构体,因此如果全部定义就需要大量的工作。而这些结构体的成员很多都是用于libmemcached内部,因此一旦在未来的版本中修改了该结构体的成员,那么我们的程序也不得不要随之改变,可以想象,这样的同步是相当痛苦的。除此之外,还有一个非常致命的缺陷,即结构体成员的字节对齐问题。如果不是一字节对齐,如VC缺省的8字节对齐,那么gcc和VC编译器在处理该类问题时就可能存在一定的差异,一旦如此,VC中定义的结构体填充的数据就不能被gcc正确的取出,从而导致libmemcached中函数无法正常的工作。

  为了避免以上问题的发生,下面我就来介绍一种通用的设计技巧用于解决该类问题。

  1. 定义一个C++的纯虚接口和两个C的导出函数,其中C++的纯虚接口用于之后的程序调用,该接口中定义了部分libmemcached中提供的功能,如add、set、replace、get、delete和CAS等。然而需要注意的是该接口中并未包含或暴露任何和libmemcached相关的信息,如memcached_st结构体、memcached_create函数等。见如下代码:

 1 class MemcachedClientWrapper
2 {
3 protected:
4 virtual ~MemcachedClientWrapper() {}
5
6 public:
7 virtual bool initialize(bool consistentHash = false, bool supportCAS = false) = 0;
8 virtual int addServer(const char* serverIP, const int port) = 0;
9 virtual void release() = 0;
10 virtual bool set(void* key,int klength,void* data,int dlength) = 0;
11 virtual bool add(void* key,int klength,void* data,int dlength) = 0;
12 virtual bool replace(void* key,int klength,void* data,int dlength) = 0;
13 virtual bool append(void* key,int klength,void* data,int dlength) = 0;
14 virtual bool get(void* key,int klength,MCFetchedData*& fetchedData) = 0;
15 virtual bool gets(void** keys,int* keysLength,int count) = 0;
16 virtual bool fetchNext(MCFetchedData*& fetchedData,bool* isEof = 0) = 0;
17 virtual bool remove(void* key,int klength) = 0;
18 virtual bool exists(void* key,int klength,bool* ok = 0) = 0;
19 virtual bool updateWithCAS(void* key,int klength,void* data,int dlength) = 0;
20 virtual bool updateWithAtomicIncrement(void* key,int klength,int step
21 ,uint64& value,int* defaultValue = 0) = 0;
22 virtual bool updateWithAtomicDecrement(void* key,int klength,int step
23 ,uint64& value,int* defaultValue = 0) = 0;
24 virtual bool clean() = 0;
25 };

  两个导出的C函数主要用于创建该接口的实现子类,以及在使用完毕后释放该接口的指针,从而保证资源释放的可靠性,见如下代码:

1 #define WRAPPER_CC __attribute__((cdcel))
2
3 extern "C" {
4 //工厂方法创建Wrapper的实现子类,但是返回接口的指针
5 MemcachedClientWrapper* WRAPPER_CC createMCWrapper();
6 //资源释放函数,通过上面函数返回的接口指针,需要通过该函数释放
7 void WRAPPER_CC releaseMCWrapper(MemcachedClientWrapper*);
8 }

这里之所以使用cdecl的调用规范,而不是Windows API常用的stdcall,主要是因为gcc在编译基于stdcall调用规范的C导出函数时,在导出函数名的后面添加了一个@和该函数参数所占的字节数作为后缀,因此当我们通过GetProcAddress传入函数名获取函数指针时,由于名称不匹配,所以返回的函数指针将为NULL。和stdcall不同的是,基于cdcel调用规范生成的导出函数不存在这样的问题。我们可以通过Windows提供的Dependency工具予以证明。

  2. 定义一个实现类继承自上面定义的纯虚接口,该类将包含大量和libmemcached相关的细节信息,同时也需要在该类中include和libmemcached相关的头文件,而不是再自行重新定义和libmemcached相关的细节信息,最后再通过动态加载的方式进行加载,见如下代码声明:

 1 #include <MemcachedClientWrapper.h>
2 #include <libmemcached/memcached.h>
3
4 class MemcachedClientWrapperImpl : public MemcachedClientWrapper
5 {
6 public:
7 MemcachedClientWrapperImpl();
8 virtual ~MemcachedClientWrapperImpl();
9
10 public:
11 virtual bool initialize(bool consistentHash = false, bool supportCAS = false);
12 void release();
13 int addServer(const char* serverIP, const int port);
14 bool set(void* key,int klength,void* data,int dlength);
15 bool add(void* key,int klength,void* data,int dlength);
16 bool replace(void* key,int klength,void* data,int dlength);
17 bool append(void* key,int klength,void* data,int dlength);
18 bool get(void* key,int klength,MCFetchedData*& fetchedData);
19 bool gets(void** keys,int* keysLength,int count);
20 bool fetchNext(MCFetchedData*& fetchedData,bool* isEof = 0);
21 bool remove(void* key,int klength);
22 bool exists(void* key,int klength,bool* ok = NULL);
23 bool updateWithCAS(void* key,int klength,void* data,int dlength);
24 bool updateWithAtomicIncrement(void* key,int klength,int step,uint64& value,int* defaultValue = 0);
25 bool updateWithAtomicDecrement(void* key,int klength,int step,uint64& value,int* defaultValue = 0);
26 bool clean();
27
28 private:
29 memcached_st* _mc;
30 memcached_return _mr;
31 uint64_t _cas;
32 bool _initialized;
33 bool _supportCAS;
34 };

  3. 还是在Mingw32环境下,编写makefile文件,生成新的动态库,该库将依赖libmemcached库。见如下makefile:

all:
      g++ -I/usr/local/include -I"/home/Administrator/MemcachedClientWrapper" ../MemcachedClientWrapperImpl.cpp -o ./MemcachedClientWrapperImpl.o -O3 -w -c -fmessage-length=0 -MMD -MP -MF"MemcachedClientWrapperImpl.d" -MT"MemcachedClientWrapperImpl.d"
      g++ -L/usr/local/bin -shared -o "./MemcachedClientWrapper.dll" ./MemcachedClientWrapperImpl.o -lmemcached-8

见以下说明:

1) MemcachedClientWrapperImpl.cpp文件中包含接口的实现部分已经两个导出C函数的实现。

2) /usr/local/include: 中包含libmemcached的相关头文件,是在libmemcached的make install中copy过去的。

3) /home/Administrator/MemcachedClientWrapper: 该目录包含该Wrapper工程头文件所在目录。

4) /usr/local/bin: 该目录包含libmemcached生成的dll文件。

5) libmemcached-8.dll: 当前版本libmemcached生成的动态库。

6) MemcachedClientWrapper.dll: 为最终生成的动态库。

  4. 在执行完步骤3之后,我们将得到两个dll,一个是原有的libmemcached.dll,另一个则为我们封装后的MemcacheClientWrapper.dll。在此之后,我们的VC程序将只是面对封装后的动态库及其导出接口,而libmemcached中的导出信息已经被很好的封装在Wrapper的内部了。我们现在需要做的便是在我们的工程中将纯虚接口所在的头文件包含进我们的工程中,然后在定义两个C函数指针,其函数签名和Wrapper中导出的两个C函数保持一致,见如下代码:

  typedef MemcachedClientWrapper* (*createWrapper)();
     typedef void (*releaseWrapper)(MemcachedClientWrapper*);

  5. 最后我们需要做的是在我们的测试用例中利用该Wrapper导出的纯虚接口来操作libmemcached,以便和Memcached服务器进行通讯和数据存储,见如下用例代码:

 1 #include <MemcachedClientWrapper.h>
2
3 int main()
4 {
5 MemcachedClientWrapper* wrapper = createMCWrapper();
6 void* key = malloc(20);
7 void* data = malloc(20);
8 memcpy(key,"helloworld",10);
9 memcpy(data,"i love you, my baby.",20);
10 assert(wrapper->add(key,10,data,20));
11 free(key);
12 free(data);
13 releaseMCWrapper(wrapper);
14 printf("All Over.\n");
15 return 0;
16 }

三、原理分析:

  该技巧有些类似于Windows中的COM技术,通过编译器生成的C++虚拟表,以达到这种跨编译器的二进制接口形式。该技巧有以下几个注意事项:

  1. 定义纯虚接口,该接口中不能定义任何成员变量,否则在编译器之间无法做到二进制兼容。

  2. 定义C接口形式的工厂方法createMCClient(),用于创建实际的子类,这样使用者便可以通过动态链接的方式加载该库,如Windows中LoadLibrary和Linux中的dlopen。

  3. 定义C接口的对象指针释放方法releaseMCClient(),在接口中已经将接口的析构方法定义为protected类型,因此调用者无法直接delete该指针,而是必须通过该接口指针导出的release()方法或者该C导出函数来释放,尽管两者都可以达到释放资源的目的,然而我们在实践中还是推荐使用该C导出函数,以便保证使用方式的一致性和未来的扩展性。

  4. 在定义纯虚接口所在的头文件中不要包含任何和实现细节相关的信息。

  总之,一切都是这样的美好,我对我的设计也是非常自信,甚至有一点点的得意,因为我曾经使用该方式完成了很多库在C++ Builder和VC++之间的迁移,从而达到这种跨编译器的效果。可以想象,就连极为复杂的webkit也被我在短短的一周内成功迁移,libmemcached应该是不在话下的。然而事实却是残酷的,我的VC测试用例无法正常调用该纯虚接口,每次都会报出和寄存器相关的错误。这让我立刻想到了之前代码中存在的问题,一定是我在声明纯虚接口时,没有为每个接口函数明确声明调用规范,而gcc和VC++的缺省调用规范恰恰又是不同的(VC++缺省为cdcel)。想到这里,我立即做出修改,将所有接口函数的调用规范都指定为cdcel,然后用gcc重新编译该Wrapper库。结果如何呢?请看下篇。

移植最新版libmemcached到VC++的艰苦历程和经验总结(上)的更多相关文章

  1. 移植最新版libmemcached到VC++的艰苦历程和经验总结(下)

    结果如何呢?我的VC++测试用例还是不能调用该接口的接口方法,只是这次的报错方式有所改变,提示是每个C/C++程序员最不愿意看到的“内存地址访问违规”,这一次我确实被郁闷了,这是为什么呢? 五.gcc ...

  2. VC++6.0在Win7以上系统上Open或Add to Project files崩溃问题 解决新办法

    崩溃原因是和office高版本冲突,比如我64位win7装了64位office2013及visio就遇到了这个问题(我很纳闷,记得重装系统前装的是32位office2013及visio就未曾遇到该问题 ...

  3. LCD驱动移植在在mini2440(linux2.6.29)和FS4412(linux3.14.78)上实现对比(deep dive)

    1.Linux帧缓冲子系统 帧缓冲(FrameBuffer)是Linux为显示设备提供的一个接口,用户可以将帧缓冲看成是显示内存的一种映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作 ...

  4. Ice-E(Embedded Internet Communications Engine)移植到s3c2440A(arm9)linux(2.6.12)上的

    2009-03-26 18:31:31 原文链接 1.前言 ICE-E是ICE在嵌入式上运行的一个版本,与ICE比较如下: Category Ice 3.3.0 Ice-E 1.3.0 Thread ...

  5. IE8升级新版Flash Player ActiveX14导致的discuz图片附件无法上传 解决方法

    架不住sb adobe的频繁升级提示,手欠升级到了了flash player 14,结果IE8下全部discuz论坛中都无法看到上传图片的button了 没办法,遇到问题就解决吧 刚好在解决IE11遇 ...

  6. Django从无到有的艰苦历程

    1, django项目下的各个文件的介绍 1.1, 项目的根目录: 实Django项目的总目录, 所有的子项目, 和需要进行的操作都在其中进行. 1.2

  7. Ubuntu16.04CPU下安装caffe的艰苦历程

    我选用的是anaconda安装,符上我参照的三个有用的教程. 1 http://www.linuxdiyf.com/linux/22442.html 主要讲anaconda的安装和python路径配置 ...

  8. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

  9. Systemc在VC++2010安装方法及如何在VC++2010运行Noxim模拟器

    Systemc在VC++2010的安装方法可以参考文档"Systemc with Microsoft Visual Studio 2008.pdf".本文档可以在"htt ...

随机推荐

  1. sql的一些知识_数据分组

    group by--------按**分组查询 SELECT rank, MIN(weight)as min_weight FROM userinfo GROUP BY rank 对分组的值进行过滤需 ...

  2. mysql: Data source rejected establishment of connection, message from server: "Too many connections"

    http://www.oschina.net/question/558677_66703 com.mysql.jdbc.exceptions.MySQLNonTransientConnectionEx ...

  3. ffplay 播放m3u8 hls Failed to open segment of playlist 0

    用ffplay 播放m3u8文件 出现 Failed to open segment of playlist 0,Error when loading first segment 'test0.ts' ...

  4. 【Sprint3冲刺之前】敏捷团队绩效考核(刘铸辉)

     TD学生助手团队已经在4.22~4.30完成了为期9天的Sprint2计划,并在Sprint2总结会议中安排了五一放假每个人的任务分配,下面发布下Sprint2冲刺周期的阶段性成果. Sprint2 ...

  5. 新一代AJAX API:FETCH

    AJAX半遮半掩的底层API是饱受诟病的一件事情. XMLHttpRequest 并不是专为Ajax而设计的. 虽然各种框架对 XHR 的封装已经足够好用, 但我们可以做得更好.更好用的API是 fe ...

  6. Markdown 语法的超快速上手

    本文支持WTFPL协议,因此你想往哪转就往哪转. Why markdown? Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式. Ma ...

  7. LeetCode215:Kth Largest Element in an Array

    Find the kth largest element in an unsorted array. Note that it is the kth largest element in the so ...

  8. 按照HashMap中value值进行排序

    import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileNotFoundException; ...

  9. 在“云基础设施即服务的魔力象限”报告中,AWS 连续三年被评为领导者

    在"2014 云基础设施即服务的魔力象限"中.Gartner 将 Amazon Web Services 定位在"领导者象限"中,并评价 AWS 拥有最完整.最 ...

  10. [IT新应用]如何拯救死机的苹果手机(iPhone X)

    突然白天接了一个电话,苹果就死机了.这是用这个手机半年来第一次.貌似还能接电话,就是屏幕上一个白色的圆圈,一直转啊转. 后来百度了一下,找到这一篇.将重点部分摘录如下: http://www.sohu ...