feat: Implement event bus and service registry for decoupled communication and lifecycle management

This commit is contained in:
2026-01-04 02:31:28 +00:00
parent b6c697196a
commit c95bcca25f
9 changed files with 759 additions and 1 deletions

View File

@@ -121,6 +121,8 @@ if(BUILD_SDL3_APP)
src/logging/logger.cpp
src/logging/string_utils.cpp
src/core/platform.cpp
src/di/service_registry.cpp
src/events/event_bus.cpp
src/app/sdl3_app_core.cpp
src/app/audio_player.cpp
src/app/sdl3_app_device.cpp

View File

@@ -4,6 +4,7 @@
"conan": {}
},
"include": [
"build/Release/generators/CMakePresets.json"
"build/Release/generators/CMakePresets.json",
"build/build/Release/generators/CMakePresets.json"
]
}

56
src/di/lifecycle.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
namespace sdl3cpp::di {
/**
* @brief Interface for services that require initialization.
*
* Similar to Spring's @PostConstruct, this interface allows services
* to perform initialization logic after construction and dependency injection.
*
* The ServiceRegistry will call Initialize() on all registered services
* that implement this interface when InitializeAll() is invoked.
*/
class IInitializable {
public:
virtual ~IInitializable() = default;
/**
* @brief Initialize the service.
*
* Called once after the service is constructed and all dependencies
* are injected. This is where you should perform initialization logic
* such as loading resources, connecting to external services, etc.
*
* @throws std::runtime_error if initialization fails
*/
virtual void Initialize() = 0;
};
/**
* @brief Interface for services that require cleanup on shutdown.
*
* Similar to Spring's @PreDestroy, this interface allows services
* to perform cleanup logic before destruction.
*
* The ServiceRegistry will call Shutdown() on all registered services
* that implement this interface when ShutdownAll() is invoked,
* in reverse order of registration.
*/
class IShutdownable {
public:
virtual ~IShutdownable() = default;
/**
* @brief Shutdown the service and release resources.
*
* Called once before the service is destroyed. This is where you
* should perform cleanup logic such as closing connections, releasing
* resources, saving state, etc.
*
* This method should not throw exceptions.
*/
virtual void Shutdown() noexcept = 0;
};
} // namespace sdl3cpp::di

View File

@@ -0,0 +1,37 @@
#include "service_registry.hpp"
#include <algorithm>
namespace sdl3cpp::di {
void ServiceRegistry::InitializeAll() {
if (initialized_) {
throw std::runtime_error("Services already initialized");
}
// Call all initialization functions in registration order
for (const auto& initFunc : initFunctions_) {
initFunc();
}
initialized_ = true;
}
void ServiceRegistry::ShutdownAll() noexcept {
if (!initialized_) {
return; // Nothing to shutdown
}
// Call all shutdown functions in reverse registration order
for (auto it = shutdownFunctions_.rbegin(); it != shutdownFunctions_.rend(); ++it) {
try {
(*it)();
} catch (...) {
// Shutdown methods must be noexcept, but just in case...
// Swallow exceptions to ensure all services get shutdown
}
}
initialized_ = false;
}
} // namespace sdl3cpp::di

211
src/di/service_registry.hpp Normal file
View File

