How-To write cross-platform plugins

Sometimes we need to extend the functionality of our program at run-time. OS APIs for this let you load shared libraries, and then find symbols within them by name.

This works well for C symbols, but when it comes to C++, things start to break down. So instead of trying to deal with this in a complicated manner that takes into account symbol mangling across compilers and platforms, the typical approach is to return a pointer from the plugin to some data structure that we can use to access the symbols we want.

In practice, though, this is a void * that the caller of the plugin code needs to cast to whichever data structure it represents. So for each plugin, you also need some header file that defines the type(s) to cast to. Treat it like an interface.

Because of this approach of casting a void *, we’d like a run-time check to see if the cast is reasonably safe. For that, we embed a “plugin type” in the plugin. And because interfaces can change even when the type of plugin does not, we also add a plugin version.

As the plugin interface, you could use a class with pure virtual functions, or take a more manual approach. We’ll do the latter here just to demonstrate it.

Plugin code (plugin.h)
 1#include <functional>
 2#include <string>
 3
 4namespace bar {
 5
 6using foo_function = std::function<void (std::string const &)>;
 7
 8struct my_interface
 9{
10  foo_function  foo;
11  // TODO you can add as many things as you'd like.
12};
13
14} // namespace bar

With this plugin interface, you can now define the plugin code:

Plugin code (plugin.cpp)
 1#include <liberate/sys/plugin.h>
 2#include "plugin.h"
 3
 4namespace bar {
 5
 6// Some implementation of foo_function
 7void plugin_foo(std::string const &)
 8{
 9  // do something
10}
11
12// We need an instance of my_interface, and might as well make it a global
13// variable.
14my_interface interface = { plugin_foo };
15
16// With the interface declared, let's create plugin metadata.
17
18liberate::sys::plugin_meta meta =
19{
20  42, // The user-defined plugin/interface type
21  1,  // The user-defined version of this interface type
22  &interface, // A reference to the interface data
23  nullptr,    // Always empty.
24};
25
26} // namespace bar
27
28// We cannot quite escape Window's need to declare exported functions,
29// but that's alright.
30#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
31  #define PLUGIN_EXPORT __declspec(dllexport)
32#else
33  #define PLUGIN_EXPORT
34#endif
35
36extern "C" {
37
38// The main plugin interface is a C function named "liberate_plugin_meta"
39// that returns a liberate::sys::plugin_meta instance. Note that it would
40// also be possible to dynamically allocate things, but the global variable
41// is actually easier here.
42PLUGIN_EXPORT void *
43liberate_plugin_meta()
44{
45  return &bar::meta;
46}
47
48} // extern "C"

As you can see, the amount of metadata to manage in the plugin code is not particularly much. We’ll leave compilation of the plugin code out of this guide, because it depends on your build system. But if you’re using meson, you might want to look at the shared_module reference entry.

What’s still missing is how to use the plugin in practice.

Plugin usage
 1#include <liberate/sys/plugin.h>
 2#include "plugin.h"
 3
 4using namespace liberate::sys;
 5
 6plugin_meta const * meta = nullptr;
 7
 8// We need a pointer to the metadata structure. If an appropriate function
 9// was found, and it yielded a pointer, then the meta structure will now
10// be filled.
11auto success = load_plugin(meta, "path/to/plugin.so");
12
13// We only know how to deal with the plugin type '42' in version '1' here.
14if (meta.type != 42 || meta.version != 1) {
15  exit(1);
16}
17
18// The plugin interface is of type my_interface.
19auto iface = static_cast<bar::my_interface>(meta->data);
20
21// And we can now invoke functions in it, etc.
22iface->foo("Hello, world!");

Note that the meta->plugin_handle field is now set; you initialized it to a nullptr in the plugin code. It contains a handle to the loaded plugin in a platform dependent format. This then allows you to unload the plugin again. Unloading also frees the metadata structure.

Unload plugin
1unload_plugin(meta);
2
3assert(meta == nullptr);

Managing the installation location of plugins, etc. is not in scope for this library.