Domain Track
Difficulty 3/5Plugin System in C++
C++ plugin architecture — dynamic loading with dlopen/LoadLibrary, plugin interfaces, versioning, hot reload, and CMake shared library setup.
TL;DR
C++ plugin systems use shared libraries loaded at runtime via dlopen (POSIX) or LoadLibrary (Windows). Define a stable C interface or versioned ABI for plugins, and use a factory function (create_plugin) to avoid C++ name mangling issues.
Plugin Interface Design
cpp
// plugin_api.h — shared between host and all plugins
// Use only C++ features with stable ABI: vtables, C linkage
#pragma once
#include <cstdint>
// Version the API so plugins can check compatibility
struct PluginVersion {
uint16_t major; // breaking changes
uint16_t minor; // backward-compatible additions
};
constexpr PluginVersion kPluginApiVersion{2, 0};
// Plugin interface — pure virtual, no data members
struct Plugin {
virtual const char* name() const = 0;
virtual const char* version() const = 0;
virtual PluginVersion api_version() const = 0;
virtual bool initialize(const char* config) = 0;
virtual void process(const void* data, size_t size) = 0;
virtual void shutdown() = 0;
virtual ~Plugin() = default;
};
// C linkage factory functions — exported from the shared library
extern "C" {
Plugin* create_plugin();
void destroy_plugin(Plugin*);
PluginVersion plugin_api_version(); // pre-load version check
}Plugin Implementation (in .so/.dll)
cpp
// my_plugin.cpp — compiled as a shared library
#include "plugin_api.h"
#include <cstring>
class MyPlugin : public Plugin {
bool initialized_ = false;
public:
const char* name() const override { return "MyPlugin"; }
const char* version() const override { return "1.0.0"; }
PluginVersion api_version() const override { return {2, 0}; }
bool initialize(const char* config) override {
// parse config, set up resources
initialized_ = true;
return true;
}
void process(const void* data, size_t size) override {
if (!initialized_) return;
// process data...
}
void shutdown() override { initialized_ = false; }
};
extern "C" {
Plugin* create_plugin() { return new MyPlugin; }
void destroy_plugin(Plugin* p){ delete p; }
PluginVersion plugin_api_version(){ return {2, 0}; }
}Plugin Loader (Host Side)
cpp
// plugin_loader.h — POSIX version
#include "plugin_api.h"
#include <dlfcn.h>
#include <stdexcept>
#include <memory>
#include <filesystem>
class PluginLoader {
void* handle_ = nullptr;
Plugin* plugin_ = nullptr;
void (*destroy_)(Plugin*) = nullptr;
public:
explicit PluginLoader(const std::filesystem::path& path) {
// RTLD_LAZY: resolve symbols on use; RTLD_LOCAL: don't pollute global namespace
handle_ = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL);
if (!handle_)
throw std::runtime_error(std::string("dlopen failed: ") + dlerror());
// Check API version before constructing plugin
using VersionFn = PluginVersion(*)();
auto version_fn = reinterpret_cast<VersionFn>(dlsym(handle_, "plugin_api_version"));
if (!version_fn)
throw std::runtime_error("missing plugin_api_version symbol");
auto ver = version_fn();
if (ver.major != kPluginApiVersion.major)
throw std::runtime_error("plugin API major version mismatch");
// Get factory and destructor
using CreateFn = Plugin*(*)();
using DestroyFn = void(*)(Plugin*);
auto create = reinterpret_cast<CreateFn>(dlsym(handle_, "create_plugin"));
destroy_ = reinterpret_cast<DestroyFn>(dlsym(handle_, "destroy_plugin"));
if (!create || !destroy_)
throw std::runtime_error("missing create/destroy symbols");
plugin_ = create();
if (!plugin_) throw std::runtime_error("create_plugin returned null");
}
~PluginLoader() {
if (plugin_ && destroy_) destroy_(plugin_);
if (handle_) dlclose(handle_);
}
// Non-copyable, movable
PluginLoader(const PluginLoader&) = delete;
PluginLoader& operator=(const PluginLoader&) = delete;
Plugin* get() { return plugin_; }
Plugin* operator->() { return plugin_; }
};Plugin Registry
cpp
class PluginRegistry {
std::vector<std::unique_ptr<PluginLoader>> loaders_;
std::unordered_map<std::string, Plugin*> by_name_;
public:
void load_dir(const std::filesystem::path& dir) {
for (const auto& entry : std::filesystem::directory_iterator(dir)) {
if (entry.path().extension() != ".so") continue;
try {
auto loader = std::make_unique<PluginLoader>(entry.path());
Plugin* p = loader->get();
by_name_[p->name()] = p;
loaders_.push_back(std::move(loader));
std::println("loaded plugin: {} v{}", p->name(), p->version());
} catch (const std::exception& e) {
std::println(stderr, "failed to load {}: {}", entry.path().string(), e.what());
}
}
}
Plugin* find(std::string_view name) const {
auto it = by_name_.find(std::string{name});
return it != by_name_.end() ? it->second : nullptr;
}
size_t size() const { return loaders_.size(); }
};
// Usage
PluginRegistry registry;
registry.load_dir("plugins/");
if (auto* p = registry.find("MyPlugin")) {
p->initialize("{}");
p->process(data.data(), data.size());
p->shutdown();
}Hot Reload Pattern
cpp
class ReloadablePlugin {
std::filesystem::path path_;
std::filesystem::file_time_type last_modified_;
std::unique_ptr<PluginLoader> loader_;
public:
explicit ReloadablePlugin(std::filesystem::path p) : path_{std::move(p)} {
reload();
}
bool check_reload() {
auto mtime = std::filesystem::last_write_time(path_);
if (mtime != last_modified_) {
reload();
return true;
}
return false;
}
Plugin* get() { return loader_ ? loader_->get() : nullptr; }
private:
void reload() {
loader_ = std::make_unique<PluginLoader>(path_);
last_modified_ = std::filesystem::last_write_time(path_);
loader_->get()->initialize("{}");
std::println("reloaded {}", path_.filename().string());
}
};
// In main loop:
ReloadablePlugin rp{"plugins/renderer.so"};
while (running) {
rp.check_reload(); // reload if .so changed on disk
if (auto* p = rp.get())
p->process(frame_data.data(), frame_data.size());
}CMake: Building a Plugin
cmake
# Host application
add_executable(host main.cpp plugin_loader.cpp)
target_link_libraries(host PRIVATE dl) # link libdl on Linux
# Plugin (shared library)
add_library(my_plugin SHARED my_plugin.cpp)
target_include_directories(my_plugin PRIVATE ${CMAKE_SOURCE_DIR}/include)
# Remove "lib" prefix on Linux so plugin can be named "my_plugin.so"
set_target_properties(my_plugin PROPERTIES PREFIX "")
# Copy plugin to output directory
add_custom_command(TARGET my_plugin POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:my_plugin>
${CMAKE_BINARY_DIR}/plugins/)ABI Stability Tips
- Avoid
std::string,std::vectorin plugin interfaces — their layout can differ between compilers and standard library versions - Use
const char*, raw pointers, and sizes for data passing - Never return C++ exceptions across the plugin boundary — catch all in plugin, return error codes
- Version the API struct explicitly; bump major version on any breaking change