|
| 1 | +// |
| 2 | +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) |
| 3 | +// |
| 4 | +// Distributed under the Boost Software License, Version 1.0. (See accompanying |
| 5 | +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) |
| 6 | +// |
| 7 | +// Official repository: https://github.com/cppalliance/corosio |
| 8 | +// |
| 9 | + |
| 10 | +#ifndef BOOST_COROSIO_DETAIL_SCHEDULER_OP_HPP |
| 11 | +#define BOOST_COROSIO_DETAIL_SCHEDULER_OP_HPP |
| 12 | + |
| 13 | +#include <boost/corosio/detail/config.hpp> |
| 14 | +#include <boost/capy/core/intrusive_queue.hpp> |
| 15 | + |
| 16 | +namespace boost { |
| 17 | +namespace corosio { |
| 18 | +namespace detail { |
| 19 | + |
| 20 | +/** Abstract base class for completion handlers. |
| 21 | +
|
| 22 | + Handlers are continuations that execute after an asynchronous |
| 23 | + operation completes. They can be queued for deferred invocation, |
| 24 | + allowing callbacks and coroutine resumptions to be posted to an |
| 25 | + executor. |
| 26 | +
|
| 27 | + Handlers should execute quickly - typically just initiating |
| 28 | + another I/O operation or suspending on a foreign task. Heavy |
| 29 | + computation should be avoided in handlers to prevent blocking |
| 30 | + the event loop. |
| 31 | +
|
| 32 | + Handlers may be heap-allocated or may be data members of an |
| 33 | + enclosing object. The allocation strategy is determined by the |
| 34 | + creator of the handler. |
| 35 | +
|
| 36 | + @par Ownership Contract |
| 37 | +
|
| 38 | + Callers must invoke exactly ONE of `operator()` or `destroy()`, |
| 39 | + never both: |
| 40 | +
|
| 41 | + @li `operator()` - Invokes the handler. The handler is |
| 42 | + responsible for its own cleanup (typically `delete this` |
| 43 | + for heap-allocated handlers). The caller must not call |
| 44 | + `destroy()` after invoking this. |
| 45 | +
|
| 46 | + @li `destroy()` - Destroys an uninvoked handler. This is |
| 47 | + called when a queued handler must be discarded without |
| 48 | + execution, such as during shutdown or exception cleanup. |
| 49 | + For heap-allocated handlers, this typically calls |
| 50 | + `delete this`. |
| 51 | +
|
| 52 | + @par Exception Safety |
| 53 | +
|
| 54 | + Implementations of `operator()` must perform cleanup before |
| 55 | + any operation that might throw. This ensures that if the handler |
| 56 | + throws, the exception propagates cleanly to the caller of |
| 57 | + `run()` without leaking resources. Typical pattern: |
| 58 | +
|
| 59 | + @code |
| 60 | + void operator()() override |
| 61 | + { |
| 62 | + auto h = h_; |
| 63 | + delete this; // cleanup FIRST |
| 64 | + h.resume(); // then resume (may throw) |
| 65 | + } |
| 66 | + @endcode |
| 67 | +
|
| 68 | + This "delete-before-invoke" pattern also enables memory |
| 69 | + recycling - the handler's memory can be reused immediately |
| 70 | + by subsequent allocations. |
| 71 | +
|
| 72 | + @note Callers must never delete handlers directly with `delete`; |
| 73 | + use `operator()` for normal invocation or `destroy()` for cleanup. |
| 74 | +
|
| 75 | + @note Heap-allocated handlers are typically allocated with |
| 76 | + custom allocators to minimize allocation overhead in |
| 77 | + high-frequency async operations. |
| 78 | +
|
| 79 | + @note Some handlers (such as those owned by containers like |
| 80 | + `std::unique_ptr` or embedded as data members) are not meant to |
| 81 | + be destroyed and should implement both functions as no-ops |
| 82 | + (for `operator()`, invoke the continuation but don't delete). |
| 83 | +
|
| 84 | + @see scheduler_op_queue |
| 85 | +*/ |
| 86 | +class scheduler_op : public capy::intrusive_queue<scheduler_op>::node |
| 87 | +{ |
| 88 | +public: |
| 89 | + virtual void operator()() = 0; |
| 90 | + virtual void destroy() = 0; |
| 91 | + |
| 92 | + /** Returns the user-defined data pointer. |
| 93 | +
|
| 94 | + Derived classes may set this to store auxiliary data |
| 95 | + such as a pointer to the most-derived object. |
| 96 | +
|
| 97 | + @par Postconditions |
| 98 | + @li Initially returns `nullptr` for newly constructed handlers. |
| 99 | + @li Returns the current value of `data_` if modified by a derived class. |
| 100 | +
|
| 101 | + @return The user-defined data pointer, or `nullptr` if not set. |
| 102 | + */ |
| 103 | + void* data() const noexcept |
| 104 | + { |
| 105 | + return data_; |
| 106 | + } |
| 107 | + |
| 108 | +protected: |
| 109 | + ~scheduler_op() = default; |
| 110 | + |
| 111 | + void* data_ = nullptr; |
| 112 | +}; |
| 113 | + |
| 114 | +//------------------------------------------------------------------------------ |
| 115 | + |
| 116 | +using op_queue = capy::intrusive_queue<scheduler_op>; |
| 117 | + |
| 118 | +//------------------------------------------------------------------------------ |
| 119 | + |
| 120 | +/** An intrusive FIFO queue of scheduler_ops. |
| 121 | +
|
| 122 | + This queue stores scheduler_ops using an intrusive linked list, |
| 123 | + avoiding additional allocations for queue nodes. Scheduler_ops |
| 124 | + are popped in the order they were pushed (first-in, first-out). |
| 125 | +
|
| 126 | + The destructor calls `destroy()` on any remaining scheduler_ops. |
| 127 | +
|
| 128 | + @note This is not thread-safe. External synchronization is |
| 129 | + required for concurrent access. |
| 130 | +
|
| 131 | + @see scheduler_op |
| 132 | +*/ |
| 133 | +class scheduler_op_queue |
| 134 | +{ |
| 135 | + op_queue q_; |
| 136 | + |
| 137 | +public: |
| 138 | + /** Default constructor. |
| 139 | +
|
| 140 | + Creates an empty queue. |
| 141 | +
|
| 142 | + @post `empty() == true` |
| 143 | + */ |
| 144 | + scheduler_op_queue() = default; |
| 145 | + |
| 146 | + /** Move constructor. |
| 147 | +
|
| 148 | + Takes ownership of all scheduler_ops from `other`, |
| 149 | + leaving `other` empty. |
| 150 | +
|
| 151 | + @param other The queue to move from. |
| 152 | +
|
| 153 | + @post `other.empty() == true` |
| 154 | + */ |
| 155 | + scheduler_op_queue(scheduler_op_queue&& other) noexcept |
| 156 | + : q_(std::move(other.q_)) |
| 157 | + { |
| 158 | + } |
| 159 | + |
| 160 | + scheduler_op_queue(scheduler_op_queue const&) = delete; |
| 161 | + scheduler_op_queue& operator=(scheduler_op_queue const&) = delete; |
| 162 | + scheduler_op_queue& operator=(scheduler_op_queue&&) = delete; |
| 163 | + |
| 164 | + /** Destructor. |
| 165 | +
|
| 166 | + Calls `destroy()` on any remaining scheduler_ops in the queue. |
| 167 | + */ |
| 168 | + ~scheduler_op_queue() |
| 169 | + { |
| 170 | + while(auto* h = q_.pop()) |
| 171 | + h->destroy(); |
| 172 | + } |
| 173 | + |
| 174 | + /** Return true if the queue is empty. |
| 175 | +
|
| 176 | + @return `true` if the queue contains no scheduler_ops. |
| 177 | + */ |
| 178 | + bool |
| 179 | + empty() const noexcept |
| 180 | + { |
| 181 | + return q_.empty(); |
| 182 | + } |
| 183 | + |
| 184 | + /** Add a scheduler_op to the back of the queue. |
| 185 | +
|
| 186 | + @param h Pointer to the scheduler_op to add. |
| 187 | +
|
| 188 | + @pre `h` is not null and not already in a queue. |
| 189 | + */ |
| 190 | + void |
| 191 | + push(scheduler_op* h) noexcept |
| 192 | + { |
| 193 | + q_.push(h); |
| 194 | + } |
| 195 | + |
| 196 | + /** Splice all scheduler_ops from another queue to the back. |
| 197 | +
|
| 198 | + All scheduler_ops from `other` are moved to the back of this |
| 199 | + queue. After this call, `other` is empty. |
| 200 | +
|
| 201 | + @param other The queue to splice from. |
| 202 | +
|
| 203 | + @post `other.empty() == true` |
| 204 | + */ |
| 205 | + void |
| 206 | + push(scheduler_op_queue& other) noexcept |
| 207 | + { |
| 208 | + q_.splice(other.q_); |
| 209 | + } |
| 210 | + |
| 211 | + /** Remove and return the front scheduler_op. |
| 212 | +
|
| 213 | + @return Pointer to the front scheduler_op, or `nullptr` |
| 214 | + if the queue is empty. |
| 215 | + */ |
| 216 | + scheduler_op* |
| 217 | + pop() noexcept |
| 218 | + { |
| 219 | + return q_.pop(); |
| 220 | + } |
| 221 | +}; |
| 222 | + |
| 223 | +} // namespace detail |
| 224 | +} // namespace corosio |
| 225 | +} // namespace boost |
| 226 | + |
| 227 | +#endif |
0 commit comments