【工作常用】C++/QT插件编程思想——即插即用

发布于:2025-07-23 ⋅ 阅读:(20) ⋅ 点赞:(0)

为企业中一个实际项目的插件框架说明
并且补充了QT的插件编程方法

C++插件编程

Plugins_Framework如何拉起一个项目

在这里插入图片描述
在这里插入图片描述

main.cpp

    // init framework
    auto f = frameworkInstance(); 
    frameworkInit(f, ExeUtils::getExeDirPath(), "Plugin");
  • frameworkInit
void frameworkInit(Framework *f, std::string pluginDirPath, std::string tag)
{
    init(f, std::move(pluginDirPath), std::move(tag));
}
  • init(关注frameworkLoadPlugins!!)
static Coroutine<void> init(Framework *f, std::string pluginDirPath, std::string tag)
{
    f->pluginDirPath = std::move(pluginDirPath);
    //加载插件(这里体现即插即用,加载了所有用的插件也就是说加载了你自己的项目要用的所用动态库所以下面都是只对动态库操作)
    frameworkLoadPlugins(f, f->pluginDirPath.c_str(), tag.c_str());

    std::sort(f->modules.begin(), f->modules.end(), [](auto &l, auto &r)
              { return l.level() < r.level(); });
//用协程拉起来~
    co_await initModule(f);//加载项目依赖,dll

    co_await runModule(f);//让项目跑起来
}

1.通过加载插件加载找到项目所有用的动态库的指针并复制给管理加载的框架(即插即用就是在这!)

  • frameworkLoadPlugins
static void frameworkLoadPlugins(Framework *f, const char *dirPath, const char *tag)
{
//在 dirPath 目录下,查找所有以 .dll 结尾的文件(即所有插件文件)。
    std::vector<std::string> plugins = FileUtils::getAllFilePaths(dirPath, ".dll");
    //遍历所有找到的插件文件。筛选:只有文件名中包含 tag 字符串的插件才会被加载
    //(用于按标签过滤插件)。
    //加载:调用 frameworkLoadPlugin 函数,把符合条件的插件加载到框架中
    //(通常会把插件里的模块对象注册到 f->modules 列表)。
    for (auto &plugin : plugins)
    {
        if (plugin.find(tag) == std::string::npos)
        {
            continue;
        }

        frameworkLoadPlugin(f, plugin.c_str());
    }

    FRAMEWORK_INFO() << "Load Library Finish! "
                     << "Size: " << f->modules.size();
    for (auto &m : f->modules)
    {
        FRAMEWORK_INFO() << "Module Info"
                         << " Path: " << m.path
                         << " Name: " << m.name()
                         << " Desc: " << m.desc();
    }
}
  • frameworkLoadPlugin
//动态加载一个DLL插件,查找并绑定其导出的模块信息和操作函数,
//调用其load函数进行初始化,成功后将模块注册到框架中,失败则关闭DLL并返回错误。
//Framework *f  传入某个Framework的句柄正是为了加载
static bool frameworkLoadPlugin(Framework *f, const char *path)
{
    void *dll = DllUtils::dllOpen(path, 0);
    if (dll == nullptr)
    {
        FRAMEWORK_WARNING() << "Open Library Failure! "
                            << "Path: " << path << "Error Code: " << GetLastError();
        return false;
    }
//通过 DllUtils::dllSymbol 查找 DLL 中导出的函数指针,并赋值给模块对象的成员:
    FrameworkModule m;
    m.dll = dll;
    m.path = path;
    m.level = (int32_t(*)())DllUtils::dllSymbol(dll, "plugin_module_level");
    m.name = (const char *(*)())DllUtils::dllSymbol(dll, "plugin_module_name");
    m.desc = (const char *(*)())DllUtils::dllSymbol(dll, "plugin_module_desc");
    m.load = (bool (*)(struct FrameworkModule *))DllUtils::dllSymbol(dll, "plugin_module_load");
    m.unload = (bool (*)(struct FrameworkModule *))DllUtils::dllSymbol(dll, "plugin_module_unload");
    m.init = nullptr;
    m.run = nullptr;
    m.stop = nullptr;

    if (m.load == nullptr || !m.load(&m))
    {
        FRAMEWORK_WARNING() << "Load Library Failure! "
                            << "Path: " << path;
        DllUtils::dllClose(dll);
        return false;
    }

    f->modules.emplace_back(m);

    return true;
}

2.通过操作动态库对象加载项目所有动态库

  • initModule

curLevel:当前正在处理的优先级(level),初始为最大值。
initModules:当前优先级下待初始化的模块指针列表。
initCoros:当前优先级下每个模块的初始化协程对象列表。

