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.
In order to run this project, you need to install some stuff and compile.
It should work on both Linux and windows.
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)
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
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
.
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:
- 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...)
- 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
).
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.
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 ofplugin_dog
plugin_frog
is a friend ofplugin_dog
plugin_frog
tries to helpplugin_cat
and tells him when the dog is in the area
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.
Any code improvements, suggestions... are welcome!!!