TLA Line data 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 HIT 35 : wait_awaitable(posix_stream_descriptor& d, wait_type w) noexcept
193 70 : : d_(d)
194 35 : , w_(w)
195 : {
196 35 : }
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 35 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
205 : {
206 35 : 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 252 : bool is_open() const noexcept
322 : {
323 252 : 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 35 : [[nodiscard]] wait_awaitable wait(wait_type w)
356 : {
357 35 : return wait_awaitable(*this, w);
358 : }
359 :
360 : protected:
361 : /// Default-construct (for derived types that initialize `io_object` directly).
362 14 : 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 419 : implementation& get() const noexcept
376 : {
377 419 : 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
|