C++ 插件机制的实现原理、过程、及使用

什么是插件机制

插件是你想开发一个好的系统所需要的一种好的架构方式。C++插件是 C++ 编写的动态链接共享对象。一种可复用的、灵活管理(维护、替换或增加、删除)的功能模块儿化组件。基于插件的扩展性,进而实现业务模块儿的独立和解耦,增加可维护性和可扩展性。插件使得第三方开发人员可以为系统做增值工作,也可以使其他开发人员协同开发相互配合,增加新的功能而不破坏现有的核心功能。插件能够促进将关注点分开,保证隐藏实现细节,且可以将测试独立开来,并最具有实践意义。

比如强大的Eclipse的平台实际上就是一个所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java开发环境)仅仅是一系列挂在核心框架上的插件。

插件机制仍需要考虑的一些问题如错误处理,数据类型,版本控制,与框架代码以及应用代码的分离等等。或许,在应用程序框架容器内,可以借助lua脚本来动态的灵活的实现业务。

为什么要用插件机制

我们为什么要用插件架构?

 现代软件工程已经从原先的通用程序库逐步过渡到应用程序框架。假设一个场景,以C++开发应用程序为例,我们的架构是基于APP+DLL的传统架构,所有的功能糅杂在一起。随着系统的日益庞大,各种模块之间耦合在一起,当修改其中一个模块时,其他模块也跟着一起受到影响。假如这两个模块式不同的开发人员负责的,那么还需要事先沟通好,这样就造成了修改维护的困难。那怎么解决这个问题?插件架构是一种选择。

“编程就是构建一个一个自己的小积木, 然后用自己的小积木搭建大系统”。但是程序还是会比积木要复杂, 我们的系统必须要保证小积木能搭建出大的系统(必须能被组合),有必须能使各个积木之间的耦合降低到最小。

传统的程序结构中也是有模块的划分,但是主要有如下几个缺点:

1: c++二进制兼容问题。

2: 模块对外暴露的东西过多,使调用者要关心的东西过多。

3: 封装的模块只是作为功能的实现者封装,而不是接口的提供者。

4: 可替换性和可扩展性差。

而插件式的系统架构就是为了解决这样的问题,插件机制也很符合设计模式的六大原则(现在是七大原则),是一种不错的设计。设计模式的六大原则里提到的单一职责原则、开放封闭原则、接口隔离原则、里氏替换原则,依赖倒转原则、迪米特法则。可以说插件机制几乎满足了这六大原则里所有的条款,当然也具备了由此带来的益处,因此学习和使用插件机制很有必要。

设计模式七大原则

都是为了更好的代码重用性,可读性,可靠性,可维护性,可扩展性。

单一职责原则:

即一个类应该只负责一项职责,降低类的复杂度,免得改了一个影响另一个。提高类的可读性,可维护性,降低变更引起的风险。插件机制的各个插件模块就是一种单一职责。

开闭原则:
一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。插件机制不正好的这个原则的实现吗。

迪米特法则:

一个对象应该对其他对象保持最少的了解。类与类关系越密切,耦合度越大。
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。插件机制的实现是这一法则很好的诠释。

接口隔离原则:

客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。后面的插件实现过程可以看到,插件所提供的接口是精简和必要的最小单元。

依赖倒转原则:

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。插件机制的实现上,插件提供的接口可以看作是一种高层模块,不依赖于底层实现细节。

里氏替换原则:

所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,引申含义为:子类可以扩展父类的功能,但不能改变父类原有的功能。

合成复用原则:

该原则阐述的是我们应该如何复用类。复用类我们可以通过“继承”和“合成”两种方式来实现。它最大的缺点就是增加了类之间的依赖,当父类发生改变时,其子类也会被动发生改变。介于继承存在的这些缺点,我们在复用类时,要优先考虑使用“合成”进行复用。合成复用原则的核心思想是:在编写代码时如果需要使用其它类,那么两个类之间尽量使用合成/聚合的方式,而不是使用继承。我们可以通过类之间的“合成”来达到“复用”代码的诉求。

插件机制的实现原理

大致思路是应用程序提供出接口,由其他同事分工或第三方实现这些接口,并编译出相应的动态链接库(即插件);将所有插件放到某个特定目录,应用程序运行时会自动搜索该目录,并动态加载目录中的插件。

插件机制的实现过程

一、首先定义一个插件接口Iplugin.h。

这有点儿类似设计模式中的中介者模式。所有的插件模块儿之间接触,都通过这一公共的中间人交互。

#ifndef SERVICEPROJECT_IPLUGIN_H
#define SERVICEPROJECT_IPLUGIN_H

#include <QString>
#include <QStringList>

class IPlugin
{
public:
  IPlugin() = default;
  virtual ~IPlugin() = default;

  IPlugin(const IPlugin&) = delete;
  IPlugin& operator=(const IPlugin&) = delete;

public:
   //获取支持的插件方法
  virtual const QStringList& getSupportCommandList() const {return supportCommandList_; }


public:
  virtual QString getVersionString() const = 0;
  //! \brief 获取业务名称
  //! \return
  virtual QString getPluginName() const = 0;

