我们可以使用动态连接库的技术实现上诉的功能。大方向说,将CalcAdd和CalcMul编译连接成动态连接库,CalcFactory.cpp动态的加载它们。这样的话,可以实现两个功能:
[1] 如果更新libCalcAdd.so或者增加一个libCalcMin.so,不需要重新编译,链接,发布主程序calc,只需要重新发布你更新或者增加的so文件
[2] 如果我们可以横向的定义出主程序的测试规范,我们甚至不需要回归测试主程序calc。
我们重新设计了文件目录的组织结构,如下图所示:
有修改的文件有CalcFactory.cpp / CalcFactory.h。主要是增加了load 动态连接库的内容。 CalcAdd.cpp和CalcMul.cpp分别增加了一个C函数,返回一个动态分配的CalcAdd或者CalcMul。
// core/src/CalcFactory.cpp
#include <dlfcn.h> //for dlopen and dlsym
#include <iostream>
#include "../include/CalcFactory.h"
using std::cerr;
using std::endl;
using std::string;
using std::map;
map<string, CalcFactory::GetNewInstanceFun> CalcFactory::calcHandlers;
ICalc* CalcFactory::getCalc(string calcType)
{
if (calcHandlers.end() == calcHandlers.find(calcType)) {
string libName = "../lib/libCalc" + calcType + ".so";
void *pdlHandle = dlopen(libName.c_str(), RTLD_NOW);
if(!pdlHandle)
{
cerr << "Fail to load " << libName << endl;
return 0;
}
char *err = dlerror();
if(err)
{
cerr << "Fail to load " << libName << " due to " << err << endl;
return 0;
}
void *rst = dlsym(pdlHandle, "getNewInstance");
if (!rst) {
cerr << "There is no \"getNewInstance\" function in " << libName << endl;
return 0;
}
GetNewInstanceFun newFun = (GetNewInstanceFun)rst;
if (!newFun) {
cerr << "Fail to cast to (ICalc *)(*)(). " << endl;
return 0;
}
err = dlerror();
if(err)
{
cerr << "There is no \"getNewInstance\" function in " << libName
<< " due to " << err <<endl;
return 0;
}
calcHandlers[calcType] = newFun;
}
GetNewInstanceFun nf = calcHandlers[calcType];
return nf();
}
注意这里的修改。
[1] 取消了这个cpp对CalcAdd.h和CalcMul.h的依赖。本质上,在Core这边里对Plug-in的使用都是完全基于接口ICalc的。
[2] 增加了一个全局的map,用来避免反复的load动态连接库。
[3] 在Unix动态连接的思路就是dlopen和dlsym,具体的技术大家可以google一下。如果有Windows的经验,可以发现这些过程Windows和Unix几乎是一致的。
// plugin/src/CalcAdd.cpp
#include "../include/CalcAdd.h"
void CalcAdd::calc(int *p, size_t size, int *result)
{
for (size_t i = 0 ; i < size; ++i)
{
(*result) += p[i];
}
}
CalcAdd::~CalcAdd() { }
extern "C"
{
ICalc *getNewInstance()
{
return new CalcAdd;
}
}
注意了,其实整个类CalcAdd.cpp的实现没有任何的改变。改变的是增加了一个函数ICalc *getNewInstance()。这个函数要注意几点:
[1] extern “C” 在这里不能省略。也许你运气好,在你的平台上省略这个东西能顺利运行通过。但是这种代码绝对不能移植,至少在我的Mac上面,不能省略这个extern "C"。本质原因是因为这个函数要被动态加载,而dlsym几乎要返回的那个函数指针是C链接的;
[2] getNewInstance()这个函数的签名,包括返回类型ICalc*, 参数个数和类型,函数名getNewInstance是不能乱改的。首先,函数名getNewInstance被CalcFactory.cpp的dlsym用作找到函数指针的索引,void *rst = dlsym(pdlHandle, "getNewInstance"); 如果函数名错了,那么这个返回的rst是NULL。如果函数签名不对,那么GetNewInstanceFun newFun = (GetNewInstanceFun)rst; 会出错。而且这种错误不容易检查,也许newFun并不是NULL。所以一旦这个Core定下来了,所有的Plug-in里面都必须是这样的函数:
extern "C"
{
ICalc *getNewInstance()
{
// do something
}
}
当然不用太担心,如果你写错了这个函数,你一测试就能发现。
其实,还有的修改就是Makefile。这个不同的平台,不同的编译器选项差别很大。我大致介绍一下:
[1] Solaris 的 CC:生成libCalcAdd.so的时候,需要使用-G参数。生成使用so的主程序需要使用-ldl
[2] Mac的g++ : 生成libCalcAdd.so的时候,需要使用-dynamiclib -flat_namespace。生成 使用so的主程序不需要使用另外的选项
[3] Linux的g++ : 生成libCalcAdd.so的时候,需要使用-fPIC -shared。生成使用so的主程序需要使用-ldl.
大家可以下载v3.tar.bz2查看源代码和Makefile。需要先进入Core 中 make -f Makefile.(mac|sun|lnx) core
然后进入plugin中 make -f Makefile.(mac|sun|lnx) plugin
然后在主目录的bin下面运行calc就可以了。
我们还可以试一下,在plugin里面加入一个CalcMax的类,找出数组中的最大值。当我们把这个libAddMax.so放到lib之后,我们主程序就可以运行成calc Max了。
这样返回类的指针的设计非常的不错,首先,如果你原有的系统,很可能CalcAdd和CalcMul就是类。这样你几乎不需要修改什么代码;其次,如果你的类CalcAdd也许没有实现那个calc接口,没关系,用Adaptor模式适配一下。当然了,也许你会觉得这里的算法类CalcAdd应该是单件啊,不应该每次都去new。这是因为有的系统,现存的CalcAdd里面有很多pirvate的变量和函数,而且这些函数都不是线程安全的。如果你改成单件类,那么使用过程中你的程序也会线程不安全。至少我们现在的系统就是这样的。所以我选择每次返回一个对象。
使用动态库不错,可以完全做到plug-in系统。而且未来完全可以做到热启动,热加载。但是我们先停一停,在下一节,我将给出一种完全不一样的实现plug-in的机制。