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 <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 HIT 35 : wait_awaitable(posix_stream_descriptor& d, wait_type w) noexcept
196 70 : : d_(d)
197 35 : , w_(w)
198 : {
199 35 : }
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 35 : dispatch(capy::continuation& cont, capy::executor_ref ex) const
208 : {
209 35 : 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 474 : bool is_open() const noexcept
325 : {
326 474 : 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 35 : [[nodiscard]] wait_awaitable wait(wait_type w)
359 : {
360 35 : return wait_awaitable(*this, w);
361 : }
362 :
363 : protected:
364 : /// Default-construct (for derived types that initialize `io_object` directly).
365 14 : 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 759 : implementation& get() const noexcept
379 : {
380 759 : 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
|