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.

Code snippets assume:

#include <boost/corosio/tcp_socket.hpp>
#include <boost/corosio/timeout.hpp>
#include <boost/corosio/wait_type.hpp>
#include <boost/capy/cond.hpp>
#include <boost/capy/task.hpp>
#include <iostream>

namespace corosio = boost::corosio;
namespace capy = boost::capy;
using namespace std::chrono_literals;

Overview

Three directions are exposed via the wait_type enum:

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.

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 and do their own I/O on an O_NONBLOCK socket. They need two things from the surrounding event loop: "tell me when the fd is ready" without stealing bytes from the stream, and "never touch my descriptor".

wait() provides the first: it never reads, writes, or consumes the socket’s pending error, so the library’s next PQconsumeInput (or equivalent) sees everything the kernel has delivered. Adoption provides the second, with one rule to follow: assign() takes ownership and will close the descriptor, so adopt a dup() of the library’s fd rather than the fd itself. Readiness lives on the open file description, which both descriptors share — the duplicate reports exactly the library’s readiness, and closing it can never close the library’s connection. Neither assign() nor wait() alters the descriptor’s flags, so the library’s non-blocking configuration is untouched.

// Adopt a duplicate: assigned means owned, and corosio closing
// the duplicate can never close the library's descriptor.
// Readiness travels through the shared open file description.
corosio::tcp_socket sock(ioc);
if (auto ec = sock.assign(::dup(foreign_socket(conn))))
    co_return ec;

// Read side: wake, then let the library take the bytes itself.
while (foreign_wants_read(conn)) {
    auto [ec] = co_await sock.wait(corosio::wait_type::read);
    if (ec) co_return ec;
    if (foreign_consume(conn) != 0)
        co_return std::make_error_code(std::errc::io_error);
}

// Write side: retry exactly when the socket can make progress.
while (foreign_flush(conn) == 1) {
    auto [ec] = co_await sock.wait(corosio::wait_type::write);
    if (ec) co_return ec;
}

Never call read_some() or write_some() on the adopted socket — the library owns the byte stream; corosio supplies readiness only.

On Windows, dup() does not duplicate a SOCKET. Either adopt the library’s socket directly and release() it before the library needs exclusive ownership again, or create a true duplicate with WSADuplicateSocketW and adopt that.

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:

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.

A connection already queued when the wait begins completes it immediately — including on an adopted listener whose backlog predates the adoption, the socket-activation handoff shape.

Cancellation

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

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

timeout() composes with wait() the same way it does with the other socket operations, since wait() yields an io_result like any other awaitable:

auto [ec] = co_await corosio::timeout(
    sock.wait(corosio::wait_type::read), 200ms);
if (ec == capy::cond::timeout)
    std::cout << "No readiness within 200ms\n";

wait_type::write Semantics

wait(wait_type::write) completes when the socket can accept a non-blocking write. On a socket that is not backpressured this is immediate; once the send buffer is full the wait parks until the peer drains enough of it for a write to make progress again.

That is the signal an external flush loop needs: code that owns its own buffers and retries "when the socket is writable" would busy-spin if the wait completed unconditionally, precisely when the socket is congested. Code that hands its buffers to write_some() does not need wait(wait_type::write) at all — write_some() already parks on the same condition.

Acceptors are the exception: writability has no meaning for a listening socket, so wait(wait_type::write) on an acceptor fails with errc::operation_not_supported on every backend.

Backend Notes

On Linux (epoll) and BSD/macOS (kqueue) a wait registers interest in the fd’s read, write, 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, whose write set includes fds with a parked write wait.

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, write-wait, 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.