Skip to content

Latest commit

 

History

History
419 lines (309 loc) · 12.9 KB

File metadata and controls

419 lines (309 loc) · 12.9 KB

EventBus

English | 简体中文

EventBus is a C++17 single-header synchronous event bus. It provides a thread-safe subscription registry, synchronous publishing, runtime argument matching, common string conversions, waiting during unsubscription, orderly shutdown, and injectable diagnostic logging.

The implementation is contained in eventbus.hpp under the eventbus namespace. It depends only on the C++17 standard library.

Features

  • Single header: include eventbus.hpp directly.
  • Synchronous dispatch: publish() invokes callbacks on the calling thread and completes the dispatch before returning.
  • Thread-safe subscription registry: supports concurrent subscribe(), publish(), unsubscribe(), queries, and clear() calls from multiple threads.
  • No callback-level execution lock: the same callback may be invoked concurrently by multiple publishing threads. Callers must synchronize shared state inside their callbacks.
  • Orderly lifecycle management: unsubscribe(), unsubscribe_all(), clear(), and close() prevent new invocations of the affected callbacks and wait for invocations already in progress to finish.
  • Type matching: callback signatures are deduced during subscription, and published argument tuples are matched at runtime.
  • String conversions: supports const char* / char* to std::string or std::string_view, and std::string to std::string_view.
  • Exception isolation: callback exceptions do not escape publish() and are counted in PublishResult::failed.
  • Injectable logging: nothing is written to std::cout or std::cerr by default. Inject a LogHandler when diagnostics are needed.

Quick Start

#include "eventbus.hpp"

#include <iostream>
#include <string>

int main()
{
    eventbus::EventBus bus;

    const auto id = bus.subscribe("greet", [](const std::string& name) {
        std::cout << "Hello, " << name << "\n";
    });

    const auto result = bus.publish("greet", "World");
    std::cout << "invoked: " << result.invoked << "\n";

    (void)bus.unsubscribe("greet", id);
}

API Reference

Types

namespace eventbus {

using callback_id = std::size_t;

enum class LogLevel
{
    Debug,
    Warning,
    Error
};

using LogHandler = std::function<void(LogLevel, const std::string&)>;

} // namespace eventbus

callback_id is returned by subscribe() and identifies a specific subscription for removal. LogHandler is an optional diagnostic callback and is unset by default.

Construction and Shutdown

explicit EventBus(bool verbose_logging = false);
EventBus(bool verbose_logging, LogHandler log_handler);
~EventBus() noexcept;

void close();
void clear();
  • verbose_logging only controls whether diagnostic messages are generated. No output is produced without a LogHandler.
  • close() enters the closed state, clears the subscription registry, and waits for callbacks already in progress to finish.
  • ~EventBus() calls close() and does not throw.
  • clear() removes only the current subscriptions without entering the closed state. New calls to subscribe() remain valid afterward.
  • EventBus is neither copyable nor movable.

After the bus is closed:

  • subscribe() returns 0.
  • publish() returns an empty PublishResult.
  • publish_if_min_subscribers() returns false.

Subscription and Unsubscription

template <typename Callback>
callback_id subscribe(const std::string& eventName, Callback&& callback);

[[nodiscard]] bool unsubscribe(const std::string& eventName, callback_id id);
[[nodiscard]] std::size_t unsubscribe_all(const std::string& eventName);
  • Callbacks must return void.
  • Non-const lvalue-reference parameters, such as int&, are rejected.
  • unsubscribe() returns true when it finds and removes the requested subscription.
  • unsubscribe_all() returns the number of removed subscriptions.
  • A callback may unsubscribe itself without waiting for its own invocation to finish.

Publishing

template <typename... Args>
PublishResult publish(const std::string& eventName, Args&&... args);

template <typename... Args>
[[nodiscard]] bool publish_if_min_subscribers(
    const std::string& eventName,
    std::size_t min_subscribers,
    Args&&... args);

publish() first takes a snapshot of the subscriptions, releases the registry lock, and then synchronously invokes each callback in that snapshot. Subscriptions added during publishing are not included in the current snapshot. A subscription removed during publishing may appear as skipped in that dispatch.

