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.
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.
name, version), resources, flags, and states in a manifest — exactly like components.Plugin inherits Messenger directly — every plugin can sendMessage and onMessage out of the box.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.
#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;
}();
};
name or version triggers a clear static_assert at compile time: [Malena] Manifest is missing: static constexpr const char* name = ...;Inheriting PluginWith
#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;
};
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.
// 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.CMakeLists.txt for a Plugin
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 ""
)