4

I'm building a set of plugins for a Qt application, using the low-level Qt plugin API. A manager object will load these plugins at runtime, and allow client programs access to any that are available. I'd like the manager to communicate with the plugin classes via signals and slots, since these plugins may live in different threads than the manager.

So the interface that each plugin must implement should declare these signals and slots. The slots are no problem, because they're really just abstract member functions that each plugin must implement. The signals are the problem.

While the signals can be declared in the interface class, their definition is auto-generated by Qt's moc during the compilation process. Thus I can define these signals in the interface class, but when creating a plugin which implements the interface, the build fails at link. This is because the definition of the signals is in the interface object file, not the plugin object file.

So the question is, how can I be sure that the auto-generated implementations of the signals defined in the Interface class are generated and/or linked when building the Plugin class?

Here is a minimal, complete example to demonstrate the problem.

Directory structure

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

In test.pro:

TEMPLATE = subdirs
SUBDIRS = app plugin

In app/app.pro:

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

In 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

In 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;

}

In 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

In plugin/plugin.pro:

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

This can be compiled by calling qmake && make from the top-level directory.

As is, Interface inherits from QObject, so that it can define the signal that all plugins share. But when compiling the plugin subdirectory, we get a linker error:

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

This makes sense to me. The moc implements the signal Interface::name(QString), so the implementation and its related symbols are in moc_interface.o. That object file is neither compiled nor linked when building the plugin sub-directory, so there is no definition of the symbols and the link fails.

I can actually fix this pretty easily, either by including the following line in the plugin.pro file:

LIBS += ../app/moc_interface.o

Or adding:

#include "moc_interface.cpp"

to the end of plugin/plugin.h.

Both of these seem like a bad idea, since these files are automatically generated during the build of app, and there's no real way for me to guarantee that they exist. I'd prefer that the writer of a new plugin only need to worry about including the "interface.h" header, and not these auto-generated files.

So the question is, how can I get qmake to include the definitions of the signals from the Interface class when building the Plugin?

Related questions:

I know that this answer solves a closely related problem. But that uses the old-style "stringified" version of connecting signals and slots. I'd prefer to use the new, pointer-to-member syntax, which offers compile-time checks. Also, that solution requires dynamic_casting the interface, which is more error-prone and less efficient than having the Interface class just inherit directly from QObject.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
bnaecker
  • 6,152
  • 1
  • 20
  • 33
  • If you don't want to use libraries shared between the plugins and the application, then the interfaces need to be abstract. The signals can be virtual too - it'll work just fine. The modern call syntax will work as long as you hack around the `QObject` class check in `QtPrivate::HasQ_OBJECT_Macro`. It's easy to work around - just add a dummy, empty `qt_metacall` to the interface. :) – Kuba hasn't forgotten Monica May 25 '18 at 00:50
  • Yeah, MOC is complicated enough without trying to hack around it :). I assume your first comment about shared libraries is the idea behind the solution proposed by @eyllanesc, is that right? – bnaecker May 25 '18 at 02:53
  • Yes. There's no need to throw libraries at the problem. Literally make all methods virtual and abstract add one method: `virtual int qt_metacall(QMetaObject::Call, int, void **) = 0;` - that's the one that makes check macros treat the interface as if it was a `QObject` even though it isn't :) – Kuba hasn't forgotten Monica May 25 '18 at 04:59

1 Answers1

4

Your main mistake is that you are combining projects that have circular dependencies.

I have restructured your project using the following structure:

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

In it we see that the Interface is an independent library. App and Plugin are 2 projects that use it.

The complete project can be found at the following link.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • This definitely solves the problem, thanks! Can you explain what's circular about the structure I tried? There's no circularity to the inheritance, so do you mean the interface and plugin libraries are somehow pointing to each other? – bnaecker May 25 '18 at 02:55
  • @bnaecker Yes that's what I mean. – eyllanesc May 25 '18 at 02:56
  • @eyllanesc Why did you miss CONFIG += plugin in the Plugin.pro and why missed slot keyword in the interfaceplugin.h ? – Denys Rogovchenko Apr 18 '19 at 08:40
  • @DenysRogovchenko 1) Why do you think it is necessary to `CONFIG += plugin`? 2) because I have not created any slots – eyllanesc Apr 18 '19 at 08:45
  • @eyllanesc 1) Well, qt uses it on examples such as "Plug & Paint Extra Filters", etc... 2) As I see you call - QTimer::singleShot(...), where substitute &Interface::getName, so getName() should be declared as virtual slot as I understand. Also Qt docs says: QTimer::singleShot This static function calls a slot after a given time interval.... – Denys Rogovchenko Apr 18 '19 at 09:08