C++ and dynamically loaded libraries having derived classes

C++ and dynamically loaded libraries having derived classes
Login

C++ and dynamically loaded libraries having derived classes

Problem: C++ and working with derived classes in dynamic libraries loaded with dlopen()

The scenario:

We want to load C++ libraries at runtime using dlopen().
We have to avoid name mangling using extern "C", but here we will treat another problem.

Loading a single library, having a concrete class derived from an abstract class, is not a problem.
The problem occurs when we want to expand a library, creating a derived class from the concrete class (that is, grandchild of the abstract class).
Loading such a "grandchild" library fails, as the symbol for the concrete parent cannot be resolved.

The example of this is shown below, using the abstract class Shape, and the two derived classes Rect: and Square:
Using both Rect and Square as derived classes of Shape works fine.

The abstract class Shape:

class Shape {
public:
    virtual ~Shape() = default;
    virtual double Area() const = 0;
...

// the types of the class factories
typedef Shape* create_t();
typedef void destroy_t(Shape*);

The derived class Rect:

class Rect : public Shape {
 public:
        virtual double Area() const;
...
// the class factories
extern "C" Shape* create() { return new Rect; }
extern "C" void destroy(Shape* p) { delete p; }

The derived class Square:

class Square : public Shape {
 public:
        virtual double Area() const {
            double one_side = _param1;
            return power2(one_side);
        }
...
 // the class factories
extern "C" Shape* create() { return new Square; }
extern "C" void destroy(Shape* p) { delete p; }

But a Square object is actually a special case of a Rect object, so it is more logical to derive Square from Rect than directly from Shape.
This is done in SquareDerivedFromRect:

class SquareDerivedFromRect : public Rect { // <--- Cannot load library: ./libmy-square-derived-from-rect.so: undefined symbol: _ZTI4Rect
 public:
        virtual double Area() const {
            double one_side = _param1;
            return power2(one_side);
        }
...
// the class factories
extern "C" Shape* create() { return new SquareDerivedFromRect; }
extern "C" void destroy(Shape* p) { delete p; }

Anyhow, the dynamic loader cannot resolve the symbol name at runtime:

./libmy-square-derived-from-rect.so: undefined symbol: _ZTI4Rect

Solutions to the undefined symbol problem

  1. One solution is to call dlopen() with the RTLD_GLOBAL flag set - but this may clutter the namespace in an expected way if many dynamic libraries are loaded.
  2. Another solution is to link dependencies to plugins which need them. Then the symbols for the dependency are guaranteed to be loaded together with the plugin.

Solutions to the "one-file-per-plugin" problem

Loading libraries dynamically often implies having a subdirectory (i.e. plugins) for .so files, and scanning that directory from the main executable.
Linking a plugin with a shared library as a dependency means having another file, (i.e. libpluginbase.so) present.

  1. One solution is use two directories (i.e. plugins and deps), and link in the dependency using -rpath.
  2. Another solution is to compile the dependency as a relocatable (-fPIC) and static library, and link it both with the plugin and the main executable (if needed).
    This second solution is probably to prefer, as the static library (i.e. libpluginbase.a) is not needed at runtime.

Re-design: Using Smart Pointers

See the smart3/ folder for the complete code of this example.

With Smart Pointers, it is easier to avoid memory leaks, as there no longer any calls to new() nor to delete().
Some other aspects may also be simplified/improved, as destroy() may be ignored completely.

For this example, we have changed the class names:

Smart Pointers : The class factories

The interface for the create() has also changed, which now takes the struct PluginParams as constructor parameters:

class PluginIface 
...

// the types of the class factories
using PluginIfacePtr = std::unique_ptr<PluginIface>;
using PluginCreator = PluginIfacePtr (*)(PluginTypes::PluginParams *params);

For a derived (grandchild) class PluginFoo, create() now looks like this:

extern "C" PluginIfacePtr create(PluginTypes::PluginParams* params) { return std::make_unique<PluginFoo>(params); }

Smart Pointers : Define a destructor functor for dlclose()

Let's take advantage of the fact that a Smart Pointer "deletes itself" when it goes out of scope.
With a Smart Pointer, we may define a functor that works as a "destructor" when the pointer is deleted.
We may use this mechanism to call dlclose() automatically when the Smart Pointer goes out of scope:

    // Use a unique_ptr for a dlopen() handle, which also calls dlclose()
    // Closer function to clean up the resource
    struct DlHandleCloser {
        void operator()(void* dlhandle) const {
            dlclose(dlhandle);
            // std::cout << "DEBUG: CLOSED DYNAMIC LIB\n";
        }
    };

    // Type alias so you don't forget to include the closer functor
    using DlHandlePtr = std::unique_ptr<void, DlHandleCloser>;

Smart Pointers : Get a unique_ptr as a handle from dlopen()

Here is how to get a unique_ptr handle from dlopen().
Note the use of the RTLD_NODELETE flag, which permits us to dlclose() the library and still keep using the library's symbols.

// Get a unique_ptr for a dlopen() handle, or nullptr on failure
DlHandlePtr GetDlHandle(const std::string& name) {
    const std::string path = "./plugins/libsmart_" + name + ".so";
    auto handle = DlHandlePtr(dlopen(path.c_str(), RTLD_NOW | RTLD_NODELETE));
    ...
    return handle;
}

Smart Pointers : Get a unique_ptr as a function pointer to create() from dlsym()

Here is how to use the handle from dlopen() to get a "creator", a function pointer to create().
As the end of this method, the handle goes out of scope, which calls dlclose() through DlHandleCloser.
Anyhow, the function pointer to create() will still be available, thanks to the RTLD_NODELETE flag.

// Get the creator from the 'create' dlsym() symbol.
PluginCreator GetCreator(const std::string& name) {
    auto handle = GetDlHandle(name);
    if (!handle) return nullptr;
    auto creator = PluginCreator(dlsym(handle.get(), "create"));
    ...
    return creator;
}

Smart Pointers : Get a unique_ptr to a plugin instance from create()

The "creator" may now be used to create one or more instances of the plugin, by calling CreatePluginInstance() as many times as needed.
If called more than once, it is recommended to store the "creator" in an object for later use.

PluginIfacePtr CreatePluginInstance(const std::string& name, PluginTypes::PluginParams& params) {
    auto creator = GetCreator(name);
    auto plugin_instance = creator(&params);
    ...
    return plugin_instance;
}

Smart Pointers : Use the plugin instance

A call to CreatePluginInstance() creates a new instance of a plugin class.
As each instance is a unique_ptr, the instance is automatically deleted when the pointer goes out of scope.

    // Create a foo instance
    std::string name = "foo";
    auto foo = CreatePluginInstance(name, params);
    ...
    foo->YourPluginMethodHere();

    // Create a second foo instance
    auto foo2 = CreatePluginInstance(name, params);
    foo2->SomeOtherMethod();

    // Create a bar instance
    auto bar = CreatePluginInstance("bar", params);
    bar->AnotherMethod();

Smart Pointers : Compiler warning with clang++

With Smart Pointers we are also using std::make_unique() in our class factories.
The create() function is declared as extern "C" to avoid name mangling, but uses std::make_unique(), which is very much not C:

extern "C" PluginIfacePtr create(PluginTypes::PluginParams* params) { return std::make_unique<PluginFoo>(params); }  // <--- THIS IS NOT C

The line above is not C-compatible, so clang++ 10.0.0-4 complains.
(On the other side, at the time of writing (2021-03-23) - g++ 9.3.0 remains silent.)

smart_foo.cc:29:27: error: 'create' has C-linkage specified, but returns incomplete type 'PluginIfacePtr' (aka 'unique_ptr<PluginIface>') which could be incompatible with C [-Werror,-Wreturn-type-c-linkage]
extern "C" PluginIfacePtr create(PluginTypes::PluginParams* params) { return std::make_unique<PluginFoo>(params); }
                          ^

This warning is really only a potential problem if "the other side" (the main program in our case) would have been written in C instead of C++, which isn't our case.
We swear dearly that our plugins will only be used by main programs always written in C++, and silence the warning, like this:

CXXFLAGS += -Wno-return-type-c-linkage

Links