example/plugin/testShape.cpp is the host program. It links Shape and dsold (and dl), and never includes Circle.hh / Square.hh / Ellipse.hh — it only knows the base.

add_executable( testShape testShape.cpp )
target_link_libraries( testShape dsold dl Shape )

Setup and the config demo

using namespace plugin;
using namespace plugin::example;
using namespace fedem::dso;

int main()
{
    DSOLoader::prefix( "lib"s );

    try { DSOLoader::load( "./libCircle.so"s );  } catch( std::exception& ) {}
    try { DSOLoader::load( "./libEllipse.so"s ); } catch( std::exception& ) {}
    try { DSOLoader::load( "./libSquare.so"s );  } catch( std::exception& ) {}
  • prefix("lib") — only matters for the loadAll path used later in the interactive loop; harmless here.
  • Three explicit load calls. Each runs the DSO's REGISTER / REGISTER_WITH_CONFIG, so afterwards Shape's catalog holds "Circle", "Ellipse", "Square". A failed load is swallowed — the program still runs with whatever loaded.
    auto circle = Shape::create( "Circle", Circle::Config{ 42 } );
    if( circle ) { std::cout << "Circle (config r=42): "; circle->printOn( std::cout ); }

    auto ellipse = Shape::create( "Ellipse", Ellipse::Config{ 10, 20 } );
    if( ellipse ) { std::cout << "Ellipse (config a=10 b=20): "; ellipse->printOn( std::cout ); }

    auto c2 = Shape::create( "Circle" );          // default ctor
    if( c2 ) { c2->setSize( 7 ); std::cout << "Circle (default + setSize 7): "; c2->printOn( std::cout ); }

    auto square = Shape::create( "Square" );       // REGISTER — no config
    if( square ) { square->setSize( 15 ); std::cout << "Square (default + setSize 15): "; square->printOn( std::cout ); }

Output:

=== Config-based creation demo ===

Circle (config r=42): Circle r:42
Ellipse (config a=10 b=20): Ellipse a:10 b:20
Circle (default + setSize 7): Circle r:7
Square (default + setSize 15): Square a:15

Every create returns std::unique_ptr<Shape> — the driver checks it before use, because an unknown key (or a DSO that failed to load) gives nullptr.

The interactive loop

    do {
        auto addons = Plugin< Shape >::catalog().names( true );   // include aliases
        std::cout << "Available addons:";
        for( auto const& n : addons ) std::cout << ' ' << n;
        std::cout << "\nshape/command : ";

        std::string cmd;  std::cin >> cmd;
        if( !std::cin || cmd == "exit" || cmd == "quit" || cmd == "q" ) break;
        if( cmd == "list" ) { for( auto const& s : objectList ) s->printOn( std::cout ); continue; }

        // not in the catalog yet? try to load lib<cmd>.so on demand
        if( std::find( addons.begin(), addons.end(), cmd ) == addons.end() )
        {
            auto file = DSOLoader::prefix() + cmd + ".so"s;       // "lib" + cmd + ".so"
            try { DSOLoader::load( "./"s + file ); }
            catch( std::exception& ) {
                try { DSOLoader::load( file ); }                  // then the search path
                catch( std::exception& ) { std::cerr << "Could not load DSO for '" << cmd << "'\n"; continue; }
            }
        }

        auto shape = Shape::create( cmd );
        if( !shape ) { std::cout << '`' << cmd << "` could not be created!\n"; continue; }

        unsigned int size;  std::cout << "size : ";  std::cin >> size;
        shape->setSize( size );
        std::cout << *shape << '\n';                              // free operator<<
        objectList.push_back( std::move( shape ) );
    } while( true );

This is the pattern that makes the toolkit worth using: type Triangle and, if libTriangle.so is next to the binary, it is loaded, its REGISTER fires, catalog().names() now lists Triangle, and Shape::create("Triangle") works — with no code in the host that mentions triangles.

catalog().names(true) includes aliases; create accepts any key, alias or not.

Next: Signing an add-on.