Intermediate
Hosting Plugins
Load, query, and safely unload plugins at runtime. PluginManager handles the full lifecycle — even when unload is called from inside a callback.
Before you start
Read Making Plugins first.
Introduction
PluginManager Overview
ml::PluginManager handles the full plugin lifecycle. It abstracts platform differences (dlopen vs LoadLibraryA) behind a clean API and inherits DeferredOperationsManager so unloads triggered from callbacks are always safe.
| Method | Description |
|---|---|
loadPlugin(path) | Open the library, construct the plugin, call onLoad(). Returns pointer or nullptr. |
loadPluginsFromDirectory(dir) | Scan a directory and load all plugin libraries found. |
getPlugins() | Return all currently active plugin pointers. |
unloadPlugin(plugin*) | Call onUnload(), force-unsubscribe all events, delete, close library. Deferred-safe. |
scanPlugins(dir) | Probe plugins temporarily to read name/version/thumbnail. Returns vector<PluginInfo>. |
1Core Concepts
Loading Plugins at Runtime
Initialization — loading plugins
cpp
class MyApp : public ml::ApplicationWith
{
ml::PluginManager _pluginManager;
public:
void initialization() override
{
// Load all plugins from a directory
_pluginManager.loadPluginsFromDirectory("plugins/");
// Or load a single plugin
ml::Plugin* score = _pluginManager.loadPlugin("plugins/ScorePlugin.dylib");
if (!score) std::cerr << "Failed to load ScorePlugin\n";
// Register any plugin that is also a Core component
for (auto* plugin : _pluginManager.getPlugins()) {
if (auto* core = plugin->getIf())
addComponent(*core);
}
}
};
2Core Concepts
Type-Safe Plugin Queries
Plugin::is<T>() and Plugin::getIf<T>() provide safe runtime type queries. Define a shared interface header that both host and plugin include.
Querying plugins by interface
cpp
// Shared interface header
class IScorePlugin {
public:
virtual int getScore() const = 0;
virtual void resetScore() = 0;
};
// In your application's registerEvents():
void MyApp::registerEvents()
{
for (auto* plugin : _pluginManager.getPlugins()) {
// Type-safe query — no raw casts
if (auto* score = plugin->getIf()) {
_resetBtn.onClick([score]{ score->resetScore(); });
}
// Just check type without using it
if (plugin->is())
std::cout << "Found: " << plugin->getName() << "\n";
}
}
3Core Concepts
scanPlugins — Plugin Carousel
scanPlugins() probes each library without keeping it loaded — perfect for a game-selection carousel. Thumbnail textures are copied to CPU memory before the library closes.
Building a plugin selection UI
cpp
void MyApp::initialization()
{
// Scan without loading — reads metadata only
std::vector infos = _pluginManager.scanPlugins("games/");
for (const auto& info : infos) {
// info.name, info.version, info.path are always populated
// info.thumbnail is std::optional
if (info.thumbnail) {
auto* sprite = new ml::Sprite();
sprite->setTexture(*info.thumbnail);
_carousel.add(*sprite);
}
_gamePaths.push_back(info.path);
}
// Launch the selected game
_launchBtn.onClick([this]{
int i = _carousel.getCurrentIndex();
if (i < (int)_gamePaths.size())
_pluginManager.loadPlugin(_gamePaths[i]);
});
}
4Core Concepts
Safe Unloading
Unloading from anywhere — even a callback
cpp
// Safe from anywhere — deferred if called during event delivery
_pluginManager.unloadPlugin(scorePlugin);
// Safe from inside the plugin's own message callback
scorePlugin->onMessage(GameMsg::Quit, [this, scorePlugin](const int&) {
// Deferred — actual unload happens after this callback returns
_pluginManager.unloadPlugin(scorePlugin);
});
// Reload pattern
void MyApp::reloadPlugin(const std::string& path)
{
for (auto* p : _pluginManager.getPlugins()) {
if (std::string(p->getName()) == "Score Plugin") {
_pluginManager.unloadPlugin(p);
break;
}
}
_pluginManager.loadPlugin(path); // load fresh version
}
After calling
unloadPlugin(p), never dereference p again — the memory has been freed. Store the path string, not the pointer, if you need to reload.