@@ -0,0 +1,211 @@
#pragma once
#include "lifecycle.hpp"
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <typeindex>
#include <unordered_map>
#include <vector>
namespace sdl3cpp::di {
/**
* @brief Manual dependency injection container (similar to Spring's ApplicationContext).
*
* ServiceRegistry manages service lifecycle and provides dependency injection
* functionality. Services are registered by interface type and retrieved by
* interface, allowing for loose coupling and testability.
*
* Example usage:
* @code
* ServiceRegistry registry;
*
* // Register services with their dependencies
* registry.RegisterService<IConfigService, JsonConfigService>("config.json");
* registry.RegisterService<IWindowService, SdlWindowService>(
* registry.GetService<IConfigService>()
* );
*
* // Initialize all services in dependency order
* registry.InitializeAll();
*
* // Use services
* auto window = registry.GetService<IWindowService>();
* window->CreateWindow({800, 600, "My App", true});
*
* // Shutdown all services in reverse order
* registry.ShutdownAll();
* @endcode
*/
class ServiceRegistry {
public:
ServiceRegistry() = default;
~ServiceRegistry() = default;
// Non-copyable, non-movable
ServiceRegistry(const ServiceRegistry&) = delete;
ServiceRegistry& operator=(const ServiceRegistry&) = delete;
ServiceRegistry(ServiceRegistry&&) = delete;
ServiceRegistry& operator=(ServiceRegistry&&) = delete;
/**
* @brief Register a service implementation by interface type.
*
* Creates an instance of Implementation and stores it as Interface.
* All constructor arguments are forwarded to the implementation.
*
* If the implementation inherits from IInitializable or IShutdownable,
* the corresponding lifecycle methods will be called during
* InitializeAll() and ShutdownAll().
*
* @tparam Interface The interface type (pure virtual base class)
* @tparam Implementation The concrete implementation type
* @tparam Args Constructor argument types (deduced)
* @param args Constructor arguments for the implementation
* @throws std::runtime_error if a service of this interface type is already registered
*/
template<typename Interface, typename Implementation, typename... Args>
void RegisterService(Args&&... args);
/**
* @brief Get a service by interface type.
*
* @tparam Interface The interface type to retrieve
* @return std::shared_ptr<Interface> Shared pointer to the service
* @throws std::runtime_error if no service of this type is registered
*/
template<typename Interface>
std::shared_ptr<Interface> GetService();
/**
* @brief Get a service by interface type (const version).
*
* @tparam Interface The interface type to retrieve
* @return std::shared_ptr<const Interface> Shared pointer to the service
* @throws std::runtime_error if no service of this type is registered
*/
template<typename Interface>
std::shared_ptr<const Interface> GetService() const;
/**
* @brief Check if a service of the given interface type is registered.
*
* @tparam Interface The interface type to check
* @return true if registered, false otherwise
*/
template<typename Interface>
bool HasService() const;
/**
* @brief Initialize all registered services in registration order.
*
* Calls Initialize() on all services that implement IInitializable.
* Services should be registered in dependency order (dependencies first).
*
* @throws std::runtime_error if already initialized
* @throws Any exception thrown by service Initialize() methods
*/
void InitializeAll();
/**
* @brief Shutdown all registered services in reverse registration order.
*
* Calls Shutdown() on all services that implement IShutdownable.
* This method does not throw exceptions (shutdown methods must be noexcept).
*/
void ShutdownAll() noexcept;
/**
* @brief Check if services have been initialized.
*
* @return true if InitializeAll() has been called, false otherwise
*/
bool IsInitialized() const noexcept { return initialized_; }
private:
// Type-erased service storage (void* actually holds std::shared_ptr<T>)
std::unordered_map<std::type_index, std::shared_ptr<void>> services_;
// Initialization functions (called in registration order)
std::vector<std::function<void()>> initFunctions_;
// Shutdown functions (called in reverse registration order)
std::vector<std::function<void()>> shutdownFunctions_;
// Initialization state
bool initialized_ = false;
};
// Template implementation
template<typename Interface, typename Implementation, typename... Args>
void ServiceRegistry::RegisterService(Args&&... args) {
const std::type_index typeIndex(typeid(Interface));
// Check if already registered
if (services_.find(typeIndex) != services_.end()) {
throw std::runtime_error(
std::string("Service already registered: ") + typeid(Interface).name()
);
}
// Create the implementation instance
auto implementation = std::make_shared<Implementation>(std::forward<Args>(args)...);
// Store as interface type (type-erased as void*)
services_[typeIndex] = std::static_pointer_cast<void>(
std::static_pointer_cast<Interface>(implementation)
);
// Register initialization if implements IInitializable
if (auto initializable = std::dynamic_pointer_cast<IInitializable>(implementation)) {
initFunctions_.push_back([initializable]() {
initializable->Initialize();
});
}
// Register shutdown if implements IShutdownable
if (auto shutdownable = std::dynamic_pointer_cast<IShutdownable>(implementation)) {
shutdownFunctions_.push_back([shutdownable]() {
shutdownable->Shutdown();
});
}
}
template<typename Interface>
std::shared_ptr<Interface> ServiceRegistry::GetService() {
const std::type_index typeIndex(typeid(Interface));
auto it = services_.find(typeIndex);
if (it == services_.end()) {
throw std::runtime_error(
std::string("Service not found: ") + typeid(Interface).name()
);
}
return std::static_pointer_cast<Interface>(it->second);
}
template<typename Interface>
std::shared_ptr<const Interface> ServiceRegistry::GetService() const {
const std::type_index typeIndex(typeid(Interface));
auto it = services_.find(typeIndex);
if (it == services_.end()) {
throw std::runtime_error(
std::string("Service not found: ") + typeid(Interface).name()
);
}
return std::static_pointer_cast<const Interface>(it->second);
}
template<typename Interface>
bool ServiceRegistry::HasService() const {
const std::type_index typeIndex(typeid(Interface));
return services_.find(typeIndex) != services_.end();
}
} // namespace sdl3cpp::di

