TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Michael Vandeberg
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/corosio
9 : //
10 :
11 : #ifndef BOOST_COROSIO_DETAIL_BUFFER_PARAM_HPP
12 : #define BOOST_COROSIO_DETAIL_BUFFER_PARAM_HPP
13 :
14 : #include <boost/corosio/detail/config.hpp>
15 : #include <boost/capy/buffers.hpp>
16 :
17 : #include <cstddef>
18 :
19 : namespace boost::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 : `buffer_param` was created. Once the coroutine resumes,
60 : returns, or is destroyed, all referenced data becomes invalid.
61 :
62 : @par Const Buffer Handling
63 :
64 : This class accepts both `ConstBufferSequence` and
65 : `MutableBufferSequence` types. However, `copy_to` always produces
66 : `mutable_buffer` descriptors, casting away constness for const
67 : buffer sequences. This design matches platform I/O structures
68 : (`iovec`, `WSABUF`) which use non-const pointers regardless of
69 : the operation direction.
70 :
71 : @warning The caller is responsible for ensuring the type system
72 : is not violated. When the original buffer sequence was const
73 : (e.g., for a write operation), the implementation MUST NOT write
74 : to the buffers obtained from `copy_to`. The const-cast exists
75 : solely to provide a uniform interface for platform I/O calls.
76 :
77 : @note Do NOT `reinterpret_cast` the `mutable_buffer` array to
78 : `iovec*`/`WSABUF*` and pass it to the OS, even when the layouts
79 : match: no object of the target type exists in that storage, so the
80 : access is undefined behavior (and `mutable_buffer` is not an
81 : implicit-lifetime type, so `std::start_lifetime_as_array` cannot
82 : rescue it). Copy field by field into a real platform array instead.
83 :
84 : @code
85 : // For write operations (const buffers):
86 : void submit_write(buffer_param p)
87 : {
88 : capy::mutable_buffer bufs[8];
89 : auto n = p.copy_to(bufs, 8);
90 : iovec iov[8];
91 : for (std::size_t i = 0; i < n; ++i)
92 : {
93 : // bufs[] may reference const data - DO NOT WRITE through iov
94 : iov[i].iov_base = bufs[i].data();
95 : iov[i].iov_len = bufs[i].size();
96 : }
97 : writev(fd, iov, n); // read-only
98 : }
99 :
100 : // For read operations (mutable buffers):
101 : void submit_read(buffer_param p)
102 : {
103 : capy::mutable_buffer bufs[8];
104 : auto n = p.copy_to(bufs, 8);
105 : iovec iov[8];
106 : for (std::size_t i = 0; i < n; ++i)
107 : {
108 : // bufs[] references mutable data - safe to write
109 : iov[i].iov_base = bufs[i].data();
110 : iov[i].iov_len = bufs[i].size();
111 : }
112 : readv(fd, iov, n); // writing
113 : }
114 : @endcode
115 :
116 : @par Correct Usage
117 :
118 : The implementation receiving `buffer_param` MUST:
119 :
120 : @li Call `copy_to` immediately upon receiving the parameter
121 : @li Use the unrolled buffer descriptors for the I/O operation
122 : @li Never store the `buffer_param` object itself
123 : @li Never store pointers obtained from `copy_to` beyond the
124 : immediate I/O operation
125 :
126 : @par Example: Correct Usage
127 :
128 : @code
129 : // Templated awaitable at the call site
130 : template<class Buffers>
131 : struct write_awaitable
132 : {
133 : Buffers bufs;
134 : io_stream* stream;
135 :
136 : bool await_ready() { return false; }
137 :
138 : void await_suspend(std::coroutine_handle<> h)
139 : {
140 : // CORRECT: Pass to virtual interface while suspended.
141 : // The buffer sequence 'bufs' remains valid because
142 : // coroutine parameters live until resumption.
143 : stream->async_write_some_impl(bufs, h);
144 : }
145 :
146 : io_result await_resume() { return stream->get_result(); }
147 : };
148 :
149 : // Virtual implementation - unrolls immediately
150 : void stream_impl::async_write_some_impl(
151 : buffer_param p,
152 : std::coroutine_handle<> h)
153 : {
154 : // CORRECT: Unroll immediately into platform structure.
155 : // Copy field by field; do NOT reinterpret_cast the
156 : // mutable_buffer array to iovec* (see the @note above).
157 : capy::mutable_buffer bufs[16];
158 : std::size_t n = p.copy_to(bufs, 16);
159 : iovec vecs[16];
160 : for (std::size_t i = 0; i < n; ++i)
161 : {
162 : vecs[i].iov_base = bufs[i].data();
163 : vecs[i].iov_len = bufs[i].size();
164 : }
165 :
166 : // CORRECT: Use unrolled buffers for system call now
167 : submit_to_io_uring(vecs, n, h);
168 :
169 : // After this function returns, 'p' must not be used again.
170 : // The iovec array is safe because it contains copies of
171 : // the pointer/size pairs, not references to 'p'.
172 : }
173 : @endcode
174 :
175 : @par UNSAFE USAGE: Storing buffer_param
176 :
177 : @warning Never store `buffer_param` for later use.
178 :
179 : @code
180 : class broken_stream
181 : {
182 : buffer_param saved_param_; // UNSAFE: member storage
183 :
184 : void async_write_impl(buffer_param p, ...)
185 : {
186 : saved_param_ = p; // UNSAFE: storing for later
187 : schedule_write_later();
188 : }
189 :
190 : void do_write_later()
191 : {
192 : // UNSAFE: The calling coroutine may have resumed
193 : // or been destroyed. saved_param_ now references
194 : // invalid memory!
195 : capy::mutable_buffer bufs[8];
196 : saved_param_.copy_to(bufs, 8); // UNDEFINED BEHAVIOR
197 : }
198 : };
199 : @endcode
200 :
201 : @par UNSAFE USAGE: Storing Unrolled Pointers
202 :
203 : @warning The pointers obtained from `copy_to` point into the
204 : caller's buffer sequence. They become invalid when the caller
205 : resumes.
206 :
207 : @code
208 : class broken_stream
209 : {
210 : capy::mutable_buffer saved_bufs_[8]; // UNSAFE
211 : std::size_t saved_count_;
212 :
213 : void async_write_impl(buffer_param p, ...)
214 : {
215 : // This copies pointer/size pairs into saved_bufs_
216 : saved_count_ = p.copy_to(saved_bufs_, 8);
217 :
218 : // UNSAFE: scheduling for later while storing the
219 : // buffer descriptors. The pointers in saved_bufs_
220 : // will dangle when the caller resumes!
221 : schedule_for_later();
222 : }
223 :
224 : void later()
225 : {
226 : // UNSAFE: saved_bufs_ contains dangling pointers
227 : for(std::size_t i = 0; i < saved_count_; ++i)
228 : write(fd_, saved_bufs_[i].data(), ...); // UB
229 : }
230 : };
231 : @endcode
232 :
233 : @par UNSAFE USAGE: Using Outside a Coroutine
234 :
235 : @warning This class relies on coroutine lifetime semantics.
236 : Using it with callbacks or non-coroutine async patterns is
237 : undefined behavior.
238 :
239 : @code
240 : // UNSAFE: No coroutine lifetime guarantee
241 : void bad_callback_pattern(std::vector<char>& data)
242 : {
243 : capy::mutable_buffer buf(data.data(), data.size());
244 :
245 : // UNSAFE: In a callback model, 'buf' may go out of scope
246 : // before the callback fires. There is no coroutine
247 : // suspension to extend the lifetime.
248 : stream.async_write(buf, [](error_code ec) {
249 : // 'buf' is already destroyed!
250 : });
251 : }
252 : @endcode
253 :
254 : @par UNSAFE USAGE: Passing to Another Coroutine
255 :
256 : @warning Do not pass `buffer_param` to a different coroutine
257 : or spawn a new coroutine that captures it.
258 :
259 : @code
260 : void broken_impl(buffer_param p, std::coroutine_handle<> h)
261 : {
262 : // UNSAFE: Spawning a new coroutine that captures 'p'.
263 : // The original coroutine may resume before this new
264 : // coroutine uses 'p'.
265 : co_spawn([p]() -> task<void> {
266 : capy::mutable_buffer bufs[8];
267 : p.copy_to(bufs, 8); // UNSAFE: original caller may
268 : // have resumed already!
269 : co_return;
270 : });
271 : }
272 : @endcode
273 :
274 : @par UNSAFE USAGE: Multiple Virtual Hops
275 :
276 : @warning Minimize indirection. Each virtual call that passes
277 : `buffer_param` without immediately unrolling it increases
278 : the risk of misuse.
279 :
280 : @code
281 : // Risky: multiple hops before unrolling
282 : void layer1(buffer_param p) {
283 : layer2(p); // Still haven't unrolled...
284 : }
285 : void layer2(buffer_param p) {
286 : layer3(p); // Still haven't unrolled...
287 : }
288 : void layer3(buffer_param p) {
289 : // Finally unrolling, but the chain is fragile.
290 : // Any intermediate layer storing 'p' breaks everything.
291 : }
292 : @endcode
293 :
294 : @par UNSAFE USAGE: Fire-and-Forget Operations
295 :
296 : @warning Do not use with detached or fire-and-forget async
297 : operations where there is no guarantee the caller remains
298 : suspended.
299 :
300 : @code
301 : task<void> caller()
302 : {
303 : char buf[1024];
304 : // UNSAFE: If async_write is fire-and-forget (doesn't
305 : // actually suspend the caller), 'buf' may be destroyed
306 : // before the I/O completes.
307 : stream.async_write_detached(capy::mutable_buffer(buf, 1024));
308 : // Returns immediately - 'buf' goes out of scope!
309 : }
310 : @endcode
311 :
312 : @par Passing Convention
313 :
314 : Pass by value. The class contains only two pointers (16 bytes
315 : on 64-bit systems), making copies trivial and clearly
316 : communicating the lightweight, transient nature of this type.
317 :
318 : @code
319 : // Preferred: pass by value
320 : void process(buffer_param buffers);
321 :
322 : // Also acceptable: pass by const reference
323 : void process(buffer_param const& buffers);
324 : @endcode
325 :
326 : @see capy::ConstBufferSequence, capy::MutableBufferSequence
327 : */
328 : class buffer_param
329 : {
330 : public:
331 : /** Construct from a const buffer sequence.
332 :
333 : @param bs The buffer sequence to adapt.
334 : */
335 : template<capy::ConstBufferSequence BS>
336 HIT 413367 : buffer_param(BS const& bs) noexcept : bs_(&bs)
337 413367 : , fn_(©_impl<BS>)
338 : {
339 413367 : }
340 :
341 : /** Fill an array with buffers from the sequence.
342 :
343 : Copies buffer descriptors from the sequence into the
344 : destination array, skipping any zero-size buffers.
345 : This ensures the output contains only buffers with
346 : actual data, suitable for direct use with system calls.
347 :
348 : @param dest Pointer to array of mutable buffer descriptors.
349 : @param n Maximum number of buffers to copy.
350 :
351 : @return The number of non-zero buffers copied.
352 : */
353 : std::size_t
354 413367 : copy_to(capy::mutable_buffer* dest, std::size_t n) const noexcept
355 : {
356 413367 : return fn_(bs_, dest, n);
357 : }
358 :
359 : private:
360 : template<capy::ConstBufferSequence BS>
361 : static std::size_t
362 413367 : copy_impl(void const* p, capy::mutable_buffer* dest, std::size_t n)
363 : {
364 413367 : auto const& bs = *static_cast<BS const*>(p);
365 413367 : auto it = capy::begin(bs);
366 413367 : auto const end_it = capy::end(bs);
367 :
368 413367 : std::size_t i = 0;
369 : if constexpr (capy::MutableBufferSequence<BS>)
370 : {
371 414226 : for (; it != end_it && i < n; ++it)
372 : {
373 207118 : capy::mutable_buffer buf(*it);
374 207118 : if (buf.size() == 0)
375 17 : continue;
376 207101 : dest[i++] = buf;
377 : }
378 : }
379 : else
380 : {
381 412540 : for (; it != end_it && i < n; ++it)
382 : {
383 206281 : capy::const_buffer buf(*it);
384 206281 : if (buf.size() == 0)
385 24 : continue;
386 412514 : dest[i++] = capy::mutable_buffer(
387 206257 : const_cast<char*>(static_cast<char const*>(buf.data())),
388 : buf.size());
389 : }
390 : }
391 413367 : return i;
392 : }
393 :
394 : using fn_t =
395 : std::size_t (*)(void const*, capy::mutable_buffer*, std::size_t);
396 :
397 : void const* bs_;
398 : fn_t fn_;
399 : };
400 :
401 : } // namespace boost::corosio
402 :
403 : #endif
|