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