100.00% Lines (11/11) 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_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 +
HITGNC   175 + 14 wait_awaitable(posix_descriptor& d, wait_type w) noexcept : d_(d), w_(w)
  176 + {
HITGNC   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<>
HITGNC   185 + 14 dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
  186 + {
HITGNC   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 + */
HITGNC   301 + 76 bool is_open() const noexcept
  302 + {
HITGNC   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 + */
HITGNC   335 + 14 [[nodiscard]] wait_awaitable wait(wait_type w)
  336 + {
HITGNC   337 + 14 return wait_awaitable(*this, w);
  338 + }
  339 +
  340 + protected:
  341 + /// Default-construct (for derived types that initialize `io_object` directly).
HITGNC   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.
HITGNC   352 + 160 implementation& get() const noexcept
  353 + {
HITGNC   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