This page walks the whole model once: a host that defines an extension point, a plugin that fills it, compiled to a separate shared object, and the host loading it at runtime and creating the type by name.

The code here is a trimmed version of example/plugin/ in the source tree — see the Examples for the full annotated version.

1. The host defines a base class

The extension point is an abstract base that derives from plugin::Plugin<Base> (CRTP — it passes itself as the template argument):

// Shape.hh  — shipped by the host, included by both host and plugins
#pragma once
#include <ostream>
#include <plugin/Plugin.hh>

class Shape : public plugin::Plugin<Shape>
{
public:
    virtual void setSize( unsigned int ) = 0;
    virtual void printOn( std::ostream& ) const = 0;
};

Plugin<Shape> gives Shape the static create() and load() functions and a per-Base catalog(). That catalog needs exactly one definition, in a .cpp the host links:

// Shape.cpp  — host only
#include "Shape.hh"
#include <plugin/PluginCatalog.hh>

CREATECATALOG( Shape );

2. A plugin registers itself

A plugin is a normal class deriving from Shape, plus one macro:

// Circle.cpp  — compiled into libCircle.so, NOT linked into the host
#include "Shape.hh"
#include <plugin/PluginRegister.hh>

class Circle : public Shape
{
    unsigned int r = 0;
public:
    void setSize( unsigned int v ) override { r = v; }
    void printOn( std::ostream& os ) const override { os << "Circle r:" << r << '\n'; }
};

REGISTER( Circle, Shape );   // "Circle" -> factory, added to Shape's catalog

REGISTER creates a file-scope object whose constructor inserts a Circle factory into Shape::catalog() under the key "Circle", and whose destructor removes it. That runs when libCircle.so is loaded and unloaded.

3. Build the plugin as a shared object

add_library( Circle MODULE Circle.cpp )
target_link_libraries( Circle PRIVATE plugin )   # header-only; brings in Shape.hh's deps
set_target_properties( Circle PROPERTIES PREFIX "lib" )

MODULE is the right CMake library type for a plugin — it is dlopen-only, never linked. The result is libCircle.so.

4. The host loads and creates

#include <dso/DSOLoader.hh>
#include "Shape.hh"

int main()
{
    fedem::dso::DSOLoader::load( "./libCircle.so" );   // REGISTER fires here

    auto shape = Shape::create( "Circle" );            // std::unique_ptr<Shape>
    if( !shape )
        return 1;                                       // unknown key -> nullptr

    shape->setSize( 7 );
    shape->printOn( std::cout );                        // Circle r:7
}

The host links dsold and plugin, and Shape.cpp. It never sees Circle.cpp or Circle.hh.

5. Pass a config

If the plugin has a Config struct, register with REGISTER_WITH_CONFIG and the host can hand one over — type-checked through std::any:

// in Circle.cpp
struct Circle::Config { unsigned int r = 0; };
Circle::Circle( Config const& c ) : r( c.r ) {}
REGISTER_WITH_CONFIG( Circle, Shape, Circle::Config );
// in the host
auto shape = Shape::create( "Circle", Circle::Config{ 42 } );  // Circle r:42

A wrong config type throws std::bad_any_cast; using the config overload on a plugin that registered without one throws std::runtime_error.

Where to go next

  • Defining a Plugin Hierarchy — the base class, the catalog, and the one-catalog-per-Base rule in detail.
  • Writing a Plugin — every registration macro, aliases, and when a factory returns nullptr versus throws.
  • Loading Shared Objectsload, loadAll, loadAllByEnvironment, the filename prefix, and error handling.
  • Signing & Trust — sign the add-on and refuse anything a trusted key did not vouch for.