publish_if_min_subscribers() publishes only when the current subscriber count meets or exceeds the threshold. Its return value indicates whether the publishing process was started.

Publish Results

struct PublishResult
{
    std::size_t subscribers;
    std::size_t invoked;
    std::size_t failed;
    std::size_t type_mismatches;
    std::size_t skipped;
};
  • subscribers: number of subscriptions in the dispatch snapshot.
  • invoked: number of callbacks invoked successfully.
  • failed: number of callbacks that threw an exception.
  • type_mismatches: number of callbacks not invoked because their parameters did not match.
  • skipped: number of subscriptions present in the snapshot but deactivated before invocation.

Queries and Statistics

[[nodiscard]] bool isEventRegistered(const std::string& eventName) const;
[[nodiscard]] std::size_t getCallbackCount(const std::string& eventName) const;
[[nodiscard]] std::vector<std::string> getAllEventNames() const;

struct EventBusStats
{
    std::size_t total_events;
    std::size_t total_callbacks;
    std::size_t max_callbacks_per_event;
    std::string most_subscribed_event;
};

[[nodiscard]] EventBusStats getStats() const;

Query functions observe only the current subscription registry and do not wait for callbacks in progress.

Logging

void setVerboseLogging(bool verbose);
void setLogHandler(LogHandler handler);

By default, EventBus writes nothing to standard output or standard error. After a LogHandler is installed:

  • LogLevel::Debug: verbose diagnostics for subscription, publishing, type mismatches, and publish results.
  • LogLevel::Warning: publishing an event with no subscribers; generated only when verbose logging is enabled.
  • LogLevel::Error: a callback threw an exception.

Example:

eventbus::EventBus bus(true, [](eventbus::LogLevel level, const std::string& message) {
    if (level == eventbus::LogLevel::Error) {
        // route to application logger
        (void)message;
    }
});

Usage Examples

Events with Multiple Arguments

eventbus::EventBus bus;

bus.subscribe("user_action",
    [](const std::string& user, const std::string& action, int priority) {
        std::cout << user << " " << action << " " << priority << "\n";
    });

auto result = bus.publish("user_action", "Alice", "login", 5);
if (result.invoked != 1) {
    std::cerr << "dispatch issue\n";
}

String Conversions

eventbus::EventBus bus;

bus.subscribe("name", [](const std::string& value) {
    std::cout << value << "\n";
});
bus.publish("name", "Alice"); // const char* -> std::string

bus.subscribe("view", [](std::string_view value) {
    std::cout << value << "\n";
});

std::string text = "payload";
bus.publish("view", text);   // std::string -> std::string_view
bus.publish("view", "text"); // const char* -> std::string_view

Member Function Callbacks

Use a lambda to bind an object's lifetime explicitly.

class Receiver
{
public:
    void on_value(int value)
    {
        std::cout << value << "\n";
    }
};

Receiver receiver;
eventbus::EventBus bus;

const auto id = bus.subscribe("value", [&receiver](int value) {
    receiver.on_value(value);
});

bus.publish("value", 42);
(void)bus.unsubscribe("value", id);

If the callback might outlive the object, do not capture a raw reference or raw this. Use a std::weak_ptr to check the lifetime:

auto receiver = std::make_shared<Receiver>();
std::weak_ptr<Receiver> weak_receiver = receiver;

bus.subscribe("value", [weak_receiver](int value) {
    if (auto locked = weak_receiver.lock()) {
        locked->on_value(value);
    }
});

Stateful Callbacks

EventBus does not serialize callback execution. Applications must protect their own state.

eventbus::EventBus bus;

std::mutex total_mutex;
int total = 0;

bus.subscribe("count", [&total_mutex, &total](int value) {
    std::lock_guard<std::mutex> lock(total_mutex);
    total += value;
});

If a callback needs to publish another event, release the application lock before calling publish():

