论坛首页 编程语言技术论坛

C++ Plug-in 技术的一些深度思考(2)

浏览 8507 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-12-02   最后修改:2009-12-03

C++ Plug-in 技术的一些深度思考(1)

 

我们可以使用动态连接库的技术实现上诉的功能。大方向说,将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的机制。

 

  • 大小: 16.1 KB
   发表时间:2009-12-02  
使用so或dll的方式实现插件的松耦和连接可行,但是不是好方法的.
最简单的说如果别人不是动态加载,那么一旦你rebuild后很有可能导致主体也要rebuild.
在实际的系统中,松耦和的实现不在于技术本身,而在于规范的约束,但是反过来,为了减少因为违规使用而带来的问题也通过技术来加以强化是必要的.

做的较好还是windows平台的COM模型,但是即使是COM也会因为开发者的认知问题而导致很多不可预期的问题.
0 请登录后投票
   发表时间:2009-12-02  
我们有考虑过这些东西,COM, Corba这种真正的组件技术。但是关键是,如果使用这些组建技术,我们就把系统的难度推倒了一个新的高度。另外,我不明白为什么plugin rebuild。主体需要rebuild。从上面的程序来看,主体程序完全不需要rebuild啊。能否讲的更具体些。
0 请登录后投票
   发表时间:2009-12-02   最后修改:2009-12-02
ankyhe 写道
我们有考虑过这些东西,COM, Corba这种真正的组件技术。但是关键是,如果使用这些组建技术,我们就把系统的难度推倒了一个新的高度。另外,我不明白为什么plugin rebuild。主体需要rebuild。从上面的程序来看,主体程序完全不需要rebuild啊。能否讲的更具体些。


最轻量级的就是用interface的方法,使用queryinterface去获得需要的实现接口.

rebuild的问题是和dll/so的export table的结构以及静态连接的实现有关,如果你在一个dll/so中增加了一个接口有可能会导致原先的export table序列被打乱,那么原来使用静态连接的主体程序就必须rebuild,否则就错了.
当然这个也是可以用过强制约束接口导出的方法来规避. 问题在于不是所有人都会遵守规则.
0 请登录后投票
   发表时间:2009-12-02  
如果使用动态加载的确不会存在主体rebuild的问题,但是一担放入一个开放环境中,这种事情是无法强行约束的.
0 请登录后投票
   发表时间:2009-12-03  
这个。。。我个人觉得组建化和插件化还是有很大差别的。。。

虽然都是松耦合的努力。。。。
0 请登录后投票
   发表时间:2009-12-03  
mikeandmore 写道
这个。。。我个人觉得组建化和插件化还是有很大差别的。。。

虽然都是松耦合的努力。。。。


这个我完全同意。组件化程度更高,付出的成本也更高。
0 请登录后投票
   发表时间:2009-12-03  
等0x吧,内存标准确立了就好做了。
0 请登录后投票
   发表时间:2009-12-05  
truekbcl 写道
等0x吧,内存标准确立了就好做了。

这个和0x的memory model没关系。。。
0 请登录后投票
   发表时间:2009-12-07  
mikeandmore 写道
truekbcl 写道
等0x吧,内存标准确立了就好做了。

这个和0x的memory model没关系。。。

跟内存分配还是有点关系的,如果内存分配不统一,在接口传输,返回的时候会有不少的问题。
我最近在研究corba,那东东复杂得简直超过人的大脑了。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics