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
- One solution is to call
dlopen()
with theRTLD_GLOBAL
flag set - but this may clutter the namespace in an expected way if many dynamic libraries are loaded. - 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.
- One solution is use two directories (i.e.
plugins
anddeps
), and link in the dependency using-rpath
. - 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:
Shape -> PluginIface :
Parent class, abstractRect -> PluginBase :
Derived child class, concrete - compile as a relocatable (-fPIC
) and static libSquare -> PluginFoo :
Derived grandchild class - the plugin
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 (*)(::PluginParams *params);
For a derived (grandchild) class PluginFoo
, create()
now looks like this:
extern "C" PluginIfacePtr create(::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(¶ms); ... 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(::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
- https://0x00sec.org/t/c-dynamic-loading-of-shared-objects-at-runtime/1498
- https://gist.github.com/tailriver/30bf0c943325330b7b6a
- https://github.com/theo-pnv/Dynamic-Loading
- https://stackoverflow.com/questions/56932785/automatically-creating-wrappers-for-classes-loaded-with-dlopen
- https://theo-penavaire.medium.com/loading-of-a-c-class-from-a-shared-library-modern-c-722d6a830a2b
- https://www.drdobbs.com/dynamically-loaded-c-objects/184401900?pgno=1
- https://www.linkedin.com/pulse/dynamic-loading-modern-c-posix-compliant-sander-jobing
- https://www.linuxjournal.com/article/3687
- https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/036/3687/3687l1.html
- https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/036/3687/3687l2.html
- https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/036/3687/3687l3.html
- https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/036/3687/3687l4.html
- https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/036/3687/3687l5.html