LCOV - code coverage report
Current view: top level - corosio - posix_descriptor.hpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 100.0 % 11 11
Test Date: 2026-09-28 20:06:38 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_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
        

Generated by: LCOV version 2.3