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

Making Plugins

Plugins are self-contained units of behavior loaded at runtime from shared libraries. They have a manifest, a lifecycle, and full access to the message bus.

Before you start

Read Manifests and Messaging first.

Introduction

What is a Plugin?

A plugin is a class that inherits ml::PluginWith<Manifest> and compiles into a shared library (.dylib, .so, .dll). PluginManager loads it at runtime, constructs it, and calls onLoad(). When unloaded, onUnload() fires and all subscriptions are cleaned up automatically.

📋
Manifest-driven
Every plugin declares its identity (name, version), resources, flags, and states in a manifest — exactly like components.
📡
Message bus built-in
Plugin inherits Messenger directly — every plugin can sendMessage and onMessage out of the box.
🔒
Safe unloading
All subscriptions are force-unsubscribed before the library closes, preventing use-after-free from dangling callbacks.

1Core Concepts

The Plugin Manifest

A plugin manifest is identical to a component manifest with one addition: it must declare name and version as static constexpr const char*. A static_assert enforces this at compile time.

ScoreManifest.h cpp
#pragma once
#include 

class ScoreManifest : public ml::Manifest
{
public:
    // ── Required for all plugins ──────────────────────────────────────
    static constexpr const char* name    = "Score Plugin";
    static constexpr const char* version = "1.0.0";

    // ── Optional assets ───────────────────────────────────────────────
    enum class Images { Background };
    enum class Fonts  { Display };

    // ── Thumbnail for plugin carousel (auto-detected name) ────────────
    // enum class Images { THUMBNAIL, Background };

    // ── Custom flags and states ───────────────────────────────────────
    enum class Flag  { Visible, Paused };
    enum class State { Hidden, Showing };

    enum class Ints { MaxScore };

private:
    inline static const auto _ = [](){
        set(Images::Background, "assets/score_bg.png");
        set(Fonts::Display,     "assets/score_font.ttf");
        set(Ints::MaxScore,     9999);
        return 0;
    }();
};
Missing name or version triggers a clear static_assert at compile time: [Malena] Manifest is missing: static constexpr const char* name = ...;

2Core Concepts

Inheriting PluginWith

ScorePlugin.h cpp
#pragma once
#include 
#include "ScoreManifest.h"

class ScorePlugin : public ml::PluginWith
{
public:
    void onLoad() override
    {
        // Access manifest resources — Resources alias is injected automatically
        auto& bg   = Resources::get(Images::Background);
        auto& font = Resources::get(Fonts::Display);

        // Set up visuals
        _background.setSize({300.f, 80.f});
        _background.setPosition({20.f, 20.f});
        _scoreText.setFont(font);
        _scoreText.setString("Score: 0");
        _scoreText.setCharacterSize(28);
        _scoreText.setPosition({30.f, 35.f});

        // Subscribe to score update messages
        onMessage(GameMsg::ScoreChanged, [this](const int& score) {
            _score = score;
            _scoreText.setString("Score: " + std::to_string(score));
        });

        setState(State::Showing);
        enableFlag(Flag::Visible);
    }

    void onUnload() override
    {
        // Called just before destruction.
        // All subscriptions cleaned up automatically AFTER this returns.
        _score = 0;
    }

    // Expose a typed interface for the host to query
    int getScore() const { return _score; }
    void resetScore() { _score = 0; }

private:
    ml::Rectangle _background;
    ml::Text      _scoreText;
    int           _score = 0;
};

3Core Concepts

Registering with ML_EXPORT

Place ML_EXPORT(ClassName) after the class definition. This emits the extern "C" factory symbols that PluginManager looks for when loading the shared library.

ScorePlugin.h — bottom of file cpp
// After the class definition, outside any namespace:
ML_EXPORT(ScorePlugin)

// This emits:
//   extern "C" ml::Plugin* createPlugin()    { return new ScorePlugin(); }
//   extern "C" void destroyPlugin(Plugin* p) { delete p; }
ML_EXPORT must be outside any namespace, after the class is fully defined.

4Core Concepts

CMakeLists.txt for a Plugin

CMakeLists.txt cmake
cmake_minimum_required(VERSION 3.21)
project(ScorePlugin VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

find_package(Malena REQUIRED)
find_package(SFML 3 COMPONENTS Graphics Window System REQUIRED)

# Build as a shared library — NOT an executable
add_library(ScorePlugin SHARED
    src/ScorePlugin.cpp
)

target_include_directories(ScorePlugin PRIVATE src)
target_link_libraries(ScorePlugin PRIVATE
    Malena::Malena sfml-graphics sfml-window sfml-system
)

set_target_properties(ScorePlugin PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/plugins"
    PREFIX ""
)