Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
** xref:4.guide/4o.file-io.adoc[File I/O]
** xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets]
** xref:4.guide/4q.udp.adoc[UDP Sockets]
** xref:4.guide/4r.wait.adoc[Readiness Wait]
* xref:5.testing/5.intro.adoc[Testing]
** xref:5.testing/5a.mocket.adoc[Mock Sockets]
** xref:5.testing/5b.socket-pair.adoc[Socket Pairs]
Expand Down
148 changes: 148 additions & 0 deletions doc/modules/ROOT/pages/4.guide/4r.wait.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/corosio
//

= Readiness Wait

The `wait()` method on every socket and acceptor suspends until the
underlying file descriptor becomes ready in a chosen direction, without
transferring any bytes. Use it to integrate with C libraries that own
the I/O on a nonblocking file descriptor and only need notification
that data is available or that the descriptor is writable.

[NOTE]
====
Code snippets assume:
[source,cpp]
----
#include <boost/corosio/tcp_socket.hpp>
#include <boost/corosio/wait_type.hpp>

namespace corosio = boost::corosio;
----
====

== Overview

Three directions are exposed via the `wait_type` enum:

[source,cpp]
----
enum class wait_type { read, write, error };
----

The awaitable yields an `error_code` with no `bytes_transferred`. On
success the socket is observed to be ready; no data has been consumed
from it.

[source,cpp]
----
auto [ec] = co_await sock.wait(corosio::wait_type::read);
if (!ec) {
// sock is readable: a subsequent read_some will return data
// without blocking.
}
----

== Wrapping a Nonblocking C API

The original motivation is libraries such as libssh and libpq that
manage their own buffers on an `O_NONBLOCK` fd. They need a "tell me
when the fd is ready" primitive that does not steal bytes from the
stream.

The typical pattern:

[source,cpp]
----
// pq is some PG connection holding a nonblocking socket fd.
corosio::tcp_socket sock = adopt_fd(ioc, PQsocket(pq));

while (PQisBusy(pq)) {
auto [ec] = co_await sock.wait(corosio::wait_type::read);
if (ec) co_return ec;
if (PQconsumeInput(pq) == 0)
co_return last_pq_error(pq);
}
----

Because `wait()` does not call `recv()`, the C library's next
`PQconsumeInput` (or equivalent) sees all the data the kernel has
delivered.

== Acceptors

`tcp_acceptor` and `local_stream_acceptor` expose the same `wait()`.
For `wait_type::read`, completion signals that a connection is pending
on the listen socket. A subsequent `accept()` will succeed without
blocking:

[source,cpp]
----
auto [wec] = co_await acceptor.wait(corosio::wait_type::read);
if (wec) co_return;

corosio::tcp_socket peer(ioc);
auto [aec] = co_await acceptor.accept(peer);
----

This is useful when application-level conditions must be checked
before consuming the next connection (rate limiting, backpressure
signaling) without holding an `accept()` call open.

== Cancellation

`wait()` honors the stop token of its `co_await` environment and the
`socket.cancel()` / `acceptor.cancel()` non-virtuals, completing with
`capy::cond::canceled`:

[source,cpp]
----
auto waiter = [&]() -> capy::task<> {
auto [ec] = co_await sock.wait(corosio::wait_type::read);
// ec == capy::cond::canceled if sock.cancel() was invoked
};
----

`cancel_after()` composes with `wait()` the same way it does with the
other socket operations.

== `wait_type::write` Semantics

`wait(wait_type::write)` always completes immediately with success on
a connected socket. This matches asio's behavior on the IOCP backend
and gives a consistent contract across all corosio backends. The
intended use is: "I want to know I can write now," not "I want to
park until the send buffer drains after backpressure."

Backpressure on the send path is already surfaced by `write_some()`
returning fewer bytes than requested (or `EAGAIN`-equivalent
behavior); use that signal rather than `wait(wait_type::write)` to
react to a full send buffer.

== Backend Notes

On Linux (epoll) and BSD/macOS (kqueue) the read and error waits
register interest in the fd's read or error event without performing
any I/O syscall. On the select backend the same registration
semantics apply through the select-loop's fd sets. Write waits
short-circuit and never enter the reactor (see above).

On Windows (IOCP), stream-socket `wait_read` uses a zero-byte
`WSARecv`: the kernel signals completion when data is available
without consuming bytes. All other waits (datagram-read,
acceptor-read, error-wait) route through an auxiliary `WSAPoll`-based
reactor that runs on a dedicated thread and bridges into the IOCP
via `PostQueuedCompletionStatus`. The public API is uniform across
platforms.

== See Also

