Plugin management module.
stdcorelib-plugin contains two layers.
- The
stdc::pluginnamespace provides plugin instances, manifest loading, dynamic library loading, static registration, runtime registration, and filesystem discovery. - The
stdc::pluginsystemnamespace builds an application lifecycle, dependency graph, and enabled-state settings on top of that foundation.
The base layer treats a plugin as an implementation of an interface identified by an IID. It does not interpret extension-specific metadata.
A PluginLoader manifest is a JSON object. It reserves two root fields:
iidis required, must be a non-empty string, and identifies the extension point implemented by the plugin.metadatais optional, must be an object when present, and belongs entirely to the extension point named byiid. Its contents are user-defined.
All other root fields are open to the user. PluginLoader preserves the complete manifest and does not interpret those additional fields.
{
"iid": "org.example.Renderer",
"metadata": {
"backend": "software"
}
}The base contract has no schema-version field such as $version. An extension point can define and version its own metadata object when needed.
A dynamic plugin derives from stdc::plugin::Plugin, normally through an IID-specific interface, and exports one instance with STDC_EXPORT_PLUGIN.
#include <stdcorelib/plugin/plugin.h>
class Renderer : public stdc::plugin::Plugin {
public:
virtual void render() = 0;
};
class SoftwareRenderer final : public Renderer {
public:
void render() override {
}
};
STDC_EXPORT_PLUGIN(SoftwareRenderer)To embed the manifest:
- Link the plugin target to
stdcorelib::plugin. - Call
stdc_add_plugin_manifest()with the target and manifest path. - On macOS, let that function give a
MODULEtarget the.dylibsuffix used by plugin discovery.
add_library(software_renderer MODULE softwarerenderer.cpp)
target_link_libraries(software_renderer PRIVATE stdcorelib::plugin)
stdc_add_plugin_manifest(software_renderer software_renderer.json)PluginLoader manages exactly one plugin:
- A filesystem plugin reads its manifest without executing plugin code.
load()opens the library and obtains its instance.unload()releases the library and invalidates the returned pointer.
- A static plugin obtains its registered instance when loaded.
- A runtime plugin wraps an instance already owned by the caller.
#include <stdcorelib/plugin/pluginloader.h>
stdc::plugin::PluginLoader loader(pluginPath);
if (loader.iid() == "org.example.Renderer" && loader.load()) {
auto renderer = dynamic_cast<Renderer *>(loader.plugin());
if (renderer) {
renderer->render();
}
}
loader.unload();The filesystem interface has two additional rules:
- Pass a second path to the constructor or
setFilePath()to read an external JSON manifest instead of the embedded manifest. - Inspect failed manifest reads and failed loads through
state(),hasError(), anderrorMessage().
PluginFactory owns loaders and discovers filesystem plugins lazily for a requested IID. Its default policy:
- Examines dynamic libraries directly below each search path.
- Accepts only libraries with readable embedded manifests.
- Silently ignores candidates whose IID does not match the requested IID.
#include <filesystem>
#include <vector>
#include <stdcorelib/plugin/pluginfactory.h>
stdc::plugin::PluginFactory factory;
const std::vector<std::filesystem::path> paths{"plugins"};
factory.setPluginPaths("org.example.Renderer", paths);
for (auto loader : factory.plugins("org.example.Renderer")) {
if (!loader->load()) {
// Report loader->filePath() and loader->errorMessage().
continue;
}
auto renderer = dynamic_cast<Renderer *>(loader->plugin());
if (renderer) {
renderer->render();
}
}Discovery has the following extension and lifetime rules:
- Subclasses can override
scanPluginPaths()andresolvePluginPath()to implement another directory layout or external manifest policy. - Replacing search paths discards previously discovered filesystem loaders that are not loaded.
- Loaded plugins are not unloaded when search paths change.
- Programs should set all search paths before the first query or load.
Static registration is lazy:
STDC_EXPORT_STATIC_PLUGINregisters a static plugin for an IID.- The manifest is evaluated when it is first requested.
- The plugin instance is created when its loader is loaded.
STDC_EXPORT_STATIC_PLUGIN(
SoftwareRenderer,
"org.example.Renderer",
(stdc::json::Object{
{"iid", "org.example.Renderer"},
{"metadata", stdc::json::Object{{"backend", "software"}}},
})
)
stdc::plugin::PluginFactory factory;
factory.addStaticPlugins("org.example.Renderer");For a runtime plugin, the caller creates and owns the instance, then passes it together with its complete manifest:
SoftwareRenderer renderer;
stdc::plugin::PluginFactory factory;
factory.addRuntimePlugin(
&renderer,
stdc::json::Object{
{"iid", "org.example.Renderer"},
{"metadata", stdc::json::Object{{"backend", "software"}}},
}
);Static and runtime plugins differ only in how their instances enter the factory:
addStaticPlugins()adds registered plugins for an IID. Their instances are created when their loaders are loaded.addRuntimePlugin()adds an instance already owned by the caller.- Neither kind can be unloaded because its loader does not control the instance lifetime.
stdc::pluginsystem::PluginSystem adds a fixed application plugin contract to the base layer. Each system:
- Accepts exactly one IID through its constructor.
- Discovers filesystem plugins only.
- Resolves plugin dependencies.
- Applies global and local enabled-state settings.
- Controls plugin startup and shutdown.
PluginSystem retains the PluginLoader contract and reserves additional fields:
- The root
iidfield is still required and must match the IID passed to thePluginSystemconstructor. - The root
metadatafield is required and follows thePluginSystemschema below. - The root
namefield is reserved by theDirectorylayout and gives the plugin library's platform-independent name. It is separate from the user-visiblemetadata.namefield. - Other root fields remain user-defined and are not interpreted by
PluginSystem.
The metadata object has the following schema:
{
"iid": "org.example.ApplicationPlugin",
"metadata": {
"id": "org.example.editor",
"name": "Editor",
"version": "2.1.0",
"compatVersion": "2.0.0",
"enabledByDefault": true,
"dependencies": [
{
"id": "org.example.core",
"version": "3.0.0",
"type": "required"
},
{
"id": "org.example.diagnostics",
"version": "1.0.0",
"type": "optional"
}
]
}
}The fields inside metadata have these meanings:
idis required, identifies the plugin, and must be unique within the system.nameis required display text and may repeat.versionis required and gives the current plugin version.compatVersionis optional and defaults toversion.enabledByDefaultis optional and defaults totrue.dependenciesis optional and defaults to an empty array. Each entry uses the reservedid,version, andtypefields.- Any other field is user-defined and is not interpreted by
PluginSystem. The complete object remains available throughPluginSpec::manifest().
A plugin with compatibility version C and current version V satisfies a requested version R when C <= R <= V.
PluginSystem supports two filesystem layouts:
Flatis the default. It puts plugin libraries directly below each search path and reads their embedded manifests.Directorygives every plugin a child directory containing its library and a sidecarplugin.json. The manifest must contain a rootnamewithout a platform library prefix or suffix. For example,"name": "editor"can resolve toeditor.dll,libeditor.dll,libeditor.so, orlibeditor.dylib.
Application plugins implement stdc::pluginsystem::IPlugin and export their instance:
#include <stdcorelib/pluginsystem/iplugin.h>
class EditorPlugin final : public stdc::pluginsystem::IPlugin {
public:
bool initialize(std::string *errorMessage) override {
return true;
}
void pluginsInitialized() override {
}
void aboutToShutdown() override {
}
};
STDC_EXPORT_PLUGIN(EditorPlugin)Configure every path and setting before loadPlugins(). Loading freezes the discovered plugin set, paths, and settings, so later changes have no effect.
A typical application treats the two settings files differently:
- The global file belongs to the application installation. An installer normally provides it. If it is absent or unreadable, the application may create and save its global defaults only when that location is writable. If writing there requires administrator or root privileges, the installer must provide the file.
- The local file belongs to the current user. It normally does not exist on first startup, so an empty
PluginSettingsvalue is the local default. - After plugin shutdown, the application saves the local settings for the next run.
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <utility>
#include <vector>
#include <stdcorelib/pluginsystem/pluginsettings.h>
#include <stdcorelib/pluginsystem/pluginsystem.h>
#include <stdcorelib/support/json.h>
using namespace stdc::pluginsystem;
std::optional<PluginSettings> readPluginSettings(const std::filesystem::path &path) {
std::ifstream file(path);
if (!file) {
return std::nullopt;
}
std::stringstream text;
text << file.rdbuf();
stdc::json::ParseError parseError;
auto value = stdc::json::Value::fromJson(text.str(), true, &parseError);
if (parseError) {
return std::nullopt;
}
return PluginSettings::fromJson(value);
}
bool writePluginSettings(const std::filesystem::path &path,
const PluginSettings &settings) {
std::error_code error;
if (!path.parent_path().empty()) {
std::filesystem::create_directories(path.parent_path(), error);
}
if (error) {
return false;
}
std::ofstream file(path);
file << settings.toJson().toJson(4);
return bool(file);
}
int runApplication(const std::filesystem::path &applicationDir,
const std::filesystem::path &userConfigDir) {
const auto globalSettingsPath = applicationDir / "plugin-settings.json";
const auto localSettingsPath = userConfigDir / "plugin-settings.json";
PluginSettings globalSettings;
if (auto stored = readPluginSettings(globalSettingsPath)) {
globalSettings = std::move(*stored);
} else {
globalSettings.setPluginEnabled("org.example.experimental", true);
if (!writePluginSettings(globalSettingsPath, globalSettings)) {
return 1;
}
}
PluginSettings localSettings;
if (auto stored = readPluginSettings(localSettingsPath)) {
localSettings = std::move(*stored);
}
PluginSystem plugins("org.example.ApplicationPlugin");
const std::vector<std::filesystem::path> paths{applicationDir / "plugins"};
plugins.setPluginPaths(paths);
plugins.setPluginSettings(PluginSystem::Global, globalSettings);
plugins.setPluginSettings(PluginSystem::Local, localSettings);
plugins.loadPlugins();
for (const auto spec : plugins.plugins()) {
if (spec->hasError()) {
// Report spec->id() and spec->errorMessage().
}
}
plugins.shutdownPlugins();
if (!writePluginSettings(localSettingsPath, plugins.pluginSettings(PluginSystem::Local))) {
return 1;
}
return 0;
}The lifecycle has the following ordering and ownership rules:
- Loading and initialization follow dependency order.
pluginsInitialized()andaboutToShutdown()run in reverse dependency order.- Shutdown unloads libraries in reverse dependency order.
- After a spec reaches a loaded lifecycle state,
PluginSpec::plugin()returns its non-owningIPluginpointer. Hosts can cast it to the IID-specific application interface. - Successful shutdown clears the plugin pointer when the library is unloaded.
- Repeated lifecycle calls have no effect.
- The
PluginSystemdestructor performs shutdown when the host does not call it explicitly.
PluginSettings represents one settings source and is independent of file I/O. A host supplies separate values to PluginSystem:
- Global settings override the manifest value and produce
enabledByGlobalSettings(). - Local settings override the global result and produce the final
isEnabled().
Each settings value has the following serialization behavior:
- It converts between in-memory JSON and explicit enabled-state overrides.
- It preserves unknown plugin IDs.
- It rejects duplicate or conflicting IDs.
- Its mutable
userData()object stores application-defined settings without couplingPluginSettingsto a particular application schema.
{
"disabledPlugins": ["org.example.diagnostics"],
"enabledPlugins": ["org.example.experimental"],
"userData": {
"theme": "dark"
}
}Disabled plugins affect dependency resolution as follows:
- A disabled required dependency makes its dependent plugin invalid.
- A disabled optional dependency is treated as absent.
- A disabled plugin does not count as an error by itself.
MIT. See LICENSE.