Skip to content
C++
Domain Track
Difficulty 3/5

Plugin 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::vector in 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