使用Qt插件系统在接口类中声明信号并使用新的信号槽语法

4
我正在使用低级Qt插件API为Qt应用程序构建一组插件。管理对象将在运行时加载这些插件,并允许客户端程序访问任何可用的插件。我希望管理器通过信号和槽与插件类通信,因为这些插件可能存在于不同的线程中。
因此,每个插件必须实现的接口应该声明这些信号和槽。槽不是问题,因为它们实际上只是每个插件必须实现的抽象成员函数。信号是个问题。
虽然信号可以在接口类中声明,但它们的定义是由Qt的moc在编译过程中自动生成的。因此,我可以在接口类中定义这些信号,但是当创建实现接口的插件时,构建会在链接时失败。这是因为信号的定义在接口对象文件中,而不是插件对象文件中。

那么问题是,我如何确保在构建 Plugin 类时生成和/或链接 Interface 类中定义的信号的自动生成实现?

这里是一个最小完整示例来演示问题。

目录结构

test
  |_ test.pro
  |_ app
      |_ app.pro
      |_ interface.h
      |_ main.cc
  |_ plugin
      |_ plugin.pro
      |_ plugin.h

test.pro中:

TEMPLATE = subdirs
SUBDIRS = app plugin

app/app.pro 中:
TEMPLATE = app
QT += testlib
HEADERS = interface.h
SOURCES = main.cc
TARGET = test-app
DESTDIR = ../

app/interface.h中:

#ifndef _INTERFACE_H_
#define _INTERFACE_H_

#include <QObject>
#include <QString>

class Interface : public QObject
{
    Q_OBJECT
    public:
        virtual ~Interface() {}

        // Slot which should cause emission of `name` signal.
        virtual void getName() = 0;

    signals:
        // Signal to be emitted in getName()
        void name(QString);
};

#define InterfaceIID "interface"
Q_DECLARE_INTERFACE(Interface, InterfaceIID)

#endif

app/main.cc中:
#include "interface.h"
#include <QtCore>
#include <QSignalSpy>

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

    // Find plugin which implements the interface
    Interface* interface;
    QDir dir(qApp->applicationDirPath());
    dir.cd("plugins");
    for (auto& filename : dir.entryList(QDir::Files)) {
        QPluginLoader loader(dir.absoluteFilePath(filename));
        auto* plugin = loader.instance();
        if (plugin) {
            interface = qobject_cast<Interface*>(plugin);
            break;
        }
    }
    if (!interface) {
        qDebug() << "Couldn't load interface!";
        return 0;
    }

    // Verify plugin emits its `name` with `QSignalSpy`.
    QSignalSpy spy(interface, &Interface::name);
    QTimer::singleShot(100, interface, &Interface::getName);
    spy.wait();
    if (spy.count() == 1) {
        auto name = spy.takeFirst().at(0).toString();
        qDebug() << "Plugin emitted name:" << name;
    } else {
        qDebug() << "Not emitted!";
    }
    return 0;

}

plugin/plugin.h中:
#ifndef _PLUGIN_H_
#define _PLUGIN_H_

#include "interface.h"

class Plugin : public Interface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "interface")
    Q_INTERFACES(Interface)

    public:
        // Override abstract function to emit the `name` signal
        void getName() override { emit name("plugin"); }
};

#endif

plugin/plugin.pro中:
TEMPLATE = lib
CONFIG += plugin
INCLUDEPATH += ../app
HEADERS = plugin.h
TARGET = $$qtLibraryTarget(plugin)
DESTDIR = ../plugins

这可以通过从顶级目录调用qmake && make进行编译。
现在,Interface继承自QObject,以便它可以定义所有插件共享的信号。但是,在编译plugin子目录时,我们会遇到链接错误:
Undefined symbols for architecture x86_64:
"Interface::qt_metacall(QMetaObject::Call, int, void**)", referenced from:
  Plugin::qt_metacall(QMetaObject::Call, int, void**) in moc_plugin.o
