mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-06-04 10:12:28 +02:00
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.
This commit is contained in:
250
src/btcsignals.h
250
src/btcsignals.h
@@ -5,10 +5,252 @@
|
||||
#ifndef BITCOIN_BTCSIGNALS_H
|
||||
#define BITCOIN_BTCSIGNALS_H
|
||||
|
||||
#include <boost/signals2/connection.hpp>
|
||||
#include <boost/signals2/optional_last_value.hpp>
|
||||
#include <boost/signals2/signal.hpp>
|
||||
#include <sync.h>
|
||||
|
||||
namespace btcsignals = boost::signals2;
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* 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<void> is undefined, void must be special-cased.
|
||||
*/
|
||||
|
||||
template <typename T>
|
||||
class optional_last_value
|
||||
{
|
||||
public:
|
||||
using result_type = std::conditional_t<std::is_void_v<T>, void, std::optional<T>>;
|
||||
};
|
||||
|
||||
template <typename Signature, typename Combiner = optional_last_value<typename std::function<Signature>::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 <typename Signature, typename Combiner>
|
||||
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<std::atomic_bool> m_connected{};
|
||||
|
||||
/**
|
||||
* Only a signal can create an enabled connection.
|
||||
*/
|
||||
explicit connection(enabled_tag_type /*unused*/) : m_connected{std::make_shared<std::atomic_bool>(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 <typename Signature, typename Combiner>
|
||||
class signal
|
||||
{
|
||||
using function_type = std::function<Signature>;
|
||||
|
||||
static_assert(std::is_same_v<Combiner, optional_last_value<typename function_type::result_type>>, "only the optional_last_value combiner is supported");
|
||||
|
||||
/*
|
||||
* Helper struct for maintaining a callback and its associated connection
|
||||
*/
|
||||
struct connection_holder {
|
||||
template <typename Callable>
|
||||
connection_holder(Callable&& callback) : m_callback{std::forward<Callable>(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<std::shared_ptr<connection_holder>> 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 <typename... Args>
|
||||
result_type operator()(Args&&... args) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
|
||||
{
|
||||
std::vector<std::shared_ptr<connection_holder>> connections;
|
||||
{
|
||||
LOCK(m_mutex);
|
||||
connections = m_connections;
|
||||
}
|
||||
if constexpr (std::is_void_v<result_type>) {
|
||||
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 <typename Callable>
|
||||
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<connection_holder>(std::forward<Callable>(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
|
||||
|
||||
Reference in New Issue
Block a user