NAV

plugins is the library designed to help developpers implement more or less complex application plugin systems based on DLL. DLL loading is based on dll library so it follows same rules as explained in previous section.

What is a plugin ?

A plugin, in PID, is a module library (a dynamically loadable object) with specific features: it define extensions and optionally extension points.

An extension point is a base class intended to be inherited by specialized classes which are called extensions. Its defines virtual methods that will be overriden by those specialized class and that will be called by application code. For instance:

#pragma once
#include <pid/plugins.h>
#include <vector>
#include <string>

namespace vehicle{

class Vehicle{//declare an interface
public:
    Vehicle() =default;
    virtual ~Vehicle() =default;
    virtual std::vector<std::string> produce() const = 0;
    virtual bool fly() const = 0;
};
}

PID_EXTENSION_POINT(vehicle::Vehicle, 1, 0)//register this interface has an extension 

As reader may see Vehicle is a normal class defining virtual methods (here and most of times pure virtual methods). It is defined as an extension point using the PID_EXTENSION_POINT macro which is in turn accessible via the pid/plugins.h header. Each extension point is bound to a version number of 2 digits: major digit must change anytime there are breaking changes in the extension point declaration ; minor digit must change anytime there are non breaking changes in the extension point declaration.

Extension points have to be exported by a library (HEADER, STATIC or SHARED), for instance in CMakeLists.txt:

PID_Component(vehicle_extensions HEADER EXPORT plugins )

We suppose here that the file declaring Vehicle class is in public headers of the library vehicle_extensions

That’s it, you can define as many extension points as needed and put them in as many libraries as you need.

Extensions are simply classes directly or undirectly (public) inheriting from an extension point. For instance in a header called plane.h:

#pragma once
#include <vehicle/vehicle.h>

namespace vehicle{
    class Plane : public Vehicle{
    public:
        Plane() =default;
        virtual ~Plane()=default;
        virtual std::vector<std::string> produce() const override{
            return std::vector<std::string>plane;
        }
        virtual bool fly() const override{
            return true;
        }
    };
}

Basically the virtual methods of Vehicle are overriden to implement specific behaviors.

Extensions are intended to be declared by plugins. For instance let suppose we define a plugin named flying_vehicles, its source file can look like:

#include <pid/plugins.h>
#include "balloon.h"
#include "plane.h"

PID_PLUGIN_DEPENDENCIES={};
PID_PLUGIN_DEFINITION(){
    pid_plugins().add_extension<vehicle::Vehicle,vehicle::Balloon>();
    pid_plugins().add_extension<vehicle::Vehicle,vehicle::Plane>();
}

It include pid/plugins.h header to get access to the plugins API, this way it can use the following macros. It also includes (or eventually directly define) all extensions it needs to declare, in the example a Plane and a Balloon.

Then it must declare its dependencies using PID_PLUGIN_DEPENDENCIES macro, here none. Finally its definition is provided using the macro PID_PLUGIN_DEFINITION: it consists in calling pid_plugins().add_extension method.

  • pid_plugins() is the global object used for managing plugins.
  • add_extension is a templated method with first type being the extension point being extended and second type being an extension of this extension point. So the previous code define 2 extensions for Vehicle: Balloon and Plane.

This source file as well as extension headers are all contained in the directory of plugin flying_vehicles.

To declare plugin flying_vehicles in CMakeLists.txt:

PID_Plugin(flying_vehicles DEPEND vehicle_extensions)

The PID_Plugin function is automatically provided as soon as a package depends on pid-modules package, so no specific action is required. This cmake function automates the creation and configruation of plugin system. You simpy needs to specify dependencies for yor module, in the example vehicle_extensions which defines extension points the plugin is extending. Any number of libraries providing extension can be provided this way.

Using plugins

Now its time to use the plugins into an application. Lets have a look at the example:

#include <vehicle/vehicle.h>
#include <pid/plugins.h>
#include <iostream>
#include <vector>