66
src/events/event_bus.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "event_bus.hpp"
namespace sdl3cpp::events {
void EventBus::Subscribe(EventType type, EventListener listener) {
listeners_[type].push_back(std::move(listener));
}
void EventBus::SubscribeAll(EventListener listener) {
globalListeners_.push_back(std::move(listener));
}
void EventBus::Publish(const Event& event) {
DispatchEvent(event);
}
void EventBus::PublishAsync(const Event& event) {
std::lock_guard<std::mutex> lock(queueMutex_);
eventQueue_.push(event);
}
void EventBus::ProcessQueue() {
// Lock to swap the queue (minimize lock time)
std::queue<Event> localQueue;
{
std::lock_guard<std::mutex> lock(queueMutex_);
localQueue.swap(eventQueue_);
}
// Process all queued events without holding the lock
while (!localQueue.empty()) {
DispatchEvent(localQueue.front());
localQueue.pop();
}
}
void EventBus::ClearListeners() {
listeners_.clear();
globalListeners_.clear();
}
size_t EventBus::GetListenerCount(EventType type) const {
auto it = listeners_.find(type);
return it != listeners_.end() ? it->second.size() : 0;
}
size_t EventBus::GetGlobalListenerCount() const {
return globalListeners_.size();
}
void EventBus::DispatchEvent(const Event& event) {
// Dispatch to type-specific listeners
auto it = listeners_.find(event.type);
if (it != listeners_.end()) {
for (const auto& listener : it->second) {
listener(event);
}
}
// Dispatch to global listeners
for (const auto& listener : globalListeners_) {
listener(event);
}
}
} // namespace sdl3cpp::events

149
src/events/event_bus.hpp Normal file
View File

