btcsignals.h raw

   1  // Copyright (c) The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #ifndef BITCOIN_UTIL_BTCSIGNALS_H
   6  #define BITCOIN_UTIL_BTCSIGNALS_H
   7  
   8  #include <sync.h>
   9  
  10  #include <algorithm>
  11  #include <atomic>
  12  #include <functional>
  13  #include <memory>
  14  #include <optional>
  15  #include <type_traits>
  16  #include <utility>
  17  #include <vector>
  18  
  19  /**
  20   * btcsignals is a simple mechanism for signaling events to multiple subscribers.
  21   * It is api-compatible with a minimal subset of boost::signals2.
  22   *
  23   * Rather than using a custom slot type, and the features/complexity that they
  24   * imply, std::function is used to store the callbacks. Lifetime management of
  25   * the callbacks is left up to the user.
  26   *
  27   * All usage is thread-safe except for interacting with a connection while
  28   * copying/moving it on another thread.
  29   */
  30  
  31  namespace btcsignals {
  32  
  33  /// The default combiner, which only returns void.
  34  class null_value
  35  {
  36  public:
  37      using result_type = void;
  38  };
  39  
  40  /// A combiner, which checks if at least one callback returned true.
  41  class any_of
  42  {
  43  public:
  44      // This is the only supported combiner with a non-void return type. As
  45      // such, its behavior is embedded into the signal functor.
  46      using result_type = bool;
  47  };
  48  
  49  template <typename Signature, typename Combiner = null_value>
  50  class signal;
  51  
  52  /*
  53   * State object representing the liveness of a registered callback.
  54   * signal::connect() returns an enabled connection which can be held and
  55   * disabled in the future.
  56   */
  57  class connection
  58  {
  59      template <typename Signature, typename Combiner>
  60      friend class signal;
  61      /**
  62       * Track liveness. Also serves as a tag for the constructor used by signal.
  63       */
  64      class liveness
  65      {
  66          friend class connection;
  67          std::atomic_bool m_connected{true};
  68  
  69          void disconnect() { m_connected.store(false); }
  70      public:
  71          bool connected() const { return m_connected.load(); }
  72      };
  73  
  74      /**
  75       * connections have shared_ptr-like copy and move semantics.
  76       */
  77      std::shared_ptr<liveness> m_state{};
  78  
  79      /**
  80       * Only a signal can create an enabled connection.
  81       */
  82      explicit connection(std::shared_ptr<liveness>&& state) : m_state{std::move(state)}{}
  83  
  84  public:
  85      /**
  86       * The default constructor creates a connection with no associated signal
  87       */
  88      constexpr connection() noexcept = default;
  89  
  90      /**
  91       * If a callback is associated with this connection, prevent it from being
  92       * called in the future.
  93       *
  94       * If a connection is disabled as part of a signal's callback function, it
  95       * will _not_ be executed in the current signal invocation.
  96       *
  97       * Note that disconnected callbacks are not removed from their owning
  98       * signals here. They are garbage collected in signal::connect().
  99       */
 100      void disconnect()
 101      {
 102          if (m_state) {
 103              m_state->disconnect();
 104          }
 105      }
 106  
 107      /**
 108       * Returns true if this connection was created by a signal and has not been
 109       * disabled.
 110       */
 111      bool connected() const
 112      {
 113          return m_state && m_state->connected();
 114      }
 115  };
 116  
 117  /*
 118   * RAII-style connection management
 119   */
 120  class scoped_connection
 121  {
 122      connection m_conn;
 123  
 124  public:
 125      explicit scoped_connection(connection rhs) noexcept : m_conn{std::move(rhs)} {}
 126  
 127      scoped_connection(scoped_connection&&) noexcept = default;
 128  
 129      /**
 130       * For simplicity, disable copy construction and copy/move assignment.
 131       */
 132      scoped_connection& operator=(scoped_connection&&) = delete;
 133      scoped_connection& operator=(const scoped_connection&) = delete;
 134      scoped_connection(const scoped_connection&) = delete;
 135  
 136      void disconnect()
 137      {
 138          m_conn.disconnect();
 139      }
 140  
 141      ~scoped_connection()
 142      {
 143          disconnect();
 144      }
 145  };
 146  
 147  /*
 148   * Functor for calling zero or more connected callbacks
 149   */
 150  template <typename Signature, typename Combiner>
 151  class signal
 152  {
 153      using function_type = std::function<Signature>;
 154  
 155      /*
 156       * Helper struct for maintaining a callback and its associated connection liveness
 157       */
 158      struct connection_holder : connection::liveness {
 159          template <typename Callable>
 160          connection_holder(Callable&& callback) : m_callback{std::forward<Callable>(callback)}
 161          {
 162          }
 163  
 164          const function_type m_callback;
 165      };
 166  
 167      mutable Mutex m_mutex;
 168  
 169      std::vector<std::shared_ptr<connection_holder>> m_connections GUARDED_BY(m_mutex){};
 170  
 171  public:
 172      using result_type = Combiner::result_type;
 173  
 174      constexpr signal() noexcept = default;
 175      ~signal() = default;
 176  
 177      /*
 178       * For simplicity, disable all moving/copying/assigning.
 179       */
 180      signal(const signal&) = delete;
 181      signal(signal&&) = delete;
 182      signal& operator=(const signal&) = delete;
 183      signal& operator=(signal&&) = delete;
 184  
 185      /*
 186       * Execute all enabled callbacks for the signal. Rather than allowing for
 187       * custom combiners, the behavior of any_of is hard-coded here.
 188       *
 189       * Callbacks which return void require special handling.
 190       *
 191       * In order to avoid locking during the callbacks, the list of callbacks is
 192       * cached before they are called. This allows a callback to call connect(),
 193       * but the newly connected callback will not be run during the current
 194       * signal invocation.
 195       *
 196       * Note that the parameters are accepted as universal references, though
 197       * they are not perfectly forwarded as that could cause a use-after-move if
 198       * more than one callback is enabled.
 199       */
 200      template <typename... Args>
 201      [[nodiscard]] result_type operator()(Args&&... args) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
 202      {
 203          std::vector<std::shared_ptr<connection_holder>> connections;
 204          {
 205              LOCK(m_mutex);
 206              connections = m_connections;
 207          }
 208          if constexpr (std::is_void_v<result_type>) {
 209              static_assert(std::is_same_v<result_type, typename function_type::result_type>,
 210                            "Callback result type must be equal to the combiner result type (void).");
 211              for (const auto& connection : connections) {
 212                  if (connection->connected()) {
 213                      connection->m_callback(args...);
 214                  }
 215              }
 216          } else {
 217              static_assert(std::is_same_v<Combiner, any_of>,
 218                            "only the any_of combiner is supported and hard-coded into this functor.");
 219              static_assert(std::is_same_v<result_type, typename function_type::result_type>,
 220                            "Callback result type must be equal to the combiner result type (bool).");
 221              result_type ret{false};
 222              for (const auto& connection : connections) {
 223                  if (connection->connected()) {
 224                      ret |= connection->m_callback(args...);
 225                  }
 226              }
 227              return ret;
 228          }
 229      }
 230  
 231      /*
 232       * Connect a new callback to the signal. A forwarding callable accepts
 233       * anything that can be stored in a std::function.
 234       */
 235      template <typename Callable>
 236      connection connect(Callable&& func) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
 237      {
 238          LOCK(m_mutex);
 239  
 240          // Garbage-collect disconnected connections to prevent unbounded growth
 241          std::erase_if(m_connections, [](const auto& holder) { return !holder->connected(); });
 242  
 243          const auto& entry = m_connections.emplace_back(std::make_shared<connection_holder>(std::forward<Callable>(func)));
 244          return connection(entry);
 245      }
 246  
 247      /*
 248       * Returns true if there are no enabled callbacks
 249       */
 250      [[nodiscard]] bool empty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
 251      {
 252          LOCK(m_mutex);
 253          return std::ranges::none_of(m_connections, [](const auto& holder) {
 254              return holder->connected();
 255          });
 256      }
 257  };
 258  
 259  } // namespace btcsignals
 260  
 261  #endif // BITCOIN_UTIL_BTCSIGNALS_H
 262