零、前言:
该篇博客的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. ");
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库。结果如何呢?请看下篇。