include/boost/corosio/native/detail/coro_op_complete.hpp

95.0% Lines (19 / 20) 100.0% Functions (2 / 2)
coro_op_complete.hpp
f(x) Functions (2)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Michael Vandeberg
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_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP
11 #define BOOST_COROSIO_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP
12
13 #include <boost/corosio/detail/dispatch_coro.hpp>
14 #include <boost/corosio/native/detail/coro_op.hpp>
15 #include <boost/capy/error.hpp>
16
17 #include <cstddef>
18 #include <memory>
19 #include <system_error>
20
21 /*
22 Shared completion-tail helpers for proactor ops. Every IOCP and io_uring
23 I/O handler ends the same way once its backend-specific result has been
24 decoded into ec_out/bytes_out:
25
26 1. disarm the stop_callback,
27 2. on the shutdown-drain path (owner == nullptr) just break the
28 impl_ptr keepalive cycle and return without resuming,
29 3. otherwise resume the coroutine on its executor, dropping the
30 keepalive only after the continuation has been handed off.
31
32 The *decode* step (raw DWORD/res -> {ec, bytes, eof, canceled}) stays
33 backend-specific because the raw encodings differ; in Phase 3 it is
34 formalized as `Traits::decode_result`. These two helpers capture the
35 backend-agnostic prologue and resume tail so the per-op handlers shrink to
36 "drain-or-decode, then resume".
37 */
38
39 namespace boost::corosio::detail {
40
41 /** Translate a decoded I/O result into `*ec_out`/`*bytes_out` using the
42 error / transfer / cancelled / EOF priority shared by every native
43 backend.
44
45 The raw error encodings differ per backend (reactor positive `errno`,
46 io_uring negative `res`, IOCP `DWORD`), so the native-error -> error_code
47 step stays backend-local: the caller passes @a err already converted
48 (an empty error_code means "no error"). This helper owns only the
49 priority logic, which is byte-for-byte identical everywhere:
50
51 bytes > 0 -> err if set, else success
52 cancelled -> operation_canceled
53 err set -> err
54 is_read && !empty -> end_of_file
55 otherwise -> success
56
57 A transfer outranks the cancellation flag: the stream contracts
58 require a completed transfer to be reported verbatim — a stop
59 request that lost the race changes nothing, and the next operation
60 on the still-stopped token reports `canceled`. With nothing
61 transferred, the flag outranks the raw completion error: a
62 cancellation request is what tears pending ops down locally (close,
63 stop), and the flag normalizes whichever error that teardown
64 surfaced (and it outranks the EOF mapping for the same reason: an
65 aborted read is `canceled`, not `eof`).
66
67 The byte count is always stored — never zeroed by cancellation.
68
69 @param ec_out Error destination (may be null).
70 @param bytes_out Byte-count destination (null for connect/wait/
71 accept, which report no count).
72 @param cancelled The op's cancellation flag.
73 @param err Backend error already converted to error_code, or a
74 default-constructed error_code on success.
75 @param is_read True only for reads that should map a 0-byte
76 completion to EOF — false for writes, connect, wait,
77 and datagrams (a 0-byte datagram is success, not EOF).
78 @param bytes Bytes transferred.
79 @param empty_buffer True when the submitted buffer was zero-length,
80 which suppresses the otherwise-spurious EOF.
81 */
82 inline void
83 98874x decode_io_result(
84 std::error_code* ec_out,
85 std::size_t* bytes_out,
86 bool cancelled,
87 std::error_code err,
88 bool is_read,
89 std::size_t bytes,
90 bool empty_buffer) noexcept
91 {
92 98874x if (bytes_out)
93 89883x *bytes_out = bytes;
94 98874x if (!ec_out)
95 ✗ return;
96 98874x if (bytes > 0)
97 89196x *ec_out = err;
98 9678x else if (cancelled)
99 706x *ec_out = capy::error::canceled;
100 8972x else if (err)
101 164x *ec_out = err;
102 8808x else if (is_read && !empty_buffer)
103 31x *ec_out = capy::error::eof;
104 else
105 8777x *ec_out = {};
106 }
107
108 /** Completion prologue shared by every proactor handler.
109
110 Disarms the stop_callback, then detects the shutdown-drain path.
111
112 @param owner The scheduler pointer (nullptr during shutdown drain).
113 @param self The completing op.
114 @return True if this was a shutdown drain — the caller must `return`
115 immediately without decoding or resuming. On that path the
116 impl_ptr keepalive is dropped here (which may destroy the impl,
117 and with it the op storage).
118 */
119 inline bool
120 coro_drain_if_shutdown(void* owner, coro_op* self) noexcept
121 {
122 self->stop_cb.reset();
123 if (owner == nullptr)
124 {
125 auto suicide = std::move(self->impl_ptr);
126 return true;
127 }
128 return false;
129 }
130
131 /** Resume tail shared by every proactor handler.
132
133 Resumes the op's coroutine on its executor and then drops the impl_ptr
134 keepalive. The keepalive is moved into a local that is released *after*
135 `resume()` returns, matching the existing io_uring ordering: the impl (and
136 therefore this op's storage) may be destroyed as the local goes out of
137 scope, so nothing may touch `*self` after the resume.
138
139 @pre `self->ec_out`/`bytes_out` have already been written by the
140 backend's decode step.
141 */
142 inline void
143 98213x coro_resume(coro_op* self) noexcept
144 {
145 98213x self->cont.h = self->h;
146 // Clear the keepalive before publishing the continuation: a strand
147 // drained on another thread can reuse this op via reset() the instant
148 // it runs, so this write must be ordered before the publish, not after.
149 98213x auto suicide = std::move(self->impl_ptr);
150 98213x auto next = dispatch_coro(self->ex, self->cont);
151 98213x next.resume();
152 // suicide drops here; may destroy impl + self.
153 98213x }
154
155 } // namespace boost::corosio::detail
156
157 #endif
158