static Coroutine<void> initModule(Framework *f)
{
    int32_t curLevel = INT32_MAX;

    std::vector<FrameworkModule*> initModules;
    std::vector<Coroutine<void>> initCoros;
    //遍历所有模块
    for (auto &m : f->modules)
    {
        auto level = m.level();
//判断优先级变化;如果当前模块的优先级(level)比上一个模块的优先级低(数字大),说明上一个优先级的模块收集完毕,需要先初始化它们。
        if (level > curLevel)
        {
            for (int i = 0; i < initCoros.size(); i++)
            {
                auto &coro = initCoros[i];
                co_await coro;
                auto &m = initModules[i];
                FRAMEWORK_INFO() << "name:" << m->name() << " desc:" << m->desc() << " 初始化完成";
                std::cout << "name:" << m->name() << " desc:" << m->desc() << " 初始化完成" << std::endl;
            }

            initCoros.clear();
            initModules.clear();
        }

        curLevel = level;
        FRAMEWORK_INFO() << "name:" << m.name() << " desc:" << m.desc() << " 正在初始化......";
        std::cout << "name:" << m.name() << " desc:" << m.desc() << " 正在初始化......" << std::endl;
        initCoros.emplace_back(m.init());
        initModules.emplace_back(&m);
    }
//初始化同一优先级的所有模块
    for (int i = 0; i < initCoros.size(); i++)
    {
        auto &coro = initCoros[i];
        co_await coro;
        auto &m = initModules[i];
        FRAMEWORK_INFO() << "name:" << m->name() << " desc:" << m->desc() << " 初始化完成";
        std::cout << "name:" << m->name() << " desc:" << m->desc() << " 初始化完成" << std::endl;
    }
    //清空,准备收集下一个优先级的模块
    initCoros.clear();
    initModules.clear();

    std::cout << "----------插件初始化完成------------" << std::endl;
}

co_await runModule(f);//让项目跑起来(显示主页面)

主页面的管理(主页面—>分页面)

在这里插入图片描述
在这里插入图片描述

框架图

main的流程

在这里插入图片描述

具体进入famework的流程(只是提供了初始化/加载等方法接口如何获取还是在plugin_main)

在这里插入图片描述

plugin_main的结构

在这里插入图片描述

dll framework运行图

在这里插入图片描述

总结

1.完整的插件群包括:插件的实现(制作有plugin名称的dll文件以便筛选)+插件管理者+plugin_main(插件主体)
plugin_main的作用:在使用framework时候,获取插件信息
2.插件主体(plugin_main)实现 init、run、stop 协程
3.主插件通过manger调用子插件
4.将所有插件+plugin_main封装为dll库
5.通过加载插件加载找到项目所有用的动态库的指针并复制给管理加载的框架,即加载所有的动态库到framework实例
6.在framework实例操作所有的动态库初始化项目所有的动态库

// 插件接口(interface)
class IPlugin
{
public:
    virtual ~IPlugin() {} // 虚析构函数,保证多态删除安全
    virtual const char* name() const = 0; // 获取插件名
    virtual bool init() = 0;              // 初始化
    virtual void run() = 0;               // 运行
    virtual void stop() = 0;              // 停止
};
//任何插件都要继承 IPlugin 并实现所有纯虚函数。

QT插件编程

Qt 提供了两种API用于创建插件:一种是高阶 API,用于扩展 Qt 本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶 API,用于扩展 Qt 应用程序。

  • QT的插件开发至少分为两部分:主程序部分和插件程序部分。

其中主程序部分定义插件的接口并提供插件的管理器用于管理插件的加载与使用;
插件程序部分用于按照主程序中定义的插件接口来定义插件,最终实现插件的功能,并生成供主程序部分调用的插件。

QT插件主程序开发流程

要想使用插件来扩展应用程序,那么首先在主程序中的步骤如下:

(1)、定义一组用于与插件通信的接口(只有纯虚函数的类)

(2)、使用 Q_DECLARE_INTERFACE() 宏来告诉 Qt 元对象系统有关接口的情况

// IGreeter.h
#ifndef IGREETER_H
#define IGREETER_H

#include <QtPlugin>
#include <QString>

// 插件接口(纯虚类)
class IGreeter
{
public:
    virtual ~IGreeter() {}
    virtual QString greet(const QString &name) = 0;
};

#define IGreeter_iid "org.example.IGreeter"
Q_DECLARE_INTERFACE(IGreeter, IGreeter_iid)

#endif // IGREETER_H

这是一个只有纯虚函数的类,作为插件的接口。
IMyPlugin_iid 是接口的唯一标识符,建议用域名倒写+接口名。
Q_DECLARE_INTERFACE 宏用于后续Qt的类型识别。

(3)、在应用程序中使用 QPluginLoader 加载插件

(4)、使用 qobject_cast() 来测试插件是否实现了指定的接口

QDir::currentPath()

获取当前程序运行目录的路径(比如你的exe所在的文件夹)。

  • “/myplugin.dll”

拼接出插件文件的完整路径。

Windows下插件通常是.dll文件,比如myplugin.dll Linux下插件通常是.so文件,比如libmyplugin.so
QPluginLoader loader(pluginPath);

创建一个插件加载器对象,准备加载指定路径的插件库文件。

