include/boost/corosio/posix_stream_descriptor.hpp

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