@@ -0,0 +1,149 @@
#pragma once
#include "event_listener.hpp"
#include "event_types.hpp"
#include <mutex>
#include <queue>
#include <unordered_map>
#include <vector>
namespace sdl3cpp::events {
/**
* @brief Event bus for decoupled component communication.
*
* Similar to Spring's ApplicationEventPublisher, the EventBus allows
* services to publish events and subscribe to events without direct
* dependencies on each other.
*
* The event bus supports both synchronous and asynchronous event publishing:
* - Publish(): Immediately invokes all listeners (useful for critical events)
* - PublishAsync(): Queues event for next ProcessQueue() call (useful for cross-thread events)
*
* Example usage:
* @code
* EventBus eventBus;
*
* // Subscribe to specific event type
* eventBus.Subscribe(EventType::KeyPressed, [](const Event& event) {
* auto keyEvent = event.GetData<KeyEvent>();
* std::cout << "Key: " << keyEvent.key << std::endl;
* });
*
* // Publish event synchronously
* KeyEvent data{SDLK_SPACE, SDL_SCANCODE_SPACE, SDL_KMOD_NONE, false};
* eventBus.Publish(Event{EventType::KeyPressed, 0.0, data});
*
* // Or publish asynchronously (queued)
* eventBus.PublishAsync(Event{EventType::KeyPressed, 0.0, data});
* eventBus.ProcessQueue(); // Call once per frame
* @endcode
*/
class EventBus {
public:
EventBus() = default;
~EventBus() = default;
// Non-copyable, non-movable
EventBus(const EventBus&) = delete;
EventBus& operator=(const EventBus&) = delete;
EventBus(EventBus&&) = delete;
EventBus& operator=(EventBus&&) = delete;
/**
* @brief Subscribe to a specific event type.
*
* The listener will be called whenever an event of the specified type
* is published (either via Publish() or PublishAsync()).
*
* @param type The event type to subscribe to
* @param listener The callback function to invoke
*/
void Subscribe(EventType type, EventListener listener);
/**
* @brief Subscribe to all event types.
*
* The listener will be called for every event published, regardless of type.
* Useful for logging, debugging, or telemetry.
*
* @param listener The callback function to invoke for all events
*/
void SubscribeAll(EventListener listener);
/**
* @brief Publish an event synchronously.
*
* Immediately invokes all listeners subscribed to this event type,
* as well as all global listeners. This blocks until all listeners complete.
*
* Use this for critical events that must be processed immediately
* (e.g., window resize, shutdown requests).
*
* @param event The event to publish
*/
void Publish(const Event& event);
/**
* @brief Publish an event asynchronously.
*
* Queues the event for later processing. The event will be dispatched
* to listeners when ProcessQueue() is called.
*
* Use this for non-critical events or when publishing from a different
* thread (e.g., audio thread, network thread).
*
* @param event The event to publish
*/
void PublishAsync(const Event& event);
/**
* @brief Process all queued asynchronous events.
*
* Dispatches all events queued via PublishAsync() to their subscribers.
* This should be called once per frame in the main loop.
*
* Thread-safe: Can be called while other threads are calling PublishAsync().
*/
void ProcessQueue();
/**
* @brief Remove all event listeners.
*
* Useful for testing or resetting the event bus state.
*/
void ClearListeners();
/**
* @brief Get the number of listeners for a specific event type.
*
* @param type The event type to query
* @return The number of listeners subscribed to this event type
*/
size_t GetListenerCount(EventType type) const;
/**
* @brief Get the number of global listeners.
*
* @return The number of listeners subscribed to all events
*/
size_t GetGlobalListenerCount() const;
private:
// Event type -> list of listeners
std::unordered_map<EventType, std::vector<EventListener>> listeners_;
// Listeners that receive all events
std::vector<EventListener> globalListeners_;
// Queue for asynchronous events
std::queue<Event> eventQueue_;
// Mutex to protect eventQueue_ (allows cross-thread PublishAsync)
mutable std::mutex queueMutex_;
// Helper to dispatch event to listeners
void DispatchEvent(const Event& event);
};
} // namespace sdl3cpp::events

View File

@@ -0,0 +1,28 @@
#pragma once
#include <functional>
namespace sdl3cpp::events {
// Forward declaration
struct Event;
/**
* @brief Type alias for event listener callbacks.
*
* Event listeners are functions that receive an Event and process it.
* Similar to Spring's @EventListener annotation, but as a function type.
*
* Example usage:
* @code
* EventListener listener = [](const Event& event) {
* if (event.type == EventType::KeyPressed) {
* auto keyEvent = event.GetData<KeyEvent>();
* std::cout << "Key pressed: " << keyEvent.key << std::endl;
* }
* };
* @endcode
*/
using EventListener = std::function<void(const Event&)>;
} // namespace sdl3cpp::events

