Examples » Plugin management

Developing, loading and using plugins.

The PluginManager::Manager class provides hierarchical plugin management functions. Main features:

  • Plugin manager version and plugin interface version checks to avoid unexpected behavior
  • Both static and dynamic plugin support
  • Plugin dependecies and aliases
  • Usage checks, the manager doesn't allow plugin unload if there are any active plugin instances

This tutorial will give you a brief introduction into how plugins are defined, compiled and managed.

Plugin interface

Plugin interface is a class with virtual methods, defining the way how to work with a particular plugin.

Every plugin interface has to be derived from PluginManager::AbstractPlugin and reimplement the PluginManager::AbstractPlugin::AbstractPlugin(PluginManager::AbstractManager&, const Containers::StringView&) constructor. This is needed for instance use checks, as described above. Plugin classes derived from that interface need to reimplement the constructor too, of course.

Additionally, an interface string should be provided by overriding the PluginManager::AbstractPlugin::pluginInterface() function in order to have an additional safety check for interface compatibility. It should return a string view literal which uniquely names that particular interface. A good practice is to use "Java package name"-style syntax because this makes the name as unique as possible. The interface name should also contain a version identifier to make sure the plugin will not be loaded with incompatible interface version. If this function is not overridden, the default implementation returns "" and effectively makes the interface compatibility check a no-op.

To make lives of the users easier, we can define a list of paths where the plugins will be searched for using pluginSearchPaths(). The paths can be either absolute (for example hardcoding a system-wide installation path) or relative (relative to the executable file). In this case the plugins will be right next to the executable, so just a single entry with "" will do. If we wouldn't specify the search paths, the user would need to pass a plugin search path to the plugin manager constructor.

class AbstractAnimal: public PluginManager::AbstractPlugin {
    public:
        static Containers::StringView pluginInterface() {
            using namespace Containers::Literals;
            return "cz.mosra.corrade.Examples.AbstractAnimal/1.0"_s;
        }

        static Containers::Array<Containers::String> pluginSearchPaths() {
            return {InPlaceInit, {""}};
        }

        explicit AbstractAnimal(PluginManager::AbstractManager& manager, Containers::StringView plugin):
            AbstractPlugin{manager, plugin} {}

        virtual Containers::String name() const = 0;
        virtual int legCount() const = 0;
        virtual bool hasTail() const = 0;
};

Plugin definition

Every plugin is represented by a class derived from a particular plugin interface. The plugin class is then registered as a static or a dynamic plugin. Every plugin also needs to have an associated metadata file, which contains information about plugin dependencies and optionally also plugin-specific data. Full specification of plugin metadata file syntax can be found in the PluginManager::PluginMetadata class documentation.

First we define one static plugin, which will be included in the application out-of-the-box:

class Canary: public AbstractAnimal {
    public:
        explicit Canary(PluginManager::AbstractManager& manager, Containers::StringView plugin):
            AbstractAnimal{manager, plugin} {}

        Containers::String name() const override { return "Achoo"; }
        int legCount() const override { return 2; }
        bool hasTail() const override { return true; }
};

After defining the plugin we have to register it with the CORRADE_PLUGIN_REGISTER() macro. The first argument is plugin name (which will be used when instancing the plugin), second argument is name of the plugin class and third is the name of used plugin interface.

CORRADE_PLUGIN_REGISTER(Canary, Canary,
    "cz.mosra.corrade.Examples.AbstractAnimal/1.0")

And a corresponding configuration file, Canary.conf:

[data]
name=I'm allergic to canaries!

Then we define one dynamic plugin. Note that the macro for registering dynamic plugin is the same, the only difference will be in CMakeLists.txt, as you will see below. This way you can decide at compile time which plugins will be dynamic, which will be static, or, for example, which will be compiled directly into the library/application, so they can be used directly without the plugin manager.

class Dog: public AbstractAnimal {
    public:
        explicit Dog(PluginManager::AbstractManager& manager, Containers::StringView plugin):
            AbstractAnimal{manager, plugin} {}

