From c95bcca25f18d9b690d045b43dd8795eaa7fd6cd Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 4 Jan 2026 02:31:28 +0000 Subject: [PATCH] feat: Implement event bus and service registry for decoupled communication and lifecycle management --- CMakeLists.txt | 2 + CMakeUserPresets.json | 3 +- src/di/lifecycle.hpp | 56 +++++++++ src/di/service_registry.cpp | 37 ++++++ src/di/service_registry.hpp | 211 ++++++++++++++++++++++++++++++++++ src/events/event_bus.cpp | 66 +++++++++++ src/events/event_bus.hpp | 149 ++++++++++++++++++++++++ src/events/event_listener.hpp | 28 +++++ src/events/event_types.hpp | 208 +++++++++++++++++++++++++++++++++ 9 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 src/di/lifecycle.hpp create mode 100644 src/di/service_registry.cpp create mode 100644 src/di/service_registry.hpp create mode 100644 src/events/event_bus.cpp create mode 100644 src/events/event_bus.hpp create mode 100644 src/events/event_listener.hpp create mode 100644 src/events/event_types.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b335c34..d2747fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/CMakeUserPresets.json b/CMakeUserPresets.json index 71aeace..ed4443e 100644 --- a/CMakeUserPresets.json +++ b/CMakeUserPresets.json @@ -4,6 +4,7 @@ "conan": {} }, "include": [ - "build/Release/generators/CMakePresets.json" + "build/Release/generators/CMakePresets.json", + "build/build/Release/generators/CMakePresets.json" ] } \ No newline at end of file diff --git a/src/di/lifecycle.hpp b/src/di/lifecycle.hpp new file mode 100644 index 0000000..ef8fc40 --- /dev/null +++ b/src/di/lifecycle.hpp @@ -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 diff --git a/src/di/service_registry.cpp b/src/di/service_registry.cpp new file mode 100644 index 0000000..b45e820 --- /dev/null +++ b/src/di/service_registry.cpp @@ -0,0 +1,37 @@ +#include "service_registry.hpp" +#include + +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 diff --git a/src/di/service_registry.hpp b/src/di/service_registry.hpp new file mode 100644 index 0000000..bc64199 --- /dev/null +++ b/src/di/service_registry.hpp @@ -0,0 +1,211 @@ +#pragma once + +#include "lifecycle.hpp" +#include +#include +#include +#include +#include +#include +#include + +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("config.json"); + * registry.RegisterService( + * registry.GetService() + * ); + * + * // Initialize all services in dependency order + * registry.InitializeAll(); + * + * // Use services + * auto window = registry.GetService(); + * 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 + void RegisterService(Args&&... args); + + /** + * @brief Get a service by interface type. + * + * @tparam Interface The interface type to retrieve + * @return std::shared_ptr Shared pointer to the service + * @throws std::runtime_error if no service of this type is registered + */ + template + std::shared_ptr GetService(); + + /** + * @brief Get a service by interface type (const version). + * + * @tparam Interface The interface type to retrieve + * @return std::shared_ptr Shared pointer to the service + * @throws std::runtime_error if no service of this type is registered + */ + template + std::shared_ptr 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 + 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) + std::unordered_map> services_; + + // Initialization functions (called in registration order) + std::vector> initFunctions_; + + // Shutdown functions (called in reverse registration order) + std::vector> shutdownFunctions_; + + // Initialization state + bool initialized_ = false; +}; + +// Template implementation + +template +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(std::forward(args)...); + + // Store as interface type (type-erased as void*) + services_[typeIndex] = std::static_pointer_cast( + std::static_pointer_cast(implementation) + ); + + // Register initialization if implements IInitializable + if (auto initializable = std::dynamic_pointer_cast(implementation)) { + initFunctions_.push_back([initializable]() { + initializable->Initialize(); + }); + } + + // Register shutdown if implements IShutdownable + if (auto shutdownable = std::dynamic_pointer_cast(implementation)) { + shutdownFunctions_.push_back([shutdownable]() { + shutdownable->Shutdown(); + }); + } +} + +template +std::shared_ptr 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(it->second); +} + +template +std::shared_ptr 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(it->second); +} + +template +bool ServiceRegistry::HasService() const { + const std::type_index typeIndex(typeid(Interface)); + return services_.find(typeIndex) != services_.end(); +} + +} // namespace sdl3cpp::di diff --git a/src/events/event_bus.cpp b/src/events/event_bus.cpp new file mode 100644 index 0000000..98a8ce3 --- /dev/null +++ b/src/events/event_bus.cpp @@ -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 lock(queueMutex_); + eventQueue_.push(event); +} + +void EventBus::ProcessQueue() { + // Lock to swap the queue (minimize lock time) + std::queue localQueue; + { + std::lock_guard 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 diff --git a/src/events/event_bus.hpp b/src/events/event_bus.hpp new file mode 100644 index 0000000..30965c9 --- /dev/null +++ b/src/events/event_bus.hpp @@ -0,0 +1,149 @@ +#pragma once + +#include "event_listener.hpp" +#include "event_types.hpp" +#include +#include +#include +#include + +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(); + * 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> listeners_; + + // Listeners that receive all events + std::vector globalListeners_; + + // Queue for asynchronous events + std::queue 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 diff --git a/src/events/event_listener.hpp b/src/events/event_listener.hpp new file mode 100644 index 0000000..446f898 --- /dev/null +++ b/src/events/event_listener.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +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(); + * std::cout << "Key pressed: " << keyEvent.key << std::endl; + * } + * }; + * @endcode + */ +using EventListener = std::function; + +} // namespace sdl3cpp::events diff --git a/src/events/event_types.hpp b/src/events/event_types.hpp new file mode 100644 index 0000000..f38da30 --- /dev/null +++ b/src/events/event_types.hpp @@ -0,0 +1,208 @@ +#pragma once + +#include +#include +#include +#include +#include + +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(). + */ +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 + const T& GetData() const { + return std::any_cast(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 + 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