int main(){
    pid_plugins().add_extension_point<vehicle::Vehicle>();
    pid_plugins().load("pid-modules", "flying_vehicles");
    pid_plugins().load("pid-modules", "road_vehicles");

    auto all_ext = pid_plugins().extensions<vehicle::Vehicle>();
    std::vector<std::shared_ptr<vehicle::Vehicle>> vehicles;
    for (auto & ext: all_ext) {
        vehicles.push_back(ext->create());
    }

    for(auto & v: vehicles) {
        std::cout << "Vehicle can fly: " << (v->fly()?"yes":"no") << std::endl;
        for(auto& model: v->produce()){
            std::cout << "Vehicle : " << model << std::endl;
        }
    }
    return 0;
}
  • code need to use the plugins API so it includes pid/plugins.h header. It will also manipulate objects of type Vehicle so it needs to include the vehicle.h header.
  • The whole plugins system is controlled using pid_plugins() accessor.
  • First the program defines an extension point using its add_extension_point method. In the example the only extension point is Vehicle.
  • Then it loads plugins using the load method. In the example, two plugins flying_vehicles and road_vehicles are loaded. In this scenario we suppose that dependencies between our application and those two plugins have been defined in CMakeLists.txt so so need to specify a version of the plugin to load. But be aware that if your application does not know the plugin it will use before runtime then you can specify a version constraint as third argument of the load function.
  • Once plugins are loaded their extensions are available and can be called by the application. To get these extensions, simply call the extensions template method with the adequte extension point type. Here we search for all extensions provided for the Vehicle extension point (calling pid_plugins().extensions<vehicle::Vehicle>()).
  • Then we create instances of each of these extensions by calling their create function: we have now “normal” objects of extensions types (Plane and Balloon for instance) all referenced as through pointers of the extension point type (Vehicle). Using those objects now becomes really trivial as it uses polymorphism of Vehicle virtual functions.

Creating plugins that define new extension points

It is also possible to define plugins that in turn define new extension points to which third party plugins will be able to contribute.

Let suppose we define a new extension point, provided by a third party library:

#pragma once
#include <pid/plugins.h>
#include <vehicle/vehicle.h>
#include <string>

namespace vehicle{
    class CarModel{
        virtual std::string model_name() const = 0;
        virtual std::string manufacturer() const = 0;
    };
}

PID_EXTENSION_POINT(vehicle::CarModel, 1, 0)//register this interface has an extension 

Then we define a plugin (we call it car_plugin) that in turn provides the extension point:

#include <pid/plugins.h>
#include <vehicle/car_model.h>

class CarManufacture {
    std::vector<std::shared_ptr<CarModel>> models_;
    CarManufacture() = default;

public:
    static CarManufacture& access() {
        static Manufacture instance;
        return instance;
    }
    virtual ~CarManufacture() = default;

    void add_model(std::shared_ptr<CarModel> mod) {
        models_.push_back(mod);
    }

    const std::vector<std::shared_ptr<CarModel>>& models() const {
        return models_;
    }
};

class Car : public vehicle::Vehicle {
public:
    Car() = default;
    virtual ~Car() = default;

    virtual std::vector<std::string> produce() const override{
        std::vector<std::string> ret;
        auto car_models = CarManufacture::access().models();
        for(auto& mod: car_models){
            ret.push_back(mod->manufacturer()+"_"+mod->model_name());
        }
        return ret;
    }

    virtual bool fly() const override {
        return false;
    }

};


PID_PLUGIN_DEPENDENCIES = {};
PID_PLUGIN_DEFINITION() {
    pid_plugins().add_extension_point<vehicle::CarModel>();
    pid_plugins().add_extension<vehicle::Vehicle, vehicle::Car>();
}

PID_PLUGIN_ADD_EXTENSIONS(package, plugin) {
    //registering new models coming from extensions
    auto new_models = pid_plugins().extensions<vehicle::CarModel>(package, plugin);
    for (auto& mod : new_models) {
        CarManufacture::access().add_model(mod->create());
    }
}

The new plugin provides the extension Car, whose behavior is adapted using the singleton CarManufacture object. It also provides an extension point CarModel that allows new plugins to enrich the set of car models. The macro PID_PLUGIN_ADD_EXTENSIONS is used by the plugin system to call the current plugin anytime another plugin depends on local plugin. Its internal code simply tells to register new car models into the CarManufacture singleton.

plugins that depends on other plugins

Finally we can create yet another plugin that extends the previous one:

#include <pid/plugins.h>
#include <vehicle/car_model.h>

class CitroenC3: public vehicle::CarModel{
    virtual std::string model_name() const override{
        return "c3";
    }
    virtual std::string manufacturer() const override{
        return "citroen";
    }
};

PID_PLUGIN_DEPENDENCIES = a_package;
PID_PLUGIN_DEFINITION() {
    pid_plugins().add_extension<vehicle::CarModel, vehicle::CitroenC3>();
}

The plugin definition is quite common except that it defines a dependency to car_plugin using PID_PLUGIN_DEPENDENCIES. PID_PLUGIN_DEPENDENCIES is a vector of tuple elements with each tuple defined this way:

  • first name is the name of the package containing the dependency
  • second name is the name of the plugin (i.e. the dependency)
  • last optional name is the version constraint. If left empty it means that version resolution should have occurred at configuration time and so that this plugin must depend on car_plugin in its CMake description.

This dependency is needed because its code provides an extension (CitroenC3) for extension point CarModel that is itself provided in car_plugin.