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