Plugin systems in C++ are a minefield. Load a shared library compiled with a different compiler version and you get silent corruption. Pass an std::string across the DLL boundary and watch it crash on a different platform’s allocator. Use virtual methods with different vtable layouts and enjoy debugging memory that looks fine until it isn’t.
Most C++ plugin frameworks try to solve this by adding complexity - COM-style interfaces, protocol buffers, IPC layers, entire RPC frameworks. Pluga goes the other direction. One header file. One macro. POD types across the boundary. The hard problems are solved by not pretending they don’t exist.
The ABI problem
C++ has no stable ABI. Standard library containers differ between compilers, platforms, and even point releases. A std::vector<int> compiled with GCC 12 is not binary compatible with one compiled with MSVC 2022. Template instantiations, vtable layouts, name mangling, exception handling - all implementation-defined.
Pluga’s answer: don’t cross the boundary with anything that can break. The plugin interface uses const char*, unsigned int, bool, and void*. Plain old data. If it’s a C type, it’s safe. If it’s not, encode it first.
Commands flow through a single dispatch method:
class IPlugin {
public:
virtual bool onCommand(const char* node, const char* data, unsigned int size) = 0;
virtual const char* lastError() = 0;
};
Command nodes are namespaced REST-style - "options:set", "file:write", "model:predict". The data buffer is opaque: JSON for structured data, raw bytes for binary. One interface handles any functionality without API changes.
One macro, one export
Plugin implementation is three lines of ceremony:
class MyPlugin : public IPlugin {
bool onCommand(const char* node, const char* data, unsigned int size) override {
if (strcmp(node, "options:set") == 0) {
// parse JSON from data, configure plugin
return true;
}
return false;
}
const char* lastError() override { return error_.c_str(); }
};
SCY_PLUGIN(MyPlugin, "My Plugin", "1.0.0")
The SCY_PLUGIN macro generates the extern "C" export block - a getPlugin() factory function (singleton pattern) and an exports descriptor with metadata:
struct PluginDetails {
int apiVersion; // ABI version check at load time
const char* fileName; // Source file (__FILE__)
const char* className; // Stringified class name
const char* pluginName; // Human-readable name
const char* pluginVersion;// Semantic version
GetPluginFunc initializeFunc; // Factory function pointer
};
The application loads the shared library, reads the exports symbol, checks the API version, and calls the factory. If the versions don’t match, it fails explicitly instead of corrupting silently.
Loading plugins
SharedLibrary lib;
lib.open("path/to/myplugin.so");
PluginDetails* info;
lib.sym("exports", reinterpret_cast<void**>(&info));
// Version gate - fail fast, fail loud
if (info->apiVersion != SCY_PLUGIN_API_VERSION)
throw std::runtime_error("ABI version mismatch");
IPlugin* plugin = info->initializeFunc();
plugin->onCommand("options:set", json.c_str(), json.size());
Cross-platform: .so on Linux, .dylib on macOS, .dll on Windows. The SharedLibrary wrapper handles platform-specific loading (dlopen/LoadLibrary) and symbol resolution (dlsym/GetProcAddress).
Why this design
Every decision optimises for one thing: plugins that work in production without requiring lockstep compiler upgrades across your entire ecosystem.
- Singleton per plugin - one instance, deterministic lifecycle, no ownership confusion
- POD-only boundary - immune to STL/ABI incompatibility
- Command dispatch - new features without API breaks. Add a command, not a method
- Version at load time - catch incompatibility before any code runs, not after a segfault
- Single header - no build system integration beyond linking the shared library
Pluga pairs with Pacm for distribution - download, verify, extract, and install plugin packages from a remote repository. Together they form a complete SDK distribution system: build your plugin, package it, host it, and let users install it with checksum verification and version pinning.