include/boost/corosio/posix_stream_descriptor.hpp

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