Skip to content
CXO2 edited this page Jul 18, 2026 · 3 revisions

Overview

The Genode Events module provides the framework's event dispatching mechanism, also known as a publish/subscribe pattern.

At the time of writing, the Events module has one core component: Gx::Events::EventDispatcher, along with its supporting types such as Gx::Events::Subscriber and Gx::Events::EventHandler.

Gx::Events::EventDispatcher implements Gx::Updatable and Gx::Module, so it can be installed into a Gx::Application as a module and driven by the application game loop. See Dispatching the queue as a module for more details.

Event Dispatcher

The Gx::Events::EventDispatcher class provides a keyed event dispatching mechanism. Components subscribe handlers to a key, and the dispatcher invokes those handlers whenever an event is dispatched with an equal key of the same type and a matching argument list. This allows components to communicate without holding direct references to each other.

Subscribing to events

Use On() to subscribe a handler to a key. The argument list of the subscription is deduced from the handler signature, so the handler parameters define which events the subscription matches.

auto dispatcher = Gx::Events::EventDispatcher();

// Subscribe to "score-changed" events carrying an int
auto subscriber = dispatcher.On("score-changed", [] (int score)
{
    std::cout << "Score: " << score << std::endl;
});

// Invoke every subscriber of "score-changed" with a matching argument list
dispatcher.Dispatch("score-changed", 100);

The same key can be subscribed with different argument lists side by side; an event only reaches the subscribers whose argument list matches the dispatched arguments. Keys and arguments are matched by their canonical types: string-like keys (string literals, std::string, std::string_view) are canonicalized to std::string and compared by content, while any other type is compared by its decayed type and operator==. Keys are not limited to strings; any equality-comparable and hashable type works, such as enums or numeric message opcodes.

Important

The handler signature must be concrete because the argument list is deduced from it. Generic callables (e.g., lambdas taking const auto&) cannot be deduced and will fail to compile.

Subscriber lifetime

On() returns a Gx::Events::Subscriber object that represents the active subscription. The subscriber owns the handler and unsubscribes it from the dispatcher automatically when destroyed. It is movable but not copyable, and a subscriber that outlives its dispatcher simply becomes inactive.

Store the subscriber as a member of the class that owns the handler, so the subscription's lifetime follows the object:

class ScoreBoard
{
public:
    explicit ScoreBoard(Gx::Events::EventDispatcher& dispatcher)
    {
        m_subscriber = dispatcher.On("score-changed", [this] (const std::string& player, int score)
        {
            OnScoreChanged(player, score);
        });
    }

    // The subscription is removed automatically when ScoreBoard is destroyed

private:
    void OnScoreChanged(const std::string& player, int score)
    {
        // Update the score board display here..
    }

    // Declare the subscriber with the key type followed by the handler argument types
    // The types may be spelled exactly as the handler takes them; they are canonicalized internally
    Gx::Events::Subscriber<const char*, const std::string&, int> m_subscriber;
};

Use IsActive() to check whether the subscriber is still registered, and Unsubscribe() to remove it manually before destruction. On() also accepts an optional onUnsubscribe callback that is invoked once when the subscriber unsubscribes itself.

Dispatching events

Dispatch() delivers an event synchronously to every matching subscriber on the calling thread. An argument passed as a non-const lvalue of its canonical type is passed by reference: subscribers may modify it, and the modifications remain visible to the caller and to later subscribers. Any other argument is materialized into a copy for the duration of the call.

auto input = PlayerInput();
dispatcher.Dispatch("input-captured", input); // Passed by reference: subscribers can alter `input`
dispatcher.Dispatch("input-captured", PlayerInput()); // Temporary: subscribers receive a copy

The dispatcher takes no ownership of the arguments; the caller is responsible for the validity of the references and pointers it passes.

A typical scenario is dispatching network messages: packets polled from Gx::TcpNetworkClient are dispatched with their opcode as the key, so each service only receives the messages it subscribed to. Note that Poll() already surfaces the received packets on the calling thread, so a plain Dispatch in the game loop is all that is needed.

// In the game frame (e.g., a module or scene Update):
for (auto& packet : m_client.Poll())
{
    std::uint16_t opcode = 0;
    if (packet >> opcode)
        m_dispatcher.Dispatch(opcode, packet);
}

