How to create a plugin system in C++
To create a plugin system in C++ you need three things.
- An interface for the plugin
- A way for the plugin to inject itself into your program
- A way for the program to load the plugins
The interface
The first two parts are handled together in this case. By having an C++ interface for the plugin you are going to write, and a simple macro you need to call with the type, the name and the version of your plugin to create the correct plumbing for your program to load the plugin correctly.
#include <string>
class Plugin {
public:
Plugin() {};
virtual ~Plugin() {};
virtual std::string command(std::string command, std::string options) {return "";}
};
#define DEFINE_PLUGIN(classType, pluginName, pluginVersion) \
extern "C" { \
std::shared_ptr<Plugin> load() \
{ \
return std::make_shared<classType>(); \
} \
\
const char* name() \
{ \
return pluginName; \
} \
\
const char* version() \
{ \
return pluginVersion; \
} \
}
Header guards have been omitted. But other than that this would be a complete header file for a plugin class with only one public method called command.
We have used std::string here, that should work fine as long as you use the same compiler for the main program and the plugins.
The macro, when called, will create three functions in a extern "C"
block. One for loading a plugin, which just calles it’s default constructor and returns a shared pointer, one for getting the name, and one for getting the version.
Define Plugins
Next up we’ll create a plugin implementing this interface, and calling the macro.
#include <plugin.h>
class MyPlugin : public Plugin {
public:
virtual std::string command(std::string command, std::string options) {
return command + " " + options;
}
};
DEFINE_PLUGIN(MyPlugin, "Simple Plugin", "0.0.1")
This defines a simple plugin which just echoes it’s command and options back. But you really could do anything in the plugin.
You could, for instance, create extra methods on the plugin. They would only be callable by your code - but you are not confined to only using the methods in the interface.
#include <plugin.h>
class MyPlugin : public Plugin {
public:
std::string get_value() {
return "THIS IS INTERNAL TO THE PLUGIN";
}
virtual std::string command(std::string command, std::string options) {
return get_value();
}
};
DEFINE_PLUGIN(MyPlugin, "Plugin with extra methods", "0.0.1")
You could also write a plugin which calls out to other classes, and that would still work, as long as your application only knows about the interface.
Loading Plugins
To load plugins you - again - need the interface for the plugin, as well as the dlfcn header (for unix like systems). To get this working on windows you need to make some changes to both the plugin.h file, and the loader.
#include <dlfcn.h>
#include <plugin.h>
class PluginHandler {
std::shared_ptr<Plugin> (*_load)();
void* handle;
char* (*_get_name)();
char* (*_get_version)();
std::shared_ptr<Plugin> instance;
public:
PluginHandler(std::string name) {
handle = dlopen(name.c_str(), RTLD_LAZY);
_load = (std::shared_ptr<Plugin> (*)())dlsym(handle, "load");
_get_name = (char* (*)())dlsym(handle, "name");
_get_version = (char* (*)())dlsym(handle, "version");
}
std::string get_name() {
return std::string(_get_name());
}
std::string get_version() {
return std::string(_get_version());
}
std::shared_ptr<Plugin> load() {
if(!instance)
instance = _load();
return instance;
}
};
First we define the functions that we want to load from the plugin, and then we make a constructor that opens the plugin using dlopen, which returns a handle to the load plugin. Using this handle we can load the addresses of the functions we want to import into our program, and map them to our defined functions.
The rest of the class is just delegating calls to the imported functions, and here the load method is the most intersting, since it is the one that actually instantiates a plugin object, and returns the shared pointer to the caller.
Using it
If you want to use this you only need to create a new plugin handler with one param: the path of the plugin.
PluginHandler ph("path/to/a/plugin.dylib");
And then use that to load the actual object.
std::shared_ptr<Plugin> plugin = ph.load();
Then you will be able to call the plugins command method.
Example
Following is an example of a program that loads plugins dynamically. The code that loads the plugins is rather naive. It assumes you have a plugins/bin directory in the working directory, and the only contents of that directory is valid plugins. (Otherwise the program might just crash, and do horrible stuff).
Then for each of the plugins loaded it will print the name and version of it, and call the command method, and print the result of that.
#include <dirent.h>
#include <plugin.h>
#include <iostream>
#include <vector>
#include "./plugin_handler.hpp"
std::vector<PluginHandler> load_plugins() {
std::vector<PluginHandler> plugins;
DIR *dir;
struct dirent *ent;
if ((dir = opendir ("plugins/bin")) != NULL) {
while ((ent = readdir (dir)) != NULL) {
if(ent->d_name[0] != '.')
plugins.push_back(PluginHandler("plugins/bin/" + std::string(ent->d_name)));
}
closedir (dir);
}
return plugins;
}
int main(int argc, char *argv[])
{
auto plugins = load_plugins();
for (auto ph : plugins) {
auto plugin = ph.load();
std::cerr << "Auto loaded plugin: " << ph.get_name() << ", version: " << ph.get_version() << std::endl;
std::cerr << "Running plugins command method: " << std::endl;
std::cerr << plugin->command("Command here", "options here") << std::endl;
}
return 0;
}