From e60a0b9a22f852378198843790d14e9e42d9c785 Mon Sep 17 00:00:00 2001 From: Cory Fields Date: Mon, 26 Sep 2022 21:13:15 +0000 Subject: [PATCH] signals: Add a simplified boost-compatible implementation This re-implements the tiny portion of boost::signals2 that we currently use. It is enough to be useful as a generic multicast callback mechanism. --- src/btcsignals.h | 250 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 4 deletions(-) diff --git a/src/btcsignals.h b/src/btcsignals.h index 415d8b8651f..f4c2530be91 100644 --- a/src/btcsignals.h +++ b/src/btcsignals.h @@ -5,10 +5,252 @@ #ifndef BITCOIN_BTCSIGNALS_H #define BITCOIN_BTCSIGNALS_H -#include -#include -#include +#include -namespace btcsignals = boost::signals2; +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * btcsignals is a simple mechanism for signaling events to multiple subscribers. + * It is api-compatible with a minimal subset of boost::signals2. + * + * Rather than using a custom slot type, and the features/complexity that they + * imply, std::function is used to store the callbacks. Lifetime management of + * the callbacks is left up to the user. + * + * All usage is thread-safe except for interacting with a connection while + * copying/moving it on another thread. + */ + +namespace btcsignals { + +/* + * optional_last_value is the default and only supported combiner. + * As such, its behavior is embedded into the signal functor. + * + * Because optional is undefined, void must be special-cased. + */ + +template +class optional_last_value +{ +public: + using result_type = std::conditional_t, void, std::optional>; +}; + +template ::result_type>> +class signal; + +/* + * State object representing the liveness of a registered callback. + * signal::connect() returns an enabled connection which can be held and + * disabled in the future. + */ +class connection +{ + template + friend class signal; + + /* + * Tag for the constructor used by signal. + */ + struct enabled_tag_type { + }; + static constexpr enabled_tag_type enabled_tag{}; + + /** + * connections have shared_ptr-like copy and move semantics. + */ + std::shared_ptr m_connected{}; + + /** + * Only a signal can create an enabled connection. + */ + explicit connection(enabled_tag_type /*unused*/) : m_connected{std::make_shared(true)} {} + +public: + /** + * The default constructor creates a connection with no associated signal + */ + constexpr connection() noexcept = default; + + /** + * If a callback is associated with this connection, prevent it from being + * called in the future. + * + * If a connection is disabled as part of a signal's callback function, it + * will _not_ be executed in the current signal invocation. + * + * Note that disconnected callbacks are not removed from their owning + * signals here. They are garbage collected in signal::connect(). + */ + void disconnect() + { + if (m_connected) { + m_connected->store(false); + } + } + + /** + * Returns true if this connection was created by a signal and has not been + * disabled. + */ + bool connected() const + { + return m_connected && m_connected->load(); + } +}; + +/* + * RAII-style connection management + */ +class scoped_connection +{ + connection m_conn; + +public: + scoped_connection(connection rhs) noexcept : m_conn{std::move(rhs)} {} + + scoped_connection(scoped_connection&&) noexcept = default; + scoped_connection& operator=(scoped_connection&&) noexcept = default; + + /** + * For simplicity, disable copy assignment and construction. + */ + scoped_connection& operator=(const scoped_connection&) = delete; + scoped_connection(const scoped_connection&) = delete; + + void disconnect() + { + m_conn.disconnect(); + } + + ~scoped_connection() + { + disconnect(); + } +}; + +/* + * Functor for calling zero or more connected callbacks + */ +template +class signal +{ + using function_type = std::function; + + static_assert(std::is_same_v>, "only the optional_last_value combiner is supported"); + + /* + * Helper struct for maintaining a callback and its associated connection + */ + struct connection_holder { + template + connection_holder(Callable&& callback) : m_callback{std::forward(callback)} + { + } + + connection m_connection{connection::enabled_tag}; + function_type m_callback; + }; + + mutable Mutex m_mutex; + + /* Store connection_holders as shared_ptrs to avoid having to copy them by + * value in operator(). + */ + std::vector> m_connections GUARDED_BY(m_mutex){}; + +public: + using result_type = Combiner::result_type; + + constexpr signal() noexcept = default; + ~signal() = default; + + /* + * For simplicity, disable all moving/copying/assigning. + */ + signal(const signal&) = delete; + signal(signal&&) = delete; + signal& operator=(const signal&) = delete; + signal& operator=(signal&&) = delete; + + /* + * Execute all enabled callbacks for the signal. Rather than allowing for + * custom combiners, the behavior of optional_last_value is hard-coded + * here. Return the value of the last executed callback, or nullopt if none + * were executed. + * + * Callbacks which return void require special handling. + * + * In order to avoid locking during the callbacks, the list of callbacks is + * cached before they are called. This allows a callback to call connect(), + * but the newly connected callback will not be run during the current + * signal invocation. + * + * Note that the parameters are accepted as universal references, though + * they are not perfectly forwarded as that could cause a use-after-move if + * more than one callback is enabled. + */ + template + result_type operator()(Args&&... args) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + std::vector> connections; + { + LOCK(m_mutex); + connections = m_connections; + } + if constexpr (std::is_void_v) { + for (const auto& connection : connections) { + if (connection->m_connection.connected()) { + connection->m_callback(args...); + } + } + } else { + result_type ret{std::nullopt}; + for (const auto& connection : connections) { + if (connection->m_connection.connected()) { + ret.emplace(connection->m_callback(args...)); + } + } + return ret; + } + } + + /* + * Connect a new callback to the signal. A forwarding callable accepts + * anything that can be stored in a std::function. + */ + template + connection connect(Callable&& func) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + LOCK(m_mutex); + + // Garbage-collect disconnected connections to prevent unbounded growth + std::erase_if(m_connections, [](const auto& holder) { return !holder->m_connection.connected(); }); + + const auto& connection = m_connections.emplace_back(std::make_shared(std::forward(func))); + return connection->m_connection; + } + + /* + * Returns true if there are no enabled callbacks + */ + [[nodiscard]] bool empty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) + { + LOCK(m_mutex); + return std::ranges::none_of(m_connections, [](const auto& holder) { + return holder->m_connection.connected(); + }); + } +}; + +} // namespace btcsignals #endif // BITCOIN_BTCSIGNALS_H