zoukankan      html  css  js  c++  java
  • 编写模块化插件式应用程序

    动态链接库技术使软件工程师们兽血沸腾,它使得应用系统(程序)可以以二进制模块的形式灵活地组建起来。比起源码级别的模块化,二进制级别的模块划分使得各模块更加独立,各模块可以分别编译和链接,模块的升级不会引起其它模块和主程序的重新编译,这点对于大系统的构建来说更加实用。另一方面,对于商业目的明显的企业,各模块可以独立设置访问权限,开发成员只能访问自己负责的模块,其它模块是不能也不给看到的,这样减少了整个系统泄漏技术的风险。

    一、动态链接库技术概况

    动态链接库技术用得很多。事实上,整个Windows就是由一个个动态链接库(DLL)构建起来的,不管是系统内核,或是系统调用的API封装,还是通用工具(如控制面板、ActiveX插件等),都是一个个动态链接库文件。动态链接库并不是微软独有的技术,它是软件工程发展到一定阶段的必然产物。在类Unix系统中,这种二进制可执行模块技术不叫动态链接库,而被称为共享对象或共享库,后缀名一般为.so(即Share Object的简写)。为简便,下文将统称这种动态链接的技术为DLL或共享库。

    其实,DLL文件跟普通的可执行文件差别不大,都是可执行文件嘛,装载到进程空间后,都是一些机器指令(函数代码)、内存分配(变量)等。在Windows中,这些可执行文件被称作PE/COFF格式文件,在Linux则称为ELF文件。从CPU的角度看来,程序中的各个要素,不管是函数还是变量,它们都是一个个地址,函数是入口地址,变量是访问地址;而C++的所谓类或对象,最后也被编译器肢解成了一个个变量和函数代码(这里是形象的说法,严谨技术解说请搜索C++对象模型)。DLL的装载(指导入进程空间,然后执行)方式比可执行文件的装载稍微复杂,因为它把模块链接过程推迟到了运行时。在动态链接库的装载过程中,首要任务就是解决地址重定向问题。我们知道,DLL装载到进程空间的位置(基址)是不确定的(动态装载嘛),即使DLL内部使用的函数调用和全局变量引用,在装载时都要重新计算其地址。Windows采用基址重定向(Rebasing)技术解决这一问题,而Linux采用地址无关代码(PIC,通过GOT和PLT表实现)技术。这两种技术各有优缺点。

    二、Qt中的动态链接库编程

    使用C++面向对象的类编写DLL是要注意很多细节的,主要是二进制(ABI)兼容问题。COM是一个很成功的例子,只要符合COM的规范,我们就能编写出很好的DLL来,然而COM是微软私生的,要想跨平台,我们还得另找它路。

    Qt的跨平台特性同样令人(至少是我)兽血沸腾。如果你认为QT仅仅是一个跨平台界面库,那就小看它了。我要说的是,它不但是一个通用的跨平台的面向对象的应用程序接口库(包括GUI、数据库、网络、多线程、XML、数据容器和算法等,常用的编辑资源都有封装,就是说,这些都可以跨平台,而不仅仅是界面),更是一种C++语言的扩展,一种编程平台和应用程序框架。信号和槽的机制简化了对象之间的通信,比MFC的消息映射直观多了;界面的布局管理机制使开发人员可以很轻松地编出优雅的窗体;界面语言翻译机制也很方便实用;QObject容器管理可以看到Qt在内存管理方面的努力;扩展的foreach循环结构也向现代语言靠拢……

    Qt的跨平台特性很好,对于本文的主题——动态链接库的支持也很好。QT对各种平台的动态链接库编程技术都有包装,QT把这种技术统一命名为共享库(Shared Libraries)。通过使用Qt包装过的类和宏,可以编写跨平台的共享库和插件——当然,这只是源代码级别的跨平台,你不要指望用MSVC编译出来的DLL,能集成到ARM平台的Linux程序上面——这是一个很美很美的理想哦。

    QT使用以下两个宏来实现符号(函数或全局变量/对象)的导出和导入(跨平台不能用def文件了):

    Q_DECL_EXPORT   // 必须添加到符号声明中(共享库项目)  
    Q_DECL_IMPORT   // 必须添加到符号声明中(使用共享库的客户项目) 
    Q_DECL_EXPORT   // 必须添加到符号声明中(共享库项目)
    Q_DECL_IMPORT   // 必须添加到符号声明中(使用共享库的客户项目)

    QT使用 QLibrary 类实现共享库的动态加载,即在运行时决定加载那个DLL程序,插件机制使用。

    三、QT共享库和插件范例

    本节通过例子,实现一个共享库和一个插件。在Windows平台上开发,使用VS2005编译,QT库版本为4.6.2。

    本例了将编写以下三类项目:

    Bil 项目:共享库项目,输出Bil.dll和Bil.lib,基础接口类库,定义一个公共的接口IAnimal(抽象类),供客户项目和插件项目使用;
    Plugin 类项目:插件类项目,现编写BilDog和BilPanda两插件项目,实现IAnimal的功能,供客户项目加载和测试。两项目输出BilDog.dll和BilPanda.dll;
    Test 项目:客户应用程序项目,输出Test.exe,界面中可以选择要加载的Animal插件,然后调用Animal的功能函数,完成测试;
    1. 编写共享库——Bil 项目的实现

    该项目定义一个抽象的 IAnimal 类作为导出接口,供客户项目和插件项目使用。项目类型为共享库,将生成Bil.lib和Bil.dll两个文件,Bil.lib供Plugin项目和Test 项目引用,而Bil.dll将给Test.exe运行时动态加载。

    新建一个头文件Bil.h,输入如下代码:

    #ifndef BIL_H  
    #define BIL_H  
    #include <Qt/qglobal.h>  
    // 定义BIL_SHARE,使用者可以不用再处理符号的导入和导出细节  
    #ifdef BIL_LIB  
    # define BIL_SHARE Q_DECL_EXPORT  
    #else  
    # define BIL_SHARE Q_DECL_IMPORT  
    #endif  
    #endif // BIL_H 
    #ifndef BIL_H
    #define BIL_H
    #include <Qt/qglobal.h>
    // 定义BIL_SHARE,使用者可以不用再处理符号的导入和导出细节
    #ifdef BIL_LIB
    # define BIL_SHARE Q_DECL_EXPORT
    #else
    # define BIL_SHARE Q_DECL_IMPORT
    #endif
    #endif // BIL_H

    你现在可能不知道BIL_SHARE宏有何用处。没关系,请继续看下面的IAnimal接口定义代码:

    #ifndef IANIMAL_H  
    #define IANIMAL_H  
    #include "Bil.h"  
    class BIL_SHARE IAnimal  
    {  
    public:  
        IAnimal();  
        virtual ~IAnimal();  
    public:  
        virtual void Eat() = 0;  
        virtual void Run() = 0;  
        virtual void Sleep() = 0;  
    };  
     
    #endif // IANIMAL_H 
    #ifndef IANIMAL_H
    #define IANIMAL_H
    #include "Bil.h"
    class BIL_SHARE IAnimal
    {
    public:
     IAnimal();
     virtual ~IAnimal();
    public:
     virtual void Eat() = 0;
     virtual void Run() = 0;
     virtual void Sleep() = 0;
    };

    #endif // IANIMAL_H

    现在知道BIL_SHARE宏的妙用了吧。BIL_SHARE宏会根据项目编译选项BIL_LIB有没有定义,自动声明IAnimal是导出类,还是导入类。所以,使用BIL_SHARE宏,我们只需要向IAnimal插件的开发者提供同一份IAnimal定义文件(IAnimal.h)即可。

    当然,我们得先在Bil项目的编译选项中定义BIL_LIB宏,使得在Bil项目内,BIL_SHARE就是导出符号的声明。插件项目就不要定义BIL_LIB了,因为在Animal插件项目中,IAnimal是导入符号。

    编译选项如何定义宏?如果使用Visual Studio工程文件,依次展开:项目属性->配置属性->C/C++->预处理器,在预处理器定义中添加宏BIL_LIB即可;如果是QT工程文件,请在QT工程文件Bil.pro中加入如下定义:

    DEFINES += BIL_LIB 
    DEFINES += BIL_LIB

    在IAnimal接口中,我们定义了三个纯虚函数Eat()、Run()和Sleep(),表示吃、跑和睡眠的动作,这是抽象的,因为不同的动物有不同的吃相和睡眠姿态,而世间的动物何止千千万——无所谓,让这些具体动物的不同表现交给IAnimal插件的编写者发挥吧——这就是接口的魅力,加上插件的思想,整个应用程序就变成开放的,可扩展的了!

    继续编写IAnimal类的实现文件IAnimal.cpp:

     #include "IAnimal.h"  
    IAnimal::IAnimal()  
    {  
    }  
    IAnimal::~IAnimal()  
    {  

    #include "IAnimal.h"
    IAnimal::IAnimal()
    {
    }
    IAnimal::~IAnimal()
    {
    }

    虽然只实现了构造和析构函数,并且什么工作也不做,但这是必要的,我们暂时不要使用内联的构造和析构函数,否则在插件项目实现IAnimal时可能会出现链接错误。

    好了,我们开始编译吧,生成整个Bil项目。最终我们得到两个输出文件:Bil.lib 和 Bil.dll。

    我们向Animal插件开发者提供:

    两个头文件:Bil.h 和 IAnimal.h
    两个库文件:Bil.lib 和 Bil.dll
    下面的插件类项目和客户项目就是依赖这些文件实现的,也许你更愿意把Bil看作是一个通用的DLL类库,就像QT或MFC一样——事实上也是如此,Bil就是这样一个动态的共享类库。

    2. 编写Animal插件——BilDog和BilPanda项目的实现

    现在,让我们来实现两个小插件。BilDog插件很简单,只是汇报下“我是Dog,我正在啃骨头”;BilPanda也是如此——这里仅仅是测试而已,实现的项目中,你可以尽情的发挥——没错,是在遵循IAnimal接口的前提下。

    创建BilDog项目,把Bil项目输出的Bil.h、IAnimal.h和Bil.lib加入到工程。

    创建Dog类的头文件Dog.h:

     #ifndef CLASS_DOG_H  
    #define CLASS_DOG_H  
    #include "IAnimal.h"  
    class Dog : public IAnimal  
    {  
    public:  
        Dog(void);  
        virtual ~Dog(void);  
    public:  
        virtual void Eat();  
        virtual void Run();  
        virtual void Sleep();  
    };  
    #endif // CLASS_DOG_H 
    #ifndef CLASS_DOG_H
    #define CLASS_DOG_H
    #include "IAnimal.h"
    class Dog : public IAnimal
    {
    public:
     Dog(void);
     virtual ~Dog(void);
    public:
     virtual void Eat();
     virtual void Run();
     virtual void Sleep();
    };
    #endif // CLASS_DOG_H

    创建Dog类的实现文件Dog.cpp:

    #include <QtGui/QMessageBox>  
    #include "Dog.h"  
    Dog::Dog(void)  
    {  
    }  
    Dog::~Dog(void)  
    {  
    }  
    void Dog::Eat()  
    {  
        QMessageBox::information(NULL, "Hello", "Dog eating ...");  
    }  
    void Dog::Run()  
    {  
        QMessageBox::information(NULL, "Hello", "Dog running ...");  
    }  
    void Dog::Sleep()  
    {  
        QMessageBox::information(NULL, "Hello", "Dog sleeping ...");  

    #include <QtGui/QMessageBox>
    #include "Dog.h"
    Dog::Dog(void)
    {
    }
    Dog::~Dog(void)
    {
    }
    void Dog::Eat()
    {
     QMessageBox::information(NULL, "Hello", "Dog eating ...");
    }
    void Dog::Run()
    {
     QMessageBox::information(NULL, "Hello", "Dog running ...");
    }
    void Dog::Sleep()
    {
     QMessageBox::information(NULL, "Hello", "Dog sleeping ...");
    }

    调用QT的QMessageBox::information()函数弹出一个信息提示框。

    还有一个非常重要的工作,我们得提供一个能够创建(释放)Animal具体对象(这里是Dog)的接口,并且把这些函数导出,让主程序(Test.exe)能够解析这个接口函数,动态创建Animal对象,并访问其功能。

    新建BilDog.h文件,输入下面的代码:

    #ifndef BILDOG_H  
    #define BILDOG_H  
    #include "Dog.h"  
     
    // extern "C" 生成的导出符号没有任何修饰,方便主程序找到它  
    extern "C" 
    {  
        Q_DECL_EXPORT IAnimal * CreateAnimal();  
        Q_DECL_EXPORT void ReleaseAnimal(IAnimal * animal);  
    }  
    #endif // BILDOG_H 
    #ifndef BILDOG_H
    #define BILDOG_H
    #include "Dog.h"

    // extern "C" 生成的导出符号没有任何修饰,方便主程序找到它
    extern "C"
    {
     Q_DECL_EXPORT IAnimal * CreateAnimal();
     Q_DECL_EXPORT void ReleaseAnimal(IAnimal * animal);
    }
    #endif // BILDOG_H

    这两个函数的工作很简单,直接创建和释放对象即可。
    下面是BilDog.cpp的代码:

    #include "bildog.h"  
     
    IAnimal * CreateAnimal()  
    {  
        return new Dog();  
    }  
     
    void ReleaseAnimal(IAnimal * animal)  
    {  
        delete animal;  

    #include "bildog.h"

    IAnimal * CreateAnimal()
    {
     return new Dog();
    }

    void ReleaseAnimal(IAnimal * animal)
    {
     delete animal;
    }

    至此,一个Animal插件总算完成了。编译,生成BilDog项目,输出BilDog.dll插件文件,以供主程序Test.exe动态调用。

    BilPanda项目和BilDog项目类似,在这里就不把代码贴出来了。以后开发Animal插件(即使是第三方)的过程都是如此。

    我们不打算输出该项目的.lib文件和那些头文件,因为我们打算让主程序在运行时刻根据需要装载dll插件和调用插件的功能,而不是让主程序项目在编译时就指定具体的插件。

    3. 编写客户程序——Test项目的实现

    Test项目是一个测试程序项目,但它的角色是主程序,是能使用Animal插件的客户程序。

    同样,这个项目用到了Bil共享库,所以得先把Bil项目的几个输出文件导入到Test项目。

    我们假设Test主程序是一个对话框,上面有一个编辑框和一个“加载并调用”按钮,终端用户在编辑框中输入Animal插件的文件名(比如BilDog,后缀名可省略,Qt会根据平台判断该查找.dll还是.so),点击“加载并调用”进行共享库的加载,并调用动态创建的IAnimal对象的Eat()函数(当然你可以调用Run()函数或Sleep(),这里仅仅是一个示例)。

    下面的函数将被“加载并调用”按钮的触发事件调用:

    // ...  
    #include <QString>  
    #include <QLibrary>  
    #include <IAnimal.h>  
     
    // ...  
     
    // strPluginName为插件的名称,可省略后缀  
    void MainDlg::LoadAndAction(QString strPluginName)  
    {  
        // 加载插件dll  
        QLibrary lib(strPluginName);  
        if (lib.load())  
        {  
            // 定义插件中的两个导出函数的原型  
            typedef IAnimal* (*CreateAnimalFunction)();  
            typedef void (*ReleaseAnimalFunction)(IAnimal* animal);  
     
            // 解析导出函数  
            CreateAnimalFunction createAnimal =   
                    (CreateAnimalFunction) lib.resolve("CreateAnimal");  
            ReleaseAnimalFunction releaseAnimal =   
                    (ReleaseAnimalFunction) lib.resolve("ReleaseAnimal");  
     
            if (createAnimal && releaseAnimal)  
            {  
                // 创建Animal对象  
                IAnimal * animal = createAnimal();  
                if (animal)  
                {  
                    // 使用插件功能  
                    animal->Eat();  
                    animal->Sleep();  
                    // 插件使用完毕,删除对象  
                    releaseAnimal(animal);  
                }  
            }  
            // 卸载插件  
            lib.unload();  
        }  

    // ...
    #include <QString>
    #include <QLibrary>
    #include <IAnimal.h>

    // ...

    // strPluginName为插件的名称,可省略后缀
    void MainDlg::LoadAndAction(QString strPluginName)
    {
     // 加载插件dll
     QLibrary lib(strPluginName);
     if (lib.load())
     {
      // 定义插件中的两个导出函数的原型
      typedef IAnimal* (*CreateAnimalFunction)();
      typedef void (*ReleaseAnimalFunction)(IAnimal* animal);

      // 解析导出函数
      CreateAnimalFunction createAnimal =
        (CreateAnimalFunction) lib.resolve("CreateAnimal");
      ReleaseAnimalFunction releaseAnimal =
        (ReleaseAnimalFunction) lib.resolve("ReleaseAnimal");

      if (createAnimal && releaseAnimal)
      {
       // 创建Animal对象
       IAnimal * animal = createAnimal();
       if (animal)
       {
        // 使用插件功能
        animal->Eat();
        animal->Sleep();
        // 插件使用完毕,删除对象
        releaseAnimal(animal);
       }
      }
      // 卸载插件
      lib.unload();
     }
    }

    生成Test项目,输出Test.exe。我们把Test.exe、Bil.dll、BilDog.dll、BilPanda.dll放在同一目录,双击运行Test.exe,赶快试下效果吧!注意BilDog.dll或BilPanda.dll依赖于基础接口库Bil.dll,如果系统找不到Bil.dll,将不能加载BilDog.dll或BilPanda.dll,所以请把它们放在同一目录。

    四、一些遗憾

    DLL的愿望是美好的,只要接口一致,用户可以任意更换模块。但如果不注意细节,很容易陷入它的泥潭中,这就是传说中的DLL Hell(DLL地狱)!

    引起DLL地狱问题的主要原因有以下几点:

    1. 版本控制不好(主要是接口的版本)

        DLL是共享的,如果某程序更新了一个共享的DLL,其它同样依赖于该DLL的程序就可能不能正常工作了!

    2. 二制兼容问题(ABI)

        即使同一平台,不同编译器(甚至同一编译器的不同版本)编出来的共享库和程序也可能不能协同工作。

        二制兼容问题对于C++来说尤其严重。C++的标准是源代码级别的,标准中并没有对如何实现C++作出统一的规定,所以不同的编译器,对标准C++采用不同的实现方式。这些差异主要有:对象在内存中的分配(C++)、构造和析构函数的实现(C++)、重载和模板的实现(C++)、虚函数表结构(C++)、多重继承和虚基类的实现(C++)、函数调用约定(C)、符号修饰(C/C++)等。此外,不同的运行时库(CRT、STL等标准库)也会引起ABI兼容问题。可以说,如果你在编写基于类的共享库,如果接口(指导出类)稍有改变,新的DLL与原程序就可能不协同工作了。

    关于二进制兼容问题,大家可以参考KDE官网上的一篇文章《Policies/Binary Compatibility Issues With C++ 》

    http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++

    不过这些都不是大问题,毕竟我们不是编写像Qt一样的通用库。我们引入DLL划分应用程序的模块,目的是减小系统开发和后期升级维护的难度,同时方便项目的管理。如果用户想自己编写插件模块,就得使用我们指定的编译平台和类接口。所以我们仍能从DLL技术中得到很大的实惠。

  • 相关阅读:
    169. Majority Element
    283. Move Zeroes
    1331. Rank Transform of an Array
    566. Reshape the Matrix
    985. Sum of Even Numbers After Queries
    1185. Day of the Week
    867. Transpose Matrix
    1217. Play with Chips
    766. Toeplitz Matrix
    1413. Minimum Value to Get Positive Step by Step Sum
  • 原文地址:https://www.cnblogs.com/kenter/p/2021551.html
Copyright © 2011-2022 走看看