Dynamic Libraries and Plugins

Dynamic libraries can be loaded at runtime and symbols can be resolved using the Pt::System::Library class. Additionally, the plugin API provides a more advanced way of creating classes, that implement a common interface, from a dynamically loadable library or module. The mechanism is non-intrusive and can be used with an existing class hierachy. Plugins can be loaded and unloded by client application code. The concrete type of the created class is opaque to the application that uses the plugin, it only needs to know the interface.

Loading Libraries

The Pt::System::Library class can be used to dynamically load shared libraries and resolve symbols from it. It also provides the static functions prefix() and suffix(), which allow to build library names in a portable way. The next example shows how to load a library with the basename "MyLib" at runtime and how to retrieve the address of the function "myFunction":

typedef int (*MyFunc)();
Pt::System::Path libPath = "MyLib";
Pt::System::Library library(libPath);
Pt::System::Symbol symbol = library.getSymbol("myFunction");
MyFunc func = reinterpret_cast<MyFunc>(symbol.sym());
int result = func();

The constructor of the Library class will try to load the library at the given path. If no library could be found, the path is extended by the platform-specific library extension first, and then also by the shared library prefix. If no library could be found at either path, an AccessFailed exception is thrown. The function getSymbol() returns a Symbol object when the library symbol could be resolved or a SymbolNotFound exception, if the symbol name could not be found. The actual address of the library symbol is returned by sym(). Alternatively, the index operator can be used to load symbols, which will return the address of the symbol as a pointer to void or a nullptr on failure. Note, that standard C++ does not allow to cast void pointers to function pointers, but nearly all runtimes implement that as an extension.

Writing Plugins

Lets assume the classes you want to create from a plugin are derived from an interface class called Greeter. The Greeter class has one abstract function called sayHello:

class Greeter
{
public:
virtual ~Greeter() {}
virtual void sayHello() const = 0;
};

Now, we want to write a plugin that implements Greeter to say "Hello World" in english. This simply means to derive from Greeter and implement the sayHello method:

class EnglishGreeter : public Greeter
{
public:
void sayHello()
{
std::cout << "Hello World\n";
}
};

So far this has nothing to do with writing the plugin, it is pretty much the situation how object-oriented applications and frameworks are designed. To build the plugin, the EnglishGreeter must be build as a shared library and export the symbol "PluginList" that we will use later to resolve our plugin. PluginList must be a null-terminated array of PluginId* and be exported with C-linkage. This array will contain a number of BasicPlugin instances that serve as builder for the class we want to load from the plugin in our client application.

extern "C"
{
Pt::System::PluginId* PluginList[] = { &_enGreeter, 0 };
}

Here we create a BasicPlugin statically in the plugin library that is able to create an EnglishGreeter which implements the Greeter interface, hence the two template parameters. The constructor takes a feature string, in this case "en", that can be used later for named construction of objects. The address of the BasicPlugin is then placed in the PluginList, so it can be resolved by the loader code. This is pretty much all you need to do to write a plugin. If we decide to write a GermanGreeter and FrenchGreeter later and do not want to compile them into a separate file we can simply add them to the PluginList. Instead of using the BasicPlugin template, we can derive from Pt::Plugin and override the create and destroy methods if we need a special way of creating or destroying. BasicPlugin is derived from Pt::Plugin and simply creates with new and destroys with delete.

Loading Plugins

To load a plugin in an application the PluginManager class is used. It is a class template that takes the Interface type, here Greeter, as parameter. It will load the plugin, resolve the pluginlist and get the plugins to be used when a class needs to be created. It is very simple to use:

manager.loadPlugin("PluginList", "/path/to/plugin.so");
Greeter* greeter = manager.create("en");
if(greeter)
{
greeter->sayHello();
manager.destroy(greeter);
}

First we need to load the shared library with PluginManager::loadPlugin(). Then we can create an instance of a Greeter by a feature string by calling PluginManager::create(). Normally, one would ask the user for a language and then see if we can create a Greeter. if the instance could be created we use it like a normal C++ class, but not delete it directly and instead use the PluginManager::destroy() method. The rationale behind this is that the allocator in a shared library can differ from the allocator in the application and the same code needs to delete it, which created it. The life-time of the created classes is bound to the life-time of the PluginManager. When the PluginManager goes out of scope it will not only destroy all created instances, it will also unload all loaded plugin libraries.