Building a simple C++ plugin framework using operating system-dependent dynamic libraries functions and APIs

Today I was trying to build a simple plugin mechanism for C++, one that could allow me dynamically load code and interact with my main application. I knew that one of the ways to do it is trough dynamic libraries (.dll on Windows, .dylib on Mac and .so on Linux). Using dynamic loadable libraries, I have implemented a very simple Plugin class.

My objectives were that the API should be clean and non-intrusive. It was for the that the solution was rather simple — specially with the fact RAII would take care of loading and unloading the library.

Enough of talk, let's start coding!

Dynamic Libraries on UNIX systems

On UNIX systems, loading and unloading is done through basically 3 methods, extracted from the Mac OS X developer documentation library:

  • dlopen

    (...) if the file is compatible with the current process and has not already been loaded into the current process, it is loaded and linked. (...) A second call to dlopen() with the same path will return the same handle, but the internal reference count for the handle will be incremented. (...)

  • dlsym

    (...) returns the address of the code or data location specified by the null-terminated character (...)

  • dlclose

    (...) releases a reference to the dynamic library or bundle referenced by handle. (...)

  • dlerror

    (...) returns a null-terminated character string describing the last error that occurred on this thread during a call to dlopen(), dlopen_preflight(), dlsym(), or dlclose(). (...)

The process on Windows is equivalent, although with different APIs.

Loading and unloading the plugin

Before we can start using plugins we have to define a model of how they are going to be loaded and unloaded. I choose to represent the loaded plugin by a class which is created by a function called PluginCreate on the plugin dynamic library:

// HelloWorldPlugin.cpp
// called once the plugin is loaded. Returns an object.
class HelloWorldPlugin {};
HelloWorldPlugin* PluginCreate() {
    std::cout << "## HelloWorldPlugin loaded!!!" << std::endl;
    // HelloWorldPlugin can be any class inside the plugin
    return new HelloWorldPlugin();
}

Any class will work

HelloWorldPlugin can be any class inside your plugin dynamic library. In fact you can return any pointer from PluginCreate, because the Plugin class implementation makes no use of it.

After we are done using the plugin, we also need to unload and free it's memory. This is done through a PluginDestroy function, also located inside the plugin dynamic library:

// HelloWorldPlugin.cpp
// called once the plugin is unloaded. "plugin" is guaranteed to be the same instance returned on "PluginCreate".
void PluginDestroy(HelloWorldPlugin* plugin) {
    std::cout << "## HelloWorldPlugin unloaded!!!" << std::endl;
    delete plugin;
}

Those two methods will get invoked automatically by our Plugin class. PluginCreate gets called as soon as the library is loaded. PluginDestroy gets called just before the library is unloaded.

The Plugin class

I started by declaring a Plugin class: it will load the library into memory, locate factory and disposal functions and instantiate the plugin. Since my intention was to make the API as simple as possible, loading is done directly on construction:

// Plugin.h
class Plugin {
private:
    std::string file;
    void* handle;

    typedef void* (*PluginCreator)();
    PluginCreator creator;

    typedef void(*PluginDestroyer)(void*);
    PluginDestroyer destroyer;

    void* instance;

public:
    Plugin(std::string file); // loads the plugin
    ~Plugin(); // unloads the plugin

    void* getPlugin(); // returns the constructed object
};

Loading on contruction might not be a good idea on the real world

Loading the library inside the constructor might not be the best idea: any exception thrown on the constructor could leave the object in invalid state and memory and/or resources could leak — not impossible, though it certainly takes more care and work.

When creating a new Plugin instance we first should open the dynamic library:

// Plugin.cpp
handle = dlopen(this->file.c_str(), RTLD_GLOBAL);

Remember to check for errors

You should check if handle is valid. In the case of failure loading the library, handle will be nullptr and dlerror() will have a string representing the error. Remember the close the library too!

This code asks the operating system to open the file, verify its contents and load it to memory as executable code. The returned handle will be used later when searching for symbols inside the library.

