95.00% Lines (19/20)
100.00% Functions (2/2)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | // | 1 | // | |||||
| 2 | // Copyright (c) 2026 Michael Vandeberg | 2 | // Copyright (c) 2026 Michael Vandeberg | |||||
| 3 | // | 3 | // | |||||
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | 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) | 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | |||||
| 6 | // | 6 | // | |||||
| 7 | // Official repository: https://github.com/cppalliance/corosio | 7 | // Official repository: https://github.com/cppalliance/corosio | |||||
| 8 | // | 8 | // | |||||
| 9 | 9 | |||||||
| 10 | #ifndef BOOST_COROSIO_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP | 10 | #ifndef BOOST_COROSIO_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP | |||||
| 11 | #define BOOST_COROSIO_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP | 11 | #define BOOST_COROSIO_NATIVE_DETAIL_CORO_OP_COMPLETE_HPP | |||||
| 12 | 12 | |||||||
| 13 | #include <boost/corosio/detail/dispatch_coro.hpp> | 13 | #include <boost/corosio/detail/dispatch_coro.hpp> | |||||
| 14 | #include <boost/corosio/native/detail/coro_op.hpp> | 14 | #include <boost/corosio/native/detail/coro_op.hpp> | |||||
| 15 | #include <boost/capy/error.hpp> | 15 | #include <boost/capy/error.hpp> | |||||
| 16 | 16 | |||||||
| 17 | #include <cstddef> | 17 | #include <cstddef> | |||||
| 18 | #include <memory> | 18 | #include <memory> | |||||
| 19 | #include <system_error> | 19 | #include <system_error> | |||||
| 20 | 20 | |||||||
| 21 | /* | 21 | /* | |||||
| 22 | Shared completion-tail helpers for proactor ops. Every IOCP and io_uring | 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 | 23 | I/O handler ends the same way once its backend-specific result has been | |||||
| 24 | decoded into ec_out/bytes_out: | 24 | decoded into ec_out/bytes_out: | |||||
| 25 | 25 | |||||||
| 26 | 1. disarm the stop_callback, | 26 | 1. disarm the stop_callback, | |||||
| 27 | 2. on the shutdown-drain path (owner == nullptr) just break the | 27 | 2. on the shutdown-drain path (owner == nullptr) just break the | |||||
| 28 | impl_ptr keepalive cycle and return without resuming, | 28 | impl_ptr keepalive cycle and return without resuming, | |||||
| 29 | 3. otherwise resume the coroutine on its executor, dropping the | 29 | 3. otherwise resume the coroutine on its executor, dropping the | |||||
| 30 | keepalive only after the continuation has been handed off. | 30 | keepalive only after the continuation has been handed off. | |||||
| 31 | 31 | |||||||
| 32 | The *decode* step (raw DWORD/res -> {ec, bytes, eof, canceled}) stays | 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 | 33 | backend-specific because the raw encodings differ; in Phase 3 it is | |||||
| 34 | formalized as `Traits::decode_result`. These two helpers capture the | 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 | 35 | backend-agnostic prologue and resume tail so the per-op handlers shrink to | |||||
| 36 | "drain-or-decode, then resume". | 36 | "drain-or-decode, then resume". | |||||
| 37 | */ | 37 | */ | |||||
| 38 | 38 | |||||||
| 39 | namespace boost::corosio::detail { | 39 | namespace boost::corosio::detail { | |||||
| 40 | 40 | |||||||
| 41 | - | /** Translate a decoded I/O result into `*ec_out` using the cancelled / | 41 | + | /** Translate a decoded I/O result into `*ec_out`/`*bytes_out` using the | |||
| 42 | - | error / EOF / success priority shared by every native backend. | 42 | + | error / transfer / cancelled / EOF priority shared by every native | |||
| 43 | + | backend. | ||||||
| 43 | 44 | |||||||
| 44 | The raw error encodings differ per backend (reactor positive `errno`, | 45 | The raw error encodings differ per backend (reactor positive `errno`, | |||||
| 45 | io_uring negative `res`, IOCP `DWORD`), so the native-error -> error_code | 46 | io_uring negative `res`, IOCP `DWORD`), so the native-error -> error_code | |||||
| 46 | step stays backend-local: the caller passes @a err already converted | 47 | step stays backend-local: the caller passes @a err already converted | |||||
| 47 | (an empty error_code means "no error"). This helper owns only the | 48 | (an empty error_code means "no error"). This helper owns only the | |||||
| 48 | priority logic, which is byte-for-byte identical everywhere: | 49 | priority logic, which is byte-for-byte identical everywhere: | |||||
| 49 | 50 | |||||||
| 51 | + | bytes > 0 -> err if set, else success | ||||||
| 50 | cancelled -> operation_canceled | 52 | cancelled -> operation_canceled | |||||
| 51 | err set -> err | 53 | err set -> err | |||||
| 52 | - | is_read && bytes == 0 && !empty -> end_of_file | 54 | + | is_read && !empty -> end_of_file | |||
| 53 | otherwise -> success | 55 | otherwise -> success | |||||
| 54 | 56 | |||||||
| 55 | - | Writes nothing when @a ec_out is null. Does not touch bytes_out — callers | 57 | + | A transfer outranks the cancellation flag: the stream contracts | |||
| 56 | - | that report a byte count write it separately (connect/wait carry none). | 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`). | ||||||
| 57 | 66 | |||||||
| 58 | - | @param ec_out Destination (may be null). | 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). | ||||||
| 59 | @param cancelled The op's cancellation flag. | 72 | @param cancelled The op's cancellation flag. | |||||
| 60 | @param err Backend error already converted to error_code, or a | 73 | @param err Backend error already converted to error_code, or a | |||||
| 61 | default-constructed error_code on success. | 74 | default-constructed error_code on success. | |||||
| 62 | @param is_read True only for reads that should map a 0-byte | 75 | @param is_read True only for reads that should map a 0-byte | |||||
| 63 | completion to EOF — false for writes, connect, wait, | 76 | completion to EOF — false for writes, connect, wait, | |||||
| 64 | and datagrams (a 0-byte datagram is success, not EOF). | 77 | and datagrams (a 0-byte datagram is success, not EOF). | |||||
| 65 | - | @param bytes Bytes transferred (consulted only for the EOF test). | 78 | + | @param bytes Bytes transferred. | |||
| 66 | @param empty_buffer True when the submitted buffer was zero-length, | 79 | @param empty_buffer True when the submitted buffer was zero-length, | |||||
| 67 | which suppresses the otherwise-spurious EOF. | 80 | which suppresses the otherwise-spurious EOF. | |||||
| 68 | */ | 81 | */ | |||||
| 69 | inline void | 82 | inline void | |||||
| HITCBC | 70 | 97380 | decode_io_result( | 83 | 95659 | decode_io_result( | ||
| 71 | std::error_code* ec_out, | 84 | std::error_code* ec_out, | |||||
| 85 | + | std::size_t* bytes_out, | ||||||
| 72 | bool cancelled, | 86 | bool cancelled, | |||||
| 73 | std::error_code err, | 87 | std::error_code err, | |||||
| 74 | bool is_read, | 88 | bool is_read, | |||||
| 75 | std::size_t bytes, | 89 | std::size_t bytes, | |||||
| 76 | bool empty_buffer) noexcept | 90 | bool empty_buffer) noexcept | |||||
| 77 | { | 91 | { | |||||
| HITGNC | 92 | + | 95659 | if (bytes_out) | ||||
| HITGNC | 93 | + | 86527 | *bytes_out = bytes; | ||||
| HITCBC | 78 | 97380 | if (!ec_out) | 94 | 95659 | if (!ec_out) | ||
| MISUBC | 79 | ✗ | return; | 95 | ✗ | return; | ||
| HITCBC | 80 | - | 97380 | if (cancelled) | 96 | + | 95659 | if (bytes > 0) |
| HITGNC | 97 | + | 85841 | *ec_out = err; | ||||
| HITGNC | 98 | + | 9818 | else if (cancelled) | ||||
| HITCBC | 81 | 662 | *ec_out = capy::error::canceled; | 99 | 705 | *ec_out = capy::error::canceled; | ||
| HITCBC | 82 | 96718 | else if (err) | 100 | 9113 | else if (err) | ||
| HITCBC | 83 | 133 | *ec_out = err; | 101 | 163 | *ec_out = err; | ||
| HITCBC | 84 | - | 96585 | else if (is_read && bytes == 0 && !empty_buffer) | 102 | + | 8950 | else if (is_read && !empty_buffer) |
| HITCBC | 85 | 14 | *ec_out = capy::error::eof; | 103 | 31 | *ec_out = capy::error::eof; | ||
| 86 | else | 104 | else | |||||
| HITCBC | 87 | 96571 | *ec_out = {}; | 105 | 8919 | *ec_out = {}; | ||
| 88 | } | 106 | } | |||||
| 89 | 107 | |||||||
| 90 | /** Completion prologue shared by every proactor handler. | 108 | /** Completion prologue shared by every proactor handler. | |||||
| 91 | 109 | |||||||
| 92 | Disarms the stop_callback, then detects the shutdown-drain path. | 110 | Disarms the stop_callback, then detects the shutdown-drain path. | |||||
| 93 | 111 | |||||||
| 94 | @param owner The scheduler pointer (nullptr during shutdown drain). | 112 | @param owner The scheduler pointer (nullptr during shutdown drain). | |||||
| 95 | @param self The completing op. | 113 | @param self The completing op. | |||||
| 96 | @return True if this was a shutdown drain — the caller must `return` | 114 | @return True if this was a shutdown drain — the caller must `return` | |||||
| 97 | immediately without decoding or resuming. On that path the | 115 | immediately without decoding or resuming. On that path the | |||||
| 98 | impl_ptr keepalive is dropped here (which may destroy the impl, | 116 | impl_ptr keepalive is dropped here (which may destroy the impl, | |||||
| 99 | and with it the op storage). | 117 | and with it the op storage). | |||||
| 100 | */ | 118 | */ | |||||
| 101 | inline bool | 119 | inline bool | |||||
| 102 | coro_drain_if_shutdown(void* owner, coro_op* self) noexcept | 120 | coro_drain_if_shutdown(void* owner, coro_op* self) noexcept | |||||
| 103 | { | 121 | { | |||||
| 104 | self->stop_cb.reset(); | 122 | self->stop_cb.reset(); | |||||
| 105 | if (owner == nullptr) | 123 | if (owner == nullptr) | |||||
| 106 | { | 124 | { | |||||
| 107 | auto suicide = std::move(self->impl_ptr); | 125 | auto suicide = std::move(self->impl_ptr); | |||||
| 108 | return true; | 126 | return true; | |||||
| 109 | } | 127 | } | |||||
| 110 | return false; | 128 | return false; | |||||
| 111 | } | 129 | } | |||||
| 112 | 130 | |||||||
| 113 | /** Resume tail shared by every proactor handler. | 131 | /** Resume tail shared by every proactor handler. | |||||
| 114 | 132 | |||||||
| 115 | Resumes the op's coroutine on its executor and then drops the impl_ptr | 133 | Resumes the op's coroutine on its executor and then drops the impl_ptr | |||||
| 116 | keepalive. The keepalive is moved into a local that is released *after* | 134 | keepalive. The keepalive is moved into a local that is released *after* | |||||
| 117 | `resume()` returns, matching the existing io_uring ordering: the impl (and | 135 | `resume()` returns, matching the existing io_uring ordering: the impl (and | |||||
| 118 | therefore this op's storage) may be destroyed as the local goes out of | 136 | therefore this op's storage) may be destroyed as the local goes out of | |||||
| 119 | scope, so nothing may touch `*self` after the resume. | 137 | scope, so nothing may touch `*self` after the resume. | |||||
| 120 | 138 | |||||||
| 121 | @pre `self->ec_out`/`bytes_out` have already been written by the | 139 | @pre `self->ec_out`/`bytes_out` have already been written by the | |||||
| 122 | backend's decode step. | 140 | backend's decode step. | |||||
| 123 | */ | 141 | */ | |||||
| 124 | inline void | 142 | inline void | |||||
| HITCBC | 125 | 97380 | coro_resume(coro_op* self) noexcept | 143 | 95021 | coro_resume(coro_op* self) noexcept | ||
| 126 | { | 144 | { | |||||
| HITCBC | 127 | 97380 | self->cont.h = self->h; | 145 | 95021 | self->cont.h = self->h; | ||
| 128 | // Clear the keepalive before publishing the continuation: a strand | 146 | // Clear the keepalive before publishing the continuation: a strand | |||||
| 129 | // drained on another thread can reuse this op via reset() the instant | 147 | // drained on another thread can reuse this op via reset() the instant | |||||
| 130 | // it runs, so this write must be ordered before the publish, not after. | 148 | // it runs, so this write must be ordered before the publish, not after. | |||||
| HITCBC | 131 | 97380 | auto suicide = std::move(self->impl_ptr); | 149 | 95021 | auto suicide = std::move(self->impl_ptr); | ||
| HITCBC | 132 | 97380 | auto next = dispatch_coro(self->ex, self->cont); | 150 | 95021 | auto next = dispatch_coro(self->ex, self->cont); | ||
| HITCBC | 133 | 97380 | next.resume(); | 151 | 95021 | next.resume(); | ||
| 134 | // suicide drops here; may destroy impl + self. | 152 | // suicide drops here; may destroy impl + self. | |||||
| HITCBC | 135 | 97380 | } | 153 | 95021 | } | ||
| 136 | 154 | |||||||
| 137 | } // namespace boost::corosio::detail | 155 | } // namespace boost::corosio::detail | |||||
| 138 | 156 | |||||||
| 139 | #endif | 157 | #endif | |||||