// main.cpp
#include <QCoreApplication>
#include <QPluginLoader>
#include <QDir>
#include <QDebug>
#include "IMyPlugin.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    // 3. 加载插件
    QString pluginPath = QDir::currentPath() + "/myplugin.dll"; // Windows下
    // QString pluginPath = QDir::currentPath() + "/libmyplugin.so"; // Linux下

    QPluginLoader loader(pluginPath);
    QObject *pluginInstance = loader.instance();
    if (!pluginInstance) {
        qDebug() << "Failed to load plugin:" << loader.errorString();
        return -1;
    }

    // 4. 使用qobject_cast测试接口
    IMyPlugin *myPlugin = qobject_cast<IMyPlugin *>(pluginInstance);
    if (myPlugin) {
        qDebug() << "Plugin name:" << myPlugin->pluginName();
        myPlugin->doWork();
    } else {
        qDebug() << "Plugin does not implement IMyPlugin interface!";
    }

    return 0;
}

QPluginLoader 用于动态加载插件(.dll/.so/.dylib)。 loader.instance()
返回插件的QObject实例指针。

QT插件程序开发流程

编写扩展 Qt 应用程序的插件,步骤如下:

(1)、声明一个继承自 QObject 的插件类,在类中定义想要提供的接口

// IGreeter.h
#ifndef IGREETER_H
#define IGREETER_H

#include <QtPlugin>
#include <QString>

// 插件接口(纯虚类)
class IGreeter
{
public:
    virtual ~IGreeter() {}
    virtual QString greet(const QString &name) = 0;
};

#define IGreeter_iid "org.example.IGreeter"
Q_DECLARE_INTERFACE(IGreeter, IGreeter_iid)

#endif // IGREETER_H

(2)、使用 Q_INTERFACES() 宏来告诉 Qt 元对象系统有关接口的情况

这个宏让Qt元对象系统知道你的插件实现了IGreeter接口,便于后续qobject_cast类型识别。

(3)、使用 Q_PLUGIN_METADATA() 宏导出插件

这个宏用于导出插件,IID必须和接口声明时的Q_DECLARE_INTERFACE保持一致。

// GreeterPlugin.h
#ifndef GREETERPLUGIN_H
#define GREETERPLUGIN_H

#include <QObject>
#include "IGreeter.h"

// 1. 插件类继承QObject和接口
class GreeterPlugin : public QObject, public IGreeter
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID IGreeter_iid) // 3. 导出插件元数据
    Q_INTERFACES(IGreeter)              // 2. 告诉Qt实现了哪个接口

public:
    QString greet(const QString &name) override {
        return QString("Hello, %1!").arg(name);
    }
};

#endif // GREETERPLUGIN_H

(4)、使用合适的 .pro 文件构建插件

QT       += core
CONFIG   += plugin c++11
TEMPLATE = lib

TARGET = greeterplugin

HEADERS += GreeterPlugin.h \
           IGreeter.h

SOURCES +=

# Windows下生成dll,Linux下生成so

CONFIG += plugin 表示这是一个插件项目。
TEMPLATE = lib 生成动态库(.dll/.so/.dylib)。
TARGET 是生成的库名。
HEADERS 包含所有头文件。
SOURCES 如果有cpp文件要加进来(本例中所有实现都在头文件里)。

主程序和插件程序区别与联系总结

  • 接口文件(如IGreeter.h):主程序和插件都要包含,保证双方“说同一种语言”。
  • 主程序通过接口指针调用插件功能,而不关心插件的具体实现。
  1. 主程序(宿主程序、Host)
    作用:负责加载插件、调用插件接口。
    代码特点:
    包含插件接口的声明(如IGreeter.h),但不实现接口。
    使用QPluginLoader加载插件库。
    用qobject_cast获取插件接口指针,调用接口方法。
  2. 插件程序(Plugin)
    作用:实现接口,提供具体功能,被主程序动态加载。
    代码特点:
    实现接口(如IGreeter),并继承QObject。
    用Q_PLUGIN_METADATA和Q_INTERFACES声明插件元数据和实现的接口。
    编译为动态库(.dll/.so/.dylib)。

在这里插入图片描述

插件和动态库的区别

两者都是用于封装部分功能的实现,并降低模块代码耦合度。但其实插件也是被部署为动态库的形式,但是和传统的动态库还是有一些差别的。

  • 插件

插件主要面向接口编程,无需访问 .lib 文件,热插拔、利于团队开发。即使在程序运行时 .dll 不存在,也可以正常启动,只是相应插件的功能无法正常使用而已。

  • 动态库

动态库需要访问 .lib 文件,而且在程序运行时必须保证 .dll 存在,否则无法正常启动。

  • 插件应用场景

一个大型项目的开发离不开插件化,可以让整个框架结构更加清晰和容易理解,比如说一个该项目经常会针对不同客户做功能定制,或者对于软件使用的不同场景,功能有所区别,那这时候插件就变得非常有用了,主工程中包含所有功能模块的调用,但是如果某些功能如果不需要,那最终程序打包只要不把插件的dll打包进去就OK了,程序依然可以正常运行,只是该插件的功能无法使用而已。


网站公告

今日签到

点亮在社区的每一天
去签到