bus.subscribe("input", [&bus, &total_mutex, &total](int value) {
    bool should_notify = false;
    int new_total = 0;

    {
        std::lock_guard<std::mutex> lock(total_mutex);
        total += value;
        new_total = total;
        should_notify = total > 100;
    }

    if (should_notify) {
        bus.publish("threshold", new_total);
    }
});

Large Payloads

Published arguments are packed into an internal tuple and may be copied or moved. For large objects, prefer a lightweight handle with explicit ownership or lifetime semantics.

auto data = std::make_shared<const std::vector<int>>(std::vector<int>{1, 2, 3});

bus.subscribe("data", [](std::shared_ptr<const std::vector<int>> payload) {
    std::cout << payload->size() << "\n";
});

bus.publish("data", data);

Conditional Publishing

if (!bus.publish_if_min_subscribers("important", 2, "payload")) {
    // not enough subscribers
}

Shutdown

eventbus::EventBus bus;

bus.subscribe("stop", [] {
    // cleanup
});

bus.publish("stop");
bus.close();

Do not reuse an EventBus instance after closing it. Create a new instance to start again.

Thread Safety

Guarantees Provided by EventBus

  • Concurrent access to the subscription registry is protected internally.
  • publish() does not execute user callbacks while holding the registry lock.
  • unsubscribe(), unsubscribe_all(), clear(), and close() wait for affected callbacks already in progress to finish.
  • A callback can unsubscribe itself without deadlocking itself.

Caller Responsibilities

  • The same callback may be invoked concurrently from multiple threads. Synchronize shared callback state explicitly.
  • Do not call unknown external code while holding an application lock.
  • Do not call publish(), unsubscribe(), clear(), or close() while holding an application lock.
  • If a callback captures an object by reference, the object must outlive the subscription; otherwise, use a weak_ptr lifetime check.
  • close() cannot protect against another thread retaining a reference to an already destroyed EventBus. The application remains responsible for object ownership and joining its threads.

Considerations and Limitations

  • This is a synchronous event bus. It does not provide an asynchronous queue, thread pool, backpressure, cancellation tokens, or cross-thread scheduling semantics.
  • The current API does not promise a stable ABI across DLL boundaries. Do not expose EventBus as a cross-module binary interface.
  • Event names use const std::string&. C++17 std::unordered_map does not provide standard heterogeneous lookup, so no std::string_view event-name API is currently offered.
  • Published arguments are stored in a tuple held by std::any. Type erasure and argument copies add overhead on hot paths.
  • Non-const lvalue-reference callback parameters are rejected so callbacks cannot mistakenly expect to modify the publisher's original object when they would actually receive an internal argument copy.
  • Callback exceptions are caught and counted in failed; they do not interrupt subsequent callbacks.
  • callback_id == 0 indicates that subscription failed, primarily because the EventBus was already closed.

Building

Requirements:

  • C++17 or later
  • Standard library support for std::shared_mutex, std::any, and std::string_view

CMake:

cmake -S . -B build
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure

Single-file compilation example:

g++ -std=c++17 -Wall -Wextra -Wpedantic -I. simple_test.cpp -o simple_test

On Windows, you can run:

build.bat
demo.bat

Test Targets

The current CMake configuration includes:

  • simple_test: basic behavior, type conversions, concurrent callbacks, waiting during unsubscription, and exception results.
  • complete_test: complete behavior, statistics, conditional publishing, and thread-safety examples.
  • complex_type_test: complex STL types and custom payload types.
  • usage_example: practical usage examples.

Repository Layout

.
|-- eventbus.hpp
|-- simple_test.cpp
|-- test_full.cpp
|-- test_complex_types.cpp
|-- example_simple.cpp
|-- CMakeLists.txt
|-- build.bat
|-- demo.bat
|-- docs
|   |-- QUICK_START.md
|   |-- QUICK_START.zh-CN.md
|   |-- PROJECT_SUMMARY.md
|   |-- PROJECT_SUMMARY.zh-CN.md
|   |-- DELIVERY_NOTES.md
|   `-- DELIVERY_NOTES.zh-CN.md
|-- README.md
`-- README.zh-CN.md