ml::docs | Your First App Manifests Events Plugins Scenes Graphics Resources
← All Tutorials
Intermediate 5 sections ~20 min C++17/20

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.

MethodDescription
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.