* xref:4d.sockets.adoc[Sockets]
* xref:4e.tcp-acceptor.adoc[Acceptors]
* xref:4q.udp.adoc[UDP Sockets]
58 changes: 58 additions & 0 deletions include/boost/corosio/local_datagram_socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <boost/corosio/local_datagram.hpp>
#include <boost/corosio/message_flags.hpp>
#include <boost/corosio/shutdown_type.hpp>
#include <boost/corosio/wait_type.hpp>
#include <boost/capy/ex/executor_ref.hpp>
#include <boost/capy/ex/execution_context.hpp>
#include <boost/capy/ex/io_env.hpp>
Expand Down Expand Up @@ -204,6 +205,27 @@ class BOOST_COROSIO_DECL local_datagram_socket : public io_object
std::error_code* ec,
std::size_t* bytes_out) = 0;

/** Initiate an asynchronous wait for socket readiness.

Completes when the socket becomes ready for the
specified direction, or an error condition is
reported. No bytes are transferred.

@param h Coroutine handle to resume on completion.
@param ex Executor for dispatching the completion.
@param w The direction to wait on.
@param token Stop token for cancellation.
@param ec Output error code.

@return Coroutine handle to resume immediately.
*/
virtual std::coroutine_handle<> wait(
std::coroutine_handle<> h,
capy::executor_ref ex,
wait_type w,
std::stop_token token,
std::error_code* ec) = 0;

/// Shut down part or all of the socket.
virtual std::error_code shutdown(shutdown_type what) noexcept = 0;

Expand Down Expand Up @@ -346,6 +368,23 @@ class BOOST_COROSIO_DECL local_datagram_socket : public io_object
}
};

/// Represent the awaitable returned by @ref wait.
struct wait_awaitable
: detail::void_op_base<wait_awaitable>
{
local_datagram_socket& s_;
wait_type w_;

wait_awaitable(local_datagram_socket& s, wait_type w) noexcept
: s_(s), w_(w) {}

std::coroutine_handle<> dispatch(
std::coroutine_handle<> h, capy::executor_ref ex) const
{
return s_.get().wait(h, ex, w_, token_, &ec_);
}
};

/** Represent the awaitable returned by @ref send.

Captures the buffer, then dispatches to the backend
Expand Down Expand Up @@ -528,6 +567,25 @@ class BOOST_COROSIO_DECL local_datagram_socket : public io_object
return connect_awaitable(*this, ep);
}

/** Wait for the socket to become ready in a given direction.

Suspends until the socket is ready for the requested
direction, or an error condition is reported. No bytes
are transferred.

@param w The wait direction (read, write, or error).

@return An awaitable that completes with `io_result<>`.

@par Preconditions
The socket must be open. This socket must outlive the
returned awaitable.
*/
[[nodiscard]] auto wait(wait_type w)
{
return wait_awaitable(*this, w);
}

/** Send a datagram to the specified destination.

Completes when the entire datagram has been accepted
Expand Down
51 changes: 51 additions & 0 deletions include/boost/corosio/local_stream_acceptor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

#include <boost/corosio/detail/config.hpp>
#include <boost/corosio/detail/except.hpp>
#include <boost/corosio/detail/op_base.hpp>
#include <boost/corosio/wait_type.hpp>
#include <boost/corosio/io/io_object.hpp>
#include <boost/capy/io_result.hpp>
#include <boost/corosio/local_endpoint.hpp>
Expand Down Expand Up @@ -75,6 +77,22 @@ enum class bind_option
*/
class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
{
struct wait_awaitable
: detail::void_op_base<wait_awaitable>
{
local_stream_acceptor& acc_;
wait_type w_;

wait_awaitable(local_stream_acceptor& acc, wait_type w) noexcept
: acc_(acc), w_(w) {}

std::coroutine_handle<> dispatch(
std::coroutine_handle<> h, capy::executor_ref ex) const
{
return acc_.get().wait(h, ex, w_, token_, &ec_);
}
};

struct move_accept_awaitable
{
local_stream_acceptor& acc_;
Expand Down Expand Up @@ -301,6 +319,27 @@ class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
return accept_awaitable(*this, peer);
}

/** Wait for an incoming connection or readiness condition.

Suspends until the listen socket is ready in the
requested direction. For `wait_type::read`, completion
signals that a subsequent @ref accept will succeed
without blocking. No connection is consumed.

@param w The wait direction.

@return An awaitable that completes with `io_result<>`.

@par Preconditions
The acceptor must be listening.
*/
[[nodiscard]] auto wait(wait_type w)
{
if (!is_open())
detail::throw_logic_error("wait: acceptor not listening");
return wait_awaitable(*this, w);
}

/** Initiate an asynchronous accept, returning the socket.

Completes when a new connection is available. Only one
Expand Down Expand Up @@ -433,6 +472,18 @@ class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
std::error_code*,
io_object::implementation**) = 0;

/** Initiate an asynchronous wait for acceptor readiness.

Completes when the listen socket becomes ready for
the specified direction. No connection is consumed.
*/
virtual std::coroutine_handle<> wait(
std::coroutine_handle<> h,
capy::executor_ref ex,
wait_type w,
std::stop_token token,
std::error_code* ec) = 0;

/// Return the cached local endpoint.
virtual corosio::local_endpoint local_endpoint() const noexcept = 0;

Expand Down
Loading
Loading