"Interface::qt_metacast(char const*)", referenced from:
  Plugin::qt_metacast(char const*) in moc_plugin.o
"Interface::staticMetaObject", referenced from:
  Plugin::staticMetaObject in moc_plugin.o
"Interface::name(QString)", referenced from:
  Plugin::getName() in moc_plugin.o
"typeinfo for Interface", referenced from:
  typeinfo for Plugin in moc_plugin.o
ld: symbol(s) not found for architecture x86_64

这对我来说很有意义。 moc 实现了信号 Interface::name(QString),因此实现及其相关符号在 moc_interface.o 中。当构建 plugin 子目录时,该对象文件既没有编译也没有链接,因此没有符号定义,链接失败。

实际上,我可以很容易地解决这个问题,只需在 plugin.pro 文件中包含以下行:

LIBS += ../app/moc_interface.o

或者添加:

#include "moc_interface.cpp"

plugin/plugin.h文件的末尾。

这两种方法都不太好,因为这些文件在构建app时会自动生成,我无法保证它们存在。我希望一个新插件的编写者只需要考虑包含"interface.h"头文件,而不是这些自动生成的文件。

所以问题是,如何让qmake在构建Plugin时包含Interface类的信号定义?

相关问题:

我知道this answer解决了一个密切相关的问题。但那个解决方案使用了旧式的“字符串化”版本来连接信号和插槽。我更喜欢使用新的成员指针语法,它提供了编译时检查。此外,那个解决方案需要对接口进行dynamic_cast,这比让Interface类直接继承QObject更容易出错且效率更低。


如果您不想在插件和应用程序之间使用共享库,则接口需要是抽象的。信号也可以是虚拟的 - 这样做没问题。只要绕过QtPrivate :: HasQ_OBJECT_Macro中的QObject类检查,现代调用语法就可以工作。这很容易解决 - 只需向接口添加一个虚拟的、空的qt_metacall即可。 :) - Kuba hasn't forgotten Monica
是的,MOC已经够复杂的了,不要试图绕过它 :)。我猜你关于共享库的第一条评论是@eyllanesc提出的解决方案背后的想法,对吗? - bnaecker
是的,没有必要用库来解决这个问题。只需将所有方法都设置为虚拟和抽象,并添加一个方法:virtual int qt_metacall(QMetaObject::Call, int, void **) = 0; - 这个方法使得检查宏将接口视为 QObject,即使它实际上并不是 :) - Kuba hasn't forgotten Monica
1个回答

4

你的主要错误在于将存在循环依赖的项目组合在一起。

我已经使用以下结构重新组织了你的项目:

test
├── test.pro
├── App
│   ├── App.pro
│   └── main.cpp
├── InterfacePlugin
│   ├── interfaceplugin_global.h
│   ├── interfaceplugin.h
│   └── InterfacePlugin.pro
└──Plugin
    ├── plugin_global.h
    ├── plugin.h
    └── Plugin.pro

从中我们可以看出,接口是一个独立的库。应用程序和插件是使用它的两个项目。

完整的项目可以在以下链接中找到。


这绝对解决了问题,谢谢!你能解释一下我尝试的结构中哪里存在循环吗?继承中没有循环性,所以你是指接口和插件库在某种程度上相互指向吗? - bnaecker
@bnaecker 是的,那就是我的意思。 - eyllanesc
@eyllanesc 为什么在 Plugin.pro 中错过了 CONFIG += plugin,以及在 interfaceplugin.h 中错过了 slot 关键字? - Denys Rogovchenko
1)你认为为什么需要使用“CONFIG += plugin”? 2)因为我还没有创建任何插槽。 - eyllanesc
  1. 嗯,Qt在示例中使用它,例如“Plug&Paint Extra Filters”等...
  2. 正如我所看到的,您调用了- QTimer :: singleShot(...),其中替换了&Interface :: getName,因此我理解getName()应声明为虚槽。此外,Qt文档说: QTimer :: singleShot此静态函数在给定时间间隔后调用插槽....
- Denys Rogovchenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接