移植最新版libmemcached到VC++的艰苦历程和经验总结(上)
零、前言:
该篇博客的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++的艰苦历程和经验总结(上)的更多相关文章
- 移植最新版libmemcached到VC++的艰苦历程和经验总结(下)
结果如何呢?我的VC++测试用例还是不能调用该接口的接口方法,只是这次的报错方式有所改变,提示是每个C/C++程序员最不愿意看到的“内存地址访问违规”,这一次我确实被郁闷了,这是为什么呢? 五.gcc ...
- VC++6.0在Win7以上系统上Open或Add to Project files崩溃问题 解决新办法
崩溃原因是和office高版本冲突,比如我64位win7装了64位office2013及visio就遇到了这个问题(我很纳闷,记得重装系统前装的是32位office2013及visio就未曾遇到该问题 ...
- LCD驱动移植在在mini2440(linux2.6.29)和FS4412(linux3.14.78)上实现对比(deep dive)
1.Linux帧缓冲子系统 帧缓冲(FrameBuffer)是Linux为显示设备提供的一个接口,用户可以将帧缓冲看成是显示内存的一种映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作 ...
- 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 ...
- IE8升级新版Flash Player ActiveX14导致的discuz图片附件无法上传 解决方法
架不住sb adobe的频繁升级提示,手欠升级到了了flash player 14,结果IE8下全部discuz论坛中都无法看到上传图片的button了 没办法,遇到问题就解决吧 刚好在解决IE11遇 ...
- Django从无到有的艰苦历程
1, django项目下的各个文件的介绍 1.1, 项目的根目录: 实Django项目的总目录, 所有的子项目, 和需要进行的操作都在其中进行. 1.2
- Ubuntu16.04CPU下安装caffe的艰苦历程
我选用的是anaconda安装,符上我参照的三个有用的教程. 1 http://www.linuxdiyf.com/linux/22442.html 主要讲anaconda的安装和python路径配置 ...
- Linux主机上使用交叉编译移植u-boot到树莓派
0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...
- Systemc在VC++2010安装方法及如何在VC++2010运行Noxim模拟器
Systemc在VC++2010的安装方法可以参考文档"Systemc with Microsoft Visual Studio 2008.pdf".本文档可以在"htt ...
随机推荐
- select中分割多组option
<optgroup style="color:gray; font-style:normal" label="——雪佛兰(五菱)——"></o ...
- Java基础:抽象类和接口
转载请注明出处:jiq•钦's technical Blog 一.引言 基于面向对象五大原则中的以下两个原则,我们应该多考虑使用接口和抽象类: 里氏替换原则:子类能够通过实现父类接口来替换父类,所以父 ...
- mysql复制表命令
http://hi.baidu.com/dwspider/item/908bf5e1746275bd2e140b03 上面命令是实现复制表的一种方法,缺陷就是索引等表信息不会复制过去,只是复制 ...
- android等待旋转圆圈动画
先创建一个动画的xml文件例如以下 <? xml version="1.0" encoding="utf-8"?> <animation-li ...
- 【Android Studio探索之路系列】之十:Gradle项目构建系统(四):Android Studio项目多渠道打包
作者:郭孝星 微博:郭孝星的新浪微博 邮箱:allenwells@163.com 博客:http://blog.csdn.net/allenwells github:https://github.co ...
- pandas-事例练习
补充: DataFrame.dropna(axis=0, how='any', thresh=None, subset=None, inplace=False) 功能:根据各标签的值中是否存在缺失数据 ...
- docker&k8s填坑记
本篇主要用于记录在实施docker和kubenetes过程中遇到的一个问题和解决办法. 本节部分内容摘自互联网,有些部分为自己在测试环境中遇到到实际问题,后面还会根据实际情况不断分享关于docker/ ...
- Vue.directive 自定义指令
一.什么是全局API? 全局API并不在构造器里,而是先声明全局变量或者直接在Vue上定义一些新功能,Vue内置了一些全局API,比如我们今天要学习的指令Vue.directive.说的简单些就是,在 ...
- ASP.NET MVC模式——WebPages
WebPages 示例 123456<html> <body> <h1>Hello Web Pages</h1> <p>The time i ...
- 基于TCP的通信程序设计
套接字(Socket)是一种跨主机进程之间的双向通信接口,每个打开的套接字都可以通过一个套接字描述符来描述,因此可以使用低级文件编程库操作套接字. TCP是一中面向连接的网络传输控制协议.它每发送一个 ...