diff options
| author | Jordan Woyak <jordan.woyak@gmail.com> | 2025-11-20 20:41:54 -0600 |
|---|---|---|
| committer | Jordan Woyak <jordan.woyak@gmail.com> | 2025-11-21 16:58:20 -0600 |
| commit | 11318e0be5c58cc165724f90ec2c42fc9b6a3d13 (patch) | |
| tree | cca061c2c9e890343ff9434449d9a6697df074b0 | |
| parent | f38a2bbb0ea0bb3e53992a492da5254f35fe0be2 (diff) | |
HookableEvent: Allow hooks to be added and removed from within a Trigger callback. This fixes a deadlock in FIFOFifoRecorder.
| -rw-r--r-- | Source/Core/Common/HookableEvent.h | 65 |
1 files changed, 50 insertions, 15 deletions
diff --git a/Source/Core/Common/HookableEvent.h b/Source/Core/Common/HookableEvent.h index 58a07c1fe9..906f3e225b 100644 --- a/Source/Core/Common/HookableEvent.h +++ b/Source/Core/Common/HookableEvent.h @@ -3,6 +3,7 @@ #pragma once +#include <algorithm> #include <functional> #include <memory> #include <mutex> @@ -49,37 +50,59 @@ public: using CallbackType = std::function<void(CallbackArgs...)>; // Returns a handle that will unregister the listener when destroyed. - // Note: Attempting to add/remove hooks of the event within the callback itself will NOT work. [[nodiscard]] EventHook Register(CallbackType callback) { DEBUG_LOG_FMT(COMMON, "Registering event hook handler"); - auto handle = std::make_unique<HookImpl>(m_storage, std::move(callback)); - std::lock_guard lg(m_storage->listeners_mutex); - m_storage->listeners.push_back(handle.get()); - return handle; + + auto& new_listener = + m_storage->listeners.emplace_back(std::make_unique<Listener>(std::move(callback))); + + return std::make_unique<HookImpl>(m_storage, new_listener.get()); } + // Invokes all registered callbacks. + // Hooks added from within a callback will be invoked. + // Hooks removed from within a callback will be skipped, + // but destruction of the hook's callback will be delayed until Trigger() completes. void Trigger(const CallbackArgs&... args) { std::lock_guard lg(m_storage->listeners_mutex); - for (auto* const handle : m_storage->listeners) - std::invoke(handle->callback, args...); + m_storage->is_triggering = true; + + // Avoiding an actual iterator because the container may be modified. + for (std::size_t i = 0; i != m_storage->listeners.size(); ++i) + { + auto& listener = m_storage->listeners[i]; + + if (listener->is_pending_removal) + continue; + + std::invoke(listener->callback, args...); + } + + m_storage->is_triggering = false; + std::erase_if(m_storage->listeners, std::mem_fn(&Listener::is_pending_removal)); } private: - struct HookImpl; + struct Listener + { + const CallbackType callback; + bool is_pending_removal{}; + }; struct Storage { - std::mutex listeners_mutex; - std::vector<HookImpl*> listeners; + std::recursive_mutex listeners_mutex; + std::vector<std::unique_ptr<Listener>> listeners; + bool is_triggering{}; }; struct HookImpl final : HookBase { - HookImpl(const std::shared_ptr<Storage> storage, CallbackType func) - : weak_storage{storage}, callback{std::move(func)} + HookImpl(std::weak_ptr<Storage> storage, Listener* listener) + : weak_storage{std::move(storage)}, listener_ptr{listener} { } @@ -95,11 +118,23 @@ private: DEBUG_LOG_FMT(COMMON, "Removing event hook handler"); std::lock_guard lg(storage->listeners_mutex); - std::erase(storage->listeners, this); + + if (storage->is_triggering) + { + // Just mark our listener for removal. + // Trigger() will erase it for us. + listener_ptr->is_pending_removal = true; + } + else + { + // Remove our listener. + storage->listeners.erase(std::ranges::find_if( + storage->listeners, [&](auto& ptr) { return ptr.get() == listener_ptr; })); + } } - std::weak_ptr<Storage> weak_storage; - const CallbackType callback; + const std::weak_ptr<Storage> weak_storage; + Listener* const listener_ptr; // "owned" by the above Storage. }; // shared_ptr storage allows hooks to forget their connection if they outlive the event itself. |
