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

Posted

技术标签:

【中文标题】使用带有新信号槽语法的 Qt 插件系统在接口类中声明信号【英文标题】:Declaring signals in interface class using Qt plugin system with new signal-slot syntax 【发布时间】:2018-05-24 18:57:08 【问题描述】:

我正在为 Qt 应用程序构建一组插件,使用 low-level Qt plugin API。管理器对象将在运行时加载这些插件,并允许客户端程序访问任何可用的插件。我希望管理器通过信号和插槽与插件类进行通信,因为这些插件可能与管理器存在于不同的线程中。

所以每个插件必须实现的接口都应该声明这些信号和槽。插槽没有问题,因为它们实际上只是每个插件必须实现的抽象成员函数。信号就是问题所在。

虽然信号可以在接口类中声明,但它们的定义是由 Qt 的moc 在编译过程中自动生成的。因此我可以在接口类中定义这些信号,但是在创建实现接口的插件时,构建在链接处失败。这是因为信号的定义在 interface 对象文件中,而不是在 plugin 对象文件中。

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

这里有一个最小的、完整的例子来演示这个问题。

目录结构

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 &amp;&amp; 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" 标头,而不是这些自动生成的文件。

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

相关问题:

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

【问题讨论】:

如果您不想使用插件和应用程序之间共享的库,那么接口需要是抽象的。信号也可以是虚拟的——它会工作得很好。只要您绕过QtPrivate::HasQ_OBJECT_Macro 中的QObject 类检查,现代调用语法就可以工作。这很容易解决 - 只需在界面中添加一个虚拟的空qt_metacall。 :) 是的,MOC 已经足够复杂了,无需尝试破解它 :)。我假设您对共享库的第一条评论是@eyllanesc 提出的解决方案背后的想法,对吗? 是的。没有必要在这个问题上扔图书馆。从字面上使所有方法虚拟和抽象添加一个方法:virtual int qt_metacall(QMetaObject::Call, int, void **) = 0; - 这是使检查宏将接口视为QObject 的方法,即使它不是:) 【参考方案1】:

您的主要错误是您正在组合具有循环依赖关系的项目。

我已使用以下结构重组了您的项目:

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

在其中我们看到接口是一个独立的库。 App 和 Plugin 是使用它的 2 个项目。

完整的项目可以在以下link找到。

【讨论】:

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

以上是关于使用带有新信号槽语法的 Qt 插件系统在接口类中声明信号的主要内容,如果未能解决你的问题,请参考以下文章

QT信号槽连接语法总结

Qt之信号槽连接——基于字符串与基于函数的连接之间的不同(译)

QT的信号和槽

通过 QT 的信号/槽系统作为参数传递通用异常

qt信号耗时10ms

QT开发(十三)——QT信号与槽机制