Skip to content

remileduc/qt_plugin_entrypoint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

License: MIT

qt_plugin_entrypoint

Demonstration of entry points in C++ thanks to Qt.

The goal is to be able to implement entry points, in the sense of Python (pkg_resources entry points), in a C++ Qt plugin architecture.

See section Why below for more information.

How to use

Why

Code explanation

Improvements

How to use

In order to run this project, you need to install some stuff and compile.

It should work on both Linux and windows.

Prerequisites

You need the following:

  • Qt 5.9+
  • a C++17 compiler (tested on MSVC 2019 and G++ 8.3)
  • CMake 3.10+ (only tested with 3.14)

Compile

You have to make the project with CMake:

mkdir build
cd build
cmake -G "Unix Makefiles" -A x64../source

You can use the CMake generator of your choice: https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html#cmake-generators

If you prefer, you can open the source/CMakeLists.txt fils in QtCreator, it will automatically generate the project for you.

Then, you can build with your IDE or directly with

make

Run

Once compiled, you can launch the created executable MainApp or MainApp.exe. If everything went fine, it will automatically load the 3 plugins that have been build next to the executable (.dll or .so files).

Note that the executable should be located in the subfolder build/main_app.

Why

For the development of SurveyPad, we need an architecture based on plugins. Plugins can be written by anybody, so their code is in seperated repositories. Thus, plugins can't have any dependencies between each others. And you can't assume that a plugin will necessarily always be here at run time...

However, we have reached a point where we would like some plugin to expose, or to use, an entry point, as defined by pkg_resources in Python:

                   +-------------+
                   | Application |
                   +------+------+
                          |
                   +------+------+
                   |    Utils    |
                   +------+------+
       +--------------^   ^   ^--------------+
       |                  |                  |
+------+------+    +------+------+    +------+------+
|  Plugin 1   |    |  Plugin 2   |    |  Plugin 3   |
+-------------+    +-------------+    +-------+-----+
         | |           |                 |
         | +-----------+                 |
         +-------------------------------+

Globally, we would like to implement the 2 lines at the bottom, between Plugin 1 and Plugin 2, and between Plugin 1 and Plugin 3, without adding any dependencies between the plugins.

One solution could be to add some interfaces in Utils, but we don't want plugin specific code there...

So we took the other solution: the one with entry points. Globally, one plugin says in its metadata that he has an entry point (represented by a class name) that others can use, if they want. Then, another plugin that wants to use this entry point can look for it amoung all the plugins that are available at run time. If it finds the correct entry point, it can use it.

In reallity, he needs to instantiate the class exposed by the the first one in its metadata. And here we face the main problems:

  1. instantiate an object from its class name as a string?
    • in Python, you can
    • in C++, NO! (the closest you can have are templates, but you need to know the class at compile time...)
  2. run a method from a class you don't know
    • in Python, you use duck typing (don't have to inherit an interface to implement it)
    • in C++, NO! (you must inherit a class to implement its interface)

Hopefully, both points can be fixed by QMetaObject (another solution is to use QMetaType, see the branch qmetatype for that). Dummy example:

#include <QDebug>
#include <QObject>

class MyClass : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE MyClass(QObject *parent = nullptr) : QObject(parent) {}
    Q_INVOKABLE int getValue() const noexcept { return value; }
    Q_INVOKABLE void setValue(int i) noexcept { value = i; }

private:
    int value;
};

int main()
{
    // This is the object that you can share everywhere, without knowing the class MyClass
    const QMetaObject &meta = MyClass::staticMetaObject;

    // from now on, we assume we don't know MyClass anymore and we'll just work with the QMetaObject
    QObject *qobj = meta.newInstance();
    if (!qobj)
        return 1;
    // Now we can use the QObject, assuming we know it has the methods getValue() and setValue()
    bool callok;
    callok = QMetaObject::invokeMethod(qobj, "setValue", Qt::DirectConnection, Q_ARG(int, 8));
    if (!callok)
        return 1;
    int value;
    callok = QMetaObject::invokeMethod(qobj, "getValue", Qt::DirectConnection, Q_RETURN_ARG(int, value));
    if (!callok)
        return 1;
    qDebug() << "LOOOL" << value;
    delete qobj;

    return 0;
}

#include "main.moc" // we need this because we are creating a class inheritting QObject in a CPP file.

Note that you need to manipulate the object through the QMetaObject interface. You assume that you know what methods the object has and howo to call them. In case it is not possible, QMetaObject will tell you (return false).

Code explanation

Architecture

The project is composed of 4 projects:

  • utils: a dynamic library used in all other projects (it is a shared library because it owns the singleton that manages the plugins and the entry points)
  • main_app: the main application
  • plugin_cat: a first plugin representing a cat
  • plugin_dog: a second plugin representing a dog
  • plugin_frog: a third plugin representing a frog

It is the same as the graphic above, with plugin_cat for Plugin 1, plugin_dog for Plugin 2 and plugin_frog for Plugin 3.

Globally, we have one executable and 4 dynamic libraries (.dll or .so), 3 created thanks to the Qt Plugin system and the utils one.

The utils project defines the interface for a plugin, this is the common point for all the others projects. Indeed, main_app needs to know this interface on order to be able to load the plugins that implements it. On top of that, it also has the PluginManager class that is a singleton, where plugins can register or use entry points.

Features

When you launch the application, the following happens:

  • plugins are loaded in main.cpp
  • the main widget is created in main.cpp
  • now you can see what the plugins have to say by selecting a plugin from the combobox.

Now, what we want is to have some communication between the plugins:

  • plugin_cat is an enemy of plugin_dog
  • plugin_frog is a friend of plugin_dog
  • plugin_frog tries to help plugin_cat and tells him when the dog is in the area

How it works

plugin_dog can have enemies and friends. Both need to be a class that implements the same function: QString cry(). However, if you want to be friend, you need to use the entrypoint PluginDog_friend, and use PluginDog_enemies to be an enemy.

Let's take a frog for instance. It want to be a friend of dogs, thus, it has a class that implements the cry() method: DogFriend. This class is registered on the PluginDog_friend entrypoint in the constructor of PluginFrog.

Thus, when plugin_dog checks for its friends, it can see that there is an entry point registered, and try to call the cry method through a QMetaObject: PluginDog.cpp#L34-L45

Same mechanism happens for the cat.

Improvements

Any code improvements, suggestions... are welcome!!!