|
| 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_IO_BUFFER_PARAM_HPP |
| 11 | +#define BOOST_COROSIO_IO_BUFFER_PARAM_HPP |
| 12 | + |
| 13 | +#include <boost/corosio/detail/config.hpp> |
| 14 | +#include <boost/capy/buffers.hpp> |
| 15 | + |
| 16 | +#include <cstddef> |
| 17 | + |
| 18 | +namespace boost { |
| 19 | +namespace corosio { |
| 20 | + |
| 21 | +/** A type-erased buffer sequence for I/O system call boundaries. |
| 22 | +
|
| 23 | + This class enables I/O objects to accept any buffer sequence type |
| 24 | + across a virtual function boundary, while preserving the caller's |
| 25 | + typed buffer sequence at the call site. The implementation can |
| 26 | + then unroll the type-erased sequence into platform-native |
| 27 | + structures (e.g., `iovec` on POSIX, `WSABUF` on Windows) for the |
| 28 | + actual system call. |
| 29 | +
|
| 30 | + @par Purpose |
| 31 | +
|
| 32 | + When building coroutine-based I/O abstractions, a common pattern |
| 33 | + emerges: a templated awaitable captures the caller's buffer |
| 34 | + sequence, and at `await_suspend` time, must pass it across a |
| 35 | + virtual interface to the I/O implementation. This class solves |
| 36 | + the type-erasure problem at that boundary without heap allocation. |
| 37 | +
|
| 38 | + @par Restricted Use Case |
| 39 | +
|
| 40 | + This is NOT a general-purpose composable abstraction. It exists |
| 41 | + solely for the final step in a coroutine I/O call chain where: |
| 42 | +
|
| 43 | + @li A templated awaitable captures the caller's buffer sequence |
| 44 | + @li The awaitable's `await_suspend` passes buffers across a |
| 45 | + virtual interface to an I/O object implementation |
| 46 | + @li The implementation immediately unrolls the buffers into |
| 47 | + platform-native structures for the system call |
| 48 | +
|
| 49 | + @par Lifetime Model |
| 50 | +
|
| 51 | + The safety of this class depends entirely on coroutine parameter |
| 52 | + lifetime extension. When a coroutine is suspended, parameters |
| 53 | + passed to the awaitable remain valid until the coroutine resumes |
| 54 | + or is destroyed. This class exploits that guarantee by holding |
| 55 | + only a pointer to the caller's buffer sequence. |
| 56 | +
|
| 57 | + The referenced buffer sequence is valid ONLY while the calling |
| 58 | + coroutine remains suspended at the exact suspension point where |
| 59 | + `io_buffer_param` was created. Once the coroutine resumes, |
| 60 | + returns, or is destroyed, all referenced data becomes invalid. |
| 61 | +
|
| 62 | + @par Correct Usage |
| 63 | +
|
| 64 | + The implementation receiving `io_buffer_param` MUST: |
| 65 | +
|
| 66 | + @li Call `copy_to` immediately upon receiving the parameter |
| 67 | + @li Use the unrolled buffer descriptors for the I/O operation |
| 68 | + @li Never store the `io_buffer_param` object itself |
| 69 | + @li Never store pointers obtained from `copy_to` beyond the |
| 70 | + immediate I/O operation |
| 71 | +
|
| 72 | + @par Example: Correct Usage |
| 73 | +
|
| 74 | + @code |
| 75 | + // Templated awaitable at the call site |
| 76 | + template<class Buffers> |
| 77 | + struct write_awaitable |
| 78 | + { |
| 79 | + Buffers bufs; |
| 80 | + io_stream* stream; |
| 81 | +
|
| 82 | + bool await_ready() { return false; } |
| 83 | +
|
| 84 | + void await_suspend(std::coroutine_handle<> h) |
| 85 | + { |
| 86 | + // CORRECT: Pass to virtual interface while suspended. |
| 87 | + // The buffer sequence 'bufs' remains valid because |
| 88 | + // coroutine parameters live until resumption. |
| 89 | + stream->async_write_some_impl(bufs, h); |
| 90 | + } |
| 91 | +
|
| 92 | + io_result await_resume() { return stream->get_result(); } |
| 93 | + }; |
| 94 | +
|
| 95 | + // Virtual implementation - unrolls immediately |
| 96 | + void stream_impl::async_write_some_impl( |
| 97 | + io_buffer_param p, |
| 98 | + std::coroutine_handle<> h) |
| 99 | + { |
| 100 | + // CORRECT: Unroll immediately into platform structure |
| 101 | + iovec vecs[16]; |
| 102 | + std::size_t n = p.copy_to( |
| 103 | + reinterpret_cast<capy::mutable_buffer*>(vecs), 16); |
| 104 | +
|
| 105 | + // CORRECT: Use unrolled buffers for system call now |
| 106 | + submit_to_io_uring(vecs, n, h); |
| 107 | +
|
| 108 | + // After this function returns, 'p' must not be used again. |
| 109 | + // The iovec array is safe because it contains copies of |
| 110 | + // the pointer/size pairs, not references to 'p'. |
| 111 | + } |
| 112 | + @endcode |
| 113 | +
|
| 114 | + @par UNSAFE USAGE: Storing io_buffer_param |
| 115 | +
|
| 116 | + @warning Never store `io_buffer_param` for later use. |
| 117 | +
|
| 118 | + @code |
| 119 | + class broken_stream |
| 120 | + { |
| 121 | + io_buffer_param saved_param_; // UNSAFE: member storage |
| 122 | +
|
| 123 | + void async_write_impl(io_buffer_param p, ...) |
| 124 | + { |
| 125 | + saved_param_ = p; // UNSAFE: storing for later |
| 126 | + schedule_write_later(); |
| 127 | + } |
| 128 | +
|
| 129 | + void do_write_later() |
| 130 | + { |
| 131 | + // UNSAFE: The calling coroutine may have resumed |
| 132 | + // or been destroyed. saved_param_ now references |
| 133 | + // invalid memory! |
| 134 | + capy::mutable_buffer bufs[8]; |
| 135 | + saved_param_.copy_to(bufs, 8); // UNDEFINED BEHAVIOR |
| 136 | + } |
| 137 | + }; |
| 138 | + @endcode |
| 139 | +
|
| 140 | + @par UNSAFE USAGE: Storing Unrolled Pointers |
| 141 | +
|
| 142 | + @warning The pointers obtained from `copy_to` point into the |
| 143 | + caller's buffer sequence. They become invalid when the caller |
| 144 | + resumes. |
| 145 | +
|
| 146 | + @code |
| 147 | + class broken_stream |
| 148 | + { |
| 149 | + capy::mutable_buffer saved_bufs_[8]; // UNSAFE |
| 150 | + std::size_t saved_count_; |
| 151 | +
|
| 152 | + void async_write_impl(io_buffer_param p, ...) |
| 153 | + { |
| 154 | + // This copies pointer/size pairs into saved_bufs_ |
| 155 | + saved_count_ = p.copy_to(saved_bufs_, 8); |
| 156 | +
|
| 157 | + // UNSAFE: scheduling for later while storing the |
| 158 | + // buffer descriptors. The pointers in saved_bufs_ |
| 159 | + // will dangle when the caller resumes! |
| 160 | + schedule_for_later(); |
| 161 | + } |
| 162 | +
|
| 163 | + void later() |
| 164 | + { |
| 165 | + // UNSAFE: saved_bufs_ contains dangling pointers |
| 166 | + for(std::size_t i = 0; i < saved_count_; ++i) |
| 167 | + write(fd_, saved_bufs_[i].data(), ...); // UB |
| 168 | + } |
| 169 | + }; |
| 170 | + @endcode |
| 171 | +
|
| 172 | + @par UNSAFE USAGE: Using Outside a Coroutine |
| 173 | +
|
| 174 | + @warning This class relies on coroutine lifetime semantics. |
| 175 | + Using it with callbacks or non-coroutine async patterns is |
| 176 | + undefined behavior. |
| 177 | +
|
| 178 | + @code |
| 179 | + // UNSAFE: No coroutine lifetime guarantee |
| 180 | + void bad_callback_pattern(std::vector<char>& data) |
| 181 | + { |
| 182 | + capy::mutable_buffer buf(data.data(), data.size()); |
| 183 | +
|
| 184 | + // UNSAFE: In a callback model, 'buf' may go out of scope |
| 185 | + // before the callback fires. There is no coroutine |
| 186 | + // suspension to extend the lifetime. |
| 187 | + stream.async_write(buf, [](error_code ec) { |
| 188 | + // 'buf' is already destroyed! |
| 189 | + }); |
| 190 | + } |
| 191 | + @endcode |
| 192 | +
|
| 193 | + @par UNSAFE USAGE: Passing to Another Coroutine |
| 194 | +
|
| 195 | + @warning Do not pass `io_buffer_param` to a different coroutine |
| 196 | + or spawn a new coroutine that captures it. |
| 197 | +
|
| 198 | + @code |
| 199 | + void broken_impl(io_buffer_param p, std::coroutine_handle<> h) |
| 200 | + { |
| 201 | + // UNSAFE: Spawning a new coroutine that captures 'p'. |
| 202 | + // The original coroutine may resume before this new |
| 203 | + // coroutine uses 'p'. |
| 204 | + co_spawn([p]() -> task<void> { |
| 205 | + capy::mutable_buffer bufs[8]; |
| 206 | + p.copy_to(bufs, 8); // UNSAFE: original caller may |
| 207 | + // have resumed already! |
| 208 | + co_return; |
| 209 | + }); |
| 210 | + } |
| 211 | + @endcode |
| 212 | +
|
| 213 | + @par UNSAFE USAGE: Multiple Virtual Hops |
| 214 | +
|
| 215 | + @warning Minimize indirection. Each virtual call that passes |
| 216 | + `io_buffer_param` without immediately unrolling it increases |
| 217 | + the risk of misuse. |
| 218 | +
|
| 219 | + @code |
| 220 | + // Risky: multiple hops before unrolling |
| 221 | + void layer1(io_buffer_param p) { |
| 222 | + layer2(p); // Still haven't unrolled... |
| 223 | + } |
| 224 | + void layer2(io_buffer_param p) { |
| 225 | + layer3(p); // Still haven't unrolled... |
| 226 | + } |
| 227 | + void layer3(io_buffer_param p) { |
| 228 | + // Finally unrolling, but the chain is fragile. |
| 229 | + // Any intermediate layer storing 'p' breaks everything. |
| 230 | + } |
| 231 | + @endcode |
| 232 | +
|
| 233 | + @par UNSAFE USAGE: Fire-and-Forget Operations |
| 234 | +
|
| 235 | + @warning Do not use with detached or fire-and-forget async |
| 236 | + operations where there is no guarantee the caller remains |
| 237 | + suspended. |
| 238 | +
|
| 239 | + @code |
| 240 | + task<void> caller() |
| 241 | + { |
| 242 | + char buf[1024]; |
| 243 | + // UNSAFE: If async_write is fire-and-forget (doesn't |
| 244 | + // actually suspend the caller), 'buf' may be destroyed |
| 245 | + // before the I/O completes. |
| 246 | + stream.async_write_detached(capy::mutable_buffer(buf, 1024)); |
| 247 | + // Returns immediately - 'buf' goes out of scope! |
| 248 | + } |
| 249 | + @endcode |
| 250 | +
|
| 251 | + @par Passing Convention |
| 252 | +
|
| 253 | + Pass by value. The class contains only two pointers (16 bytes |
| 254 | + on 64-bit systems), making copies trivial and clearly |
| 255 | + communicating the lightweight, transient nature of this type. |
| 256 | +
|
| 257 | + @code |
| 258 | + // Preferred: pass by value |
| 259 | + void process(io_buffer_param buffers); |
| 260 | +
|
| 261 | + // Also acceptable: pass by const reference |
| 262 | + void process(io_buffer_param const& buffers); |
| 263 | + @endcode |
| 264 | +
|
| 265 | + @see capy::ConstBufferSequence, capy::MutableBufferSequence |
| 266 | +*/ |
| 267 | +class io_buffer_param |
| 268 | +{ |
| 269 | +public: |
| 270 | + /** Construct from a const buffer sequence. |
| 271 | +
|
| 272 | + @param bs The buffer sequence to adapt. |
| 273 | + */ |
| 274 | + template<capy::ConstBufferSequence BS> |
| 275 | + io_buffer_param(BS const& bs) noexcept |
| 276 | + : bs_(&bs) |
| 277 | + , fn_(©_impl<BS>) |
| 278 | + { |
| 279 | + } |
| 280 | + |
| 281 | + /** Fill an array with buffers from the sequence. |
| 282 | +
|
| 283 | + Copies buffer descriptors from the sequence into the |
| 284 | + destination array. If the total number of bytes across |
| 285 | + all copied buffers is zero, returns 0 regardless of |
| 286 | + how many buffer descriptors were copied. |
| 287 | +
|
| 288 | + @param dest Pointer to array of mutable buffer descriptors. |
| 289 | + @param n Maximum number of buffers to copy. |
| 290 | +
|
| 291 | + @return The number of buffers actually copied, or 0 if |
| 292 | + the total byte count is zero. |
| 293 | + */ |
| 294 | + std::size_t |
| 295 | + copy_to( |
| 296 | + capy::mutable_buffer* dest, |
| 297 | + std::size_t n) const noexcept |
| 298 | + { |
| 299 | + return fn_(bs_, dest, n); |
| 300 | + } |
| 301 | + |
| 302 | +private: |
| 303 | + template<capy::ConstBufferSequence BS> |
| 304 | + static std::size_t |
| 305 | + copy_impl( |
| 306 | + void const* p, |
| 307 | + capy::mutable_buffer* dest, |
| 308 | + std::size_t n) |
| 309 | + { |
| 310 | + auto const& bs = *static_cast<BS const*>(p); |
| 311 | + auto it = capy::begin(bs); |
| 312 | + auto const end_it = capy::end(bs); |
| 313 | + |
| 314 | + std::size_t i = 0; |
| 315 | + std::size_t bytes = 0; |
| 316 | + if constexpr (capy::MutableBufferSequence<BS>) |
| 317 | + { |
| 318 | + for(; it != end_it && i < n; ++it, ++i) |
| 319 | + { |
| 320 | + dest[i] = *it; |
| 321 | + bytes += dest[i].size(); |
| 322 | + } |
| 323 | + } |
| 324 | + else |
| 325 | + { |
| 326 | + for(; it != end_it && i < n; ++it, ++i) |
| 327 | + { |
| 328 | + auto const& buf = *it; |
| 329 | + dest[i] = capy::mutable_buffer( |
| 330 | + const_cast<char*>( |
| 331 | + static_cast<char const*>(buf.data())), |
| 332 | + buf.size()); |
| 333 | + bytes += buf.size(); |
| 334 | + } |
| 335 | + } |
| 336 | + return bytes == 0 ? 0 : i; |
| 337 | + } |
| 338 | + |
| 339 | + using fn_t = std::size_t(*)(void const*, |
| 340 | + capy::mutable_buffer*, std::size_t); |
| 341 | + |
| 342 | + void const* bs_; |
| 343 | + fn_t fn_; |
| 344 | +}; |
| 345 | + |
| 346 | +} // namespace corosio |
| 347 | +} // namespace boost |
| 348 | + |
| 349 | +#endif |
0 commit comments