100.00% Lines (13/13) 100.00% Functions (6/6)
TLA Baseline Branch
Line Hits Code Line Hits 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 +
HITGNC   192 + 35 wait_awaitable(posix_stream_descriptor& d, wait_type w) noexcept
HITGNC   193 + 70 : d_(d)
HITGNC   194 + 35 , w_(w)
  195 + {
HITGNC   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<>
HITGNC   204 + 35 dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
  205 + {
HITGNC   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 + */
HITGNC   321 + 252 bool is_open() const noexcept
  322 + {
HITGNC   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 + */
HITGNC   355 + 35 [[nodiscard]] wait_awaitable wait(wait_type w)
  356 + {
HITGNC   357 + 35 return wait_awaitable(*this, w);
  358 + }
  359 +
  360 + protected:
  361 + /// Default-construct (for derived types that initialize `io_object` directly).
HITGNC   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.
HITGNC   375 + 419 implementation& get() const noexcept
  376 + {
HITGNC   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