In this article, I explain changes to the plugin loading mechanism in KDE Frameworks 5. The article is intended for a technical audience with some affinity to KDE development.
Over the past weeks, I’ve spent some time reworking the plugin system in KDE Frameworks 5. The original issue I started with is that we are shifting from a plugin system that is mostly KDE specific to making more use of Qt’s native plugin system. Qt’s plugin system has changed in a few ways that caused many of our more complex plugins to not work anymore. On the other side, moving closer to Qt’s plugins makes our code easier to use from a wider range of applications and reduces dependencies for those that just want to do plugin loading (or extending their app with plugins). A mostly complete, and I must say spiffy, solution is now in place, so here’s a good opportunity to tell a little about the technical background of this, what the implications for application developers are, and how you can use a few new features in your plugins.
Bye bye, K_EXPORT_PLUGIN
In the KDE Platform 4, the K_EXPORT_PLUGIN macro did two things. It provided an entry point (qt_plugin_instance()) function which loads the plugin. With Qt5, the need for the entry point is gone, since plugins are now QObject based, so the methods defined in Q_INTERFACE can be relied on as entry points. K_EXPORT_PLUGIN also provided PLUGIN_VERIFICATION_DATA, which can be used to coarsely identify if a plugin was built against the right version. In most cases, this wasn’t very useful, as it would only catch a relatively small class of errors. The plugin verification data is missing in the new implementation so far, but we plan to get it back in another form: being able to specify the version in the plugin, and checking against that. This part is not yet there, but it’s also not a problem for now, as it’s not required and won’t produce fatal errors.
The heavy lifting is done by this macro, which is often used together with K_EXPORT_PLUGIN: You create a factory class using this macro, and then, in the old world, you’d use K_EXPORT_PLUGIN to create the necessary entry points. Since we’re already defining the plugin factory instance using Q_DECLARE_INTERFACE, Qt is happy about that, and the stuff in K_EXPORT_PLUGIN becomes useless. Basically, we’ve moved the interesting bits from K_EXPORT_PLUGIN to K_PLUGIN_FACTORY. For porting that means, in the vast majority of cases, you can just remove K_EXPORT_PLUGIN from your code, and be done. (If you don’t remove it, it’ll warn during build, but will still work, so it’s source-compatible. Mostly, in some cases, .moc can’t pick up the macro, in this case, either move it into the .h file, or include the corresponding .moc file in your .cpp code.)
K_PLUGIN_FACTORY, or rather its base class, KPluginFactory is pretty neat. It’s mostly assembled by macros and templates, which makes it a bit hard to read and understand, but once you realize what kind of effort is saved for you by that, you’ll happily go for it (you don’t have to care about its internals as it is well encapsulated, of course). The really interesting piece is this:
T *create(const QString &keyword, QObject *parent = 0, const QVariantList &args = QVariantList());
This is a method available in the factory (generated by K_PLUGIN_FACTORY) that is the base of your plugin, basically what you get from QPluginLoader::instance() from your plugin once you’ve loaded the .so file. You basically call (roughly)
MyFancyObject* obj = pluginLoader->instance()->create<MyFancyObject*>(this);
to load your code into the app hosting the plugin. (Of course, MyFancyObject can be either the class actually defined in the plugin, or, more commonly, the baseclass of it (you don’t want to include your plugin’s header in the app, as that defeats the point of the plugin in the first place). You only do the above if you go through QPluginLoader directly, KService and Plasma::PluginLoader can do most of this work for you (also, here the API didn’t change, so no worries).
K_PLUGIN_FACTORY_WITH_JSON or where is the metadata?
Qt5’s new plugin system allows you to bake metadata into the plugin binary itself. They’re specified as an extra argument to the Q_PLUGIN_METADATA macro, and basically point to a json file containing whatever info you want in the plugin. The metadata is compiled into the ELF section of the plugin, can be found very fast, and the plugin itself doesn’t need to be dlopened in order to read it. With Qt’s previous plugin system, the plugin shared object files would have to be loaded, which significantly impacts performance.
This mechanism is very useful for something we’ve been doing in KDE for a long time, namely the data included in the .desktop files. Those are being installed separately, into a services install dir, indexed by ksycoca for faster access and searching. These .desktop files (which really are the plugin’s metadata contain all the usual stuff, name, icon, author, etc., but also the plugin name, dependencies, and most importantly, the ServiceType (e.g. Plasma/DataEngine). KService uses them to find a plugin (often by service type) and load it from the plugin name.
Having the metadata baked into the plugin allows us to not use KServiceTypeTrader (which handles the searching through the sycoca cache) but to ask QPluginLoader directly. Right now, we’re still using sycoca for the lookup, but this mechanism allows us to move away from it in the future.
Something we do use the metadata for already, at least in Plasma::DataEngine is the creation of a KPluginInfo object. (This object basically exposes the metadata, and can be instantiated from a .desktop file. With the above changes, I also added a constructor to KPluginInfo that instantiates a KPluginInfo object from the json metadata baked into the plugin. This is one nail in the coffin of KServiceTypeTrader (and in extension KSyCoCa), but obviously not its death blow.
K_PLUGIN_FACTORY_WITH_JSON simply takes an extra argument, the metadata file, and bakes that into the plugin (by inserting it, internally, into the Q_PLUGIN_METADATA macro which is included in the KPluginFactory implementation.
In order to ease the transition from .desktop files to baked-in metadata, we introduced a cmake macro to help you with that. It’s pretty simple, you just write (in your CMakeLists.txt):
and during build time, a file called mypluginmetadata.json will be generated. You can include this file using the K_PLUGIN_FACTORY_WITH_JSON macro in your code, and the metadata will be baked in. When the plugin is loaded, your ctor will have a QVariantList as argument, which you can just pass to KPluginInfo, and get a valid plugininfo object back. If you’re interested what the .json file looks like, either peak into your build directory, or use the command
$ desktoptojson -i mydesktopfile.desktop
to generate a json file. (You usually want to run this at build time, and not put it in your repo, since otherwise, changes to the .desktop file, for example translations, will not be picked up.)
Edit: the kservice_desktop_to_json macro is deprecated in favor of kcoreaddons_desktop_to_json, use that.
- Changes are largely source-compatible (K_EXPORT_PLUGIN can just go away, you might have to include the .moc file explicitely)
- You can optionally use JSON metadata in your plugin to create KPluginInfo objects
If you want to create a plugin, do the following:
- In your CMakeLists.txt file, convert your old .desktop file at build-time using kservice_desktop_to_json() and use the resulting file (replace .desktop with .json) in the following step
- In your plugin .cpp file, add a K_PLUGIN_FACTORY macro, this does Q_DECLARE_INTERFACE and Q_PLUGIN_METADATA for you. Optionally pass a .json file
- Use QPluginLoader, KServiceTypeTrader or Plasma::PluginLoader to load your plugin.
Personal thanks go out to kdelibs hacker extraordinaire David Faure, who has been patiently guiding me through making these changes to our plugin system.
Update by David Faure
One thing it doesn’t detail (because you didn’t directly work on that) is differences in where plugins get installed (the install dir changed), and how they are found ($QT_PLUGIN_PATH).
One of the porting ideas is: if you don’t need the trader to find your plugins, don’t use it anymore. E.g. if you can put all your plugins into a subdirectory of the plugin path, and you don’t need any filtering (you just want to load them all), iterate over that, no servicetype and no trader needed. We still have to solve the use case of filtering/querying though, ideally in Qt (so that the json metadata can actually be useful).