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