首页 > 解决方案 > 使用 Qt 将应用程序的功能拆分为插件

问题描述

就像标题所说的那样,我想将我的 Qt 应用程序的某些部分拆分为插件,这样我就可以在运行时添加新功能。理想情况下,插件将单独编译并放入插件的专用路径;当应用程序启动时,已安装的扩展程序会自动加载,或者可以随时根据用户请求重新加载。

我应该提一下,我想放入插件的对象不是QObjects,但如果它可以使解决方案更简单,那么它们继承自 是可以接受的QObject

我怎样才能做到这一点?我想要最简单的可移植解决方案,除了 Qt(没有外部依赖项)之外不需要其他任何东西。

标签: c++qtpluginsshared-libraries

解决方案


虽然我回答了自己的问题,但我更感兴趣的是听听别人的!

首先,您需要在插件之间有一个通用接口。这是一个例子:

class MyPlugin
{
public:
    virtual ~MyPlugin() {}  // Needs to be virtual. Important!

    // Put here your method(s)
    virtual void frobnicate() = 0;
};

但是,不要这样命名您的界面。例如,如果您的插件代表视频编解码器,请将其命名为“VideoCodec”。有些人喜欢在接口名称前加上“I”(例如IVideoCodec)。此外,有些人会告诉您使用公共方法调用受保护的虚拟对象,但这并不是绝对必要的。

为什么是接口?这是因为这是应用程序在不事先了解类本身的情况下使用插件的唯一方式。这意味着由于应用程序不知道类,插件必须允许通过工厂创建插件组件。事实上,唯一需要声明的函数是创建“插件”新实例的工厂函数。这个工厂函数可以这样声明:

extern "C" std::unique_ptr<MyPlugin> MyPlugin_new();

(你需要extern "C",否则你会QLibrary因为 C++ 名称修改而遇到麻烦——见下文)

工厂函数不必没有参数,但参数必须对所有类型的插件都有意义。例如,这可能是一个哈希表或包含一般配置信息的文件,或者甚至更好的是配置对象的接口。

现在加载部分。最简单的方法是使用一个QDirIterator初始化的插件目录,遍历所有文件并尝试加载它们。类似于...的东西

void load_plugins_from_path(const QString &plugin_dir)
{
    QDirIterator it(plugin_dir, QDir::Files, QDir::Readable);

    while (it.hasNext()) {
        try_load_plugin(it.next());
    }
}

(写的好像是函数,但应该是方法)

不要尝试以任何方式通过扩展名或使用QDir::Executable标志来过滤文件:这将不必要地降低程序的可移植性 - 每个操作系统都有自己的文件扩展名,并且QDir::Executable只能在 unices 上工作(可能是因为 Windows 上没有 exec 位)。在这里,该方法load_plugins_from_path只是从一个给定的路径加载插件;例如,调用者可以在包含搜索插件的所有路径的列表元素上调用该方法。try_load_plugin可以这样定义:

void try_load_plugin(const QString &filename)
{
    QLibrary lib(filename);

    auto factory = reinterpret_cast<decltype (MyPlugin_new) *>(lib.resolve("MyPlugin_new"));

    if (factory) {
        std::unique_ptr<MyPlugin> plugin(factory());

        // Do something with "plugin", e.g. store in a std::vector
    }
}

decltype使用 onMyPlugin_new所以我们不必指定它的类型 ( std::unique_ptr<MyPlugin> (*)()) 并且auto如果你更改MyPlugin_new.

此方法只是尝试将文件作为库加载(无论它是否是有效的库文件!)并尝试解析所需的函数,nullptr如果我们没有处理有效的库文件或请求的符号(我们的函数) 不存在。请注意,因为我们直接在动态库中进行搜索,所以我们必须知道该库中实体的确切名称。因为 C++ 会修改名称,并且这种修改取决于实现,所以唯一明智的做法是使用extern "C"函数。不过不用担心:extern "C"只会阻止该函数的重载,否则所有 C++ 都可以在该函数内部使用。此外,即使工厂函数不在任何命名空间内,它也不会与其他库中的其他工厂函数发生冲突,因为我们使用显式链接;这样,我们可以 MyPlugin_new从插件 A 和MyPlugin_new插件 B 获得,它们将位于不同的地址。

最后,如果您的插件集过于多样化而无法由一个接口表示,一种解决方案是在插件内部简单地定义(可能)多个工厂,每个工厂返回一个指向不同类型接口的指针。


推荐阅读