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