        Containers::String name() const override { return "Doug"; }
        int legCount() const override { return 4; }
        bool hasTail() const override { return true; }
};
CORRADE_PLUGIN_REGISTER(Dog, Dog,
    "cz.mosra.corrade.Examples.AbstractAnimal/1.0")

And a corresponding configuration file, Dog.conf:

[data]
name=A simple dog plugin

Plugin compilation

Requiring the Corrade package using find_package() will define two useful macros for plugin compilation:

find_package(Corrade REQUIRED PluginManager)
set_directory_properties(PROPERTIES CORRADE_USE_PEDANTIC_FLAGS ON)

corrade_add_plugin(Dog ${CMAKE_CURRENT_BINARY_DIR} "" Dog.conf Dog.cpp)
if(CORRADE_TARGET_WINDOWS)
    target_link_libraries(Dog PRIVATE Corrade::PluginManager)
endif()
corrade_add_static_plugin(Canary ${CMAKE_CURRENT_BINARY_DIR} Canary.conf Canary.cpp)

The corrade_add_plugin() macro takes plugin name as first argument, second argument is a directory where to install the plugin files, third argument is name of configuration file and after that comes one or more source files. We use the build directory for storing the plugins to avoid the need for installation.

The corrade_add_static_plugin() macro is similar to the above, except that it creates a static plugin instead of a dynamic one.

Plugin management

Now it's time to initialize PluginManager::Manager and make use of the plugins. PluginManager::Manager is a templated class and that means it will load and make available only plugins with the interface specified as template.

In order to make the plugin manager find the static plugins, we have to import them with the CORRADE_PLUGIN_IMPORT() macro (for example at the beginning of the main() function). It takes a plugin name as an argument.

This example application will load plugin specified as command-line argument and then displays brief info about a given animal. For convenient argument parsing and usage documentation we used Utility::Arguments.

int main(int argc, char** argv) {
    /* Import static plugin using the same name as in Canary.cpp */
    CORRADE_PLUGIN_IMPORT(Canary)

    Utility::Arguments args;
    args.addArgument("plugin").setHelp("plugin", "animal plugin name")
        .setGlobalHelp("Displays info about given animal.")
        .parse(argc, argv);

    /* Initialize plugin manager with given directory */
    PluginManager::Manager<Examples::AbstractAnimal> manager;

    /* Try to load a plugin */
    if(!(manager.load(args.value("plugin")) & PluginManager::LoadState::Loaded)) {
        Utility::Error{} << "The requested plugin" << args.value("plugin") << "cannot be loaded.";
        return 2;
    }

    /* Instance of an animal */
    Containers::Pointer<Examples::AbstractAnimal> animal = manager.instantiate(args.value("plugin"));

    Utility::Debug{} << "Using plugin" << '\'' + animal->metadata()->data().value("name") + '\''
                     << "...\n";

    Utility::Debug{} << "Name:     " << animal->name();
    Utility::Debug{} << "Leg count:" << animal->legCount();
    Utility::Debug{} << "Has tail: " << (animal->hasTail() ? "yes" : "no");

    return 0;
}

Note that here we're including Corrade/PluginManager/Manager.hpp instead of a regular *.h file as we implemented our own plugin interface and thus need the full template definitons. See Custom plugin interfaces and template definitions for more information.

Compile the application with a simple CMake add_executable() command and don't forget to link in all the static plugins compiled above:

add_executable(PluginTest main.cpp)
target_link_libraries(PluginTest PRIVATE Canary Corrade::PluginManager)

After a successful compilation we can run the application with plugin name as an argument:

$ ./PluginTest --help
Usage:
    ./PluginTest [-h|--help] [--] plugin

Displays info about given animal.

Arguments:
  plugin            animal plugin name
  -h, --help        display this help message and exit

$ ./PluginTest Canary
Using plugin 'I'm allergic to canaries!'

Name:      Achoo
Leg count: 2
Has tail:  yes

$ ./PluginTest Dog
Using plugin 'A simple dog plugin'

Name:      Doug
Leg count: 4
Has tail:  yes

The full file content is linked below. Full source code is also available in the GitHub repository.