Link-Time Plug-In Management
C++11 header-only library for registration and management of link-time plug-ins.
Here's a talk that introduces the motivation and implementation of link-time plug-ins: https://youtu.be/BxOiUv6bEqU
What is it?
In order to achieve a modular structure, many applications use a plug-in based architecture that cleanly separates the core logic from the implementation of individual drivers/handlers that do the actual work. For example:
- A web server has a plug-in for every URI path.
- A unit test framework has a plug-in for every test case.
- A command line application has a plug-in for every subcommand.
- An image viewer has a plug-in for every image file type.
- An SNMP agent has a plug-in for every OID.
There are many possible implementations of such a plug-in architecture:
- Plug-ins may be shared libraries (.so/.dll files) which the application loads
- Plug-ins may be executables to which the application talks via stdin/stdout
- Plug-ins may be scripts executed by an interpreter that's built into the application
All these architectures allow plug-ins to be added or removed dynamically at run-time, without having to rebuild, re-install, or (sometimes) even restart the application. We'll call these run-time plug-ins. Run-time plug-ins are not what this repo is about.
This repo is about link-time plug-ins, which are plug-ins that are compiled and linked into the application. Adding or removing plug-ins requires the application to be rebuilt, so there are no run-time dependencies on external files. Link-time plug-ins are as independent and isolated from the application core as run-time plugins:
- Link-time plug-ins can be added to the application simply by adding their .cpp file name to the build configuration (e. g. cmake file).
- Link-time plug-ins are self-contained within their .cpp files and don't export any implementation details.
- Link-time plug-ins are registered from within their .cpp file. No external list of plug-ins needs to be maintained.
- Link-time plug-ins don't know anything about the application that invokes them.
- The application doesn't know anything about the plug-ins beyond their predefined API.
- The application can iterate over existing plug-ins and invoke functions in the plug-ins.
What's the benefit?
If all plug-ins are linked into the executable, what's the point of having a plug-in architecture in the first place?
The answer is isolation. Imagine a git-like command line program that executes subcommands like this:
int main(int argc, char** argv) {
...
if (strcmp(argv[1], "pull")==0) {
doPull(argc, argv);
} else if (strcmp(argv[1], "commit")==0) {
doCommit(argc, argv);
} else if (strcmp(argv[1], "status")==0) {
doStatus(argc, argv);
} else if (strcmp(argv[1], "diff")==0) {
doDiff(argc, argv);
}
...
}
Every subcommand (pull, commit, etc.) needs to be implemented (doPull, doCommit, etc.) and added to the list in the main function, and probably to other lists (e. g. some showUsage function). The list of subcommands is scattered and multiplied throughout the application, making it tedious to add or remove commands.
With link-time plug-ins, it's much easier because the implementation of subcommands is perfectly isolated from the code that invokes them:
int main(int argc, char** argv) {
...
for (const auto cmd : linktimeplugin::plugins<Command>()) {
if (strcmp(argv[1], cmd->name())==0) {
cmd->execute(argc, argv);
}
}
...
}
No list of subcommands needs to be maintained anywhere in the source code. New commands are added simply by implementing their name()
and execute()
functions in a class derived from the Command
base class, hidden in an anonymous namespace somewhere in their own .cpp files. The "show usage" function might look like this:
void showUsage(char** argv) {
std::cout << "Usage: " << argv[0] << " subcommand [ parameter ... ]\n";
for (const auto cmd : linktimeplugin::plugins<Command>()) {
std::cout << cmd->usage() << "\n";
}
}
So, when you add a new subcommand, it immediately becomes available on the command line (because main
picks it up), and in the usage message (because showUsage
picks it up), with no need to modify either of the two.
Same source, multiple executables
Sometimes you want to build more than one executable from the same source code, where each executable contains a certain combination of the available plug-ins only. Or maybe you're building on various platforms, and certain plug-ins are available only on some of them (and cannot even be compiled on others).
Note that the "if-else chain" solution shown above will work in these cases only with a lot of nasty preprocessor conditionals.
Link-time plug-ins introduce a clean separation between the application core and the plug-in code. The decision which plug-ins go into which executable takes place in the build configuration and need not be replicated anywhere in the source code.
How it works
All plug-ins of the same type (=that use the same API) are derived from a common base class. A "registrar object" is created for each such plug-in class that creates an instance of the plug-in class in its constructor. This instance is made available through a public template function, which the application uses to iterate over the plug-ins.
How to use it
Copy linktimeplugin.hpp
into an include directory of your choice.
Define a base class for your plug-ins which defines the plug-ins' API as a set of pure virtual functions.
For every plug-in, derive a class from this base class and implement the API functions. The plug-in class is usually defined in its own .cpp file in an anonymous namespace.
Register every plug-in with the REGISTER_PLUGIN
macro (immediately after the plug-in class definition).
In the application, invoke linktimeplugin::plugins<Base>()
to retrieve a list of all the plug-ins (where Base
is the plug-in base class).
You can have more than one plug-in base class per application, each with its own API and its own set of plug-ins.
Example
Overview:
// In all files:
#include <linktimeplugin.hpp>
// In a header file:
class PluginBase {
public:
using Base = PluginBase;
virtual void dosomething() = 0;
};
// In the plug-in cpp file:
namespace {
class Plugin : public PluginBase {
void dosomething() override { ... }
};
REGISTER_PLUGIN(Plugin);
}
// In the application:
for (const auto plugin : linktimeplugin::plugins<PluginBase>()) {
plugin->dosomething();
}
This repository contains a complete demonstration program. A plug-in base class is defined in demo.hpp
, and three plug-ins are defined in demo-cat.cpp
, demo-dog.cpp
, and demo-bird.cpp
. Note that these three files don't export any public symbols (everything is inside an anonymous namespace). A single main function (demo-main.cpp
) is used with various combinations of these plug-ins to build three demo programs (demo1
, demo2
, and demo3
). The selection which demo program contains which plug-in takes place in the build configuration (CMakeLists.txt
).
To build and run the example programs:
$ mkdir build
$ cd build
$ cmake ..
$ make
$ ./demo1
$ ./demo2
$ ./demo3
Wolfram Rösler • wolfram@roesler-ac.de • https://gitlab.com/wolframroesler • https://twitter.com/wolframroesler • https://www.linkedin.com/in/wolframroesler/