  enum pluginState_type
  {
    idle_,
    starting_,
    started_,
    stopping_,
    stopped_,
    excepting_,
  };
  //! 获取插件的运行状态。
  //! \return
  virtual pluginState_type getPluginState() const = 0;
  //! 获取插件的运行状态的解释
  //! \param state
  //! \return
  static QString getPluginStateMessage(const pluginState_type& state)
  {
    QString msg = "no msg.";
    switch (state)
    {
      case pluginState_type::idle_:
        msg = "plugin is on idle statement.";
        break;
      case pluginState_type::starting_:
        msg = "plugin is on starting statement.";
        break;
      case pluginState_type::started_:
        msg = "plugin is on started statement.";
        break;
      case pluginState_type::stopping_:
        msg = "plugin is on stopping statement.";
        break;
      case pluginState_type::stopped_:
        msg = "plugin is on stopped statement.";
        break;
      case pluginState_type::excepting_:
        msg = "plugin is on excepting statement.";
        break;
    }

    return msg;
  }

  //! \brief 执行业务的主要接口方法
  //! \param msg 由外部程序发送的通讯协议(比如json报文)
  //! \return 错误信息
  virtual std::error_code exec(const QString& msg) = 0;
  //! \brief 业务停止运行
  //! \return
  virtual std::error_code stop() { return {}; }
  //! \brief 释放动态库资源
  //! \return 错误信息
  virtual std::error_code release() = 0;

protected:
  QStringList supportCommandList_;
};

#endif  // SERVICEPROJECT_IPLUGIN_H

二、实现插件加载,注册等操作的管理类PluginManager。

遍历lib目录中的各个插件动态库,如plugin1.dll,plugin2.dll,等,完成插件的加载和注册。使用QT的QLibrary,(instance)lib->resolve("getInstance"),这里很关键,调用resolve()函数找到dll库中的getInstance函数,并强制转换为函数指针。后又强制转换为(IPlugin *)类型指针存储进QHash。

#include <QHash>
#include <QObject>

#if defined(PLUGIN_MANAGER_BUILD_SHARED)
#define PLUGIN_MANAGER_EXPORT __declspec(dllexport)
#else
#define PLUGIN_MANAGER_EXPORT __declspec(dllimport)
#endif

class IPlugin;
class QLibrary;

class PLUGIN_MANAGER_EXPORT PluginManager : public QObject
{
  Q_OBJECT
private:
  explicit PluginManager(QObject* parent = nullptr);

public:
  static PluginManager* getInstance();
  ~PluginManager() override;

  static const char* getLibraryName();
  static const char* getLibraryVersion();

public:
  void loadAll();
  void unloadAll();
  std::error_code load(const QString& name);
  std::error_code unload(const QString& name);

  QStringList getNames() const { return plugins_.keys(); }
  IPlugin* get(const QString& name);

private:
  QHash<QString, IPlugin*> plugins_;
  QHash<QString, QLibrary*> libs_;
};
//......
typedef void *(*instance)();

PluginManager::PluginManager(QObject *parent) : QObject(parent)
{
}
PluginManager::~PluginManager()
{
  unloadAll();
}
void PluginManager::loadAll()
{
  QDir pluginDir(QCoreApplication::applicationDirPath() + R"(/../lib)");
  QStringList filters;
  filters << QString("*%1").arg(LIB_SUFFIX);
  pluginDir.setNameFilters(filters);

  auto entryInfoList = pluginDir.entryInfoList();
  for (const auto &info : entryInfoList)
  {
    auto lib = new QLibrary(QCoreApplication::applicationDirPath() + R"(/../lib/)" + info.fileName());
    if (lib->isLoaded())
    {
      LOGGING_WARN("%s is loaded.", info.fileName().toStdString().c_str());
      continue;
    }

    if (lib->load())
    {
      auto func = (instance)lib->resolve("getInstance");
      if (func)
      {
        auto plugin = (IPlugin *)func();
        if (plugin)
        {
          auto pluginName = plugin->getPluginName();
          if (plugins_.contains(pluginName))
          {
            LOGGING_WARN("%s repeated loading.", pluginName.toStdString().c_str());
            lib->unload();
            lib->deleteLater();
            lib = nullptr;
            continue;
          }

          plugins_.insert(pluginName, plugin);
          libs_.insert(pluginName, lib);

          LOGGING_DEBUG("%s version: %s", plugin->getPluginName().toStdString().c_str(),
                        plugin->getVersionString().toStdString().c_str());
        }
        else
        {
          LOGGING_ERROR("%s object create failed.", info.fileName().toStdString().c_str());
          lib->unload();
          lib->deleteLater();
          lib = nullptr;
        }
      }
      else
      {
        LOGGING_ERROR("%s cannot find symbol.", info.fileName().toStdString().c_str());
        lib->unload();
        lib->deleteLater();
        lib = nullptr;
      }
    }
    else
    {
      LOGGING_ERROR("%s load failed. error message: %s", info.fileName().toStdString().c_str(),
                    lib->errorString().toStdString().c_str());
      lib->deleteLater();
      lib = nullptr;
    }
  }
}
void PluginManager::unloadAll()
{
  for (auto &item : plugins_)
  {
    item->release();
  }

  for (auto &item : libs_)
  {
    item->unload();
    item->deleteLater();
    item = nullptr;
  }

  plugins_.clear();
  libs_.clear();
}
......
IPlugin *PluginManager::get(const QString &name)
{
  if (plugins_.contains(name))
  {
    return plugins_.value(name);
  }

  LOGGING_ERROR("%s is not found.", name.toStdString().c_str());
  return nullptr;
}
PluginManager *PluginManager::getInstance()
{
  static PluginManager w;
  return &w;
}
const char *PluginManager::getLibraryName()
{
  return PROJECT_NAME;
}
const char *PluginManager::getLibraryVersion()
{
  return PROJECT_VERSION;
}

