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
89 changes: 52 additions & 37 deletions doc/modules/ROOT/pages/4.guide/4r.wait.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,35 @@ include::example$snippets/4r_wait.cpp[tag=wait_read,indent=0]
== 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.
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.

The typical pattern (sketched against a hypothetical libpq
integration):

[source,cpp,role=pseudocode]
[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);
}
include::example$snippets/4r_wait.cpp[tag=foreign_adopt,indent=0]
----

Because `wait()` does not call `recv()`, the C library's next
`PQconsumeInput` (or equivalent) sees all the data the kernel has
delivered.
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

Expand All @@ -85,6 +91,10 @@ 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
Expand All @@ -107,32 +117,37 @@ include::example$snippets/4r_wait.cpp[tag=wait_timeout,indent=0]

== `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."
`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.

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.
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) 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 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, 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.
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.

== See Also

Expand Down
15 changes: 15 additions & 0 deletions include/boost/corosio/detail/local_stream_acceptor_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ class BOOST_COROSIO_DECL local_stream_acceptor_service
int type,
int protocol) = 0;

/** Adopt an existing listening socket.

Validates @p fd, closes any socket the implementation already
holds, and registers the adopted descriptor with the backend.
Listen state is not verified.

@param impl The acceptor implementation to assign to.
@param fd The native socket to adopt. Ownership transfers only
on success.
@return Error code on failure, empty on success.
*/
virtual std::error_code assign_socket(
local_stream_acceptor::implementation& impl,
native_handle_type fd) = 0;

/** Bind an open acceptor to a local endpoint.

@pre @p impl was opened via open_acceptor_socket().
Expand Down
15 changes: 15 additions & 0 deletions include/boost/corosio/detail/tcp_acceptor_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ class BOOST_COROSIO_DECL tcp_acceptor_service
int type,
int protocol) = 0;

/** Adopt an existing listening socket.

Validates @p fd, closes any socket the implementation already
holds, and registers the adopted descriptor with the backend.
Listen state is not verified.

@param impl The acceptor implementation to assign to.
@param fd The native socket to adopt. Ownership transfers only
on success.
@return Error code on failure, empty on success.
*/
virtual std::error_code assign_socket(
tcp_acceptor::implementation& impl,
native_handle_type fd) = 0;

/** Bind an open acceptor to a local endpoint.

@param impl The acceptor implementation to bind.
Expand Down
16 changes: 16 additions & 0 deletions include/boost/corosio/detail/tcp_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ class BOOST_COROSIO_DECL tcp_service
int type,
int protocol) = 0;

/** Assign an existing native socket handle to a socket.

Adopts a pre-created socket handle. On success the impl
takes ownership and will close the handle. On failure the
caller retains ownership and must close it. If the impl is
already open, its pending operations are cancelled and the
held socket is closed before the new one is adopted.

@param impl The socket implementation to assign to.
@param fd The native socket handle to adopt.
@return Error code on failure, empty on success.
*/
virtual std::error_code assign_socket(
tcp_socket::implementation& impl,
native_handle_type fd) = 0;

/** Bind a stream socket to a local endpoint.

@param impl The socket implementation to bind.
Expand Down
16 changes: 16 additions & 0 deletions include/boost/corosio/detail/udp_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ class BOOST_COROSIO_DECL udp_service
int type,
int protocol) = 0;

/** Assign an existing native socket handle to a socket.

Adopts a pre-created socket handle. On success the impl
takes ownership and will close the handle. On failure the
caller retains ownership and must close it. If the impl is
already open, its pending operations are cancelled and the
held socket is closed before the new one is adopted.

@param impl The socket implementation to assign to.
@param fd The native socket handle to adopt.
@return Error code on failure, empty on success.
*/
virtual std::error_code assign_socket(
udp_socket::implementation& impl,
native_handle_type fd) = 0;