// Elsewhere, a service subscribes to the opcode it cares about:
class ChatService
{
public:
    explicit ChatService(Gx::Events::EventDispatcher& dispatcher)
    {
        m_subscriber = dispatcher.On<std::uint16_t>(0x03E8, [this] (Gx::Packet& packet)
        {
            std::string message;
            if (packet >> message)
            {
                // Handle the chat message here..
            }
        });
    }

private:
    Gx::Events::Subscriber<std::uint16_t, Gx::Packet&> m_subscriber;
};

Queueing events

The dispatcher itself is not synchronized, except for its event queue. When an event originates from another thread (or should be deferred to a known point of the frame), use Enqueue() instead of Dispatch(); it is safe to call from any thread. Call DispatchQueue() to deliver every enqueued event on the calling thread in their enqueue order. Events enqueued while the queue is dispatching (e.g., from a handler) are delivered by the next DispatchQueue() call.

A typical scenario is a background worker thread that reports its progress to the game: the worker calls Enqueue from its own thread, and the game frame delivers the events on the main thread by calling DispatchQueue, so the handlers can safely touch scene objects.

class LoadingScene : public Gx::Scene
{
protected:
    void Initialize() override
    {
        // Subscribe to the progress events
        // The handler is only ever invoked by DispatchQueue below, which runs on the main thread,
        // so it is safe to manipulate scene objects from here
        m_subscriber = m_dispatcher.On("loading-progress", [this] (float progress)
        {
            // Update the loading indicator here..
        });

        // Load the assets in a background thread
        m_worker = std::thread([this]
        {
            const std::size_t total = 100; // e.g., number of assets to load
            for (std::size_t i = 0; i < total; i++)
            {
                // Perform the heavy loading work here..

                // Enqueue is safe to call from this worker thread
                // The event is stored in the queue and will not be delivered until the main thread dispatches it
                m_dispatcher.Enqueue("loading-progress", static_cast<float>(i + 1) / total);
            }
        });
    }

    void Update(const sf::Time& delta) override
    {
        Gx::Scene::Update(delta);

        // Deliver the events enqueued by the worker thread on the main thread
        m_dispatcher.DispatchQueue();
    }

    void Finalize() override
    {
        if (m_worker.joinable())
            m_worker.join();

        Gx::Scene::Finalize();
    }

private:
    Gx::Events::EventDispatcher m_dispatcher;
    std::thread m_worker;
    Gx::Events::Subscriber<const char*, float> m_subscriber;
};

Important

Arguments enqueued as non-const lvalues of their canonical type are stored by reference and must remain valid until DispatchQueue() delivers the event. Anything else is copied into the queue.

Dispatching the queue as a module

Gx::Events::EventDispatcher implements Gx::Updatable, and its Update() calls DispatchQueue(). Since it also implements Gx::Module, it can be installed into a Gx::Application as a module, which lets the application drive the queue for you: the module is updated every frame on the main thread, so the enqueued events are delivered without you calling DispatchQueue() manually.

class MyGame : public Gx::Application
{
protected:
    void Boot() override
    {
        // Install the dispatcher as a module
        Install<Gx::Events::EventDispatcher>();
    }

    // No need to override Update just to call DispatchQueue();
    // the application updates the installed module every frame, which dispatches the queue
};

// Elsewhere, resolve the shared dispatcher from the context and use it as usual
Gx::Events::EventDispatcher& dispatcher = GetModule<Gx::Events::EventDispatcher>();

This is convenient when the dispatcher is shared across the game (e.g., injected into services via the System Context) and its queue should be drained at a single, well-defined point each frame. See the Modules documentation for more details on installing and resolving modules.

Note

Installing the dispatcher as a module only automates DispatchQueue(). You still call Enqueue() (from any thread) and Dispatch() (on the calling thread) yourself; only the queue delivery is bound to the module update.

A dispatcher that is not installed as a module (e.g., one owned by a scene) is driven exactly as shown in the previous section: call DispatchQueue() from your own Update().

Extending the dispatcher

Gx::Events::EventDispatcher exposes its internal operations as protected virtual functions (Subscribe, Unsubscribe, Rebind, Publish, and Store), allowing derived classes to customize how subscriptions are stored and how events are delivered, while reusing the type-erasure and matching mechanism. Destroying a dispatcher deactivates every subscriber still registered on it.

Clone this wiki locally