include/boost/corosio/native/detail/coro_op_complete.hpp
95.0% Lines (19 / 20)
100.0% Functions (2 / 2)
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 |