Branch data Line data Source code
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_BTCSIGNALS_H
6 : : #define BITCOIN_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 : : /*
34 : : * optional_last_value is the default and only supported combiner.
35 : : * As such, its behavior is embedded into the signal functor.
36 : : *
37 : : * Because optional<void> is undefined, void must be special-cased.
38 : : */
39 : :
40 : : template <typename T>
41 : : class optional_last_value
42 : : {
43 : : public:
44 : : using result_type = std::conditional_t<std::is_void_v<T>, void, std::optional<T>>;
45 : : };
46 : :
47 : : template <typename Signature, typename Combiner = optional_last_value<typename std::function<Signature>::result_type>>
48 : : class signal;
49 : :
50 : : /*
51 : : * State object representing the liveness of a registered callback.
52 : : * signal::connect() returns an enabled connection which can be held and
53 : : * disabled in the future.
54 : : */
55 [ # # # # : 264 : class connection
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # #
# ][ - - -
- - - - +
- + - + ]
[ # # # # ]
[ # # ][ # #
# # # # ]
[ # # # #
# # # # #
# # # # #
# # # # #
# # # #
# ]
56 : : {
57 : : template <typename Signature, typename Combiner>
58 : : friend class signal;
59 : : /**
60 : : * Track liveness. Also serves as a tag for the constructor used by signal.
61 : : */
62 : 264 : class liveness
63 : : {
64 : : friend class connection;
65 : : std::atomic_bool m_connected{true};
66 : :
67 : 0 : void disconnect() { m_connected.store(false); }
68 : : public:
69 [ # # # # : 0 : bool connected() const { return m_connected.load(); }
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # ][ # #
# # # # #
# ][ # # #
# # # # #
# # # # #
# # # # #
# # ]
70 : : };
71 : :
72 : : /**
73 : : * connections have shared_ptr-like copy and move semantics.
74 : : */
75 : : std::shared_ptr<liveness> m_state{};
76 : :
77 : : /**
78 : : * Only a signal can create an enabled connection.
79 : : */
80 : 264 : explicit connection(std::shared_ptr<liveness>&& state) : m_state{std::move(state)}{}
81 : :
82 : : public:
83 : : /**
84 : : * The default constructor creates a connection with no associated signal
85 : : */
86 : : constexpr connection() noexcept = default;
87 : :
88 : : /**
89 : : * If a callback is associated with this connection, prevent it from being
90 : : * called in the future.
91 : : *
92 : : * If a connection is disabled as part of a signal's callback function, it
93 : : * will _not_ be executed in the current signal invocation.
94 : : *
95 : : * Note that disconnected callbacks are not removed from their owning
96 : : * signals here. They are garbage collected in signal::connect().
97 : : */
98 : 0 : void disconnect()
99 : : {
100 [ # # ]: 0 : if (m_state) {
101 : 0 : m_state->disconnect();
102 : : }
103 : 0 : }
104 : :
105 : : /**
106 : : * Returns true if this connection was created by a signal and has not been
107 : : * disabled.
108 : : */
109 : : bool connected() const
110 : : {
111 : : return m_state && m_state->connected();
112 : : }
113 : : };
114 : :
115 : : /*
116 : : * RAII-style connection management
117 : : */
118 : : class scoped_connection
119 : : {
120 : : connection m_conn;
121 : :
122 : : public:
123 : 0 : scoped_connection(connection rhs) noexcept : m_conn{std::move(rhs)} {}
124 : :
125 : : scoped_connection(scoped_connection&&) noexcept = default;
126 : : scoped_connection& operator=(scoped_connection&&) noexcept = default;
127 : :
128 : : /**
129 : : * For simplicity, disable copy assignment and construction.
130 : : */
131 : : scoped_connection& operator=(const scoped_connection&) = delete;
132 : : scoped_connection(const scoped_connection&) = delete;
133 : :
134 : 0 : void disconnect()
135 : : {
136 : 0 : m_conn.disconnect();
137 : : }
138 : :
139 : 0 : ~scoped_connection()
140 : : {
141 : 0 : disconnect();
142 [ # # ]: 0 : }
143 : : };
144 : :
145 : : /*
146 : : * Functor for calling zero or more connected callbacks
147 : : */
148 : : template <typename Signature, typename Combiner>
149 : : class signal
150 : : {
151 : : using function_type = std::function<Signature>;
152 : :
153 : : static_assert(std::is_same_v<Combiner, optional_last_value<typename function_type::result_type>>, "only the optional_last_value combiner is supported");
154 : :
155 : : /*
156 : : * Helper struct for maintaining a callback and its associated connection liveness
157 : : */
158 : 264 : struct connection_holder : connection::liveness {
159 : : template <typename Callable>
160 [ - - - - : 264 : 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 : 29148 : constexpr signal() noexcept = default;
175 : 29148 : ~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 optional_last_value is hard-coded
188 : : * here. Return the value of the last executed callback, or nullopt if none
189 : : * were executed.
190 : : *
191 : : * Callbacks which return void require special handling.
192 : : *
193 : : * In order to avoid locking during the callbacks, the list of callbacks is
194 : : * cached before they are called. This allows a callback to call connect(),
195 : : * but the newly connected callback will not be run during the current
196 : : * signal invocation.
197 : : *
198 : : * Note that the parameters are accepted as universal references, though
199 : : * they are not perfectly forwarded as that could cause a use-after-move if
200 : : * more than one callback is enabled.
201 : : */
202 : : template <typename... Args>
203 : 836346 : result_type operator()(Args&&... args) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
204 : : {
205 : 836346 : std::vector<std::shared_ptr<connection_holder>> connections;
206 : : {
207 [ + - ]: 836346 : LOCK(m_mutex);
208 [ + - ]: 836346 : connections = m_connections;
209 : 0 : }
210 : : if constexpr (std::is_void_v<result_type>) {
211 [ - - - + ]: 836346 : for (const auto& connection : connections) {
212 [ # # ]: 0 : if (connection->connected()) {
213 [ # # ]: 0 : connection->m_callback(args...);
[ # # # # ]
214 : : }
215 : : }
216 : : } else {
217 : 0 : result_type ret{std::nullopt};
218 [ # # # # ]: 0 : for (const auto& connection : connections) {
219 [ # # ]: 0 : if (connection->connected()) {
220 [ # # # # ]: 0 : ret.emplace(connection->m_callback(args...));
221 : : }
222 : : }
223 : 0 : return ret;
224 : : }
225 : 836346 : }
226 : :
227 : : /*
228 : : * Connect a new callback to the signal. A forwarding callable accepts
229 : : * anything that can be stored in a std::function.
230 : : */
231 : : template <typename Callable>
232 : 264 : connection connect(Callable&& func) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
233 : : {
234 : 264 : LOCK(m_mutex);
235 : :
236 : : // Garbage-collect disconnected connections to prevent unbounded growth
237 [ # # # # : 264 : std::erase_if(m_connections, [](const auto& holder) { return !holder->connected(); });
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # # # #
# # ][ # #
# # # # #
# ][ # # #
# # # # #
# # # # #
# # # # #
# # ]
238 : :
239 [ + - + - : 264 : const auto& entry = m_connections.emplace_back(std::make_shared<connection_holder>(std::forward<Callable>(func)));
- + + - ]
240 [ + - ]: 264 : return connection(entry);
241 : 264 : }
242 : :
243 : : /*
244 : : * Returns true if there are no enabled callbacks
245 : : */
246 : 11344 : [[nodiscard]] bool empty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
247 : : {
248 : 11344 : LOCK(m_mutex);
249 [ - - + - ]: 11344 : return std::ranges::none_of(m_connections, [](const auto& holder) {
250 [ # # ]: 0 : return holder->connected();
251 : 11344 : });
252 : 11344 : }
253 : : };
254 : :
255 : : } // namespace btcsignals
256 : :
257 : : #endif // BITCOIN_BTCSIGNALS_H
|