include/boost/corosio/posix_descriptor.hpp

100.0% Lines (11 / 11) 100.0% Functions (6 / 6)
posix_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_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 14x wait_awaitable(posix_descriptor& d, wait_type w) noexcept : d_(d), w_(w)
176 {
177 14x }
178
179 friend detail::void_op_base<wait_awaitable>;
180
181 posix_descriptor& d_;
182 wait_type w_;
183
184 std::coroutine_handle<>
185 14x dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
186 {
187 14x 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 76x bool is_open() const noexcept
302 {
303 76x 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 14x [[nodiscard]] wait_awaitable wait(wait_type w)
336 {
337 14x return wait_awaitable(*this, w);
338 }
339
340 protected:
341 /// Default-construct (for derived types that initialize `io_object` directly).
342 12x 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 160x implementation& get() const noexcept
353 {
354 160x 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
363