100.00% Lines (13/13) 100.00% Functions (6/6)
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_POSIX_STREAM_DESCRIPTOR_HPP 10   #ifndef BOOST_COROSIO_POSIX_STREAM_DESCRIPTOR_HPP
11   #define BOOST_COROSIO_POSIX_STREAM_DESCRIPTOR_HPP 11   #define BOOST_COROSIO_POSIX_STREAM_DESCRIPTOR_HPP
12   12  
13   #include <boost/corosio/detail/config.hpp> 13   #include <boost/corosio/detail/config.hpp>
14   #include <boost/corosio/detail/platform.hpp> 14   #include <boost/corosio/detail/platform.hpp>
15   15  
16   #if BOOST_COROSIO_POSIX || defined(BOOST_COROSIO_MRDOCS) 16   #if BOOST_COROSIO_POSIX || defined(BOOST_COROSIO_MRDOCS)
17   17  
18   #include <boost/corosio/detail/except.hpp> 18   #include <boost/corosio/detail/except.hpp>
19   #include <boost/corosio/detail/native_handle.hpp> 19   #include <boost/corosio/detail/native_handle.hpp>
20   #include <boost/corosio/detail/op_base.hpp> 20   #include <boost/corosio/detail/op_base.hpp>
21   #include <boost/corosio/error.hpp> 21   #include <boost/corosio/error.hpp>
22   #include <boost/corosio/io/io_stream.hpp> 22   #include <boost/corosio/io/io_stream.hpp>
23   #include <boost/corosio/wait_type.hpp> 23   #include <boost/corosio/wait_type.hpp>
24   #include <boost/capy/ex/executor_ref.hpp> 24   #include <boost/capy/ex/executor_ref.hpp>
25   #include <boost/capy/ex/execution_context.hpp> 25   #include <boost/capy/ex/execution_context.hpp>
26   #include <boost/capy/concept/executor.hpp> 26   #include <boost/capy/concept/executor.hpp>
27   27  
28   #include <concepts> 28   #include <concepts>
29   #include <coroutine> 29   #include <coroutine>
30   #include <stop_token> 30   #include <stop_token>
31   #include <system_error> 31   #include <system_error>
32   #include <type_traits> 32   #include <type_traits>
33   33  
34   /* Adoption of an already-open pollable POSIX descriptor. 34   /* Adoption of an already-open pollable POSIX descriptor.
35   35  
36   The two contract points that are not obvious from the 36   The two contract points that are not obvious from the
37   declarations: 37   declarations:
38   38  
39   assign() requires a closed object and never touches an open one. 39   assign() requires a closed object and never touches an open one.
40   Every failure, validation or kernel refusal, leaves the object 40   Every failure, validation or kernel refusal, leaves the object
41   closed and the fd with the caller. 41   closed and the fd with the caller.
42   42  
43   On the reactor backends O_NONBLOCK is applied lazily, at the first 43   On the reactor backends O_NONBLOCK is applied lazily, at the first
44   read_some/write_some, and never restored; io_uring never touches 44   read_some/write_some, and never restored; io_uring never touches
45   it. A wait()-only user never triggers it, which is what makes 45   it. A wait()-only user never triggers it, which is what makes
46   adopting STDIN_FILENO safe: flipping the flag would change the 46   adopting STDIN_FILENO safe: flipping the flag would change the
47   parent shell's terminal, because the flag lives on the shared open 47   parent shell's terminal, because the flag lives on the shared open
48   file description, not on the descriptor. 48   file description, not on the descriptor.
49   */ 49   */
50   50  
51   namespace boost::corosio { 51   namespace boost::corosio {
52   52  
53   /** Drives an already-open POSIX descriptor from an `io_context`. 53   /** Drives an already-open POSIX descriptor from an `io_context`.
54   54  
55   Wraps an already-open pollable file descriptor and drives it 55   Wraps an already-open pollable file descriptor and drives it
56   from the `io_context`. The kinds in scope are character devices, 56   from the `io_context`. The kinds in scope are character devices,
57   `inotify`, `eventfd`, `timerfd`, `pidfd`, pipes, ttys, and socket 57   `inotify`, `eventfd`, `timerfd`, `pidfd`, pipes, ttys, and socket
58   kinds corosio does not otherwise wrap. The descriptor must come 58   kinds corosio does not otherwise wrap. The descriptor must come
59   from the caller; this type never creates one. 59   from the caller; this type never creates one.
60   60  
61   The type name is deliberately platform-qualified. Portability 61   The type name is deliberately platform-qualified. Portability
62   comes from the interfaces it implements, not from the name. A 62   comes from the interfaces it implements, not from the name. A
63   `posix_stream_descriptor` is an @ref io_stream. `capy::read`, 63   `posix_stream_descriptor` is an @ref io_stream. `capy::read`,
64   `capy::write`, other `capy::Stream`-constrained algorithms and 64   `capy::write`, other `capy::Stream`-constrained algorithms and
65   TLS layering therefore work on it exactly as they do on a 65   TLS layering therefore work on it exactly as they do on a
66   socket. 66   socket.
67   67  
68   @par Ownership 68   @par Ownership
69   `assign()` takes ownership and `close()` closes the 69   `assign()` takes ownership and `close()` closes the
70   descriptor. To integrate with a library that owns the fd, adopt 70   descriptor. To integrate with a library that owns the fd, adopt
71   a `dup()` of it: readiness lives on the open file description, 71   a `dup()` of it: readiness lives on the open file description,
72   which both descriptors share. 72   which both descriptors share.
73   73  
74   @par Descriptor Flags 74   @par Descriptor Flags
75   `assign()` and `wait()` never modify the descriptor on any 75   `assign()` and `wait()` never modify the descriptor on any
76   backend. On epoll, kqueue and select the first `read_some()` or 76   backend. On epoll, kqueue and select the first `read_some()` or
77   `write_some()` sets `O_NONBLOCK` and never restores it. On 77   `write_some()` sets `O_NONBLOCK` and never restores it. On
78   io_uring nothing is ever modified. A transfer the kernel cannot 78   io_uring nothing is ever modified. A transfer the kernel cannot
79   complete through its internal poll waits in a kernel worker 79   complete through its internal poll waits in a kernel worker
80   thread. Cancellation reaches it only if the driver's wait is 80   thread. Cancellation reaches it only if the driver's wait is
81   interruptible. The flag lives on the shared 81   interruptible. The flag lives on the shared
82   open file description, so restoring it would race every other 82   open file description, so restoring it would race every other
83   holder. A 83   holder. A
84   `dup()` is no escape: the duplicate shares that same description, 84   `dup()` is no escape: the duplicate shares that same description,
85   so the flag change reaches the other holder anyway. When another 85   so the flag change reaches the other holder anyway. When another
86   party owns the descriptor and cannot tolerate `O_NONBLOCK`, use 86   party owns the descriptor and cannot tolerate `O_NONBLOCK`, use
87   `wait()` -- which never modifies the descriptor -- and do the I/O 87   `wait()` -- which never modifies the descriptor -- and do the I/O
88   yourself. 88   yourself.
89   89  
90   @par Rejected Descriptors 90   @par Rejected Descriptors
91   Regular files, block devices, and directories are rejected with 91   Regular files, block devices, and directories are rejected with
92   `errc::operation_not_supported`. @ref stream_file and 92   `errc::operation_not_supported`. @ref stream_file and
93   @ref random_access_file adopt regular files and block devices. A 93   @ref random_access_file adopt regular files and block devices. A
94   directory is adoptable by no corosio type. A character device 94   directory is adoptable by no corosio type. A character device
95   the I/O backend cannot watch, such as `/dev/null`, is adopted on 95   the I/O backend cannot watch, such as `/dev/null`, is adopted on
96   every I/O backend. On epoll, kqueue, and io_uring, an operation 96   every I/O backend. On epoll, kqueue, and io_uring, an operation
97   on it that would have to wait for readiness completes with 97   on it that would have to wait for readiness completes with
98   `errc::operation_not_supported`. The exception is a transfer on 98   `errc::operation_not_supported`. The exception is a transfer on
99   io_uring when the descriptor is blocking: it waits in a kernel 99   io_uring when the descriptor is blocking: it waits in a kernel
100   worker thread instead. On select, the device is always ready for 100   worker thread instead. On select, the device is always ready for
101   reading and writing. Its `wait(wait_type::error)` waits until 101   reading and writing. Its `wait(wait_type::error)` waits until
102   cancelled, except on macOS, where it completes at once with an 102   cancelled, except on macOS, where it completes at once with an
103   error. On select, a descriptor at or above `FD_SETSIZE` is 103   error. On select, a descriptor at or above `FD_SETSIZE` is
104   rejected with `errc::too_many_files_open`. Where a kernel refusal 104   rejected with `errc::too_many_files_open`. Where a kernel refusal
105   surfaces depends on the backend. The epoll and kqueue backends 105   surfaces depends on the backend. The epoll and kqueue backends
106   register the descriptor during `assign()`, so a refusal fails 106   register the descriptor during `assign()`, so a refusal fails
107   there. What remains to refuse is resource exhaustion (`ENOMEM`, 107   there. What remains to refuse is resource exhaustion (`ENOMEM`,
108   `ENOSPC`). 108   `ENOSPC`).
109   kqueue watches writes only once a write-direction operation first 109   kqueue watches writes only once a write-direction operation first
110   has to wait. A descriptor that refuses write watching is still 110   has to wait. A descriptor that refuses write watching is still
111   adopted, and such a write or `wait(wait_type::write)` completes 111   adopted, and such a write or `wait(wait_type::write)` completes
112   with the kernel's refusal. The io_uring backend has no adopt-time 112   with the kernel's refusal. The io_uring backend has no adopt-time
113   registration, so `assign()` succeeds and takes ownership. The 113   registration, so `assign()` succeeds and takes ownership. The
114   refusal appears at the first `read_some()` or `write_some()`. 114   refusal appears at the first `read_some()` or `write_some()`.
115   select registers nothing with the kernel, so it has no refusal to 115   select registers nothing with the kernel, so it has no refusal to
116   report. 116   report.
117   117  
118   @par Signals 118   @par Signals
119   Writing to a descriptor whose peer has closed raises `SIGPIPE` 119   Writing to a descriptor whose peer has closed raises `SIGPIPE`
120   in the default disposition -- unlike the socket types, which 120   in the default disposition -- unlike the socket types, which
121   suppress it. `MSG_NOSIGNAL` is a `send()` flag with no `writev` 121   suppress it. `MSG_NOSIGNAL` is a `send()` flag with no `writev`
122   equivalent, and `SO_NOSIGPIPE` is a socket option, so neither 122   equivalent, and `SO_NOSIGPIPE` is a socket option, so neither
123   applies to an arbitrary descriptor. Callers must install 123   applies to an arbitrary descriptor. Callers must install
124   `SIG_IGN` for `SIGPIPE` if that is not already the process's 124   `SIG_IGN` for `SIGPIPE` if that is not already the process's
125   disposition. 125   disposition.
126   126  
127   @par Thread Safety 127   @par Thread Safety
128   Distinct objects: Safe.@n 128   Distinct objects: Safe.@n
129   Shared objects: Unsafe. A descriptor must not have concurrent 129   Shared objects: Unsafe. A descriptor must not have concurrent
130   operations of the same type (e.g. two simultaneous reads). One 130   operations of the same type (e.g. two simultaneous reads). One
131   read and one write may be in flight simultaneously. 131   read and one write may be in flight simultaneously.
132   132  
133   @see io_stream, stream_file, wait_type 133   @see io_stream, stream_file, wait_type
134   */ 134   */
135   class BOOST_COROSIO_DECL posix_stream_descriptor : public io_stream 135   class BOOST_COROSIO_DECL posix_stream_descriptor : public io_stream
136   { 136   {
137   public: 137   public:
138   /** Define backend hooks for descriptor operations. 138   /** Define backend hooks for descriptor operations.
139   139  
140   Platform backends (epoll, kqueue, select, io_uring) derive 140   Platform backends (epoll, kqueue, select, io_uring) derive
141   from this to implement descriptor I/O. 141   from this to implement descriptor I/O.
142   */ 142   */
143   struct implementation : io_stream::implementation 143   struct implementation : io_stream::implementation
144   { 144   {
145   /** Initiate an asynchronous wait for descriptor readiness. 145   /** Initiate an asynchronous wait for descriptor readiness.
146   146  
147   Completes when the descriptor becomes ready in the 147   Completes when the descriptor becomes ready in the
148   given direction, or an error condition is reported. No 148   given direction, or an error condition is reported. No
149   bytes are transferred and no descriptor flag is changed. 149   bytes are transferred and no descriptor flag is changed.
150   150  
151   @param h Coroutine handle to resume on completion. 151   @param h Coroutine handle to resume on completion.
152   @param ex Executor for dispatching the completion. 152   @param ex Executor for dispatching the completion.
153   @param w The direction to wait on. 153   @param w The direction to wait on.
154   @param token Stop token for cancellation. 154   @param token Stop token for cancellation.
155   @param ec Output error code. 155   @param ec Output error code.
156   @return Coroutine handle to resume immediately. 156   @return Coroutine handle to resume immediately.
157   */ 157   */
158   virtual std::coroutine_handle<> wait( 158   virtual std::coroutine_handle<> wait(
159   std::coroutine_handle<> h, 159   std::coroutine_handle<> h,
160   capy::executor_ref ex, 160   capy::executor_ref ex,
161   wait_type w, 161   wait_type w,
162   std::stop_token token, 162   std::stop_token token,
163   std::error_code* ec) = 0; 163   std::error_code* ec) = 0;
164   164  
165   /// Return the platform descriptor, or -1 when not open. 165   /// Return the platform descriptor, or -1 when not open.
166   virtual native_handle_type native_handle() const noexcept = 0; 166   virtual native_handle_type native_handle() const noexcept = 0;
167   167  
168   /** Release ownership of the native descriptor. 168   /** Release ownership of the native descriptor.
169   169  
170   Stops tracking the descriptor and cancels its pending 170   Stops tracking the descriptor and cancels its pending
171   operations, without closing it. The caller takes 171   operations, without closing it. The caller takes
172   ownership. 172   ownership.
173   173  
174   @return The native descriptor. 174   @return The native descriptor.
175   */ 175   */
176   virtual native_handle_type release_descriptor() noexcept = 0; 176   virtual native_handle_type release_descriptor() noexcept = 0;
177   177  
178   /** Request cancellation of pending asynchronous operations. 178   /** Request cancellation of pending asynchronous operations.
179   179  
180   All outstanding operations complete with a code that 180   All outstanding operations complete with a code that
181   compares equal to `capy::cond::canceled`. 181   compares equal to `capy::cond::canceled`.
182   */ 182   */
183   virtual void cancel() noexcept = 0; 183   virtual void cancel() noexcept = 0;
184   }; 184   };
185   185  
186   /// Represent the awaitable returned by @ref wait. 186   /// Represent the awaitable returned by @ref wait.
187   struct wait_awaitable : detail::void_op_base<wait_awaitable> 187   struct wait_awaitable : detail::void_op_base<wait_awaitable>
188   { 188   {
189   private: 189   private:
190   friend posix_stream_descriptor; 190   friend posix_stream_descriptor;
191   191  
HITCBC 192   35 wait_awaitable(posix_stream_descriptor& d, wait_type w) noexcept 192   35 wait_awaitable(posix_stream_descriptor& d, wait_type w) noexcept
HITCBC 193   70 : d_(d) 193   70 : d_(d)
HITCBC 194   35 , w_(w) 194   35 , w_(w)
195   { 195   {
HITCBC 196   35 } 196   35 }
197   197  
198   friend detail::void_op_base<wait_awaitable>; 198   friend detail::void_op_base<wait_awaitable>;
199   199  
200   posix_stream_descriptor& d_; 200   posix_stream_descriptor& d_;
201   wait_type w_; 201   wait_type w_;
202   202  
203   std::coroutine_handle<> 203   std::coroutine_handle<>
HITCBC 204   35 dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const 204   35 dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
205   { 205   {
HITCBC 206   35 return d_.get().wait(h, ex, w_, token_, &ec_); 206   35 return d_.get().wait(h, ex, w_, token_, &ec_);
207   } 207   }
208   }; 208   };
209   209  
210   /** Closes the descriptor if open, cancelling pending operations. */ 210   /** Closes the descriptor if open, cancelling pending operations. */
211   ~posix_stream_descriptor() override; 211   ~posix_stream_descriptor() override;
212   212  
213   /** Construct from an execution context. 213   /** Construct from an execution context.
214   214  
215   @param ctx The execution context that owns this object. 215   @param ctx The execution context that owns this object.
216   */ 216   */
217   explicit posix_stream_descriptor(capy::execution_context& ctx); 217   explicit posix_stream_descriptor(capy::execution_context& ctx);
218   218  
219   /** Construct from an executor. 219   /** Construct from an executor.
220   220  
221   The overload excludes `posix_stream_descriptor` itself so that it 221   The overload excludes `posix_stream_descriptor` itself so that it
222   cannot displace the move constructor. 222   cannot displace the move constructor.
223   223  
224   @tparam Ex A type satisfying `capy::Executor`. 224   @tparam Ex A type satisfying `capy::Executor`.
225   @param ex The executor whose context owns this object. 225   @param ex The executor whose context owns this object.
226   */ 226   */
227   template<class Ex> 227   template<class Ex>
228   requires(!std::same_as< 228   requires(!std::same_as<
229   std::remove_cvref_t<Ex>, 229   std::remove_cvref_t<Ex>,
230   posix_stream_descriptor>) && 230   posix_stream_descriptor>) &&
231   capy::Executor<Ex> 231   capy::Executor<Ex>
232   explicit posix_stream_descriptor(Ex const& ex) 232   explicit posix_stream_descriptor(Ex const& ex)
233   : posix_stream_descriptor(ex.context()) 233   : posix_stream_descriptor(ex.context())
234   { 234   {
235   } 235   }
236   236  
237   /** Transfer ownership of the descriptor from @p other. 237   /** Transfer ownership of the descriptor from @p other.
238   238  
239   After the move, @p other is in a moved-from state and may only 239   After the move, @p other is in a moved-from state and may only
240   be destroyed or assigned to. 240   be destroyed or assigned to.
241   241  
242   @param other The object to move from. 242   @param other The object to move from.
243   @pre No awaitables returned by @p other's methods exist. 243   @pre No awaitables returned by @p other's methods exist.
244   */ 244   */
245   posix_stream_descriptor(posix_stream_descriptor&& other) noexcept 245   posix_stream_descriptor(posix_stream_descriptor&& other) noexcept
246   : io_object(std::move(other)) 246   : io_object(std::move(other))
247   { 247   {
248   } 248   }
249   249  
250   /** Close any held descriptor and transfer ownership from @p other. 250   /** Close any held descriptor and transfer ownership from @p other.
251   251  
252   After the move, @p other is in a moved-from state and may only 252   After the move, @p other is in a moved-from state and may only
253   be destroyed or assigned to. 253   be destroyed or assigned to.
254   254  
255   @param other The object to move from. 255   @param other The object to move from.
256   @return `*this`. 256   @return `*this`.
257   @pre No awaitables returned by either object's methods exist. 257   @pre No awaitables returned by either object's methods exist.
258   */ 258   */
259   posix_stream_descriptor& operator=(posix_stream_descriptor&& other) noexcept 259   posix_stream_descriptor& operator=(posix_stream_descriptor&& other) noexcept
260   { 260   {
261   io_object::operator=(std::move(other)); 261   io_object::operator=(std::move(other));
262   return *this; 262   return *this;
263   } 263   }
264   264  
265   /// Copy construction is disabled; the descriptor is uniquely owned. 265   /// Copy construction is disabled; the descriptor is uniquely owned.
266   posix_stream_descriptor(posix_stream_descriptor const&) = delete; 266   posix_stream_descriptor(posix_stream_descriptor const&) = delete;
267   /// Copy assignment is disabled; the descriptor is uniquely owned. 267   /// Copy assignment is disabled; the descriptor is uniquely owned.
268   posix_stream_descriptor& operator=(posix_stream_descriptor const&) = delete; 268   posix_stream_descriptor& operator=(posix_stream_descriptor const&) = delete;
269   269  
270   /** Adopt an existing native descriptor. 270   /** Adopt an existing native descriptor.
271   271  
272   The object must be closed. To replace a held descriptor, 272   The object must be closed. To replace a held descriptor,
273   `close()` or `release()` it first. On success the object takes 273   `close()` or `release()` it first. On success the object takes
274   ownership and @p fd is closed by `close()` or the destructor. 274   ownership and @p fd is closed by `close()` or the destructor.
275   275  
276   No descriptor flag is modified here, `O_NONBLOCK` included. 276   No descriptor flag is modified here, `O_NONBLOCK` included.
277   277  
278   @param fd The native descriptor to adopt. 278   @param fd The native descriptor to adopt.
279   279  
280   @return `error::already_open` if this object is open. 280   @return `error::already_open` if this object is open.
281   `errc::bad_file_descriptor` when @p fd is negative or 281   `errc::bad_file_descriptor` when @p fd is negative or
282   closed. `errc::operation_not_supported` when @p fd names 282   closed. `errc::operation_not_supported` when @p fd names
283   a regular file, block device, or directory. 283   a regular file, block device, or directory.
284   `errc::too_many_files_open` on select when @p fd is at or 284   `errc::too_many_files_open` on select when @p fd is at or
285   above `FD_SETSIZE`. Otherwise the error the system 285   above `FD_SETSIZE`. Otherwise the error the system
286   reported, or an empty code. 286   reported, or an empty code.
287   287  
288   @par Exception Safety 288   @par Exception Safety
289   Throws nothing. On failure the object is unchanged and @p fd 289   Throws nothing. On failure the object is unchanged and @p fd
290   stays with the caller. 290   stays with the caller.
291   291  
292   @see release 292   @see release
293   */ 293   */
294   [[nodiscard]] std::error_code assign(native_handle_type fd) noexcept; 294   [[nodiscard]] std::error_code assign(native_handle_type fd) noexcept;
295   295  
296   /** Release ownership of the native descriptor. 296   /** Release ownership of the native descriptor.
297   297  
298   The object becomes not-open and pending operations are 298   The object becomes not-open and pending operations are
299   cancelled. The caller is responsible for closing the result. 299   cancelled. The caller is responsible for closing the result.
300   300  
301   @return The native descriptor. 301   @return The native descriptor.
302   302  
303   @throws std::system_error `errc::bad_file_descriptor` if the 303   @throws std::system_error `errc::bad_file_descriptor` if the
304   object is not open. 304   object is not open.
305   305  
306   @post `is_open() == false` 306   @post `is_open() == false`
307   */ 307   */
308   native_handle_type release(); 308   native_handle_type release();
309   309  
310   /** Close the descriptor. 310   /** Close the descriptor.
311   311  
312   Pending operations complete with a code that compares equal 312   Pending operations complete with a code that compares equal
313   to `capy::cond::canceled`. Does nothing when not open. 313   to `capy::cond::canceled`. Does nothing when not open.
314   */ 314   */
315   void close() noexcept; 315   void close() noexcept;
316   316  
317   /** Check whether a descriptor is held. 317   /** Check whether a descriptor is held.
318   318  
319   @return `true` if a descriptor is held. 319   @return `true` if a descriptor is held.
320   */ 320   */
HITCBC 321   252 bool is_open() const noexcept 321   252 bool is_open() const noexcept
322   { 322   {
HITCBC 323   252 return h_ && get().native_handle() >= 0; 323   252 return h_ && get().native_handle() >= 0;
324   } 324   }
325   325  
326   /** Get the native descriptor. 326   /** Get the native descriptor.
327   327  
328   @return The native descriptor, or -1 when not open. 328   @return The native descriptor, or -1 when not open.
329   */ 329   */
330   native_handle_type native_handle() const noexcept; 330   native_handle_type native_handle() const noexcept;
331   331  
332   /** Cancel pending asynchronous operations. 332   /** Cancel pending asynchronous operations.
333   333  
334   Outstanding operations complete with a code that compares 334   Outstanding operations complete with a code that compares
335   equal to `capy::cond::canceled`. 335   equal to `capy::cond::canceled`.
336   */ 336   */
337   void cancel() noexcept; 337   void cancel() noexcept;
338   338  
339   /** Wait for readiness without transferring bytes. 339   /** Wait for readiness without transferring bytes.
340   340  
341   Never reads, writes or modifies the descriptor -- including 341   Never reads, writes or modifies the descriptor -- including
342   its flags -- which is what makes it safe on a descriptor 342   its flags -- which is what makes it safe on a descriptor
343   another library owns. 343   another library owns.
344   344  
345   @param w The direction to wait on. 345   @param w The direction to wait on.
346   346  
347   @return An awaitable yielding `capy::io_result<>`. Yields 347   @return An awaitable yielding `capy::io_result<>`. Yields
348   `errc::bad_file_descriptor` when not open. 348   `errc::bad_file_descriptor` when not open.
349   349  
350   @par Example 350   @par Example
351   @par !example wait 351   @par !example wait
352   352  
353   @see wait_type 353   @see wait_type
354   */ 354   */
HITCBC 355   35 [[nodiscard]] wait_awaitable wait(wait_type w) 355   35 [[nodiscard]] wait_awaitable wait(wait_type w)
356   { 356   {
HITCBC 357   35 return wait_awaitable(*this, w); 357   35 return wait_awaitable(*this, w);
358   } 358   }
359   359  
360   protected: 360   protected:
361   /// Default-construct (for derived types that initialize `io_object` directly). 361   /// Default-construct (for derived types that initialize `io_object` directly).
HITCBC 362   14 posix_stream_descriptor() noexcept = default; 362   14 posix_stream_descriptor() noexcept = default;
363   363  
364   /** Construct from a handle. 364   /** Construct from a handle.
365   365  
366   @param h The handle this object takes ownership of. 366   @param h The handle this object takes ownership of.
367   */ 367   */
368   explicit posix_stream_descriptor(handle h) noexcept 368   explicit posix_stream_descriptor(handle h) noexcept
369   : io_object(std::move(h)) 369   : io_object(std::move(h))
370   { 370   {
371   } 371   }
372   372  
373   private: 373   private:
374   /// Return the implementation downcast to this type's interface. 374   /// Return the implementation downcast to this type's interface.
HITCBC 375   419 implementation& get() const noexcept 375   419 implementation& get() const noexcept
376   { 376   {
HITCBC 377   419 return *static_cast<implementation*>(h_.get()); 377   419 return *static_cast<implementation*>(h_.get());
378   } 378   }
379   }; 379   };
380   380  
381   } // namespace boost::corosio 381   } // namespace boost::corosio
382   382  
383   #endif // BOOST_COROSIO_POSIX || BOOST_COROSIO_MRDOCS 383   #endif // BOOST_COROSIO_POSIX || BOOST_COROSIO_MRDOCS
384   384  
385   #endif 385   #endif