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:
|
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.