/** Bind a datagram socket to a local endpoint.

@param impl The socket implementation to bind.
Expand Down
33 changes: 25 additions & 8 deletions include/boost/corosio/local_datagram_socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,14 +831,31 @@ class BOOST_COROSIO_DECL local_datagram_socket : public io_object
return opt;
}

/** Assign an existing file descriptor to this socket.

The socket must not already be open. The fd is adopted
and registered with the platform reactor.

@param fd The file descriptor to adopt.

@throws std::system_error on failure.
/** Assign an existing native socket to this object.

Adopts a Unix domain datagram socket created outside the
library — from `socketpair()`, received over `SCM_RIGHTS`,
or made natively — and registers it with the backend. The
socket must be a datagram socket in the `AF_UNIX` family.
Adoption never alters the descriptor's flags or options; the
fd must already be non-blocking.

If this object is already open, pending operations complete
with `errc::operation_canceled` and the held socket is
closed before the new one is adopted.

@par Exception Safety
Strong guarantee on validation failure: the object is
unchanged. If backend registration fails, the object either
retains its previous socket or is left closed, depending on
the backend. In all failure cases the caller retains
ownership of `fd`.

@param fd The native socket to adopt. On success the object
owns it and will close it.

@throws std::system_error On validation or registration
failure.
*/
void assign(native_handle_type fd);

Expand Down
54 changes: 53 additions & 1 deletion include/boost/corosio/local_stream_acceptor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,14 @@ class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
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.
without blocking; a connection already queued when the
wait begins completes it immediately. No connection is
consumed.

@note `wait_type::write` is not usable on an acceptor:
writability carries no meaning for a listening socket, so
the wait fails with `errc::operation_not_supported` on
every backend.

@param w The wait direction.

Expand Down Expand Up @@ -384,6 +391,48 @@ class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
*/
native_handle_type release();

/** Get the native socket handle.

@return The native socket handle, or -1/INVALID_SOCKET if not
open.

@par Preconditions
None. May be called on closed acceptors.
*/
native_handle_type native_handle() const noexcept;

/** Assign an existing native socket to this acceptor.

Adopts a listening socket created outside the library —
received from a service manager, inherited, or made natively —
and registers it with the backend. The socket must be a
listening stream socket in the local IPC family. Adoption
never alters the descriptor's flags or options: on POSIX the
fd must already be non-blocking, and on Windows the socket
must be overlapped-capable.

Adoption does not verify listen state; @ref accept reports the
error if the socket is not listening.

If this object is already open, pending operations complete
with `errc::operation_canceled` and the held socket is closed
before the new one is adopted.

@par Exception Safety
Strong guarantee on validation failure: the object is
unchanged. If backend registration fails, the object either
retains its previous socket or is left closed, depending on
the backend. In all failure cases the caller retains
ownership of `fd`.

@param fd The native socket to adopt. On success the object
owns it and will close it.

@throws std::system_error On validation or registration
failure.
*/
void assign(native_handle_type fd);

/** Return the local endpoint the acceptor is bound to.

Returns a default-constructed (empty) endpoint if the
Expand Down Expand Up @@ -490,6 +539,9 @@ class BOOST_COROSIO_DECL local_stream_acceptor : public io_object
/// Return whether the underlying socket is open.
virtual bool is_open() const noexcept = 0;

/// Return the native handle, or the platform sentinel if closed.
virtual native_handle_type native_handle() const noexcept = 0;

/// Release and return the native handle without closing.
virtual native_handle_type release_socket() noexcept = 0;

Expand Down
36 changes: 26 additions & 10 deletions include/boost/corosio/local_stream_socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,16 +466,32 @@ class BOOST_COROSIO_DECL local_stream_socket : public io_stream
return opt;
}

/** Assign an existing file descriptor to this socket.

The socket must not already be open. The fd is adopted
and registered with the platform reactor. Used by
connect_pair() to wrap socketpair() fds.

@param fd The file descriptor to adopt. Must be a valid,
open, non-blocking Unix stream socket.

@throws std::system_error on failure.
/** Assign an existing native socket to this object.

Adopts a Unix domain stream socket created outside the
library — from `socketpair()`, received over `SCM_RIGHTS`,
or made natively — and registers it with the backend. The
socket must be a stream socket in the `AF_UNIX` family.
Adoption never alters the descriptor's flags or options: on
POSIX the fd must already be non-blocking, and on Windows
the socket must be overlapped-capable.

If this object is already open, pending operations complete
with `errc::operation_canceled` and the held socket is
closed before the new one is adopted.

@par Exception Safety
Strong guarantee on validation failure: the object is
unchanged. If backend registration fails, the object either
retains its previous socket or is left closed, depending on
the backend. In all failure cases the caller retains
ownership of `fd`.

@param fd The native socket to adopt. On success the object
owns it and will close it.

@throws std::system_error On validation or registration
failure.
*/
void assign(native_handle_type fd);

Expand Down
Loading
Loading