208
src/events/event_types.hpp Normal file
View File

@@ -0,0 +1,208 @@
#pragma once
#include <any>
#include <chrono>
#include <cstdint>
#include <string>
#include <SDL3/SDL.h>
namespace sdl3cpp::events {
/**
* @brief Event type enumeration.
*
* Defines all event types that can be published on the event bus.
* Similar to Spring's ApplicationEvent hierarchy, but using an enum
* with type-erased data instead of inheritance.
*/
enum class EventType {
// Window events
WindowResized,
WindowClosed,
WindowMinimized,
WindowMaximized,
WindowRestored,
WindowFocusGained,
WindowFocusLost,
// Input events
KeyPressed,
KeyReleased,
MouseMoved,
MouseButtonPressed,
MouseButtonReleased,
MouseWheel,
TextInput,
// Rendering events
FrameBegin,
FrameEnd,
SwapchainRecreated,
RenderError,
// Audio events
AudioPlayRequested,
AudioStopped,
AudioError,
// Script events
ScriptLoaded,
ScriptError,
SceneLoaded,
// Physics events
PhysicsStepComplete,
CollisionDetected,
// Application lifecycle events
ApplicationStarted,
ApplicationShutdown,
ApplicationPaused,
ApplicationResumed,
};
/**
* @brief Base event structure.
*
* Contains event type, timestamp, and type-erased data payload.
* Services publish events and subscribers retrieve typed data using GetData<T>().
*/
struct Event {
EventType type;
double timestamp; // Seconds since application start
std::any data; // Type-erased payload
/**
* @brief Retrieve typed data from the event.
*
* @tparam T The expected data type
* @return const T& Reference to the data
* @throws std::bad_any_cast if data is not of type T
*/
template<typename T>
const T& GetData() const {
return std::any_cast<const T&>(data);
}
/**
* @brief Check if event contains data of a specific type.
*
* @tparam T The type to check for
* @return true if data is of type T, false otherwise
*/
template<typename T>
bool HasData() const {
return data.type() == typeid(T);
}
};
// ============================================================================
// Event Data Structures
// ============================================================================
/**
* @brief Window resize event data.
*/
struct WindowResizedEvent {
uint32_t width;
uint32_t height;
};
/**
* @brief Key press/release event data.
*/
struct KeyEvent {
SDL_Keycode key;
SDL_Scancode scancode;
SDL_Keymod modifiers;
bool repeat;
};
/**
* @brief Mouse movement event data.
*/
struct MouseMovedEvent {
float x;
float y;
float deltaX;
float deltaY;
};
/**
* @brief Mouse button press/release event data.
*/
struct MouseButtonEvent {
uint8_t button;
uint8_t clicks;
float x;
float y;
};
/**
* @brief Mouse wheel event data.
*/
struct MouseWheelEvent {
float deltaX;
float deltaY;
bool flipped;
};
/**
* @brief Text input event data.
*/
struct TextInputEvent {
std::string text;
};
/**
* @brief Frame timing event data.
*/
struct FrameEvent {
uint64_t frameNumber;
double deltaTime; // Seconds since last frame
double totalTime; // Seconds since application start
};
/**
* @brief Swapchain recreation event data.
*/
struct SwapchainRecreatedEvent {
uint32_t newWidth;
uint32_t newHeight;
};
/**
* @brief Error event data.
*/
struct ErrorEvent {
std::string message;
std::string component;
};
/**
* @brief Audio playback event data.
*/
struct AudioPlayEvent {
std::string filePath;
bool loop;
bool background; // true for music, false for sound effects
};
/**
* @brief Script loaded event data.
*/
struct ScriptLoadedEvent {
std::string scriptPath;
bool debugMode;
};
/**
* @brief Collision detection event data.
*/
struct CollisionEvent {
std::string objectA;
std::string objectB;
float impactForce;
};
} // namespace sdl3cpp::events