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

94.7% Lines (18 / 19) 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 object_ref_ 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 112534x decode_results(
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 112534x if (bytes_out)
93 98032x *bytes_out = bytes;
94 112534x if (!ec_out)
95 ✗ return;
96 112534x if (bytes > 0)
97 96075x *ec_out = err;
98 16459x else if (cancelled)
99 1269x *ec_out = capy::error::canceled;
100 15190x else if (err)
101 426x *ec_out = err;
102 14764x else if (is_read && !empty_buffer)
103 39x *ec_out = capy::error::eof;
104 else
105 14725x *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 awaiting coroutine is abandoned (see `coro_op::destroy_awaiting`)
117 and the object_ref_ keepalive dropped, which may destroy the impl
118 and with it the op storage.
119 */
120 inline bool
121 coro_drain_if_shutdown(void* owner, coro_op* self) noexcept
122 {
123 if (owner == nullptr)
124 {
125 self->destroy_awaiting();
126 return true;
127 }
128 self->stop_cb.reset();
129 return false;
130 }
131
132 /** Resume tail shared by every proactor handler.
133
134 Resumes the op's coroutine on its executor and then drops the object_ref_
135 keepalive. The keepalive is moved into a local that is released *after*
136 `resume()` returns, matching the existing io_uring ordering: the impl (and
137 therefore this op's storage) may be destroyed as the local goes out of
138 scope, so nothing may touch `*self` after the resume.
139
140 @pre `self->ec_out`/`bytes_out` have already been written by the
141 backend's decode step.
142 */
143 inline void
144 111370x coro_resume(coro_op* self) noexcept
145 {
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 111370x auto suicide = std::move(self->object_ref_);
150 111370x auto next = dispatch_coro(self->ex, *self->cont);
151 111370x next.resume();
152 // suicide drops here; may destroy impl + self.
153 111370x }
154
155 } // namespace boost::corosio::detail
156
157 #endif
158