LCOV - code coverage report
Current view: top level - corosio - posix_stream_descriptor.hpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 100.0 % 13 13
Test Date: 2026-10-08 17:58:26 Functions: 100.0 % 6 6

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

Generated by: LCOV version 2.3