三、插件模块的实现

各个插件模块对外提供一唯一的入口函数getInstance()。

注意:该函数必须为 extern "C"声明的。why?

为什么要这么做呢?原因是C++的编译器会对程序中符号进行修饰,这个过程在编译器中叫符号修饰(Name Decoration)或者符号改编(Name Mangling)。如果不改为c的方式,那么动态库resolve这种查找入口方式,会找不到句柄handle入口。

在这个getInstance函数内部,可以实例化具体的类对象(前提这类对象实现了IPlugin接口)。

//myplugin1dll.h
extern "C" __declspec(dllexport) void *getInstance();

//myplugin1dll.cpp
void* getInstance()
{
  static MyPlugin1 w;
  return (void*)&w;
}

//MyPlugin1.h
class MyPlugin1: public IPlugin
{
public:
  MyPlugin1();
  ~MyPlugin1() override;

public:
  QString getVersionString() const override;
  //! \brief 获取业务名称
  //! \return
  QString getPluginName() const override;
  //! 获取插件的运行状态。
  //! \return
  pluginState_type getPluginState() const override;

  //! \brief 执行业务
  //! \param msg 由外部程序发送的通讯协议
  //! \return 错误信息
  std::error_code exec(const QString &msg) override;
  //! \brief 业务停止运行
  //! \return
  std::error_code stop() override;
  //! \brief 释放动态库资源
  //! \return 错误信息
  std::error_code release() override;
};

//MyPlugin1.cpp
......
#define MY_PLUGIN_NAME "myplugin1"
MyPlugin1::MyPlugin1()
{
    //关键,这里追定了本插件的插件名,插件管理类对象PluginManager会根据这个找到自己
    supportCommandList_.push_back(MY_PLUGIN_NAME);
}
MyPlugin1::~MyPlugin1()
{
}

QString MyPlugin1::getVersionString() const
{
  return PROJECT_VERSION;
}
QString MyPlugin1::getPluginName() const
{
  return PROJECT_NAME;
}
IPlugin::pluginState_type MyPlugin1::getPluginState() const
{
  return IPlugin::starting_;
}
std::error_code MyPlugin1::exec(const QString& msge)
{
   //业务功能实现......
}

举例插件机制的使用

经过前面三步,准备工作做好了。如何使用看看效果呢?

我们写一个测试的do_pluginWork(const QString& msg, const QString& cmd)。

其中的cmd内容指定插件名称。

实现过程为遍历PluginManager中管理的所有插件名,找到对应的并传递调用参数msg。

假如实现了两个插件plugin1.dll和plugin2.dll ,do_pluginWork("hello","plugin1"),则会调用plugin1中的exec函数功能。

//......
do_pluginWork(const QString& msg, const QString& cmd)
{
  auto allPluginNames = PluginManager::getInstance()->getNames();
  for (const auto& name : allPluginNames)
  {
    auto pluginPtr = PluginManager::getInstance()->get(name);
    if (pluginPtr)
    {
      if (pluginPtr->getSupportCommandList().contains(cmd))
      {
        auto errorCode = pluginPtr->exec(msg);
        if (errorCode.value())
        {
          LOGGING_ERROR("[%s] result message: %s", pluginPtr->getPluginName().toStdString().c_str(),
                        errorCode.message().c_str());
        }
        LOGGING_DEBUG("%s is executed.", cmd.toStdString().c_str());
        return;
      }
    }
  }

  LOGGING_ERROR("cannot found match command.");
}

最后,插件机制是不是挺不错的,愉快的使用吧。

引用:

利用C++实现插件系统_猫咪的晴天的博客-CSDN博客_c++ 插件系统

设计模式六大原则 - 迪米特 - 博客园

C++ 插件系统_qq_32250025的博客-CSDN博客_c++ 插件

C++插件架构浅谈与初步实现_臣有一事不知当不当讲的博客-CSDN博客_c++插件

构建自己的C/C++插件开发框架_加油努力4ever的博客-CSDN博客_c++插件框架

C/C++:构建你自己的插件框架_石头的博客-CSDN博客_c 插件框架

软件设计七大原则,看完这一篇就够了_凹凸曼蓝博one的博客-CSDN博客_合成复用原则

C++实现插件化开发_gnr_123的博客-CSDN博客_c++ 插件化

C++插件架构浅谈与初步实现_周旭光的博客-CSDN博客_c++插件框架

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值