RTLD_GLOBAL makes sure the symbols loaded from the library are avaiable to other plugins too! If you don't want (or need) this behavior, you can safely use RTLD_LOCAL.

Next, we need to look for PluginCreate and PluginDestroy symbols and keep their address stored on creator and destroyer variables — they are really important! Without them, you cannot allocate a new object or unloading a library would cause a memory leak.

// Plugin.cpp
creator = (PluginCreator) dlsym(handle, "PluginCreate");
destroyer = (PluginDestroyer) dlsym(handle, "PluginDestroy");
instance = creator(); // calls the PluginCreate function

Don't forget to check for errors

Remember that in case creator, destroyer or instance is nullptr you got a serious error. You should close the library with dlclose and thrown an exception!

Sweet! If everything went fine so far, we have the plugin already running! Your constructor should look something like this:

// Plugin.cpp
Plugin::Plugin() {
    handle = dlopen(this->file.c_str(), RTLD_GLOBAL);
    creator = (PluginCreator) dlsym(handle, "PluginCreate");
    destroyer = (PluginDestroyer) dlsym(handle, "PluginDestroy");
    instance = creator();
}

Now, with the plugin working, we can already test it:

// main.cpp
#include "Plugin.h"
int main() {
    Plugin plugin("libHelloWorldPlugin.dylib");
    return 0;
}

Dynamic library extensions vary from operating systems

The extension will change according to the operating system you are using:

  • Mac OS X: .dylib
  • Linux: .so
  • Windows: .dll

Although not strictly required to use any specific extension, it is always a good idea to stick to the operating system standard.

This simple snippet will already print to console:

## HelloWorldPlugin loaded!!!

Nice huh? But, it appears that the plugin was not unloaded! That's because we haven't implemented the destructor yet:

// Plugin.cpp
Plugin::~Plugin() {
    destroyer(instance);
    dlclose(handle);
}

Again, check for errors!

In case destroyer throws any exceptiom dlclose() will never get called and the operating system will never unload your plugin code from memory!

This is actually pretty simple: it first calls destroyer to destroy the plugin object. Next, it tells the operating system we no longer wan't this library by closing it. Placing this on the destructor ensures that whenever Plugin goes out of scope (or is deleted) the associated dynamic library is also unloaded.

Running our application again, we should now get:

## HelloWorldPlugin loaded!!!
## HelloWorldPlugin unloaded!!!

Everything is working great! But how do we interact with the plugin?

Interacting with the plugin

To interact with the plugin we first need to define a header file which will act as a protocol that both the plugin and the application know how to use. We can define a simple interface as following:

// MyPluginInterface.h
class IMyPlugin {
public:
    virtual void helloWorld() = 0;
}

Externally callable methods must be virtual

Notice that helloWorld() method is virtual. This is a requirement, otherwise you will not be able to invoke the method from your application. Virtual methods on C++ are implemented using a vtable, which allows to lookup the method to be invoked at runtime.

Methods that will only be called from inside the plugin don't need to be virtual.

With that in place, we need to import this header on both of our projects. This will be like a "common language" for both the plugin and the application. We also have to make HelloWorldPlugin implement our newly created interface IMyPlugin:

// MyPlugin.h
#include "MyPluginInterface.h"; // common header
class HelloWorldPlugin : public IMyPlugin {
    virtual void helloWorld() {
        std::cout << "Hello world!" << std::endl;
    }
}

You can now compile your plugin.

At last, we update our main method with the following

// main.cpp
#include "MyPluginInterface.h"; // common header
int main() {
    Plugin plugin("libHelloWorldPlugin.dylib");
    IMyPlugin* myPlugin = (IMyPlugin*) plugin->getPlugin();
    plugin->helloWorld();
    return 0;
}

When we run our main application again, everything works as expected:

## HelloWorldPlugin loaded!!!
Hello world!
## HelloWorldPlugin unloaded!!!

Code with some error handling and Xcode project for this implementation is available on GitHub.