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. Such libraries need only
notification that data is available, or that the descriptor is writable.
|
Code snippets assume:
|
Overview
Three directions are exposed via the wait_type enum.
The awaitable yields an error_code with no bytes_transferred. On
success the socket is ready; the wait consumes no data 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. First,
"tell me when the fd is ready" without stealing bytes from the stream.
Second, "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
closes 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 therefore 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.
This applies to sockets. A library that owns a non-socket descriptor —
a pipe, a tty, inotify, eventfd — needs the same readiness
notification.
posix_descriptor provides it
with the same wait() on the same terms.
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() succeeds 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.
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. That would happen 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 reactor runs on a dedicated thread and bridges into the
IOCP via PostQueuedCompletionStatus. The public API is
uniform across platforms.