From 64cbfc78859b0714a8ba3a125c4e96dc6959fad6 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Thu, 9 Apr 2026 12:12:42 -0600 Subject: [PATCH] Add Unix domain socket support and collapse reactor backends Add local_stream_socket, local_stream_acceptor, local_datagram_socket, local_endpoint, and local_socket_pair for Unix domain socket I/O. The public API mirrors TCP/UDP with protocol-appropriate differences (path-based endpoints, socketpair, release_socket for fd passing). Replace 42 per-backend reactor files (14 each for epoll, kqueue, select) with parameterized templates instantiated from per-backend traits structs. Each backend provides a traits type capturing its platform-specific socket creation, write policy, and accept policy. The reactor_types template in reactor_backend.hpp generates all concrete socket, service, acceptor, and op types. Virtual method overrides move into the CRTP bases, and select's notify_reactor() moves into register_op() via a compile-time trait, eliminating all select-specific method overrides. New files: - {epoll,kqueue,select}_traits.hpp: per-backend policies - reactor_{stream,datagram}_ops.hpp: parameterized op types - reactor_socket_finals.hpp: final socket/acceptor types - reactor_service_finals.hpp: per-protocol service types - reactor_backend.hpp: accept impl and reactor_types bundle --- doc/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/4.guide/4a.tcp-networking.adoc | 14 +- .../ROOT/pages/4.guide/4p.unix-sockets.adoc | 267 +++++++ doc/modules/ROOT/pages/glossary.adoc | 12 + doc/modules/ROOT/pages/index.adoc | 8 +- include/boost/corosio.hpp | 10 + include/boost/corosio/backend.hpp | 126 ++- include/boost/corosio/detail/config.hpp | 21 + .../corosio/detail/local_datagram_service.hpp | 82 ++ .../detail/local_stream_acceptor_service.hpp | 76 ++ .../corosio/detail/local_stream_service.hpp | 71 ++ include/boost/corosio/detail/op_base.hpp | 111 +++ .../corosio/detail/tcp_acceptor_service.hpp | 6 +- include/boost/corosio/detail/tcp_service.hpp | 6 +- include/boost/corosio/detail/udp_service.hpp | 6 +- include/boost/corosio/io/io_object.hpp | 13 +- include/boost/corosio/io/io_read_stream.hpp | 34 +- include/boost/corosio/io/io_write_stream.hpp | 34 +- include/boost/corosio/local_datagram.hpp | 49 ++ .../boost/corosio/local_datagram_socket.hpp | 744 ++++++++++++++++++ include/boost/corosio/local_endpoint.hpp | 127 +++ include/boost/corosio/local_socket_pair.hpp | 60 ++ include/boost/corosio/local_stream.hpp | 58 ++ .../boost/corosio/local_stream_acceptor.hpp | 401 ++++++++++ include/boost/corosio/local_stream_socket.hpp | 339 ++++++++ include/boost/corosio/message_flags.hpp | 61 ++ .../native/detail/endpoint_convert.hpp | 137 ++++ .../corosio/native/detail/epoll/epoll_op.hpp | 113 --- .../detail/epoll/epoll_tcp_acceptor.hpp | 54 -- .../epoll/epoll_tcp_acceptor_service.hpp | 364 --------- .../native/detail/epoll/epoll_tcp_service.hpp | 250 ------ .../native/detail/epoll/epoll_tcp_socket.hpp | 72 -- .../native/detail/epoll/epoll_traits.hpp | 146 ++++ .../native/detail/epoll/epoll_udp_service.hpp | 272 ------- .../native/detail/epoll/epoll_udp_socket.hpp | 136 ---- .../native/detail/iocp/win_udp_service.hpp | 37 +- .../native/detail/iocp/win_udp_socket.hpp | 8 + .../native/detail/kqueue/kqueue_op.hpp | 154 ---- .../detail/kqueue/kqueue_tcp_acceptor.hpp | 75 -- .../kqueue/kqueue_tcp_acceptor_service.hpp | 417 ---------- .../detail/kqueue/kqueue_tcp_service.hpp | 357 --------- .../detail/kqueue/kqueue_tcp_socket.hpp | 82 -- .../native/detail/kqueue/kqueue_traits.hpp | 250 ++++++ .../detail/kqueue/kqueue_udp_service.hpp | 299 ------- .../detail/kqueue/kqueue_udp_socket.hpp | 136 ---- .../detail/reactor/reactor_acceptor.hpp | 142 +++- .../reactor/reactor_acceptor_service.hpp | 138 ++++ .../native/detail/reactor/reactor_backend.hpp | 185 +++++ .../detail/reactor/reactor_basic_socket.hpp | 150 +++- .../detail/reactor/reactor_datagram_ops.hpp | 117 +++ .../reactor/reactor_datagram_socket.hpp | 252 +++++- .../native/detail/reactor/reactor_op.hpp | 63 +- .../detail/reactor/reactor_op_complete.hpp | 60 +- .../detail/reactor/reactor_scheduler.hpp | 3 + .../detail/reactor/reactor_service_finals.hpp | 396 ++++++++++ .../detail/reactor/reactor_socket_finals.hpp | 350 ++++++++ .../detail/reactor/reactor_socket_service.hpp | 7 + .../detail/reactor/reactor_stream_ops.hpp | 111 +++ .../detail/reactor/reactor_stream_socket.hpp | 139 +++- .../native/detail/select/select_op.hpp | 161 ---- .../native/detail/select/select_scheduler.hpp | 3 + .../detail/select/select_tcp_acceptor.hpp | 54 -- .../select/select_tcp_acceptor_service.hpp | 437 ---------- .../detail/select/select_tcp_service.hpp | 251 ------ .../detail/select/select_tcp_socket.hpp | 72 -- .../native/detail/select/select_traits.hpp | 237 ++++++ .../detail/select/select_udp_service.hpp | 315 -------- .../detail/select/select_udp_socket.hpp | 136 ---- .../corosio/native/native_tcp_acceptor.hpp | 12 - .../corosio/native/native_tcp_socket.hpp | 12 - .../corosio/native/native_udp_socket.hpp | 20 +- include/boost/corosio/shutdown_type.hpp | 36 + include/boost/corosio/socket_option.hpp | 2 +- include/boost/corosio/tcp.hpp | 6 +- include/boost/corosio/tcp_socket.hpp | 51 +- include/boost/corosio/udp.hpp | 6 +- include/boost/corosio/udp_socket.hpp | 272 +++---- perf/bench/CMakeLists.txt | 10 +- perf/bench/asio/callback/benchmarks.hpp | 6 + .../callback/unix_socket_latency_bench.cpp | 238 ++++++ .../callback/unix_socket_throughput_bench.cpp | 190 +++++ perf/bench/asio/coroutine/benchmarks.hpp | 7 + .../coroutine/unix_socket_latency_bench.cpp | 187 +++++ .../unix_socket_throughput_bench.cpp | 228 ++++++ perf/bench/asio/unix_socket_utils.hpp | 39 + perf/bench/corosio/benchmarks.hpp | 15 + .../corosio/unix_socket_latency_bench.cpp | 268 +++++++ .../corosio/unix_socket_throughput_bench.cpp | 352 +++++++++ perf/bench/main.cpp | 8 + perf/common/native_includes.hpp | 6 + src/corosio/src/io_context.cpp | 49 +- src/corosio/src/local_datagram.cpp | 46 ++ src/corosio/src/local_datagram_socket.cpp | 155 ++++ src/corosio/src/local_endpoint.cpp | 61 ++ src/corosio/src/local_socket_pair.cpp | 134 ++++ src/corosio/src/local_stream.cpp | 46 ++ src/corosio/src/local_stream_acceptor.cpp | 131 +++ src/corosio/src/local_stream_socket.cpp | 144 ++++ src/corosio/src/tcp_socket.cpp | 8 + test/unit/local_datagram_socket.cpp | 608 ++++++++++++++ test/unit/local_stream_socket.cpp | 437 ++++++++++ test/unit/tcp_socket.cpp | 22 +- test/unit/udp_socket.cpp | 87 ++ 103 files changed, 9048 insertions(+), 4726 deletions(-) create mode 100644 doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc create mode 100644 include/boost/corosio/detail/local_datagram_service.hpp create mode 100644 include/boost/corosio/detail/local_stream_acceptor_service.hpp create mode 100644 include/boost/corosio/detail/local_stream_service.hpp create mode 100644 include/boost/corosio/detail/op_base.hpp create mode 100644 include/boost/corosio/local_datagram.hpp create mode 100644 include/boost/corosio/local_datagram_socket.hpp create mode 100644 include/boost/corosio/local_endpoint.hpp create mode 100644 include/boost/corosio/local_socket_pair.hpp create mode 100644 include/boost/corosio/local_stream.hpp create mode 100644 include/boost/corosio/local_stream_acceptor.hpp create mode 100644 include/boost/corosio/local_stream_socket.hpp create mode 100644 include/boost/corosio/message_flags.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor_service.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_tcp_socket.hpp create mode 100644 include/boost/corosio/native/detail/epoll/epoll_traits.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_udp_service.hpp delete mode 100644 include/boost/corosio/native/detail/epoll/epoll_udp_socket.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor_service.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_tcp_socket.hpp create mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_traits.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_udp_service.hpp delete mode 100644 include/boost/corosio/native/detail/kqueue/kqueue_udp_socket.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_acceptor_service.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_backend.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_datagram_ops.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_service_finals.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_socket_finals.hpp create mode 100644 include/boost/corosio/native/detail/reactor/reactor_stream_ops.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_tcp_acceptor.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_tcp_acceptor_service.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_tcp_service.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_tcp_socket.hpp create mode 100644 include/boost/corosio/native/detail/select/select_traits.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_udp_service.hpp delete mode 100644 include/boost/corosio/native/detail/select/select_udp_socket.hpp create mode 100644 include/boost/corosio/shutdown_type.hpp create mode 100644 perf/bench/asio/callback/unix_socket_latency_bench.cpp create mode 100644 perf/bench/asio/callback/unix_socket_throughput_bench.cpp create mode 100644 perf/bench/asio/coroutine/unix_socket_latency_bench.cpp create mode 100644 perf/bench/asio/coroutine/unix_socket_throughput_bench.cpp create mode 100644 perf/bench/asio/unix_socket_utils.hpp create mode 100644 perf/bench/corosio/unix_socket_latency_bench.cpp create mode 100644 perf/bench/corosio/unix_socket_throughput_bench.cpp create mode 100644 src/corosio/src/local_datagram.cpp create mode 100644 src/corosio/src/local_datagram_socket.cpp create mode 100644 src/corosio/src/local_endpoint.cpp create mode 100644 src/corosio/src/local_socket_pair.cpp create mode 100644 src/corosio/src/local_stream.cpp create mode 100644 src/corosio/src/local_stream_acceptor.cpp create mode 100644 src/corosio/src/local_stream_socket.cpp create mode 100644 test/unit/local_datagram_socket.cpp create mode 100644 test/unit/local_stream_socket.cpp diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index a543e96d4..f04b3df5c 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -36,6 +36,7 @@ ** xref:4.guide/4m.error-handling.adoc[Error Handling] ** xref:4.guide/4n.buffers.adoc[Buffer Sequences] ** xref:4.guide/4o.file-io.adoc[File I/O] +** xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets] * xref:5.testing/5.intro.adoc[Testing] ** xref:5.testing/5a.mocket.adoc[Mock Sockets] * xref:benchmark-report.adoc[Benchmarks] diff --git a/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc index 4696ecb21..cdeea66c6 100644 --- a/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc +++ b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc @@ -473,7 +473,9 @@ UDP includes a checksum covering the header and data. The receiver verifies the checksum and discards corrupted packets. Unlike TCP, UDP doesn't retransmit—the data is simply lost. -Corosio currently supports only TCP. UDP may be added in future versions. +Corosio supports TCP, UDP, and Unix domain sockets. See +xref:4p.unix-sockets.adoc[Unix Domain Sockets] for local inter-process +communication without the TCP/IP stack overhead. == Ports and Sockets @@ -699,10 +701,14 @@ listening socket. Corosio wraps the complexity of TCP programming in a coroutine-friendly API: -* **socket** — Connect to servers, send and receive data -* **tcp_acceptor** — Listen for and accept incoming connections +* **tcp_socket** — Connect to servers, send and receive data over TCP +* **udp_socket** — Send and receive datagrams over UDP +* **tcp_acceptor** — Listen for and accept incoming TCP connections +* **local_stream_socket** — Stream-oriented Unix domain sockets for local IPC +* **local_datagram_socket** — Datagram-oriented Unix domain sockets for local IPC * **resolver** — Translate hostnames to IP addresses -* **endpoint** — Represent addresses and ports +* **endpoint** — Represent IP addresses and ports +* **local_endpoint** — Represent Unix socket paths All operations are asynchronous and return awaitables. You don't manage raw socket handles or deal with platform-specific APIs directly. diff --git a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc new file mode 100644 index 000000000..14010fe13 --- /dev/null +++ b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc @@ -0,0 +1,267 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + += Unix Domain Sockets + +Unix domain sockets provide inter-process communication (IPC) on the same +machine without going through the TCP/IP network stack. They use filesystem +paths instead of IP addresses and ports, offering lower latency and higher +throughput than loopback TCP. + +[NOTE] +==== +Code snippets assume: +[source,cpp] +---- +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +---- +==== + +== When to Use Unix Sockets + +Use Unix domain sockets instead of TCP when: + +* Both endpoints are on the same machine +* You need lower latency (no TCP/IP stack overhead) +* You need higher throughput for local communication +* You want filesystem-based access control (file permissions on the socket path) + +Common use cases include database connections (PostgreSQL, MySQL, Redis), +container networking, and microservice communication on a single host. + +== Socket Types + +Corosio provides two Unix socket types, mirroring the TCP/UDP split: + +[cols="1,1,2"] +|=== +| Class | Protocol | Description + +| `local_stream_socket` +| `SOCK_STREAM` +| Reliable, ordered byte stream (like TCP). Supports connect/accept. + +| `local_datagram_socket` +| `SOCK_DGRAM` +| Message-oriented datagrams (like UDP). Preserves message boundaries. +|=== + +== Stream Sockets + +Stream sockets work like TCP: a server binds and listens on a path, clients +connect, and both sides read and write byte streams. + +=== Server (Acceptor) + +[source,cpp] +---- +capy::task<> server(corosio::io_context& ioc) +{ + corosio::local_stream_acceptor acc(ioc); + acc.open(); + + auto ec = acc.bind(corosio::local_endpoint("/tmp/my_app.sock")); + if (ec) co_return; + + ec = acc.listen(); + if (ec) co_return; + + corosio::local_stream_socket peer(ioc); + auto [accept_ec] = co_await acc.accept(peer); + if (accept_ec) co_return; + + // peer is now connected — read and write as with tcp_socket + char buf[1024]; + auto [read_ec, n] = co_await peer.read_some( + capy::mutable_buffer(buf, sizeof(buf))); +} +---- + +The acceptor does **not** automatically remove the socket file on close. +You must `unlink()` the path before binding (if it exists) and after you +are done: + +[source,cpp] +---- +::unlink("/tmp/my_app.sock"); // remove stale socket +acc.bind(corosio::local_endpoint("/tmp/my_app.sock")); +---- + +=== Client + +[source,cpp] +---- +capy::task<> client(corosio::io_context& ioc) +{ + corosio::local_stream_socket s(ioc); + + // connect() opens the socket automatically + auto [ec] = co_await s.connect( + corosio::local_endpoint("/tmp/my_app.sock")); + if (ec) co_return; + + char const msg[] = "hello"; + auto [wec, n] = co_await s.write_some( + capy::const_buffer(msg, sizeof(msg))); +} +---- + +=== Socket Pairs + +For bidirectional IPC between a parent and child (or two coroutines), +use `make_local_stream_pair()` which calls the `socketpair()` system call: + +[source,cpp] +---- +auto [s1, s2] = corosio::make_local_stream_pair(ioc); + +// Data written to s1 can be read from s2, and vice versa. +co_await s1.write_some(capy::const_buffer("ping", 4)); + +char buf[16]; +auto [ec, n] = co_await s2.read_some( + capy::mutable_buffer(buf, sizeof(buf))); +// buf contains "ping" +---- + +This is the fastest way to create a connected pair — it uses a single +`socketpair()` syscall with no filesystem paths involved. + +== Datagram Sockets + +Datagram sockets preserve message boundaries. Each `send` delivers exactly +one message that the receiver gets as a complete unit from `recv`. + +=== Connectionless Mode + +Both sides bind to paths, then use `send_to`/`recv_from`: + +[source,cpp] +---- +corosio::local_datagram_socket s(ioc); +s.open(); +s.bind(corosio::local_endpoint("/tmp/my_dgram.sock")); + +// Send to a specific peer +co_await s.send_to( + capy::const_buffer("hello", 5), + corosio::local_endpoint("/tmp/peer.sock")); + +// Receive from any sender +corosio::local_endpoint sender; +auto [ec, n] = co_await s.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender); +---- + +=== Connected Mode + +After calling `connect()`, use `send`/`recv` without specifying the peer: + +[source,cpp] +---- +auto [s1, s2] = corosio::make_local_datagram_pair(ioc); + +co_await s1.send(capy::const_buffer("msg", 3)); + +auto [ec, n] = co_await s2.recv( + capy::mutable_buffer(buf, sizeof(buf))); +---- + +== Local Endpoints + +Unix socket endpoints use filesystem paths instead of IP+port: + +[source,cpp] +---- +// Create from a path +corosio::local_endpoint ep("/tmp/my_app.sock"); + +// Query the path +std::string_view path = ep.path(); + +// Check if empty (unbound) +bool bound = !ep.empty(); +---- + +The maximum path length is 107 bytes (the `sun_path` field in `sockaddr_un` +minus the null terminator). Paths longer than this throw +`std::errc::filename_too_long`. + +=== Abstract Sockets (Linux Only) + +On Linux, paths starting with a null byte (`'\0'`) create abstract sockets +that exist in a kernel namespace rather than the filesystem. They don't leave +socket files behind and don't need cleanup: + +[source,cpp] +---- +// Abstract socket — no file created +corosio::local_endpoint ep(std::string_view("\0/my_app", 8)); +assert(ep.is_abstract()); +---- + +== Comparison with TCP + +[cols="1,1,1"] +|=== +| Feature | TCP (`tcp_socket`) | Unix (`local_stream_socket`) + +| Addressing +| IP address + port +| Filesystem path + +| Scope +| Network (any machine) +| Local machine only + +| Latency +| Higher (TCP/IP stack) +| Lower (kernel shortcut) + +| Throughput +| Limited by network stack +| Higher for local IPC + +| Access control +| Firewall rules +| File permissions + +| DNS resolution +| Yes (via `resolver`) +| No (direct paths) + +| Platform +| All platforms +| POSIX only (Linux, macOS, BSD) +|=== + +== Platform Support + +Unix domain sockets are available on all POSIX platforms: + +* **Linux** — Full support including abstract sockets +* **macOS** — Full support (no abstract sockets) +* **FreeBSD** — Full support (no abstract sockets) + +Windows has limited AF_UNIX support (since Windows 10 1803) but Corosio +does not currently support Unix sockets on Windows. + +== Next Steps + +* xref:4d.sockets.adoc[TCP Sockets] — TCP socket operations +* xref:4e.tcp-acceptor.adoc[TCP Acceptors] — TCP listener operations +* xref:4f.endpoints.adoc[IP Endpoints] — IP address and port endpoints diff --git a/doc/modules/ROOT/pages/glossary.adoc b/doc/modules/ROOT/pages/glossary.adoc index 96aa52e15..5e6569acf 100644 --- a/doc/modules/ROOT/pages/glossary.adoc +++ b/doc/modules/ROOT/pages/glossary.adoc @@ -148,6 +148,10 @@ Lazy:: A coroutine or operation that doesn't start until explicitly triggered. `capy::task` is lazy—it starts when awaited. +Local Endpoint:: +A filesystem path used as the address for a Unix domain socket. Represented +by `corosio::local_endpoint`. See xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets]. + == M Mocket:: @@ -254,6 +258,14 @@ Type Erasure:: Hiding concrete types behind an abstract interface. Enables runtime polymorphism without templates. +== U + +Unix Domain Socket:: +A socket that communicates between processes on the same machine using +filesystem paths instead of IP addresses and ports. Available as stream +(`local_stream_socket`) and datagram (`local_datagram_socket`) variants. +See xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets]. + == W Wait:: diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 2788724b4..c950e36d2 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -20,9 +20,12 @@ the _IoAwaitable_ protocol, ensuring your coroutines resume on the correct executor without manual dispatch. * **io_context** — Event loop for processing asynchronous operations -* **socket** — Asynchronous TCP socket with connect, read, and write +* **tcp_socket** — Asynchronous TCP socket with connect, read, and write * **tcp_acceptor** — TCP listener for accepting incoming connections * **tcp_server** — Server framework with worker pools +* **udp_socket** — Asynchronous UDP socket for datagrams +* **local_stream_socket** — Unix domain stream socket for local IPC +* **local_datagram_socket** — Unix domain datagram socket for local IPC * **resolver** — Asynchronous DNS resolution * **timer** — Asynchronous timer for delays and timeouts * **signal_set** — Asynchronous signal handling @@ -35,7 +38,7 @@ Corosio focuses on coroutine-first I/O primitives. It does not include: * General-purpose executor abstractions (use Boost.Capy) * The sender/receiver execution model (P2300) * HTTP, WebSocket, or other application protocols (use Boost.Http or Boost.Beast2) -* UDP or other transport protocols (TCP only for now) +* UDP multicast or raw sockets Corosio works with Boost.Capy for task management and execution contexts. @@ -154,3 +157,4 @@ int main() * xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands * xref:4.guide/4c.io-context.adoc[I/O Context] — Understand the event loop * xref:4.guide/4d.sockets.adoc[Sockets] — Learn socket operations in detail +* xref:4.guide/4p.unix-sockets.adoc[Unix Domain Sockets] — Local IPC with stream and datagram sockets diff --git a/include/boost/corosio.hpp b/include/boost/corosio.hpp index e0b2aacd6..49d9a6265 100644 --- a/include/boost/corosio.hpp +++ b/include/boost/corosio.hpp @@ -17,16 +17,26 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include #include +#include #include #include #include #include +#include +#include #include #include diff --git a/include/boost/corosio/backend.hpp b/include/boost/corosio/backend.hpp index 045a843f8..765b11319 100644 --- a/include/boost/corosio/backend.hpp +++ b/include/boost/corosio/backend.hpp @@ -13,6 +13,48 @@ #include #include +#if BOOST_COROSIO_HAS_EPOLL || BOOST_COROSIO_HAS_SELECT || BOOST_COROSIO_HAS_KQUEUE +#include +#endif + +#if BOOST_COROSIO_HAS_EPOLL +#include +#endif + +#if BOOST_COROSIO_HAS_SELECT +#include +#endif + +#if BOOST_COROSIO_HAS_KQUEUE +#include +#endif + +// Per-backend scheduler headers pull in platform system headers +// (, , ...) and define the per-backend +// `descriptor_state`. Under BOOST_COROSIO_MRDOCS every HAS_* macro is +// forced to 1 so that all backends appear in the docs, which would +// cause a Linux MrDocs build to fail on and on two +// backends both defining `descriptor_state`. The forward declarations +// already present in this header are sufficient for the type-alias +// chain below; the full scheduler definitions are pulled in by +// native_io_context.hpp for normal builds (which also guards them +// under MRDOCS). +#ifndef BOOST_COROSIO_MRDOCS + +#if BOOST_COROSIO_HAS_EPOLL +#include +#endif + +#if BOOST_COROSIO_HAS_SELECT +#include +#endif + +#if BOOST_COROSIO_HAS_KQUEUE +#include +#endif + +#endif // !BOOST_COROSIO_MRDOCS + namespace boost::capy { class execution_context; } // namespace boost::capy @@ -27,31 +69,33 @@ struct scheduler; namespace detail { -class epoll_tcp_socket; -class epoll_tcp_service; -class epoll_udp_socket; -class epoll_udp_service; -class epoll_tcp_acceptor; -class epoll_tcp_acceptor_service; class epoll_scheduler; - class posix_signal; class posix_signal_service; class posix_resolver; class posix_resolver_service; +using epoll_types = reactor_types; + } // namespace detail /// Backend tag for the Linux epoll I/O multiplexer. struct epoll_t { using scheduler_type = detail::epoll_scheduler; - using tcp_socket_type = detail::epoll_tcp_socket; - using tcp_service_type = detail::epoll_tcp_service; - using udp_socket_type = detail::epoll_udp_socket; - using udp_service_type = detail::epoll_udp_service; - using tcp_acceptor_type = detail::epoll_tcp_acceptor; - using tcp_acceptor_service_type = detail::epoll_tcp_acceptor_service; + using tcp_socket_type = detail::epoll_types::tcp_socket_type; + using tcp_service_type = detail::epoll_types::tcp_service_type; + using udp_socket_type = detail::epoll_types::udp_socket_type; + using udp_service_type = detail::epoll_types::udp_service_type; + using tcp_acceptor_type = detail::epoll_types::tcp_acceptor_type; + using tcp_acceptor_service_type = detail::epoll_types::tcp_acceptor_service_type; + + using local_stream_socket_type = detail::epoll_types::local_stream_socket_type; + using local_stream_service_type = detail::epoll_types::local_stream_service_type; + using local_stream_acceptor_type = detail::epoll_types::local_stream_acceptor_type; + using local_stream_acceptor_service_type = detail::epoll_types::local_stream_acceptor_service_type; + using local_datagram_socket_type = detail::epoll_types::local_datagram_socket_type; + using local_datagram_service_type = detail::epoll_types::local_datagram_service_type; using signal_type = detail::posix_signal; using signal_service_type = detail::posix_signal_service; @@ -72,31 +116,33 @@ inline constexpr epoll_t epoll{}; namespace detail { -class select_tcp_socket; -class select_tcp_service; -class select_udp_socket; -class select_udp_service; -class select_tcp_acceptor; -class select_tcp_acceptor_service; class select_scheduler; - class posix_signal; class posix_signal_service; class posix_resolver; class posix_resolver_service; +using select_types = reactor_types; + } // namespace detail /// Backend tag for the portable select() I/O multiplexer. struct select_t { using scheduler_type = detail::select_scheduler; - using tcp_socket_type = detail::select_tcp_socket; - using tcp_service_type = detail::select_tcp_service; - using udp_socket_type = detail::select_udp_socket; - using udp_service_type = detail::select_udp_service; - using tcp_acceptor_type = detail::select_tcp_acceptor; - using tcp_acceptor_service_type = detail::select_tcp_acceptor_service; + using tcp_socket_type = detail::select_types::tcp_socket_type; + using tcp_service_type = detail::select_types::tcp_service_type; + using udp_socket_type = detail::select_types::udp_socket_type; + using udp_service_type = detail::select_types::udp_service_type; + using tcp_acceptor_type = detail::select_types::tcp_acceptor_type; + using tcp_acceptor_service_type = detail::select_types::tcp_acceptor_service_type; + + using local_stream_socket_type = detail::select_types::local_stream_socket_type; + using local_stream_service_type = detail::select_types::local_stream_service_type; + using local_stream_acceptor_type = detail::select_types::local_stream_acceptor_type; + using local_stream_acceptor_service_type = detail::select_types::local_stream_acceptor_service_type; + using local_datagram_socket_type = detail::select_types::local_datagram_socket_type; + using local_datagram_service_type = detail::select_types::local_datagram_service_type; using signal_type = detail::posix_signal; using signal_service_type = detail::posix_signal_service; @@ -117,31 +163,33 @@ inline constexpr select_t select{}; namespace detail { -class kqueue_tcp_socket; -class kqueue_tcp_service; -class kqueue_udp_socket; -class kqueue_udp_service; -class kqueue_tcp_acceptor; -class kqueue_tcp_acceptor_service; class kqueue_scheduler; - class posix_signal; class posix_signal_service; class posix_resolver; class posix_resolver_service; +using kqueue_types = reactor_types; + } // namespace detail /// Backend tag for the BSD kqueue I/O multiplexer. struct kqueue_t { using scheduler_type = detail::kqueue_scheduler; - using tcp_socket_type = detail::kqueue_tcp_socket; - using tcp_service_type = detail::kqueue_tcp_service; - using udp_socket_type = detail::kqueue_udp_socket; - using udp_service_type = detail::kqueue_udp_service; - using tcp_acceptor_type = detail::kqueue_tcp_acceptor; - using tcp_acceptor_service_type = detail::kqueue_tcp_acceptor_service; + using tcp_socket_type = detail::kqueue_types::tcp_socket_type; + using tcp_service_type = detail::kqueue_types::tcp_service_type; + using udp_socket_type = detail::kqueue_types::udp_socket_type; + using udp_service_type = detail::kqueue_types::udp_service_type; + using tcp_acceptor_type = detail::kqueue_types::tcp_acceptor_type; + using tcp_acceptor_service_type = detail::kqueue_types::tcp_acceptor_service_type; + + using local_stream_socket_type = detail::kqueue_types::local_stream_socket_type; + using local_stream_service_type = detail::kqueue_types::local_stream_service_type; + using local_stream_acceptor_type = detail::kqueue_types::local_stream_acceptor_type; + using local_stream_acceptor_service_type = detail::kqueue_types::local_stream_acceptor_service_type; + using local_datagram_socket_type = detail::kqueue_types::local_datagram_socket_type; + using local_datagram_service_type = detail::kqueue_types::local_datagram_service_type; using signal_type = detail::posix_signal; using signal_service_type = detail::posix_signal_service; diff --git a/include/boost/corosio/detail/config.hpp b/include/boost/corosio/detail/config.hpp index 17d892771..018edf9ca 100644 --- a/include/boost/corosio/detail/config.hpp +++ b/include/boost/corosio/detail/config.hpp @@ -17,6 +17,27 @@ #define BOOST_COROSIO_ASSERT(expr) assert(expr) #endif +// Clang warning suppression helpers. +// On clang these expand to _Pragma(); elsewhere they are empty. +// Usage: +// BOOST_COROSIO_CLANG_WARNING_PUSH +// BOOST_COROSIO_CLANG_WARNING_DISABLE("-Winconsistent-missing-override") +// ...code... +// BOOST_COROSIO_CLANG_WARNING_POP +#if defined(__clang__) +#define BOOST_COROSIO_CLANG_PRAGMA_(x) _Pragma(#x) +#define BOOST_COROSIO_CLANG_WARNING_PUSH \ + BOOST_COROSIO_CLANG_PRAGMA_(clang diagnostic push) +#define BOOST_COROSIO_CLANG_WARNING_DISABLE(x) \ + BOOST_COROSIO_CLANG_PRAGMA_(clang diagnostic ignored x) +#define BOOST_COROSIO_CLANG_WARNING_POP \ + BOOST_COROSIO_CLANG_PRAGMA_(clang diagnostic pop) +#else +#define BOOST_COROSIO_CLANG_WARNING_PUSH +#define BOOST_COROSIO_CLANG_WARNING_DISABLE(x) +#define BOOST_COROSIO_CLANG_WARNING_POP +#endif + // Symbol export/import for shared libraries #if defined(_WIN32) || defined(__CYGWIN__) #define BOOST_COROSIO_SYMBOL_EXPORT __declspec(dllexport) diff --git a/include/boost/corosio/detail/local_datagram_service.hpp b/include/boost/corosio/detail/local_datagram_service.hpp new file mode 100644 index 000000000..15d52a3a5 --- /dev/null +++ b/include/boost/corosio/detail/local_datagram_service.hpp @@ -0,0 +1,82 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_LOCAL_DATAGRAM_SERVICE_HPP +#define BOOST_COROSIO_DETAIL_LOCAL_DATAGRAM_SERVICE_HPP + +#include +#include +#include +#include +#include + +namespace boost::corosio::detail { + +/* Abstract local datagram service base class. + + Concrete implementations (epoll, select, kqueue) inherit from + this class and provide platform-specific datagram socket operations + for Unix domain sockets. The context constructor installs + whichever backend via make_service, and local_datagram_socket.cpp + retrieves it via use_service(). +*/ +class BOOST_COROSIO_DECL local_datagram_service + : public capy::execution_context::service + , public io_object::io_service +{ +public: + /// Identifies this service for execution_context lookup. + using key_type = local_datagram_service; + + /** Open a Unix datagram socket. + + Creates a socket and associates it with the platform reactor. + + @param impl The socket implementation to open. + @param family Unix domain address family. + @param type Datagram socket type. + @param protocol Protocol number (default 0). + @return Error code on failure, empty on success. + */ + virtual std::error_code open_socket( + local_datagram_socket::implementation& impl, + int family, + int type, + int protocol) = 0; + + /** Assign an existing file descriptor to a socket. + + Used by socketpair() to adopt pre-created fds. + + @param impl The socket implementation to assign to. + @param fd The file descriptor to adopt. + @return Error code on failure, empty on success. + */ + virtual std::error_code assign_socket( + local_datagram_socket::implementation& impl, + int fd) = 0; + + /** Bind a datagram socket to a local endpoint. + + @param impl The socket implementation to bind. + @param ep The local endpoint to bind to. + @return Error code on failure, empty on success. + */ + virtual std::error_code bind_socket( + local_datagram_socket::implementation& impl, + corosio::local_endpoint ep) = 0; + +protected: + local_datagram_service() = default; + ~local_datagram_service() override = default; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_LOCAL_DATAGRAM_SERVICE_HPP diff --git a/include/boost/corosio/detail/local_stream_acceptor_service.hpp b/include/boost/corosio/detail/local_stream_acceptor_service.hpp new file mode 100644 index 000000000..b2e27e702 --- /dev/null +++ b/include/boost/corosio/detail/local_stream_acceptor_service.hpp @@ -0,0 +1,76 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_LOCAL_STREAM_ACCEPTOR_SERVICE_HPP +#define BOOST_COROSIO_DETAIL_LOCAL_STREAM_ACCEPTOR_SERVICE_HPP + +#include +#include +#include +#include +#include + +namespace boost::corosio::detail { + +/* Abstract local stream acceptor service base class. + + Concrete implementations (epoll, select, kqueue) inherit + from this class and provide platform-specific acceptor + operations for Unix domain sockets. +*/ +class BOOST_COROSIO_DECL local_stream_acceptor_service + : public capy::execution_context::service + , public io_object::io_service +{ +public: + /// Identifies this service for execution_context lookup. + using key_type = local_stream_acceptor_service; + + /** Create the acceptor socket. + + @param impl The acceptor implementation to open. + @param family Unix domain address family. + @param type Stream socket type. + @param protocol Protocol number (default 0). + @return Error code on failure, empty on success. + */ + virtual std::error_code open_acceptor_socket( + local_stream_acceptor::implementation& impl, + int family, + int type, + int protocol) = 0; + + /** Bind an open acceptor to a local endpoint. + + @param impl The acceptor implementation to bind. + @param ep The local endpoint (path) to bind to. + @return Error code on failure, empty on success. + */ + virtual std::error_code bind_acceptor( + local_stream_acceptor::implementation& impl, + local_endpoint ep) = 0; + + /** Start listening for incoming connections. + + @param impl The acceptor implementation to listen on. + @param backlog The maximum pending connection queue length. + @return Error code on failure, empty on success. + */ + virtual std::error_code listen_acceptor( + local_stream_acceptor::implementation& impl, + int backlog) = 0; + +protected: + local_stream_acceptor_service() = default; + ~local_stream_acceptor_service() override = default; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_LOCAL_STREAM_ACCEPTOR_SERVICE_HPP diff --git a/include/boost/corosio/detail/local_stream_service.hpp b/include/boost/corosio/detail/local_stream_service.hpp new file mode 100644 index 000000000..2c0a7c579 --- /dev/null +++ b/include/boost/corosio/detail/local_stream_service.hpp @@ -0,0 +1,71 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_LOCAL_STREAM_SERVICE_HPP +#define BOOST_COROSIO_DETAIL_LOCAL_STREAM_SERVICE_HPP + +#include +#include +#include +#include + +namespace boost::corosio::detail { + +/* Abstract local stream service base class. + + Concrete implementations (epoll, select, kqueue) inherit from + this class and provide platform-specific stream socket operations + for Unix domain sockets. The context constructor installs + whichever backend via make_service, and local_stream_socket.cpp + retrieves it via use_service(). +*/ +class BOOST_COROSIO_DECL local_stream_service + : public capy::execution_context::service + , public io_object::io_service +{ +public: + /// Identifies this service for execution_context lookup. + using key_type = local_stream_service; + + /** Open a Unix stream socket. + + Creates a socket and associates it with the platform reactor. + + @param impl The socket implementation to open. + @param family Unix domain address family. + @param type Stream socket type. + @param protocol Protocol number (default 0). + @return Error code on failure, empty on success. + */ + virtual std::error_code open_socket( + local_stream_socket::implementation& impl, + int family, + int type, + int protocol) = 0; + + /** Assign an existing file descriptor to a socket. + + Used by socketpair() to adopt pre-created fds. + + @param impl The socket implementation to assign to. + @param fd The file descriptor to adopt. + @return Error code on failure, empty on success. + */ + virtual std::error_code assign_socket( + local_stream_socket::implementation& impl, + int fd) = 0; + +protected: + local_stream_service() = default; + ~local_stream_service() override = default; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_LOCAL_STREAM_SERVICE_HPP diff --git a/include/boost/corosio/detail/op_base.hpp b/include/boost/corosio/detail/op_base.hpp new file mode 100644 index 000000000..5a8317cab --- /dev/null +++ b/include/boost/corosio/detail/op_base.hpp @@ -0,0 +1,111 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_OP_BASE_HPP +#define BOOST_COROSIO_DETAIL_OP_BASE_HPP + +#include +#include +#include + +#include +#include +#include +#include + +namespace boost::corosio::detail { + +/* CRTP base for awaitables that return io_result. + + Derived classes must provide: + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, + capy::executor_ref ex) const; + + which forwards to the backend implementation method, passing + token_, &ec_, and &bytes_ as the cancellation/output parameters. +*/ +template +class bytes_op_base +{ + friend Derived; + bytes_op_base() = default; + +public: + std::stop_token token_; + mutable std::error_code ec_; + mutable std::size_t bytes_ = 0; + + bool await_ready() const noexcept + { + return token_.stop_requested(); + } + + capy::io_result await_resume() const noexcept + { + if (token_.stop_requested()) + return {make_error_code(std::errc::operation_canceled), 0}; + return {ec_, bytes_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + return static_cast(this)->dispatch( + h, env->executor); + } +}; + +/* CRTP base for awaitables that return io_result<>. + + Derived classes must provide: + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, + capy::executor_ref ex) const; + + which forwards to the backend implementation method, passing + token_ and &ec_ as the cancellation/output parameters. +*/ +template +class void_op_base +{ + friend Derived; + void_op_base() = default; + +public: + std::stop_token token_; + mutable std::error_code ec_; + + bool await_ready() const noexcept + { + return token_.stop_requested(); + } + + capy::io_result<> await_resume() const noexcept + { + if (token_.stop_requested()) + return {make_error_code(std::errc::operation_canceled)}; + return {ec_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + return static_cast(this)->dispatch( + h, env->executor); + } +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_OP_BASE_HPP diff --git a/include/boost/corosio/detail/tcp_acceptor_service.hpp b/include/boost/corosio/detail/tcp_acceptor_service.hpp index c74e8451a..97036e6ba 100644 --- a/include/boost/corosio/detail/tcp_acceptor_service.hpp +++ b/include/boost/corosio/detail/tcp_acceptor_service.hpp @@ -40,9 +40,9 @@ class BOOST_COROSIO_DECL tcp_acceptor_service not bind or listen. Does not set SO_REUSEADDR. @param impl The acceptor implementation to open. - @param family Address family (e.g. `AF_INET`, `AF_INET6`). - @param type Socket type (e.g. `SOCK_STREAM`). - @param protocol Protocol number (e.g. `IPPROTO_TCP`). + @param family Internet address family (IPv4 or IPv6). + @param type Stream socket type. + @param protocol TCP protocol number. @return Error code on failure, empty on success. */ virtual std::error_code open_acceptor_socket( diff --git a/include/boost/corosio/detail/tcp_service.hpp b/include/boost/corosio/detail/tcp_service.hpp index 94a5fb92d..d34d7d7c4 100644 --- a/include/boost/corosio/detail/tcp_service.hpp +++ b/include/boost/corosio/detail/tcp_service.hpp @@ -38,9 +38,9 @@ class BOOST_COROSIO_DECL tcp_service Creates a socket and associates it with the platform reactor. @param impl The socket implementation to open. - @param family Address family (e.g. `AF_INET`, `AF_INET6`). - @param type Socket type (e.g. `SOCK_STREAM`). - @param protocol Protocol number (e.g. `IPPROTO_TCP`). + @param family Internet address family (IPv4 or IPv6). + @param type Stream socket type. + @param protocol TCP protocol number. @return Error code on failure, empty on success. */ virtual std::error_code open_socket( diff --git a/include/boost/corosio/detail/udp_service.hpp b/include/boost/corosio/detail/udp_service.hpp index 3674f81f6..e7ad3cd50 100644 --- a/include/boost/corosio/detail/udp_service.hpp +++ b/include/boost/corosio/detail/udp_service.hpp @@ -40,9 +40,9 @@ class BOOST_COROSIO_DECL udp_service Creates a socket and associates it with the platform reactor. @param impl The socket implementation to open. - @param family Address family (e.g. `AF_INET`, `AF_INET6`). - @param type Socket type (`SOCK_DGRAM`). - @param protocol Protocol number (`IPPROTO_UDP`). + @param family Internet address family (IPv4 or IPv6). + @param type Datagram socket type. + @param protocol UDP protocol number. @return Error code on failure, empty on success. */ virtual std::error_code open_datagram_socket( diff --git a/include/boost/corosio/io/io_object.hpp b/include/boost/corosio/io/io_object.hpp index 1cb7aaafc..452c0f6fa 100644 --- a/include/boost/corosio/io/io_object.hpp +++ b/include/boost/corosio/io/io_object.hpp @@ -199,7 +199,18 @@ class BOOST_COROSIO_DECL io_object template static handle create_handle(capy::execution_context& ctx) { - auto* svc = ctx.find_service(); + Service* svc = nullptr; + + // Prefer key_type lookup — stable across TUs even for template + // instantiations where type_id() may differ per object file + // on platforms that don't merge inline-function statics (FreeBSD). + if constexpr (requires { typename Service::key_type; }) + svc = static_cast( + ctx.find_service()); + + if (!svc) + svc = ctx.find_service(); + if (!svc) detail::throw_logic_error( "io_object::create_handle: service not installed"); diff --git a/include/boost/corosio/io/io_read_stream.hpp b/include/boost/corosio/io/io_read_stream.hpp index 5ac36039b..4ac9b8026 100644 --- a/include/boost/corosio/io/io_read_stream.hpp +++ b/include/boost/corosio/io/io_read_stream.hpp @@ -11,6 +11,7 @@ #define BOOST_COROSIO_IO_IO_READ_STREAM_HPP #include +#include #include #include #include @@ -46,38 +47,20 @@ class BOOST_COROSIO_DECL io_read_stream : virtual public io_object /// Awaitable for async read operations. template struct read_some_awaitable + : detail::bytes_op_base> { io_read_stream& ios_; MutableBufferSequence buffers_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_transferred_ = 0; read_some_awaitable( io_read_stream& ios, MutableBufferSequence buffers) noexcept - : ios_(ios) - , buffers_(std::move(buffers)) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + : ios_(ios), buffers_(std::move(buffers)) {} - capy::io_result await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_transferred_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; return ios_.do_read_some( - h, env->executor, buffers_, token_, &ec_, &bytes_transferred_); + h, ex, buffers_, this->token_, &this->ec_, &this->bytes_); } }; @@ -123,6 +106,11 @@ class BOOST_COROSIO_DECL io_read_stream : virtual public io_object @param buffers The buffer sequence to read data into. + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling the owning stream's `cancel()` member. On cancellation, + yields `errc::operation_canceled`. + @return An awaitable yielding `(error_code, std::size_t)`. @see io_stream::write_some diff --git a/include/boost/corosio/io/io_write_stream.hpp b/include/boost/corosio/io/io_write_stream.hpp index 3b628755d..b9dace963 100644 --- a/include/boost/corosio/io/io_write_stream.hpp +++ b/include/boost/corosio/io/io_write_stream.hpp @@ -11,6 +11,7 @@ #define BOOST_COROSIO_IO_IO_WRITE_STREAM_HPP #include +#include #include #include #include @@ -46,38 +47,20 @@ class BOOST_COROSIO_DECL io_write_stream : virtual public io_object /// Awaitable for async write operations. template struct write_some_awaitable + : detail::bytes_op_base> { io_write_stream& ios_; ConstBufferSequence buffers_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_transferred_ = 0; write_some_awaitable( io_write_stream& ios, ConstBufferSequence buffers) noexcept - : ios_(ios) - , buffers_(std::move(buffers)) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + : ios_(ios), buffers_(std::move(buffers)) {} - capy::io_result await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_transferred_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; return ios_.do_write_some( - h, env->executor, buffers_, token_, &ec_, &bytes_transferred_); + h, ex, buffers_, this->token_, &this->ec_, &this->bytes_); } }; @@ -123,6 +106,11 @@ class BOOST_COROSIO_DECL io_write_stream : virtual public io_object @param buffers The buffer sequence containing data to write. + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling the owning stream's `cancel()` member. On cancellation, + yields `errc::operation_canceled`. + @return An awaitable yielding `(error_code, std::size_t)`. @see io_stream::read_some diff --git a/include/boost/corosio/local_datagram.hpp b/include/boost/corosio/local_datagram.hpp new file mode 100644 index 000000000..a03c14771 --- /dev/null +++ b/include/boost/corosio/local_datagram.hpp @@ -0,0 +1,49 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_DATAGRAM_HPP +#define BOOST_COROSIO_LOCAL_DATAGRAM_HPP + +#include + +namespace boost::corosio { + +class local_datagram_socket; + +/* Encapsulate the Unix datagram protocol for socket creation. + + This class identifies the Unix domain datagram protocol. It is + used to parameterize socket open() calls with a self-documenting + type. + + The family(), type(), and protocol() members are implemented + in the compiled library to avoid exposing platform socket + headers. + + See local_datagram_socket +*/ +class BOOST_COROSIO_DECL local_datagram +{ +public: + /// Return the Unix domain address family. + static int family() noexcept; + + /// Return the datagram socket type. + static int type() noexcept; + + /// Return the protocol number (default 0). + static int protocol() noexcept; + + /// The associated socket type. + using socket = local_datagram_socket; +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_DATAGRAM_HPP diff --git a/include/boost/corosio/local_datagram_socket.hpp b/include/boost/corosio/local_datagram_socket.hpp new file mode 100644 index 000000000..f641d0b75 --- /dev/null +++ b/include/boost/corosio/local_datagram_socket.hpp @@ -0,0 +1,744 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_DATAGRAM_SOCKET_HPP +#define BOOST_COROSIO_LOCAL_DATAGRAM_SOCKET_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace boost::corosio { + +/** An asynchronous Unix datagram socket for coroutine I/O. + + This class provides asynchronous Unix domain datagram socket + operations that return awaitable types. Each operation + participates in the affine awaitable protocol, ensuring + coroutines resume on the correct executor. + + Supports two modes of operation: + + Connectionless mode: each send_to specifies a destination + endpoint, and each recv_from captures the source endpoint. + The socket must be opened (and optionally bound) before I/O. + + Connected mode: call connect() to set a default peer, + then use send()/recv() without endpoint arguments. The kernel + filters incoming datagrams to those from the connected peer. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. A socket must not have concurrent + operations of the same type (e.g., two simultaneous recv_from). + One send_to and one recv_from may be in flight simultaneously. + Note that recv and recv_from share the same internal read slot, + so they must not overlap; likewise send and send_to share the + write slot. +*/ +class BOOST_COROSIO_DECL local_datagram_socket : public io_object +{ +public: + using shutdown_type = corosio::shutdown_type; + using enum corosio::shutdown_type; + + /** Define backend hooks for local datagram socket operations. + + Platform backends (epoll, kqueue, select) derive from this + to implement datagram I/O, connection, and option management. + */ + struct implementation : io_object::implementation + { + /** Initiate an asynchronous send_to operation. + + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer data to send. + @param dest The destination endpoint. + @param flags Message flags (e.g. MSG_DONTROUTE). + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> send_to( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + corosio::local_endpoint dest, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /** Initiate an asynchronous recv_from operation. + + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer to receive into. + @param source Output endpoint for the sender's address. + @param flags Message flags (e.g. MSG_PEEK). + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> recv_from( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + corosio::local_endpoint* source, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /** Initiate an asynchronous connect to set the default peer. + + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param ep The remote endpoint to connect to. + @param token Stop token for cancellation. + @param ec Output error code. + + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> connect( + std::coroutine_handle<> h, + capy::executor_ref ex, + corosio::local_endpoint ep, + std::stop_token token, + std::error_code* ec) = 0; + + /** Initiate an asynchronous connected send operation. + + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer data to send. + @param flags Message flags (e.g. MSG_DONTROUTE). + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> send( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /** Initiate an asynchronous connected recv operation. + + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer to receive into. + @param flags Message flags (e.g. MSG_PEEK). + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> recv( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /// Shut down part or all of the socket. + virtual std::error_code shutdown(shutdown_type what) noexcept = 0; + + /// Return the platform socket descriptor. + virtual native_handle_type native_handle() const noexcept = 0; + + virtual native_handle_type release_socket() noexcept = 0; + + /** Request cancellation of pending asynchronous operations. + + All outstanding operations complete with operation_canceled + error. Check ec == cond::canceled for portable comparison. + */ + virtual void cancel() noexcept = 0; + + /** Set a socket option. + + @param level The protocol level (e.g. SOL_SOCKET). + @param optname The option name. + @param data Pointer to the option value. + @param size Size of the option value in bytes. + @return Error code on failure, empty on success. + */ + virtual std::error_code set_option( + int level, + int optname, + void const* data, + std::size_t size) noexcept = 0; + + /** Get a socket option. + + @param level The protocol level (e.g. SOL_SOCKET). + @param optname The option name. + @param data Pointer to receive the option value. + @param size On entry, the size of the buffer. On exit, + the size of the option value. + @return Error code on failure, empty on success. + */ + virtual std::error_code + get_option(int level, int optname, void* data, std::size_t* size) + const noexcept = 0; + + /// Return the cached local endpoint. + virtual corosio::local_endpoint local_endpoint() const noexcept = 0; + + /// Return the cached remote endpoint (connected mode). + virtual corosio::local_endpoint remote_endpoint() const noexcept = 0; + + /** Bind the socket to a local endpoint. + + @param ep The local endpoint to bind to. + @return Error code on failure, empty on success. + */ + virtual std::error_code + bind(corosio::local_endpoint ep) noexcept = 0; + }; + + /** Represent the awaitable returned by @ref send_to. + + Captures the destination endpoint and buffer, then dispatches + to the backend implementation on suspension. + */ + struct send_to_awaitable + : detail::bytes_op_base + { + local_datagram_socket& s_; + buffer_param buf_; + corosio::local_endpoint dest_; + int flags_; + + send_to_awaitable( + local_datagram_socket& s, buffer_param buf, + corosio::local_endpoint dest, int flags = 0) noexcept + : s_(s), buf_(buf), dest_(dest), flags_(flags) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().send_to( + h, ex, buf_, dest_, flags_, token_, &ec_, &bytes_); + } + }; + + struct recv_from_awaitable + : detail::bytes_op_base + { + local_datagram_socket& s_; + buffer_param buf_; + corosio::local_endpoint& source_; + int flags_; + + recv_from_awaitable( + local_datagram_socket& s, buffer_param buf, + corosio::local_endpoint& source, int flags = 0) noexcept + : s_(s), buf_(buf), source_(source), flags_(flags) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().recv_from( + h, ex, buf_, &source_, flags_, token_, &ec_, &bytes_); + } + }; + + struct connect_awaitable + : detail::void_op_base + { + local_datagram_socket& s_; + corosio::local_endpoint endpoint_; + + connect_awaitable( + local_datagram_socket& s, + corosio::local_endpoint ep) noexcept + : s_(s), endpoint_(ep) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().connect( + h, ex, endpoint_, token_, &ec_); + } + }; + + struct send_awaitable + : detail::bytes_op_base + { + local_datagram_socket& s_; + buffer_param buf_; + int flags_; + + send_awaitable( + local_datagram_socket& s, buffer_param buf, + int flags = 0) noexcept + : s_(s), buf_(buf), flags_(flags) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().send( + h, ex, buf_, flags_, token_, &ec_, &bytes_); + } + }; + + struct recv_awaitable + : detail::bytes_op_base + { + local_datagram_socket& s_; + buffer_param buf_; + int flags_; + + recv_awaitable( + local_datagram_socket& s, buffer_param buf, + int flags = 0) noexcept + : s_(s), buf_(buf), flags_(flags) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().recv( + h, ex, buf_, flags_, token_, &ec_, &bytes_); + } + }; + +public: + /** Destructor. + + Closes the socket if open, cancelling any pending operations. + */ + ~local_datagram_socket() override; + + /** Construct a socket from an execution context. + + @param ctx The execution context that will own this socket. + */ + explicit local_datagram_socket(capy::execution_context& ctx); + + /** Construct a socket from an executor. + + The socket is associated with the executor's context. + + @param ex The executor whose context will own the socket. + */ + template + requires( + !std::same_as, local_datagram_socket>) && + capy::Executor + explicit local_datagram_socket(Ex const& ex) + : local_datagram_socket(ex.context()) + { + } + + /** Move constructor. + + Transfers ownership of the socket resources. + + @param other The socket to move from. + */ + local_datagram_socket(local_datagram_socket&& other) noexcept + : io_object(std::move(other)) + { + } + + /** Move assignment operator. + + Closes any existing socket and transfers ownership. + + @param other The socket to move from. + @return Reference to this socket. + */ + local_datagram_socket& operator=(local_datagram_socket&& other) noexcept + { + if (this != &other) + { + close(); + io_object::operator=(std::move(other)); + } + return *this; + } + + local_datagram_socket(local_datagram_socket const&) = delete; + local_datagram_socket& operator=(local_datagram_socket const&) = delete; + + /** Open the socket. + + Creates a Unix datagram socket and associates it with + the platform reactor. + + @param proto The protocol. Defaults to local_datagram{}. + + @throws std::system_error on failure. + */ + void open(local_datagram proto = {}); + + /// Close the socket. + void close(); + + /// Check if the socket is open. + bool is_open() const noexcept + { + return h_ && get().native_handle() >= 0; + } + + /** Bind the socket to a local endpoint. + + Associates the socket with a local address (filesystem path). + Required before calling recv_from in connectionless mode. + + @param ep The local endpoint to bind to. + + @return Error code on failure, empty on success. + + @throws std::logic_error if the socket is not open. + */ + std::error_code bind(corosio::local_endpoint ep); + + /** Initiate an asynchronous connect to set the default peer. + + If the socket is not already open, it is opened automatically. + + @param ep The remote endpoint to connect to. + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with io_result<>. + + @throws std::system_error if the socket needs to be opened + and the open fails. + */ + auto connect(corosio::local_endpoint ep) + { + if (!is_open()) + open(); + return connect_awaitable(*this, ep); + } + + /** Send a datagram to the specified destination. + + @param buf The buffer containing data to send. + @param dest The destination endpoint. + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with + io_result. + + @throws std::logic_error if the socket is not open. + */ + template + auto send_to( + Buffers const& buf, + corosio::local_endpoint dest, + corosio::message_flags flags) + { + if (!is_open()) + detail::throw_logic_error("send_to: socket not open"); + return send_to_awaitable( + *this, buf, dest, static_cast(flags)); + } + + /// @overload + template + auto send_to(Buffers const& buf, corosio::local_endpoint dest) + { + return send_to(buf, dest, corosio::message_flags::none); + } + + /** Receive a datagram and capture the sender's endpoint. + + @param buf The buffer to receive data into. + @param source Reference to an endpoint that will be set to + the sender's address on successful completion. + @param flags Message flags (e.g. message_flags::peek). + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with + io_result. + + @throws std::logic_error if the socket is not open. + */ + template + auto recv_from( + Buffers const& buf, + corosio::local_endpoint& source, + corosio::message_flags flags) + { + if (!is_open()) + detail::throw_logic_error("recv_from: socket not open"); + return recv_from_awaitable( + *this, buf, source, static_cast(flags)); + } + + /// @overload + template + auto recv_from(Buffers const& buf, corosio::local_endpoint& source) + { + return recv_from(buf, source, corosio::message_flags::none); + } + + /** Send a datagram to the connected peer. + + @param buf The buffer containing data to send. + @param flags Message flags. + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with + io_result. + + @throws std::logic_error if the socket is not open. + */ + template + auto send(Buffers const& buf, corosio::message_flags flags) + { + if (!is_open()) + detail::throw_logic_error("send: socket not open"); + return send_awaitable( + *this, buf, static_cast(flags)); + } + + /// @overload + template + auto send(Buffers const& buf) + { + return send(buf, corosio::message_flags::none); + } + + /** Receive a datagram from the connected peer. + + @param buf The buffer to receive data into. + @param flags Message flags (e.g. message_flags::peek). + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with + io_result. + + @throws std::logic_error if the socket is not open. + */ + template + auto recv(Buffers const& buf, corosio::message_flags flags) + { + if (!is_open()) + detail::throw_logic_error("recv: socket not open"); + return recv_awaitable( + *this, buf, static_cast(flags)); + } + + /// @overload + template + auto recv(Buffers const& buf) + { + return recv(buf, corosio::message_flags::none); + } + + /** Cancel any pending asynchronous operations. + + All outstanding operations complete with + errc::operation_canceled. Check ec == cond::canceled + for portable comparison. + */ + void cancel(); + + /** Get the native socket handle. + + @return The native socket handle, or -1 if not open. + */ + native_handle_type native_handle() const noexcept; + + /** Release ownership of the native socket handle. + + Deregisters the socket from the reactor and cancels pending + operations without closing the fd. The caller takes ownership + of the returned descriptor. + + @return The native handle. + + @throws std::logic_error if the socket is not open. + */ + native_handle_type release(); + + /** Query the number of bytes available for reading. + + @return The number of bytes that can be read without blocking. + + @throws std::logic_error if the socket is not open. + @throws std::system_error on ioctl failure. + */ + std::size_t available() const; + + /** Shut down part or all of the socket (best-effort). + + Calls `::shutdown` on the underlying descriptor when open. + Errors from the syscall (such as `ENOTCONN` on a peer that + already closed) are swallowed because they are typically + unhelpful at this layer; if the socket is not open, the call + is a no-op. To observe errors, use the + @ref shutdown(shutdown_type,std::error_code&) overload. + + @param what Which direction to shut down. + */ + void shutdown(shutdown_type what); + + /** Shut down part or all of the socket (non-throwing). + + @param what Which direction to shut down. + @param ec Set to the error code on failure. + */ + void shutdown(shutdown_type what, std::error_code& ec) noexcept; + + /** Set a socket option. + + @param opt The option to set. + + @throws std::logic_error if the socket is not open. + @throws std::system_error on failure. + */ + template + void set_option(Option const& opt) + { + if (!is_open()) + detail::throw_logic_error("set_option: socket not open"); + std::error_code ec = get().set_option( + Option::level(), Option::name(), opt.data(), opt.size()); + if (ec) + detail::throw_system_error( + ec, "local_datagram_socket::set_option"); + } + + /** Get a socket option. + + @return The current option value. + + @throws std::logic_error if the socket is not open. + @throws std::system_error on failure. + */ + template + Option get_option() const + { + if (!is_open()) + detail::throw_logic_error("get_option: socket not open"); + Option opt{}; + std::size_t sz = opt.size(); + std::error_code ec = + get().get_option(Option::level(), Option::name(), opt.data(), &sz); + if (ec) + detail::throw_system_error( + ec, "local_datagram_socket::get_option"); + opt.resize(sz); + return opt; + } + + /** Assign an existing file descriptor to this socket. + + The fd is adopted and registered with the platform reactor. + + @param fd The file descriptor to adopt. + + @par Preconditions + The socket must not already be open. + + @throws std::logic_error if the precondition is violated + (the socket is already open). + @throws std::system_error on any other failure (e.g. the + fd is not an AF_UNIX datagram socket, or backend + configuration fails). + */ + void assign(int fd); + + /** Get the local endpoint of the socket. + + @return The local endpoint, or a default endpoint if not bound. + */ + corosio::local_endpoint local_endpoint() const noexcept; + + /** Get the remote endpoint of the socket. + + Returns the address of the connected peer. + + @return The remote endpoint, or a default endpoint if + not connected. + */ + corosio::local_endpoint remote_endpoint() const noexcept; + +protected: + /// Default-construct (for derived types). + local_datagram_socket() noexcept = default; + + /// Construct from a pre-built handle. + explicit local_datagram_socket(handle h) noexcept + : io_object(std::move(h)) + { + } + +private: + void open_for_family(int family, int type, int protocol); + + inline implementation& get() const noexcept + { + return *static_cast(h_.get()); + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_DATAGRAM_SOCKET_HPP diff --git a/include/boost/corosio/local_endpoint.hpp b/include/boost/corosio/local_endpoint.hpp new file mode 100644 index 000000000..74276bd21 --- /dev/null +++ b/include/boost/corosio/local_endpoint.hpp @@ -0,0 +1,127 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_ENDPOINT_HPP +#define BOOST_COROSIO_LOCAL_ENDPOINT_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::corosio { + +/** A Unix domain socket endpoint (filesystem path). + + Stores the path in a fixed-size buffer, avoiding heap + allocation. The object is trivially copyable. + + Abstract sockets (Linux-only) are represented by paths whose + first character is '\0'. The full path including the leading + null byte is stored. + + The library does NOT automatically unlink the socket path on + close — callers are responsible for cleanup. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Safe. +*/ +class BOOST_COROSIO_DECL local_endpoint +{ + // sun_path is 108 on Linux, 104 on macOS/FreeBSD. Use the + // minimum so local_endpoint is portable across all three. + char path_[104]{}; + std::uint8_t len_ = 0; + +public: + /// Maximum path length for a Unix domain socket (excluding null terminator). + static constexpr std::size_t max_path_length = 103; + + /// Default constructor. Creates an empty (unbound) endpoint. + local_endpoint() noexcept = default; + + /** Construct from a path. + + @param path The filesystem path for the socket. + Must not exceed @ref max_path_length bytes. + + @throws std::system_error if the path is too long. + */ + explicit local_endpoint(std::string_view path); + + /** Construct from a path (no-throw). + + @param path The filesystem path for the socket. + @param ec Set to an error if the path is too long. + */ + local_endpoint(std::string_view path, std::error_code& ec) noexcept; + + /// Return the socket path. + std::string_view path() const noexcept + { + return std::string_view(path_, len_); + } + + /** Check if this is an abstract socket (Linux-only). + + Abstract sockets live in a kernel namespace rather than + the filesystem. They are identified by a leading null byte + in the path. + + @return `true` if the path starts with '\0'. + */ + bool is_abstract() const noexcept + { + return len_ > 0 && path_[0] == '\0'; + } + + /// Return true if the endpoint has no path. + bool empty() const noexcept + { + return len_ == 0; + } + + /// Compare endpoints for equality. + friend bool + operator==(local_endpoint const& a, local_endpoint const& b) noexcept + { + return a.len_ == b.len_ && + std::memcmp(a.path_, b.path_, a.len_) == 0; + } + + /** Format the endpoint for stream output. + + Non-abstract paths are printed as-is. Abstract paths + (leading null byte) are printed as `[abstract:name]`. + Empty endpoints produce no output. + */ + friend BOOST_COROSIO_DECL std::ostream& + operator<<(std::ostream& os, local_endpoint const& ep); + + /// Lexicographic ordering on stored path bytes. + friend std::strong_ordering + operator<=>(local_endpoint const& a, local_endpoint const& b) noexcept + { + auto common = (std::min)(a.len_, b.len_); + if (int cmp = std::memcmp(a.path_, b.path_, common); cmp != 0) + return cmp <=> 0; + return a.len_ <=> b.len_; + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_ENDPOINT_HPP diff --git a/include/boost/corosio/local_socket_pair.hpp b/include/boost/corosio/local_socket_pair.hpp new file mode 100644 index 000000000..4c1dc0b11 --- /dev/null +++ b/include/boost/corosio/local_socket_pair.hpp @@ -0,0 +1,60 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP +#define BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP + +#include +#include + +#if BOOST_COROSIO_POSIX + +#include +#include + +#include + +namespace boost::corosio { + +class io_context; + +/** Create a connected pair of local stream sockets. + + Uses `socketpair` to create two pre-connected Unix domain + stream sockets. Data written to one can be read from the + other. + + @param ctx The I/O context for the sockets. + + @return A pair of connected local stream sockets. + + @throws std::system_error on failure. +*/ +BOOST_COROSIO_DECL std::pair +make_local_stream_pair(io_context& ctx); + +/** Create a connected pair of local datagram sockets. + + Uses `socketpair` to create two pre-connected Unix domain + datagram sockets. + + @param ctx The I/O context for the sockets. + + @return A pair of connected local datagram sockets. + + @throws std::system_error on failure. +*/ +BOOST_COROSIO_DECL std::pair +make_local_datagram_pair(io_context& ctx); + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX + +#endif // BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP diff --git a/include/boost/corosio/local_stream.hpp b/include/boost/corosio/local_stream.hpp new file mode 100644 index 000000000..66f9c2f6b --- /dev/null +++ b/include/boost/corosio/local_stream.hpp @@ -0,0 +1,58 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_STREAM_HPP +#define BOOST_COROSIO_LOCAL_STREAM_HPP + +#include + +namespace boost::corosio { + +class local_stream_socket; +class local_stream_acceptor; + +/** Encapsulate the Unix stream protocol for socket creation. + + This class identifies the Unix domain stream protocol. It is + used to parameterize socket and acceptor `open()` calls with a + self-documenting type. + + The `family()`, `type()`, and `protocol()` members are + implemented in the compiled library to avoid exposing + platform socket headers. For an inline variant that includes + platform headers, use @ref native_local_stream. + + @par Preconditions + Unix domain sockets are POSIX-only. This protocol is + meaningful only on platforms that support Unix domain sockets. + + @see native_local_stream, local_stream_socket, local_stream_acceptor +*/ +class BOOST_COROSIO_DECL local_stream +{ +public: + /// Return the Unix domain address family. + static int family() noexcept; + + /// Return the stream socket type. + static int type() noexcept; + + /// Return the protocol number (default 0 for Unix domain). + static int protocol() noexcept; + + /// The associated socket type. + using socket = local_stream_socket; + + /// The associated acceptor type. + using acceptor = local_stream_acceptor; +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_STREAM_HPP diff --git a/include/boost/corosio/local_stream_acceptor.hpp b/include/boost/corosio/local_stream_acceptor.hpp new file mode 100644 index 000000000..be9f697ca --- /dev/null +++ b/include/boost/corosio/local_stream_acceptor.hpp @@ -0,0 +1,401 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_STREAM_ACCEPTOR_HPP +#define BOOST_COROSIO_LOCAL_STREAM_ACCEPTOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace boost::corosio { + +/** Options for @ref local_stream_acceptor::bind. */ +enum class bind_option +{ + /// Default: do not unlink the socket path. + none, + /// Unlink the socket path before binding (ignored for abstract paths). + unlink_existing +}; + +/** An asynchronous Unix stream acceptor for coroutine I/O. + + This class provides asynchronous Unix domain stream accept + operations that return awaitable types. The acceptor binds + to a local endpoint (filesystem path) and listens for + incoming connections. + + The library does NOT automatically unlink the socket path + on close. Callers are responsible for removing the socket + file before bind or after close, or pass + @ref bind_option::unlink_existing to @ref bind. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. An acceptor must not have concurrent + accept operations. + + @par Semantics + Wraps the platform Unix domain socket listener. Operations + dispatch to OS accept APIs via the io_context reactor. + Cancellation propagates through the IoAwaitable stop_token + or via @ref cancel; cancelled operations resume with + `errc::operation_canceled`. + + @par Example + @code + io_context ioc; + local_stream_acceptor acc(ioc); + acc.open(); + if (auto ec = acc.bind( + local_endpoint("/tmp/my.sock"), + bind_option::unlink_existing)) + return ec; + if (auto ec = acc.listen()) + return ec; + + local_stream_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + if (!ec) { + // peer is now connected + } + @endcode + + @see local_stream_socket, local_endpoint, local_stream +*/ +class BOOST_COROSIO_DECL local_stream_acceptor : public io_object +{ + struct move_accept_awaitable + { + local_stream_acceptor& acc_; + std::stop_token token_; + mutable std::error_code ec_; + mutable io_object::implementation* peer_impl_ = nullptr; + + explicit move_accept_awaitable( + local_stream_acceptor& acc) noexcept + : acc_(acc) + { + } + + bool await_ready() const noexcept + { + return token_.stop_requested(); + } + + capy::io_result await_resume() const noexcept + { + if (token_.stop_requested()) + return {make_error_code(std::errc::operation_canceled), + local_stream_socket()}; + + if (ec_ || !peer_impl_) + return {ec_, local_stream_socket()}; + + local_stream_socket peer(acc_.ctx_); + reset_peer_impl(peer, peer_impl_); + return {ec_, std::move(peer)}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + if (token_.stop_requested()) + { + ec_ = make_error_code(std::errc::operation_canceled); + return h; + } + return acc_.get().accept( + h, env->executor, token_, &ec_, &peer_impl_); + } + }; + + struct accept_awaitable + { + local_stream_acceptor& acc_; + local_stream_socket& peer_; + std::stop_token token_; + mutable std::error_code ec_; + mutable io_object::implementation* peer_impl_ = nullptr; + + accept_awaitable( + local_stream_acceptor& acc, local_stream_socket& peer) noexcept + : acc_(acc) + , peer_(peer) + { + } + + bool await_ready() const noexcept + { + return token_.stop_requested(); + } + + capy::io_result<> await_resume() const noexcept + { + if (token_.stop_requested()) + return {make_error_code(std::errc::operation_canceled)}; + + if (!ec_ && peer_impl_) + peer_.h_.reset(peer_impl_); + return {ec_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + if (token_.stop_requested()) + { + ec_ = make_error_code(std::errc::operation_canceled); + return h; + } + return acc_.get().accept( + h, env->executor, token_, &ec_, &peer_impl_); + } + }; + +public: + ~local_stream_acceptor() override; + + explicit local_stream_acceptor(capy::execution_context& ctx); + + template + requires(!std::same_as, local_stream_acceptor>) && + capy::Executor + explicit local_stream_acceptor(Ex const& ex) : local_stream_acceptor(ex.context()) + { + } + + local_stream_acceptor(local_stream_acceptor&& other) noexcept + : local_stream_acceptor(other.ctx_, std::move(other)) + { + } + + local_stream_acceptor& operator=(local_stream_acceptor&& other) noexcept + { + assert(&ctx_ == &other.ctx_ && + "move-assign requires the same execution_context"); + if (this != &other) + { + close(); + io_object::operator=(std::move(other)); + } + return *this; + } + + local_stream_acceptor(local_stream_acceptor const&) = delete; + local_stream_acceptor& operator=(local_stream_acceptor const&) = delete; + + /** Create the acceptor socket. + + @param proto The protocol. Defaults to local_stream{}. + + @throws std::system_error on failure. + */ + void open(local_stream proto = {}); + + /** Bind to a local endpoint. + + @param ep The local endpoint (path) to bind to. + @param opt Bind options. Pass bind_option::unlink_existing + to unlink the socket path before binding (ignored for + abstract sockets and empty endpoints). + + @return An error code on failure, empty on success. + + @throws std::logic_error if the acceptor is not open. + */ + [[nodiscard]] std::error_code + bind(corosio::local_endpoint ep, + bind_option opt = bind_option::none); + + /** Start listening for incoming connections. + + @param backlog The maximum pending connection queue length. + + @return An error code on failure, empty on success. + + @throws std::logic_error if the acceptor is not open. + */ + [[nodiscard]] std::error_code listen(int backlog = 128); + + /// Close the acceptor. + void close(); + + /** Check if the acceptor has a native handle. + + Returns true once @ref open succeeds and until @ref close is + called. This does not indicate that @ref listen has been + invoked — an open-but-not-listening acceptor will still + report `true`. + */ + bool is_open() const noexcept + { + return h_ && get().is_open(); + } + + /** Initiate an asynchronous accept operation. + + @param peer The socket to receive the accepted connection. + + @return An awaitable that completes with io_result<>. + + @throws std::logic_error if the native acceptor handle is + absent (i.e., `!is_open()`). Calling accept on an + open-but-not-listening acceptor does not throw; the + awaitable completes with a kernel error such as + `errc::invalid_argument` (EINVAL). + */ + auto accept(local_stream_socket& peer) + { + if (!is_open()) + detail::throw_logic_error("accept: acceptor not open"); + return accept_awaitable(*this, peer); + } + + /** Initiate an asynchronous accept, returning the socket. + + @return An awaitable that completes with + io_result. + + @throws std::logic_error if the native acceptor handle is + absent (i.e., `!is_open()`). Calling accept on an + open-but-not-listening acceptor does not throw; the + awaitable completes with a kernel error such as + `errc::invalid_argument` (EINVAL). + */ + auto accept() + { + if (!is_open()) + detail::throw_logic_error("accept: acceptor not open"); + return move_accept_awaitable(*this); + } + + void cancel(); + + /** Release ownership of the native socket handle. + + Deregisters the acceptor from the reactor and cancels + pending operations without closing the fd. + + @return The native handle. + + @throws std::logic_error if the acceptor is not open. + */ + native_handle_type release(); + + corosio::local_endpoint local_endpoint() const noexcept; + + template + void set_option(Option const& opt) + { + if (!is_open()) + detail::throw_logic_error("set_option: acceptor not open"); + std::error_code ec = get().set_option( + Option::level(), Option::name(), opt.data(), opt.size()); + if (ec) + detail::throw_system_error(ec, "local_stream_acceptor::set_option"); + } + + template + Option get_option() const + { + if (!is_open()) + detail::throw_logic_error("get_option: acceptor not open"); + Option opt{}; + std::size_t sz = opt.size(); + std::error_code ec = + get().get_option(Option::level(), Option::name(), opt.data(), &sz); + if (ec) + detail::throw_system_error(ec, "local_stream_acceptor::get_option"); + opt.resize(sz); + return opt; + } + + /** Define backend hooks for local stream acceptor operations. */ + struct implementation : io_object::implementation + { + virtual std::coroutine_handle<> accept( + std::coroutine_handle<>, + capy::executor_ref, + std::stop_token, + std::error_code*, + io_object::implementation**) = 0; + + virtual corosio::local_endpoint local_endpoint() const noexcept = 0; + + virtual bool is_open() const noexcept = 0; + + virtual native_handle_type release_socket() noexcept = 0; + + virtual void cancel() noexcept = 0; + + virtual std::error_code set_option( + int level, + int optname, + void const* data, + std::size_t size) noexcept = 0; + + virtual std::error_code + get_option(int level, int optname, void* data, std::size_t* size) + const noexcept = 0; + }; + +protected: + local_stream_acceptor(handle h, capy::execution_context& ctx) noexcept + : io_object(std::move(h)) + , ctx_(ctx) + { + } + + local_stream_acceptor( + capy::execution_context& ctx, local_stream_acceptor&& other) noexcept + : io_object(std::move(other)) + , ctx_(ctx) + { + } + + static void reset_peer_impl( + local_stream_socket& peer, io_object::implementation* impl) noexcept + { + if (impl) + peer.h_.reset(impl); + } + +private: + capy::execution_context& ctx_; + + inline implementation& get() const noexcept + { + return *static_cast(h_.get()); + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_STREAM_ACCEPTOR_HPP diff --git a/include/boost/corosio/local_stream_socket.hpp b/include/boost/corosio/local_stream_socket.hpp new file mode 100644 index 000000000..ea8a5d004 --- /dev/null +++ b/include/boost/corosio/local_stream_socket.hpp @@ -0,0 +1,339 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_STREAM_SOCKET_HPP +#define BOOST_COROSIO_LOCAL_STREAM_SOCKET_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace boost::corosio { + +/** An asynchronous Unix stream socket for coroutine I/O. + + This class provides asynchronous Unix domain stream socket + operations that return awaitable types. Each operation + participates in the affine awaitable protocol, ensuring + coroutines resume on the correct executor. + + The socket must be opened before performing I/O operations. + Operations support cancellation through std::stop_token via + the affine protocol, or explicitly through cancel(). + + Satisfies capy::Stream. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. A socket must not have concurrent + operations of the same type. One read and one write may be + in flight simultaneously. +*/ +class BOOST_COROSIO_DECL local_stream_socket : public io_stream +{ +public: + using shutdown_type = corosio::shutdown_type; + using enum corosio::shutdown_type; + + /** Define backend hooks for local stream socket operations. + + Platform backends (epoll, kqueue, select) derive from this + to implement socket I/O, connection, and option management. + */ + struct implementation : io_stream::implementation + { + virtual std::coroutine_handle<> connect( + std::coroutine_handle<> h, + capy::executor_ref ex, + corosio::local_endpoint ep, + std::stop_token token, + std::error_code* ec) = 0; + + virtual std::error_code shutdown(shutdown_type what) noexcept = 0; + + virtual native_handle_type native_handle() const noexcept = 0; + + virtual native_handle_type release_socket() noexcept = 0; + + virtual void cancel() noexcept = 0; + + virtual std::error_code set_option( + int level, + int optname, + void const* data, + std::size_t size) noexcept = 0; + + virtual std::error_code + get_option(int level, int optname, void* data, std::size_t* size) + const noexcept = 0; + + virtual corosio::local_endpoint local_endpoint() const noexcept = 0; + + virtual corosio::local_endpoint remote_endpoint() const noexcept = 0; + }; + + /// Represent the awaitable returned by connect. + struct connect_awaitable + : detail::void_op_base + { + local_stream_socket& s_; + corosio::local_endpoint endpoint_; + + connect_awaitable( + local_stream_socket& s, corosio::local_endpoint ep) noexcept + : s_(s), endpoint_(ep) {} + + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const + { + return s_.get().connect(h, ex, endpoint_, token_, &ec_); + } + }; + +public: + ~local_stream_socket() override; + + explicit local_stream_socket(capy::execution_context& ctx); + + template + requires(!std::same_as, local_stream_socket>) && + capy::Executor + explicit local_stream_socket(Ex const& ex) : local_stream_socket(ex.context()) + { + } + + local_stream_socket(local_stream_socket&& other) noexcept + : io_object(std::move(other)) + { + } + + local_stream_socket& operator=(local_stream_socket&& other) noexcept + { + if (this != &other) + { + close(); + io_object::operator=(std::move(other)); + } + return *this; + } + + local_stream_socket(local_stream_socket const&) = delete; + local_stream_socket& operator=(local_stream_socket const&) = delete; + + /** Open the socket. + + Creates a Unix stream socket and associates it with + the platform reactor. + + @param proto The protocol. Defaults to local_stream{}. + + @throws std::system_error on failure. + */ + void open(local_stream proto = {}); + + /// Close the socket. + void close(); + + /// Check if the socket is open. + bool is_open() const noexcept + { + return h_ && get().native_handle() >= 0; + } + + /** Initiate an asynchronous connect operation. + + If the socket is not already open, it is opened automatically. + + @param ep The peer endpoint (path) to connect to. + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + + @return An awaitable that completes with io_result<>. + + @throws std::system_error if the socket needs to be opened + and the open fails. + */ + auto connect(corosio::local_endpoint ep) + { + if (!is_open()) + open(); + return connect_awaitable(*this, ep); + } + + void cancel(); + + native_handle_type native_handle() const noexcept; + + /** Query the number of bytes available for reading. + + @return The number of bytes that can be read without blocking. + + @throws std::logic_error if the socket is not open. + @throws std::system_error on ioctl failure. + */ + std::size_t available() const; + + /** Release ownership of the native socket handle. + + Deregisters the socket from the reactor and cancels pending + operations without closing the fd. The caller takes ownership + of the returned descriptor. + + @return The native handle. + + @throws std::logic_error if the socket is not open. + */ + native_handle_type release(); + + /** Shut down part or all of the socket (best-effort). + + Unix stream sockets are full-duplex: each direction (send and + receive) operates independently. This function allows you to + close one or both directions without destroying the socket. + + @li @ref shutdown_send signals end-of-stream to the peer: their + subsequent reads will complete with `capy::cond::eof` after + they drain any data already in flight. You can still + receive data from the peer until they also close their + send direction. This is the cleanest way to end a session + — preferable to @ref close() because it gives the peer an + explicit EOF rather than tearing the socket down abruptly. + + @li @ref shutdown_receive disables reading on the socket. The + peer is not informed and may continue sending; data + already buffered or arriving later is discarded. + + @li @ref shutdown_both combines both effects. + + When the peer shuts down their send direction, subsequent read + operations on this socket complete with `capy::cond::eof`. Use + the portable condition test rather than comparing error codes + directly: + + @code + auto [ec, n] = co_await sock.read_some(buffer); + if (ec == capy::cond::eof) + { + // Peer closed their send direction + } + @endcode + + Calls `::shutdown` on the underlying descriptor when open. + Errors from the syscall (such as `ENOTCONN` on a peer that + already closed) are swallowed because they are typically + unhelpful at this layer; if the socket is not open, the call + is a no-op. To observe errors, use the + @ref shutdown(shutdown_type,std::error_code&) overload. + + @param what Which direction to shut down. + */ + void shutdown(shutdown_type what); + + /** Shut down part or all of the socket (non-throwing). + + Same semantics as @ref shutdown(shutdown_type) but reports + syscall errors via @p ec instead of swallowing them. + + @param what Which direction to shut down. + @param ec Set to the error code on failure. + */ + void shutdown(shutdown_type what, std::error_code& ec) noexcept; + + template + void set_option(Option const& opt) + { + if (!is_open()) + detail::throw_logic_error("set_option: socket not open"); + std::error_code ec = get().set_option( + Option::level(), Option::name(), opt.data(), opt.size()); + if (ec) + detail::throw_system_error(ec, "local_stream_socket::set_option"); + } + + template + Option get_option() const + { + if (!is_open()) + detail::throw_logic_error("get_option: socket not open"); + Option opt{}; + std::size_t sz = opt.size(); + std::error_code ec = + get().get_option(Option::level(), Option::name(), opt.data(), &sz); + if (ec) + detail::throw_system_error(ec, "local_stream_socket::get_option"); + opt.resize(sz); + return opt; + } + + /** Assign an existing file descriptor to this socket. + + The fd is adopted and registered with the platform reactor. + Used by @ref make_local_stream_pair to wrap `socketpair` + fds. + + @param fd The file descriptor to adopt. Must be a valid, + open Unix domain stream socket. + + @par Preconditions + The socket must not already be open. + + @throws std::logic_error if the precondition is violated + (the socket is already open). + @throws std::system_error on any other failure (e.g. the + fd is not a Unix domain stream socket, or backend + configuration fails). + */ + void assign(int fd); + + corosio::local_endpoint local_endpoint() const noexcept; + + corosio::local_endpoint remote_endpoint() const noexcept; + +protected: + local_stream_socket() noexcept = default; + + explicit local_stream_socket(handle h) noexcept : io_object(std::move(h)) {} + +private: + friend class local_stream_acceptor; + + void open_for_family(int family, int type, int protocol); + + inline implementation& get() const noexcept + { + return *static_cast(h_.get()); + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_LOCAL_STREAM_SOCKET_HPP diff --git a/include/boost/corosio/message_flags.hpp b/include/boost/corosio/message_flags.hpp new file mode 100644 index 000000000..5f7284fd8 --- /dev/null +++ b/include/boost/corosio/message_flags.hpp @@ -0,0 +1,61 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_MESSAGE_FLAGS_HPP +#define BOOST_COROSIO_MESSAGE_FLAGS_HPP + +namespace boost::corosio { + +/** Flags for datagram send/recv operations. + + Platform-agnostic flag values that are mapped to native + socket constants (`MSG_PEEK`, `MSG_OOB`, `MSG_DONTROUTE`) + at the syscall boundary in the reactor implementation. + + Values may be combined with `operator|`, masked with + `operator&`, and inverted with `operator~`. +*/ +enum class message_flags : int +{ + /// No flags set. + none = 0, + /// Peek at incoming data without removing it from the queue (`MSG_PEEK`). + peek = 1, + /// Send or receive out-of-band data (`MSG_OOB`). + out_of_band = 2, + /// Bypass routing tables; send only to directly connected hosts (`MSG_DONTROUTE`). + do_not_route = 4 +}; + +/// Bitwise OR — combine two flag sets. +inline constexpr message_flags +operator|(message_flags a, message_flags b) noexcept +{ + return static_cast( + static_cast(a) | static_cast(b)); +} + +/// Bitwise AND — intersect two flag sets. +inline constexpr message_flags +operator&(message_flags a, message_flags b) noexcept +{ + return static_cast( + static_cast(a) & static_cast(b)); +} + +/// Bitwise NOT — invert all flag bits. +inline constexpr message_flags +operator~(message_flags a) noexcept +{ + return static_cast(~static_cast(a)); +} + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_MESSAGE_FLAGS_HPP diff --git a/include/boost/corosio/native/detail/endpoint_convert.hpp b/include/boost/corosio/native/detail/endpoint_convert.hpp index 590a6db96..0f80a901c 100644 --- a/include/boost/corosio/native/detail/endpoint_convert.hpp +++ b/include/boost/corosio/native/detail/endpoint_convert.hpp @@ -11,12 +11,14 @@ #define BOOST_COROSIO_NATIVE_DETAIL_ENDPOINT_CONVERT_HPP #include +#include #include #include #if BOOST_COROSIO_POSIX #include +#include #include #include #else @@ -227,6 +229,141 @@ socket_family( return storage.ss_family; } +//---------------------------------------------------------- +// local_endpoint (AF_UNIX) conversions +//---------------------------------------------------------- + +#if BOOST_COROSIO_POSIX + +/** Convert a local_endpoint to sockaddr_storage. + + @param ep The local endpoint to convert. + @param storage Output parameter filled with the sockaddr_un. + @return The length of the filled sockaddr structure. +*/ +inline socklen_t +to_sockaddr(local_endpoint const& ep, sockaddr_storage& storage) noexcept +{ + std::memset(&storage, 0, sizeof(storage)); + sockaddr_un sa{}; + sa.sun_family = AF_UNIX; + auto path = ep.path(); + auto copy_len = (std::min)(path.size(), sizeof(sa.sun_path)); + if (copy_len > 0) + std::memcpy(sa.sun_path, path.data(), copy_len); + std::memcpy(&storage, &sa, sizeof(sa)); + + if (ep.is_abstract()) + return static_cast( + offsetof(sockaddr_un, sun_path) + path.size()); + return static_cast(sizeof(sa)); +} + +/** Convert a local_endpoint to sockaddr_storage (family-aware overload). + + The socket_family parameter is ignored for Unix sockets since + there is no dual-stack mapping. + + @param ep The local endpoint to convert. + @param socket_family Ignored. + @param storage Output parameter filled with the sockaddr_un. + @return The length of the filled sockaddr structure. +*/ +inline socklen_t +to_sockaddr( + local_endpoint const& ep, + int /*socket_family*/, + sockaddr_storage& storage) noexcept +{ + return to_sockaddr(ep, storage); +} + +/** Create a local_endpoint from sockaddr_storage. + + @param storage The sockaddr_storage (must have ss_family == AF_UNIX). + @param len The address length returned by the kernel. + @return A local_endpoint with the path extracted from the + sockaddr_un, or an empty endpoint if the family is not AF_UNIX. +*/ +inline local_endpoint +from_sockaddr_local( + sockaddr_storage const& storage, socklen_t len) noexcept +{ + if (storage.ss_family != AF_UNIX) + return local_endpoint{}; + + sockaddr_un sa{}; + auto bytes_copied = + (std::min)(static_cast(len), sizeof(sa)); + std::memcpy(&sa, &storage, bytes_copied); + + // Derive path_len from bytes_copied (NOT len) so memchr and the + // string_view below can never read past sa.sun_path. The kernel + // can return an addrlen larger than sizeof(sockaddr_un) (e.g., + // sizeof(sockaddr_storage) from a misbehaving caller); without + // clamping, the OOB read could expose adjacent stack bytes. + auto path_offset = offsetof(sockaddr_un, sun_path); + if (bytes_copied <= path_offset) + return local_endpoint{}; + + auto path_len = bytes_copied - path_offset; + + // Non-abstract paths may be null-terminated by the kernel + if (path_len > 0 && sa.sun_path[0] != '\0') + { + auto* end = static_cast( + std::memchr(sa.sun_path, '\0', path_len)); + if (end) + path_len = static_cast(end - sa.sun_path); + } + + std::error_code ec; + local_endpoint ep(std::string_view(sa.sun_path, path_len), ec); + if (ec) + return local_endpoint{}; + return ep; +} + +#endif // BOOST_COROSIO_POSIX + +//---------------------------------------------------------- +// Tag-dispatch helpers for templatized reactor code. +// Overload resolution selects the correct conversion based +// on the Endpoint type. +//---------------------------------------------------------- + +/** Convert sockaddr_storage to an IP endpoint (tag overload). + + @param storage The sockaddr_storage with fields in network byte order. + @param len The address length returned by the kernel. + @return An endpoint with address and port extracted from storage. +*/ +inline endpoint +from_sockaddr_as( + sockaddr_storage const& storage, socklen_t /*len*/, endpoint const&) noexcept +{ + return from_sockaddr(storage); +} + +#if BOOST_COROSIO_POSIX + +/** Convert sockaddr_storage to a local_endpoint (tag overload). + + @param storage The sockaddr_storage. + @param len The address length returned by the kernel. + @return A local_endpoint with path extracted from storage. +*/ +inline local_endpoint +from_sockaddr_as( + sockaddr_storage const& storage, + socklen_t len, + local_endpoint const&) noexcept +{ + return from_sockaddr_local(storage, len); +} + +#endif // BOOST_COROSIO_POSIX + } // namespace boost::corosio::detail #endif diff --git a/include/boost/corosio/native/detail/epoll/epoll_op.hpp b/include/boost/corosio/native/detail/epoll/epoll_op.hpp index 76f923195..e3736af95 100644 --- a/include/boost/corosio/native/detail/epoll/epoll_op.hpp +++ b/include/boost/corosio/native/detail/epoll/epoll_op.hpp @@ -14,127 +14,14 @@ #if BOOST_COROSIO_HAS_EPOLL -#include #include -/* - epoll Operation State - ===================== - - Each async I/O operation has a corresponding epoll_op-derived struct that - holds the operation's state while it's in flight. The socket impl owns - fixed slots for each operation type (conn_, rd_, wr_), so only one - operation of each type can be pending per socket at a time. - - Persistent Registration - ----------------------- - File descriptors are registered with epoll once (via descriptor_state) and - stay registered until closed. The descriptor_state tracks which operations - are pending (read_op, write_op, connect_op). When an event arrives, the - reactor dispatches to the appropriate pending operation. - - Impl Lifetime Management - ------------------------ - When cancel() posts an op to the scheduler's ready queue, the socket impl - might be destroyed before the scheduler processes the op. The `impl_ptr` - member holds a shared_ptr to the impl, keeping it alive until the op - completes. This is set by cancel() and cleared in operator() after the - coroutine is resumed. - - EOF Detection - ------------- - For reads, 0 bytes with no error means EOF. But an empty user buffer also - returns 0 bytes. The `empty_buffer_read` flag distinguishes these cases. - - SIGPIPE Prevention - ------------------ - Writes use sendmsg() with MSG_NOSIGNAL instead of writev() to prevent - SIGPIPE when the peer has closed. -*/ - namespace boost::corosio::detail { -// Forward declarations -class epoll_tcp_socket; -class epoll_tcp_acceptor; -struct epoll_op; - -// Forward declaration -class epoll_scheduler; - /// Per-descriptor state for persistent epoll registration. struct descriptor_state final : reactor_descriptor_state {}; -/// epoll base operation — thin wrapper over reactor_op. -struct epoll_op : reactor_op -{ - void operator()() override; -}; - -/// epoll connect operation. -struct epoll_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// epoll scatter-read operation. -struct epoll_read_op final : reactor_read_op -{ - void cancel() noexcept override; -}; - -/** Provides sendmsg(MSG_NOSIGNAL) with EINTR retry for epoll writes. */ -struct epoll_write_policy -{ - static ssize_t write(int fd, iovec* iovecs, int count) noexcept - { - msghdr msg{}; - msg.msg_iov = iovecs; - msg.msg_iovlen = static_cast(count); - - ssize_t n; - do - { - n = ::sendmsg(fd, &msg, MSG_NOSIGNAL); - } - while (n < 0 && errno == EINTR); - return n; - } -}; - -/// epoll gather-write operation. -struct epoll_write_op final : reactor_write_op -{ - void cancel() noexcept override; -}; - -/** Provides accept4(SOCK_NONBLOCK|SOCK_CLOEXEC) with EINTR retry. */ -struct epoll_accept_policy -{ - static int do_accept(int fd, sockaddr_storage& peer) noexcept - { - socklen_t addrlen = sizeof(peer); - int new_fd; - do - { - new_fd = ::accept4( - fd, reinterpret_cast(&peer), &addrlen, - SOCK_NONBLOCK | SOCK_CLOEXEC); - } - while (new_fd < 0 && errno == EINTR); - return new_fd; - } -}; - -/// epoll accept operation. -struct epoll_accept_op final : reactor_accept_op -{ - void operator()() override; - void cancel() noexcept override; -}; - } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_EPOLL diff --git a/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor.hpp b/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor.hpp deleted file mode 100644 index e5ec50108..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include -#include - -namespace boost::corosio::detail { - -class epoll_tcp_acceptor_service; - -/// Acceptor implementation for epoll backend. -class epoll_tcp_acceptor final - : public reactor_acceptor< - epoll_tcp_acceptor, - epoll_tcp_acceptor_service, - epoll_op, - epoll_accept_op, - descriptor_state> -{ - friend class epoll_tcp_acceptor_service; - -public: - explicit epoll_tcp_acceptor(epoll_tcp_acceptor_service& svc) noexcept; - - std::coroutine_handle<> accept( - std::coroutine_handle<>, - capy::executor_ref, - std::stop_token, - std::error_code*, - io_object::implementation**) override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor_service.hpp b/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor_service.hpp deleted file mode 100644 index 137581784..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_tcp_acceptor_service.hpp +++ /dev/null @@ -1,364 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/// State for epoll acceptor service. -using epoll_tcp_acceptor_state = - reactor_service_state; - -/** epoll acceptor service implementation. - - Inherits from tcp_acceptor_service to enable runtime polymorphism. - Uses key_type = tcp_acceptor_service for service lookup. -*/ -class BOOST_COROSIO_DECL epoll_tcp_acceptor_service final - : public tcp_acceptor_service -{ -public: - explicit epoll_tcp_acceptor_service( - capy::execution_context& ctx, epoll_tcp_service& tcp_svc); - ~epoll_tcp_acceptor_service() override; - - epoll_tcp_acceptor_service(epoll_tcp_acceptor_service const&) = delete; - epoll_tcp_acceptor_service& - operator=(epoll_tcp_acceptor_service const&) = delete; - - void shutdown() override; - - io_object::implementation* construct() override; - void destroy(io_object::implementation*) override; - void close(io_object::handle&) override; - std::error_code open_acceptor_socket( - tcp_acceptor::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_acceptor(tcp_acceptor::implementation& impl, endpoint ep) override; - std::error_code - listen_acceptor(tcp_acceptor::implementation& impl, int backlog) override; - - epoll_scheduler& scheduler() const noexcept - { - return state_->sched_; - } - void post(scheduler_op* op); - void work_started() noexcept; - void work_finished() noexcept; - - /** Get the TCP service for creating peer sockets during accept. */ - epoll_tcp_service* tcp_service() const noexcept; - -private: - epoll_tcp_service* tcp_svc_; - std::unique_ptr state_; -}; - -inline void -epoll_accept_op::cancel() noexcept -{ - if (acceptor_impl_) - acceptor_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_accept_op::operator()() -{ - complete_accept_op(*this); -} - -inline epoll_tcp_acceptor::epoll_tcp_acceptor( - epoll_tcp_acceptor_service& svc) noexcept - : reactor_acceptor(svc) -{ -} - -inline std::coroutine_handle<> -epoll_tcp_acceptor::accept( - std::coroutine_handle<> h, - capy::executor_ref ex, - std::stop_token token, - std::error_code* ec, - io_object::implementation** impl_out) -{ - auto& op = acc_; - op.reset(); - op.h = h; - op.ex = ex; - op.ec_out = ec; - op.impl_out = impl_out; - op.fd = fd_; - op.start(token, this); - - sockaddr_storage peer_storage{}; - socklen_t addrlen = sizeof(peer_storage); - int accepted; - do - { - accepted = ::accept4( - fd_, reinterpret_cast(&peer_storage), &addrlen, - SOCK_NONBLOCK | SOCK_CLOEXEC); - } - while (accepted < 0 && errno == EINTR); - - if (accepted >= 0) - { - { - std::lock_guard lock(desc_state_.mutex); - desc_state_.read_ready = false; - } - - if (svc_.scheduler().try_consume_inline_budget()) - { - auto* socket_svc = svc_.tcp_service(); - if (socket_svc) - { - auto& impl = - static_cast(*socket_svc->construct()); - impl.set_socket(accepted); - - impl.desc_state_.fd = accepted; - { - std::lock_guard lock(impl.desc_state_.mutex); - impl.desc_state_.read_op = nullptr; - impl.desc_state_.write_op = nullptr; - impl.desc_state_.connect_op = nullptr; - } - socket_svc->scheduler().register_descriptor( - accepted, &impl.desc_state_); - - impl.set_endpoints( - local_endpoint_, from_sockaddr(peer_storage)); - - *ec = {}; - if (impl_out) - *impl_out = &impl; - } - else - { - ::close(accepted); - *ec = make_err(ENOENT); - if (impl_out) - *impl_out = nullptr; - } - op.cont_op.cont.h = h; - return dispatch_coro(ex, op.cont_op.cont); - } - - op.accepted_fd = accepted; - op.peer_storage = peer_storage; - op.complete(0, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - op.impl_ptr = shared_from_this(); - svc_.work_started(); - - std::lock_guard lock(desc_state_.mutex); - bool io_done = false; - if (desc_state_.read_ready) - { - desc_state_.read_ready = false; - op.perform_io(); - io_done = (op.errn != EAGAIN && op.errn != EWOULDBLOCK); - if (!io_done) - op.errn = 0; - } - - if (io_done || op.cancelled.load(std::memory_order_acquire)) - { - svc_.post(&op); - svc_.work_finished(); - } - else - { - desc_state_.read_op = &op; - } - return std::noop_coroutine(); - } - - op.complete(errno, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - // completion is always posted to scheduler queue, never inline. - return std::noop_coroutine(); -} - -inline void -epoll_tcp_acceptor::cancel() noexcept -{ - do_cancel(); -} - -inline void -epoll_tcp_acceptor::close_socket() noexcept -{ - do_close_socket(); -} - -inline epoll_tcp_acceptor_service::epoll_tcp_acceptor_service( - capy::execution_context& ctx, epoll_tcp_service& tcp_svc) - : tcp_svc_(&tcp_svc) - , state_( - std::make_unique( - ctx.use_service())) -{ -} - -inline epoll_tcp_acceptor_service::~epoll_tcp_acceptor_service() {} - -inline void -epoll_tcp_acceptor_service::shutdown() -{ - std::lock_guard lock(state_->mutex_); - - while (auto* impl = state_->impl_list_.pop_front()) - impl->close_socket(); - - // Don't clear impl_ptrs_ here — same rationale as - // epoll_tcp_service::shutdown(). Let ~state_ release ptrs - // after scheduler shutdown has drained all queued ops. -} - -inline io_object::implementation* -epoll_tcp_acceptor_service::construct() -{ - auto impl = std::make_shared(*this); - auto* raw = impl.get(); - - std::lock_guard lock(state_->mutex_); - state_->impl_ptrs_.emplace(raw, std::move(impl)); - state_->impl_list_.push_back(raw); - - return raw; -} - -inline void -epoll_tcp_acceptor_service::destroy(io_object::implementation* impl) -{ - auto* epoll_impl = static_cast(impl); - epoll_impl->close_socket(); - std::lock_guard lock(state_->mutex_); - state_->impl_list_.remove(epoll_impl); - state_->impl_ptrs_.erase(epoll_impl); -} - -inline void -epoll_tcp_acceptor_service::close(io_object::handle& h) -{ - static_cast(h.get())->close_socket(); -} - -inline std::error_code -epoll_tcp_acceptor_service::open_acceptor_socket( - tcp_acceptor::implementation& impl, int family, int type, int protocol) -{ - auto* epoll_impl = static_cast(&impl); - epoll_impl->close_socket(); - - int fd = ::socket(family, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int val = 0; // dual-stack default - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)); - } - - epoll_impl->fd_ = fd; - - // Set up descriptor state but do NOT register with epoll yet - epoll_impl->desc_state_.fd = fd; - { - std::lock_guard lock(epoll_impl->desc_state_.mutex); - epoll_impl->desc_state_.read_op = nullptr; - } - - return {}; -} - -inline std::error_code -epoll_tcp_acceptor_service::bind_acceptor( - tcp_acceptor::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -inline std::error_code -epoll_tcp_acceptor_service::listen_acceptor( - tcp_acceptor::implementation& impl, int backlog) -{ - return static_cast(&impl)->do_listen(backlog); -} - -inline void -epoll_tcp_acceptor_service::post(scheduler_op* op) -{ - state_->sched_.post(op); -} - -inline void -epoll_tcp_acceptor_service::work_started() noexcept -{ - state_->sched_.work_started(); -} - -inline void -epoll_tcp_acceptor_service::work_finished() noexcept -{ - state_->sched_.work_finished(); -} - -inline epoll_tcp_service* -epoll_tcp_acceptor_service::tcp_service() const noexcept -{ - return tcp_svc_; -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_ACCEPTOR_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp b/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp deleted file mode 100644 index 723ec2e0e..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_tcp_service.hpp +++ /dev/null @@ -1,250 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include - -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include - -/* - epoll Socket Implementation - =========================== - - Each I/O operation follows the same pattern: - 1. Try the syscall immediately (non-blocking socket) - 2. If it succeeds or fails with a real error, post to completion queue - 3. If EAGAIN/EWOULDBLOCK, register with epoll and wait - - This "try first" approach avoids unnecessary epoll round-trips for - operations that can complete immediately (common for small reads/writes - on fast local connections). - - One-Shot Registration - --------------------- - We use one-shot epoll registration: each operation registers, waits for - one event, then unregisters. This simplifies the state machine since we - don't need to track whether an fd is currently registered or handle - re-arming. The tradeoff is slightly more epoll_ctl calls, but the - simplicity is worth it. - - Cancellation - ------------ - See op.hpp for the completion/cancellation race handling via the - `registered` atomic. cancel() must complete pending operations (post - them with cancelled flag) so coroutines waiting on them can resume. - close_socket() calls cancel() first to ensure this. - - Impl Lifetime with shared_ptr - ----------------------------- - Socket impls use enable_shared_from_this. The service owns impls via - shared_ptr maps (impl_ptrs_) keyed by raw pointer for O(1) lookup and - removal. When a user calls close(), we call cancel() which posts pending - ops to the scheduler. - - CRITICAL: The posted ops must keep the impl alive until they complete. - Otherwise the scheduler would process a freed op (use-after-free). The - cancel() method captures shared_from_this() into op.impl_ptr before - posting. When the op completes, impl_ptr is cleared, allowing the impl - to be destroyed if no other references exist. - - Service Ownership - ----------------- - epoll_tcp_service owns all socket impls. destroy_impl() removes the - shared_ptr from the map, but the impl may survive if ops still hold - impl_ptr refs. shutdown() closes all sockets and clears the map; any - in-flight ops will complete and release their refs. -*/ - -namespace boost::corosio::detail { - -/** epoll TCP service implementation. - - Inherits from tcp_service to enable runtime polymorphism. - Uses key_type = tcp_service for service lookup. -*/ -class BOOST_COROSIO_DECL epoll_tcp_service final - : public reactor_socket_service< - epoll_tcp_service, - tcp_service, - epoll_scheduler, - epoll_tcp_socket> -{ -public: - explicit epoll_tcp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_socket( - tcp_socket::implementation& impl, - int family, - int type, - int protocol) override; - - std::error_code - bind_socket(tcp_socket::implementation& impl, endpoint ep) override; -}; - -inline void -epoll_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_read_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_write_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_op::operator()() -{ - complete_io_op(*this); -} - -inline void -epoll_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline epoll_tcp_socket::epoll_tcp_socket(epoll_tcp_service& svc) noexcept - : reactor_stream_socket(svc) -{ -} - -inline epoll_tcp_socket::~epoll_tcp_socket() = default; - -inline std::coroutine_handle<> -epoll_tcp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - return do_connect(h, ex, ep, token, ec); -} - -inline std::coroutine_handle<> -epoll_tcp_socket::read_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_read_some(h, ex, param, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -epoll_tcp_socket::write_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_write_some(h, ex, param, token, ec, bytes_out); -} - -inline void -epoll_tcp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -epoll_tcp_socket::close_socket() noexcept -{ - do_close_socket(); -} - -inline std::error_code -epoll_tcp_service::open_socket( - tcp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* epoll_impl = static_cast(&impl); - epoll_impl->close_socket(); - - int fd = ::socket(family, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int one = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)); - } - - epoll_impl->fd_ = fd; - - // Register fd with epoll (edge-triggered mode) - epoll_impl->desc_state_.fd = fd; - { - std::lock_guard lock(epoll_impl->desc_state_.mutex); - epoll_impl->desc_state_.read_op = nullptr; - epoll_impl->desc_state_.write_op = nullptr; - epoll_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &epoll_impl->desc_state_); - - return {}; -} - -inline std::error_code -epoll_tcp_service::bind_socket( - tcp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_tcp_socket.hpp b/include/boost/corosio/native/detail/epoll/epoll_tcp_socket.hpp deleted file mode 100644 index 3abde6f4a..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_tcp_socket.hpp +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include -#include - -namespace boost::corosio::detail { - -class epoll_tcp_service; - -/// Stream socket implementation for epoll backend. -class epoll_tcp_socket final - : public reactor_stream_socket< - epoll_tcp_socket, - epoll_tcp_service, - epoll_connect_op, - epoll_read_op, - epoll_write_op, - descriptor_state> -{ - friend class epoll_tcp_service; - -public: - explicit epoll_tcp_socket(epoll_tcp_service& svc) noexcept; - ~epoll_tcp_socket() override; - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> read_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> write_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TCP_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_traits.hpp b/include/boost/corosio/native/detail/epoll/epoll_traits.hpp new file mode 100644 index 000000000..32a8dbabf --- /dev/null +++ b/include/boost/corosio/native/detail/epoll/epoll_traits.hpp @@ -0,0 +1,146 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TRAITS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TRAITS_HPP + +#include + +#if BOOST_COROSIO_HAS_EPOLL + +#include +#include + +#include + +#include +#include +#include + +/* epoll backend traits. + + Captures the platform-specific behavior of the Linux epoll backend: + atomic SOCK_NONBLOCK|SOCK_CLOEXEC on socket(), accept4() for + accepted connections, and sendmsg(MSG_NOSIGNAL) for writes. +*/ + +namespace boost::corosio::detail { + +class epoll_scheduler; +struct descriptor_state; + +struct epoll_traits +{ + using scheduler_type = epoll_scheduler; + using desc_state_type = descriptor_state; + + static constexpr bool needs_write_notification = false; + + /// No extra per-socket state or lifecycle hooks needed for epoll. + struct stream_socket_hook + { + std::error_code on_set_option( + int fd, int level, int optname, + void const* data, std::size_t size) noexcept + { + if (::setsockopt( + fd, level, optname, data, + static_cast(size)) != 0) + return make_err(errno); + return {}; + } + static void pre_shutdown(int) noexcept {} + static void pre_destroy(int) noexcept {} + }; + + struct write_policy + { + static ssize_t write(int fd, iovec* iovecs, int count) noexcept + { + msghdr msg{}; + msg.msg_iov = iovecs; + msg.msg_iovlen = static_cast(count); + + ssize_t n; + do + { + n = ::sendmsg(fd, &msg, MSG_NOSIGNAL); + } + while (n < 0 && errno == EINTR); + return n; + } + }; + + struct accept_policy + { + static int do_accept( + int fd, sockaddr_storage& peer, socklen_t& addrlen) noexcept + { + addrlen = sizeof(peer); + int new_fd; + do + { + new_fd = ::accept4( + fd, reinterpret_cast(&peer), &addrlen, + SOCK_NONBLOCK | SOCK_CLOEXEC); + } + while (new_fd < 0 && errno == EINTR); + return new_fd; + } + }; + + /// Create a nonblocking, close-on-exec socket using Linux's atomic flags. + static int create_socket(int family, int type, int protocol) noexcept + { + return ::socket(family, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol); + } + + /// Apply protocol-specific options after socket creation. + /// For IP sockets, sets IPV6_V6ONLY on AF_INET6. + static std::error_code + configure_ip_socket(int fd, int family) noexcept + { + if (family == AF_INET6) + { + int one = 1; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)) != 0) + return make_err(errno); + } + return {}; + } + + /// Apply protocol-specific options for acceptor sockets. + /// For IP acceptors, sets IPV6_V6ONLY=0 (dual-stack). + static std::error_code + configure_ip_acceptor(int fd, int family) noexcept + { + if (family == AF_INET6) + { + int val = 0; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)) != 0) + return make_err(errno); + } + return {}; + } + + /// No extra configuration needed for local (unix) sockets on epoll. + static std::error_code + configure_local_socket(int /*fd*/) noexcept + { + return {}; + } +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_EPOLL + +#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_TRAITS_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_udp_service.hpp b/include/boost/corosio/native/detail/epoll/epoll_udp_service.hpp deleted file mode 100644 index 1149941b5..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_udp_service.hpp +++ /dev/null @@ -1,272 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include - -#include -#include -#include - -#include - -#include -#include - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/** epoll UDP service implementation. - - Inherits from udp_service to enable runtime polymorphism. - Uses key_type = udp_service for service lookup. -*/ -class BOOST_COROSIO_DECL epoll_udp_service final - : public reactor_socket_service< - epoll_udp_service, - udp_service, - epoll_scheduler, - epoll_udp_socket> -{ -public: - explicit epoll_udp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_datagram_socket( - udp_socket::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_datagram(udp_socket::implementation& impl, endpoint ep) override; -}; - -// Cancellation for connectionless ops - -inline void -epoll_send_to_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_recv_from_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Cancellation for connected-mode ops - -inline void -epoll_udp_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_send_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -epoll_recv_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Completion handlers - -inline void -epoll_datagram_op::operator()() -{ - complete_io_op(*this); -} - -inline void -epoll_recv_from_op::operator()() -{ - complete_datagram_op(*this, this->source_out); -} - -inline void -epoll_udp_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline void -epoll_recv_op::operator()() -{ - complete_io_op(*this); -} - -// Socket construction/destruction - -inline epoll_udp_socket::epoll_udp_socket(epoll_udp_service& svc) noexcept - : reactor_datagram_socket(svc) -{ -} - -inline epoll_udp_socket::~epoll_udp_socket() = default; - -// Connectionless I/O - -inline std::coroutine_handle<> -epoll_udp_socket::send_to( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint dest, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_send_to(h, ex, buf, dest, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -epoll_udp_socket::recv_from( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint* source, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv_from(h, ex, buf, source, token, ec, bytes_out); -} - -// Connected-mode I/O - -inline std::coroutine_handle<> -epoll_udp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - return do_connect(h, ex, ep, token, ec); -} - -inline std::coroutine_handle<> -epoll_udp_socket::send( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_send(h, ex, buf, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -epoll_udp_socket::recv( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv(h, ex, buf, token, ec, bytes_out); -} - -inline endpoint -epoll_udp_socket::remote_endpoint() const noexcept -{ - return reactor_datagram_socket::remote_endpoint(); -} - -inline void -epoll_udp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -epoll_udp_socket::close_socket() noexcept -{ - do_close_socket(); -} - -inline std::error_code -epoll_udp_service::open_datagram_socket( - udp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* epoll_impl = static_cast(&impl); - epoll_impl->close_socket(); - - int fd = ::socket(family, type | SOCK_NONBLOCK | SOCK_CLOEXEC, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int one = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)); - } - - epoll_impl->fd_ = fd; - - epoll_impl->desc_state_.fd = fd; - { - std::lock_guard lock(epoll_impl->desc_state_.mutex); - epoll_impl->desc_state_.read_op = nullptr; - epoll_impl->desc_state_.write_op = nullptr; - epoll_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &epoll_impl->desc_state_); - - return {}; -} - -inline std::error_code -epoll_udp_service::bind_datagram(udp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_udp_socket.hpp b/include/boost/corosio/native/detail/epoll/epoll_udp_socket.hpp deleted file mode 100644 index 32d2b7a2a..000000000 --- a/include/boost/corosio/native/detail/epoll/epoll_udp_socket.hpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_EPOLL - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -class epoll_udp_service; -class epoll_udp_socket; - -/// epoll datagram base operation. -struct epoll_datagram_op : reactor_op -{ - void operator()() override; -}; - -/// epoll send_to operation. -struct epoll_send_to_op final : reactor_send_to_op -{ - void cancel() noexcept override; -}; - -/// epoll recv_from operation. -struct epoll_recv_from_op final : reactor_recv_from_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// epoll connect operation for UDP. -struct epoll_udp_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// epoll connected send operation. -struct epoll_send_op final : reactor_send_op -{ - void cancel() noexcept override; -}; - -/// epoll connected recv operation. -struct epoll_recv_op final : reactor_recv_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// Datagram socket implementation for epoll backend. -class epoll_udp_socket final - : public reactor_datagram_socket< - epoll_udp_socket, - epoll_udp_service, - epoll_udp_connect_op, - epoll_send_to_op, - epoll_recv_from_op, - epoll_send_op, - epoll_recv_op, - descriptor_state> -{ - friend class epoll_udp_service; - -public: - explicit epoll_udp_socket(epoll_udp_service& svc) noexcept; - ~epoll_udp_socket() override; - - std::coroutine_handle<> send_to( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv_from( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint*, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> send( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - endpoint remote_endpoint() const noexcept override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_EPOLL - -#endif // BOOST_COROSIO_NATIVE_DETAIL_EPOLL_EPOLL_UDP_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_udp_service.hpp b/include/boost/corosio/native/detail/iocp/win_udp_service.hpp index ea57f66fa..48dd4cbb0 100644 --- a/include/boost/corosio/native/detail/iocp/win_udp_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_udp_service.hpp @@ -33,6 +33,17 @@ namespace boost::corosio::detail { +/* Map portable message_flags values to native MSG_* constants. */ +inline DWORD +to_native_msg_flags(int flags) noexcept +{ + DWORD native = 0; + if (flags & 1) native |= MSG_PEEK; + if (flags & 2) native |= MSG_OOB; + if (flags & 4) native |= MSG_DONTROUTE; + return native; +} + /** IOCP UDP service implementation. Inherits from udp_service to enable runtime polymorphism. @@ -360,6 +371,7 @@ win_udp_socket_internal::send_to( capy::executor_ref d, buffer_param param, endpoint dest, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) @@ -392,7 +404,8 @@ win_udp_socket_internal::send_to( op.dest_len = static_cast(to_sockaddr(dest, family_, op.dest_storage)); int result = ::WSASendTo( - socket_, op.wsabufs, op.wsabuf_count, nullptr, 0, + socket_, op.wsabufs, op.wsabuf_count, nullptr, + to_native_msg_flags(flags), reinterpret_cast(&op.dest_storage), op.dest_len, &op, nullptr); @@ -421,6 +434,7 @@ win_udp_socket_internal::recv_from( capy::executor_ref d, buffer_param param, endpoint* source, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) @@ -458,7 +472,7 @@ win_udp_socket_internal::recv_from( op.wsabufs[i].len = static_cast(bufs[i].size()); } - op.flags = 0; + op.flags = to_native_msg_flags(flags); std::memset(&op.source_storage, 0, sizeof(op.source_storage)); op.source_len = sizeof(op.source_storage); @@ -526,6 +540,7 @@ win_udp_socket_internal::send( std::coroutine_handle<> h, capy::executor_ref d, buffer_param param, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) @@ -553,7 +568,8 @@ win_udp_socket_internal::send( } int result = ::WSASend( - socket_, op.wsabufs, op.wsabuf_count, nullptr, 0, &op, nullptr); + socket_, op.wsabufs, op.wsabuf_count, nullptr, + to_native_msg_flags(flags), &op, nullptr); if (result == SOCKET_ERROR) { @@ -578,6 +594,7 @@ win_udp_socket_internal::recv( std::coroutine_handle<> h, capy::executor_ref d, buffer_param param, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) @@ -611,7 +628,7 @@ win_udp_socket_internal::recv( op.wsabufs[i].len = static_cast(bufs[i].size()); } - op.flags = 0; + op.flags = to_native_msg_flags(flags); int result = ::WSARecv( socket_, op.wsabufs, op.wsabuf_count, nullptr, &op.flags, &op, nullptr); @@ -689,11 +706,12 @@ win_udp_socket::send_to( capy::executor_ref d, buffer_param buf, endpoint dest, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) { - return internal_->send_to(h, d, buf, dest, token, ec, bytes); + return internal_->send_to(h, d, buf, dest, flags, token, ec, bytes); } inline std::coroutine_handle<> @@ -702,11 +720,12 @@ win_udp_socket::recv_from( capy::executor_ref d, buffer_param buf, endpoint* source, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) { - return internal_->recv_from(h, d, buf, source, token, ec, bytes); + return internal_->recv_from(h, d, buf, source, flags, token, ec, bytes); } inline std::coroutine_handle<> @@ -725,11 +744,12 @@ win_udp_socket::send( std::coroutine_handle<> h, capy::executor_ref d, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) { - return internal_->send(h, d, buf, token, ec, bytes); + return internal_->send(h, d, buf, flags, token, ec, bytes); } inline std::coroutine_handle<> @@ -737,11 +757,12 @@ win_udp_socket::recv( std::coroutine_handle<> h, capy::executor_ref d, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) { - return internal_->recv(h, d, buf, token, ec, bytes); + return internal_->recv(h, d, buf, flags, token, ec, bytes); } inline native_handle_type diff --git a/include/boost/corosio/native/detail/iocp/win_udp_socket.hpp b/include/boost/corosio/native/detail/iocp/win_udp_socket.hpp index 1929c9162..437714416 100644 --- a/include/boost/corosio/native/detail/iocp/win_udp_socket.hpp +++ b/include/boost/corosio/native/detail/iocp/win_udp_socket.hpp @@ -168,6 +168,7 @@ class win_udp_socket_internal capy::executor_ref, buffer_param, endpoint, + int flags, std::stop_token, std::error_code*, std::size_t*); @@ -177,6 +178,7 @@ class win_udp_socket_internal capy::executor_ref, buffer_param, endpoint*, + int flags, std::stop_token, std::error_code*, std::size_t*); @@ -192,6 +194,7 @@ class win_udp_socket_internal std::coroutine_handle<>, capy::executor_ref, buffer_param, + int flags, std::stop_token, std::error_code*, std::size_t*); @@ -200,6 +203,7 @@ class win_udp_socket_internal std::coroutine_handle<>, capy::executor_ref, buffer_param, + int flags, std::stop_token, std::error_code*, std::size_t*); @@ -241,6 +245,7 @@ class win_udp_socket final capy::executor_ref d, buffer_param buf, endpoint dest, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) override; @@ -250,6 +255,7 @@ class win_udp_socket final capy::executor_ref d, buffer_param buf, endpoint* source, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) override; @@ -265,6 +271,7 @@ class win_udp_socket final std::coroutine_handle<> h, capy::executor_ref d, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) override; @@ -273,6 +280,7 @@ class win_udp_socket final std::coroutine_handle<> h, capy::executor_ref d, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes) override; diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_op.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_op.hpp index 4137bd03c..80227bf41 100644 --- a/include/boost/corosio/native/detail/kqueue/kqueue_op.hpp +++ b/include/boost/corosio/native/detail/kqueue/kqueue_op.hpp @@ -15,173 +15,19 @@ #if BOOST_COROSIO_HAS_KQUEUE -#include #include -#include -#include - -/* - kqueue Operation State - ====================== - - Each async I/O operation has a corresponding kqueue_op-derived struct that - holds the operation's state while it's in flight. The socket impl owns - fixed slots for each operation type (conn_, rd_, wr_), so only one - operation of each type can be pending per socket at a time. - - Persistent Registration - ----------------------- - File descriptors are registered with kqueue once (via descriptor_state) and - stay registered until closed. Uses EV_CLEAR for edge-triggered semantics - (equivalent to epoll's EPOLLET). The descriptor_state tracks which operations - are pending (read_op, write_op, connect_op). When an event arrives, the - reactor dispatches to the appropriate pending operation. - - Impl Lifetime Management - ------------------------ - When cancel() posts an op to the scheduler's ready queue, the socket impl - might be destroyed before the scheduler processes the op. The `impl_ptr` - member holds a shared_ptr to the impl, keeping it alive until the op - completes. This is set by cancel() and cleared in operator() after the - coroutine is resumed. - - EOF Detection - ------------- - For reads, 0 bytes with no error means EOF. But an empty user buffer also - returns 0 bytes. The `empty_buffer_read` flag distinguishes these cases. - - SIGPIPE Prevention - ------------------ - SO_NOSIGPIPE is set on each socket at creation time (see sockets.cpp). - Writes use writev() which is safe because the socket-level option suppresses - SIGPIPE delivery. -*/ - namespace boost::corosio::detail { // Aliases for shared reactor event constants. -// Kept for backward compatibility in kqueue-specific code. static constexpr std::uint32_t kqueue_event_read = reactor_event_read; static constexpr std::uint32_t kqueue_event_write = reactor_event_write; static constexpr std::uint32_t kqueue_event_error = reactor_event_error; -// Forward declarations -class kqueue_tcp_socket; -class kqueue_tcp_acceptor; -struct kqueue_op; - -class kqueue_scheduler; - /// Per-descriptor state for persistent kqueue registration. struct descriptor_state final : reactor_descriptor_state {}; -/// kqueue base operation — thin wrapper over reactor_op. -struct kqueue_op : reactor_op -{ - void operator()() override; -}; - -/// kqueue connect operation. -struct kqueue_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// kqueue scatter-read operation. -struct kqueue_read_op final : reactor_read_op -{ - void cancel() noexcept override; -}; - -/** Provides writev() for kqueue writes. - - SO_NOSIGPIPE is set on the socket at creation time (macOS lacks - MSG_NOSIGNAL), so writev() is safe from SIGPIPE. -*/ -struct kqueue_write_policy -{ - static ssize_t write(int fd, iovec* iovecs, int count) noexcept - { - ssize_t n; - do - { - n = ::writev(fd, iovecs, count); - } - while (n < 0 && errno == EINTR); - return n; - } -}; - -/// kqueue gather-write operation. -struct kqueue_write_op final : reactor_write_op -{ - void cancel() noexcept override; -}; - -/** Provides accept() + fcntl() + SO_NOSIGPIPE for kqueue accepts. - - Unlike Linux's accept4(), BSD accept() does not support atomic - flag setting. Non-blocking, close-on-exec, and SIGPIPE suppression - are applied via separate syscalls after accept(). -*/ -struct kqueue_accept_policy -{ - static int do_accept(int fd, sockaddr_storage& peer) noexcept - { - int new_fd; - do - { - socklen_t addrlen = sizeof(peer); - new_fd = ::accept(fd, reinterpret_cast(&peer), &addrlen); - } - while (new_fd < 0 && errno == EINTR); - - if (new_fd < 0) - return new_fd; - - int flags = ::fcntl(new_fd, F_GETFL, 0); - if (flags == -1 || ::fcntl(new_fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - - if (::fcntl(new_fd, F_SETFD, FD_CLOEXEC) == -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - - // macOS lacks MSG_NOSIGNAL - int one = 1; - if (::setsockopt(new_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) == - -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - - return new_fd; - } -}; - -/// kqueue accept operation. -struct kqueue_accept_op final - : reactor_accept_op -{ - void operator()() override; - void cancel() noexcept override; -}; - } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_KQUEUE diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor.hpp deleted file mode 100644 index 124b6e4e7..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor.hpp +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include -#include - -namespace boost::corosio::detail { - -class kqueue_tcp_acceptor_service; - -/// Acceptor implementation for kqueue backend. -class kqueue_tcp_acceptor final - : public reactor_acceptor< - kqueue_tcp_acceptor, - kqueue_tcp_acceptor_service, - kqueue_op, - kqueue_accept_op, - descriptor_state> -{ - friend class kqueue_tcp_acceptor_service; - -public: - explicit kqueue_tcp_acceptor(kqueue_tcp_acceptor_service& svc) noexcept; - - /** Initiate an asynchronous accept on the listening socket. - - Attempts a synchronous accept first. If the socket would block - (EAGAIN), the operation is parked in desc_state_ until the - reactor delivers a read-readiness event, at which point the - accept is retried. On completion (success, error, or - cancellation) the operation is posted to the scheduler and - @a caller is resumed via @a ex. - - Only one accept may be outstanding at a time; overlapping - calls produce undefined behavior. - - @param caller Coroutine handle resumed on completion. - @param ex Executor through which @a caller is resumed. - @param token Stop token for cancellation. - @param ec Points to storage for the result error code. - @param out_impl Points to storage for the accepted socket impl. - - @return std::noop_coroutine() unconditionally. - */ - std::coroutine_handle<> accept( - std::coroutine_handle<> caller, - capy::executor_ref ex, - std::stop_token token, - std::error_code* ec, - io_object::implementation** out_impl) override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor_service.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor_service.hpp deleted file mode 100644 index ed2816694..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_acceptor_service.hpp +++ /dev/null @@ -1,417 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/// State for kqueue acceptor service. -using kqueue_tcp_acceptor_state = - reactor_service_state; - -/** kqueue acceptor service implementation. - - Inherits from tcp_acceptor_service to enable runtime polymorphism. - Uses key_type = tcp_acceptor_service for service lookup. -*/ -class BOOST_COROSIO_DECL kqueue_tcp_acceptor_service final - : public tcp_acceptor_service -{ -public: - explicit kqueue_tcp_acceptor_service( - capy::execution_context& ctx, kqueue_tcp_service& tcp_svc); - ~kqueue_tcp_acceptor_service(); - - kqueue_tcp_acceptor_service(kqueue_tcp_acceptor_service const&) = delete; - kqueue_tcp_acceptor_service& - operator=(kqueue_tcp_acceptor_service const&) = delete; - - void shutdown() override; - io_object::implementation* construct() override; - void destroy(io_object::implementation*) override; - void close(io_object::handle&) override; - std::error_code open_acceptor_socket( - tcp_acceptor::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_acceptor(tcp_acceptor::implementation& impl, endpoint ep) override; - std::error_code - listen_acceptor(tcp_acceptor::implementation& impl, int backlog) override; - - kqueue_scheduler& scheduler() const noexcept - { - return state_->sched_; - } - void post(scheduler_op* op); - void work_started() noexcept; - void work_finished() noexcept; - - /** Get the TCP service for creating peer sockets during accept. */ - kqueue_tcp_service* tcp_service() const noexcept; - -private: - kqueue_tcp_service* tcp_svc_; - std::unique_ptr state_; -}; - -inline void -kqueue_accept_op::cancel() noexcept -{ - if (acceptor_impl_) - acceptor_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_accept_op::operator()() -{ - complete_accept_op(*this); -} - -inline kqueue_tcp_acceptor::kqueue_tcp_acceptor( - kqueue_tcp_acceptor_service& svc) noexcept - : reactor_acceptor(svc) -{ -} - -inline std::coroutine_handle<> -kqueue_tcp_acceptor::accept( - std::coroutine_handle<> h, - capy::executor_ref ex, - std::stop_token token, - std::error_code* ec, - io_object::implementation** impl_out) -{ - auto& op = acc_; - op.reset(); - op.h = h; - op.ex = ex; - op.ec_out = ec; - op.impl_out = impl_out; - op.fd = fd_; - op.start(token, this); - - sockaddr_storage peer_storage{}; - socklen_t addrlen = sizeof(peer_storage); - - // FreeBSD: Can use accept4(fd_, addr, addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC) - int accepted = - ::accept(fd_, reinterpret_cast(&peer_storage), &addrlen); - - if (accepted >= 0) - { - // Set non-blocking and close-on-exec on the accepted socket - int flags = ::fcntl(accepted, F_GETFL, 0); - if (flags == -1 || ::fcntl(accepted, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(accepted); - op.complete(errn, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - if (::fcntl(accepted, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(accepted); - op.complete(errn, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - // SO_NOSIGPIPE before budget check so both inline and - // queued paths have it applied (macOS lacks MSG_NOSIGNAL) - int one = 1; - if (::setsockopt( - accepted, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) == -1) - { - int errn = errno; - ::close(accepted); - op.complete(errn, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - { - std::lock_guard lock(desc_state_.mutex); - desc_state_.read_ready = false; - } - - if (svc_.scheduler().try_consume_inline_budget()) - { - auto* socket_svc = svc_.tcp_service(); - if (socket_svc) - { - auto& impl = - static_cast(*socket_svc->construct()); - impl.set_socket(accepted); - - impl.desc_state_.fd = accepted; - { - std::lock_guard lock(impl.desc_state_.mutex); - impl.desc_state_.read_op = nullptr; - impl.desc_state_.write_op = nullptr; - impl.desc_state_.connect_op = nullptr; - } - socket_svc->scheduler().register_descriptor( - accepted, &impl.desc_state_); - - impl.set_endpoints( - local_endpoint_, from_sockaddr(peer_storage)); - - *ec = {}; - if (impl_out) - *impl_out = &impl; - } - else - { - ::close(accepted); - *ec = make_err(ENOENT); - if (impl_out) - *impl_out = nullptr; - } - op.cont_op.cont.h = h; - return dispatch_coro(ex, op.cont_op.cont); - } - - op.accepted_fd = accepted; - op.peer_storage = peer_storage; - op.complete(0, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - op.impl_ptr = shared_from_this(); - svc_.work_started(); - - std::lock_guard lock(desc_state_.mutex); - bool io_done = false; - if (desc_state_.read_ready) - { - desc_state_.read_ready = false; - op.perform_io(); - io_done = (op.errn != EAGAIN && op.errn != EWOULDBLOCK); - if (!io_done) - op.errn = 0; - } - - if (io_done || op.cancelled.load(std::memory_order_acquire)) - { - svc_.post(&op); - svc_.work_finished(); - } - else - { - desc_state_.read_op = &op; - } - return std::noop_coroutine(); - } - - op.complete(errno, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); -} - -inline void -kqueue_tcp_acceptor::cancel() noexcept -{ - do_cancel(); -} - -inline void -kqueue_tcp_acceptor::close_socket() noexcept -{ - do_close_socket(); -} - -inline kqueue_tcp_acceptor_service::kqueue_tcp_acceptor_service( - capy::execution_context& ctx, kqueue_tcp_service& tcp_svc) - : tcp_svc_(&tcp_svc) - , state_( - std::make_unique( - ctx.use_service())) -{ -} - -inline kqueue_tcp_acceptor_service::~kqueue_tcp_acceptor_service() = default; - -inline void -kqueue_tcp_acceptor_service::shutdown() -{ - std::lock_guard lock(state_->mutex_); - - while (auto* impl = state_->impl_list_.pop_front()) - impl->close_socket(); -} - -inline io_object::implementation* -kqueue_tcp_acceptor_service::construct() -{ - auto impl = std::make_shared(*this); - auto* raw = impl.get(); - - std::lock_guard lock(state_->mutex_); - state_->impl_ptrs_.emplace(raw, std::move(impl)); - state_->impl_list_.push_back(raw); - - return raw; -} - -inline void -kqueue_tcp_acceptor_service::destroy(io_object::implementation* impl) -{ - auto* kq_impl = static_cast(impl); - kq_impl->close_socket(); - std::lock_guard lock(state_->mutex_); - state_->impl_list_.remove(kq_impl); - state_->impl_ptrs_.erase(kq_impl); -} - -inline void -kqueue_tcp_acceptor_service::close(io_object::handle& h) -{ - static_cast(h.get())->close_socket(); -} - -inline std::error_code -kqueue_tcp_acceptor_service::open_acceptor_socket( - tcp_acceptor::implementation& impl, int family, int type, int protocol) -{ - auto* kq_impl = static_cast(&impl); - kq_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - // Set non-blocking and close-on-exec - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - if (family == AF_INET6) - { - int val = 0; // dual-stack default - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)); - } - - // SO_NOSIGPIPE on macOS (where MSG_NOSIGNAL doesn't exist) -#ifdef SO_NOSIGPIPE - int nosig = 1; - ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &nosig, sizeof(nosig)); -#endif - - kq_impl->fd_ = fd; - - // Set up descriptor state but do NOT register with kqueue yet - kq_impl->desc_state_.fd = fd; - { - std::lock_guard lock(kq_impl->desc_state_.mutex); - kq_impl->desc_state_.read_op = nullptr; - } - - return {}; -} - -inline std::error_code -kqueue_tcp_acceptor_service::bind_acceptor( - tcp_acceptor::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -inline std::error_code -kqueue_tcp_acceptor_service::listen_acceptor( - tcp_acceptor::implementation& impl, int backlog) -{ - return static_cast(&impl)->do_listen(backlog); -} - -inline void -kqueue_tcp_acceptor_service::post(scheduler_op* op) -{ - state_->sched_.post(op); -} - -inline void -kqueue_tcp_acceptor_service::work_started() noexcept -{ - state_->sched_.work_started(); -} - -inline void -kqueue_tcp_acceptor_service::work_finished() noexcept -{ - state_->sched_.work_finished(); -} - -inline kqueue_tcp_service* -kqueue_tcp_acceptor_service::tcp_service() const noexcept -{ - return tcp_svc_; -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_ACCEPTOR_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp deleted file mode 100644 index ed82586d1..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_service.hpp +++ /dev/null @@ -1,357 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include - -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include - -/* - kqueue Socket Implementation - ============================ - - Each I/O operation follows the same pattern: - 1. Try the syscall speculatively (readv/writev) before suspending - 2. On success, return via symmetric transfer (the "pump" fast path) - 3. On budget exhaustion, post to the scheduler queue for fairness - 4. On EAGAIN, register_op() parks the op in the descriptor_state - - The speculative path avoids scheduler queue, mutex, and reactor - round-trips entirely. An inline budget limits consecutive inline - completions to prevent starvation of other connections. - - Cancellation - ------------ - See op.hpp for the completion/cancellation race handling via the - descriptor_state mutex. cancel() must complete pending operations (post - them with cancelled flag) so coroutines waiting on them can resume. - close_socket() calls cancel() first to ensure this. - - Impl Lifetime with shared_ptr - ----------------------------- - Socket impls use enable_shared_from_this. The service owns impls via - shared_ptr maps (impl_ptrs_) keyed by raw pointer for O(1) lookup and - removal. When a user calls close(), we call cancel() which posts pending - ops to the scheduler. - - CRITICAL: The posted ops must keep the impl alive until they complete. - Otherwise the scheduler would process a freed op (use-after-free). The - cancel() method captures shared_from_this() into op.impl_ptr before - posting. When the op completes, impl_ptr is cleared, allowing the impl - to be destroyed if no other references exist. - - Service Ownership - ----------------- - kqueue_tcp_service owns all socket impls. destroy_impl() removes the - shared_ptr from the map, but the impl may survive if ops still hold - impl_ptr refs. shutdown() closes all sockets and clears the map; any - in-flight ops will complete and release their refs. -*/ - -/* - kqueue socket implementation - ============================ - - Each kqueue_tcp_socket owns a descriptor_state that is persistently - registered with kqueue (EVFILT_READ + EVFILT_WRITE, both EV_CLEAR for - edge-triggered semantics). The descriptor_state tracks three operation - slots (read_op, write_op, connect_op) and two ready flags - (read_ready, write_ready) under a per-descriptor mutex. - - Speculative I/O and the pump - ---------------------------- - read_some() and write_some() attempt the syscall (readv/writev) - speculatively before suspending the caller. If data is available the - result is returned via symmetric transfer — no scheduler queue, no - mutex, no reactor round-trip. An inline budget limits consecutive - inline completions to prevent starvation of other connections. - - When the speculative attempt returns EAGAIN, register_op() parks the - operation in its descriptor_state slot under the per-descriptor mutex. - If a cached ready flag fires before parking, register_op() retries - the I/O once under the mutex. This eliminates the cached_initiator - coroutine frame that previously trampolined into do_read_io() / - do_write_io() after the caller suspended. - - Ready-flag protocol - ------------------- - When a kqueue event fires and no operation is pending for that - direction, the reactor sets the corresponding ready flag instead of - dropping the event. When register_op() finds the ready flag set, it - performs I/O immediately rather than parking. This prevents lost - wakeups under edge-triggered notification. -*/ - -namespace boost::corosio::detail { - -/** kqueue TCP service implementation. - - Inherits from tcp_service to enable runtime polymorphism. - Uses key_type = tcp_service for service lookup. -*/ -class BOOST_COROSIO_DECL kqueue_tcp_service final - : public reactor_socket_service< - kqueue_tcp_service, - tcp_service, - kqueue_scheduler, - kqueue_tcp_socket> -{ - using base_service = reactor_socket_service< - kqueue_tcp_service, - tcp_service, - kqueue_scheduler, - kqueue_tcp_socket>; - friend base_service; - - // Clear SO_LINGER before close so the destructor doesn't block - // and close() sends FIN instead of RST. RST doesn't reliably - // trigger EV_EOF on macOS kqueue. - static void reset_linger(kqueue_tcp_socket* impl) noexcept - { - if (impl->user_set_linger_ && impl->fd_ >= 0) - { - struct ::linger lg; - lg.l_onoff = 0; - lg.l_linger = 0; - ::setsockopt(impl->fd_, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); - } - } - - void pre_shutdown(kqueue_tcp_socket* impl) noexcept - { - reset_linger(impl); - } - - void pre_destroy(kqueue_tcp_socket* impl) noexcept - { - reset_linger(impl); - } - -public: - explicit kqueue_tcp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_socket( - tcp_socket::implementation& impl, - int family, - int type, - int protocol) override; - - std::error_code - bind_socket(tcp_socket::implementation& impl, endpoint ep) override; -}; - -inline void -kqueue_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_read_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_write_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_op::operator()() -{ - complete_io_op(*this); -} - -inline void -kqueue_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline kqueue_tcp_socket::kqueue_tcp_socket(kqueue_tcp_service& svc) noexcept - : reactor_stream_socket(svc) -{ -} - -inline kqueue_tcp_socket::~kqueue_tcp_socket() = default; - -inline std::coroutine_handle<> -kqueue_tcp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - return do_connect(h, ex, ep, token, ec); -} - -inline std::coroutine_handle<> -kqueue_tcp_socket::read_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_read_some(h, ex, param, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -kqueue_tcp_socket::write_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_write_some(h, ex, param, token, ec, bytes_out); -} - -inline std::error_code -kqueue_tcp_socket::set_option( - int level, int optname, void const* data, std::size_t size) noexcept -{ - if (::setsockopt(fd_, level, optname, data, static_cast(size)) != - 0) - return make_err(errno); - if (level == SOL_SOCKET && optname == SO_LINGER && - size >= sizeof(struct ::linger)) - user_set_linger_ = - static_cast(data)->l_onoff != 0; - return {}; -} - -inline void -kqueue_tcp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -kqueue_tcp_socket::close_socket() noexcept -{ - do_close_socket(); - user_set_linger_ = false; -} - -inline std::error_code -kqueue_tcp_service::open_socket( - tcp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* kq_impl = static_cast(&impl); - kq_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int v6only = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)); - } - - // Set non-blocking - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - // Set close-on-exec - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - // Suppress SIGPIPE on this socket; writev() has no MSG_NOSIGNAL - // equivalent, so SO_NOSIGPIPE is required on macOS/FreeBSD. - int one = 1; - if (::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) != 0) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - kq_impl->fd_ = fd; - - // Register fd with kqueue (edge-triggered mode via EV_CLEAR) - kq_impl->desc_state_.fd = fd; - { - std::lock_guard lock(kq_impl->desc_state_.mutex); - kq_impl->desc_state_.read_op = nullptr; - kq_impl->desc_state_.write_op = nullptr; - kq_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &kq_impl->desc_state_); - - return {}; -} - -inline std::error_code -kqueue_tcp_service::bind_socket( - tcp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_socket.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_tcp_socket.hpp deleted file mode 100644 index d5903d358..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_tcp_socket.hpp +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include -#include - -namespace boost::corosio::detail { - -class kqueue_tcp_service; - -/// Stream socket implementation for kqueue backend. -class kqueue_tcp_socket final - : public reactor_stream_socket< - kqueue_tcp_socket, - kqueue_tcp_service, - kqueue_connect_op, - kqueue_read_op, - kqueue_write_op, - descriptor_state> -{ - friend class kqueue_tcp_service; - - bool user_set_linger_ = false; - -public: - explicit kqueue_tcp_socket(kqueue_tcp_service& svc) noexcept; - ~kqueue_tcp_socket(); - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> read_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> write_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - /// Track SO_LINGER for macOS kqueue workaround. - std::error_code set_option( - int level, - int optname, - void const* data, - std::size_t size) noexcept override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TCP_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_traits.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_traits.hpp new file mode 100644 index 000000000..0c070b7c8 --- /dev/null +++ b/include/boost/corosio/native/detail/kqueue/kqueue_traits.hpp @@ -0,0 +1,250 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TRAITS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TRAITS_HPP + +#include + +#if BOOST_COROSIO_HAS_KQUEUE + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +/* kqueue backend traits. + + Captures the platform-specific behavior of the BSD/macOS kqueue backend: + manual fcntl for O_NONBLOCK/FD_CLOEXEC, mandatory SO_NOSIGPIPE (macOS + lacks MSG_NOSIGNAL), writev() for writes, and accept()+fcntl for + accepted connections. +*/ + +namespace boost::corosio::detail { + +class kqueue_scheduler; +struct descriptor_state; // kqueue's descriptor_state in kqueue_op.hpp + +struct kqueue_traits +{ + using scheduler_type = kqueue_scheduler; + using desc_state_type = descriptor_state; + + static constexpr bool needs_write_notification = false; + + /* macOS kqueue workaround: RST doesn't reliably trigger EV_EOF. + If the user sets SO_LINGER, we clear it before close so the + destructor doesn't block and close() sends FIN instead of RST. + + The hook tracks whether the user explicitly set SO_LINGER via + set_option(). On pre_shutdown/pre_destroy, if the flag is set, + we reset linger to off before the fd is closed. + */ + struct stream_socket_hook + { + bool user_set_linger_ = false; + + std::error_code on_set_option( + int fd, int level, int optname, + void const* data, std::size_t size) noexcept + { + if (::setsockopt( + fd, level, optname, data, + static_cast(size)) != 0) + return make_err(errno); + + if (level == SOL_SOCKET && optname == SO_LINGER && + size >= sizeof(struct ::linger)) + user_set_linger_ = + static_cast(data)->l_onoff != 0; + + return {}; + } + + void pre_shutdown(int fd) noexcept + { + reset_linger(fd); + } + + void pre_destroy(int fd) noexcept + { + reset_linger(fd); + } + + private: + void reset_linger(int fd) noexcept + { + if (user_set_linger_ && fd >= 0) + { + struct ::linger lg; + lg.l_onoff = 0; + lg.l_linger = 0; + ::setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); + } + user_set_linger_ = false; + } + }; + + struct write_policy + { + static ssize_t write(int fd, iovec* iovecs, int count) noexcept + { + ssize_t n; + do + { + n = ::writev(fd, iovecs, count); + } + while (n < 0 && errno == EINTR); + return n; + } + }; + + struct accept_policy + { + static int do_accept( + int fd, sockaddr_storage& peer, socklen_t& addrlen) noexcept + { + int new_fd; + do + { + addrlen = sizeof(peer); + new_fd = ::accept( + fd, reinterpret_cast(&peer), &addrlen); + } + while (new_fd < 0 && errno == EINTR); + + if (new_fd < 0) + return new_fd; + + int flags = ::fcntl(new_fd, F_GETFL, 0); + if (flags == -1 || + ::fcntl(new_fd, F_SETFL, flags | O_NONBLOCK) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } + + if (::fcntl(new_fd, F_SETFD, FD_CLOEXEC) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } + +#ifndef BOOST_COROSIO_MRDOCS + // SO_NOSIGPIPE is mandatory on kqueue platforms (macOS lacks + // MSG_NOSIGNAL). Skipped under MRDOCS so the docs build can + // parse this header on Linux, where SO_NOSIGPIPE is absent. + int one = 1; + if (::setsockopt( + new_fd, SOL_SOCKET, SO_NOSIGPIPE, + &one, sizeof(one)) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } +#endif + + return new_fd; + } + }; + + /// Create a plain socket. Fd options are applied by configure_*(). + static int create_socket(int family, int type, int protocol) noexcept + { + return ::socket(family, type, protocol); + } + + /// Set O_NONBLOCK, FD_CLOEXEC, and SO_NOSIGPIPE on a new fd. + /// Caller is responsible for closing fd on error. + static std::error_code set_fd_options(int fd) noexcept + { + int flags = ::fcntl(fd, F_GETFL, 0); + if (flags == -1) + return make_err(errno); + if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) + return make_err(errno); + if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) + return make_err(errno); + +#ifndef BOOST_COROSIO_MRDOCS + // SO_NOSIGPIPE is mandatory on kqueue platforms (see accept_policy). + int one = 1; + if (::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) != 0) + return make_err(errno); +#endif + + return {}; + } + + /// Apply protocol-specific options after socket creation. + /// For IP sockets, sets IPV6_V6ONLY on AF_INET6. + static std::error_code + configure_ip_socket(int fd, int family) noexcept + { + auto ec = set_fd_options(fd); + if (ec) + return ec; + + if (family == AF_INET6) + { + int v6only = 1; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, + &v6only, sizeof(v6only)) != 0) + return make_err(errno); + } + return {}; + } + + /// Apply protocol-specific options for acceptor sockets. + /// For IP acceptors, sets IPV6_V6ONLY=0 (dual-stack). + static std::error_code + configure_ip_acceptor(int fd, int family) noexcept + { + auto ec = set_fd_options(fd); + if (ec) + return ec; + + if (family == AF_INET6) + { + int val = 0; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)) != 0) + return make_err(errno); + } + return {}; + } + + /// Apply options for local (unix) sockets. + static std::error_code + configure_local_socket(int fd) noexcept + { + return set_fd_options(fd); + } +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_KQUEUE + +#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_TRAITS_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_udp_service.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_udp_service.hpp deleted file mode 100644 index a3493705e..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_udp_service.hpp +++ /dev/null @@ -1,299 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include - -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/** kqueue UDP service implementation. - - Inherits from udp_service to enable runtime polymorphism. - Uses key_type = udp_service for service lookup. -*/ -class BOOST_COROSIO_DECL kqueue_udp_service final - : public reactor_socket_service< - kqueue_udp_service, - udp_service, - kqueue_scheduler, - kqueue_udp_socket> -{ -public: - explicit kqueue_udp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_datagram_socket( - udp_socket::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_datagram(udp_socket::implementation& impl, endpoint ep) override; -}; - -// Cancellation for connectionless ops - -inline void -kqueue_send_to_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_recv_from_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Cancellation for connected-mode ops - -inline void -kqueue_udp_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_send_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -kqueue_recv_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Completion handlers - -inline void -kqueue_datagram_op::operator()() -{ - complete_io_op(*this); -} - -inline void -kqueue_recv_from_op::operator()() -{ - complete_datagram_op(*this, this->source_out); -} - -inline void -kqueue_udp_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline void -kqueue_recv_op::operator()() -{ - complete_io_op(*this); -} - -// Socket construction/destruction - -inline kqueue_udp_socket::kqueue_udp_socket(kqueue_udp_service& svc) noexcept - : reactor_datagram_socket(svc) -{ -} - -inline kqueue_udp_socket::~kqueue_udp_socket() = default; - -// Connectionless I/O - -inline std::coroutine_handle<> -kqueue_udp_socket::send_to( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint dest, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_send_to(h, ex, buf, dest, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -kqueue_udp_socket::recv_from( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint* source, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv_from(h, ex, buf, source, token, ec, bytes_out); -} - -// Connected-mode I/O - -inline std::coroutine_handle<> -kqueue_udp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - return do_connect(h, ex, ep, token, ec); -} - -inline std::coroutine_handle<> -kqueue_udp_socket::send( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_send(h, ex, buf, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -kqueue_udp_socket::recv( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv(h, ex, buf, token, ec, bytes_out); -} - -inline endpoint -kqueue_udp_socket::remote_endpoint() const noexcept -{ - return reactor_datagram_socket::remote_endpoint(); -} - -inline void -kqueue_udp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -kqueue_udp_socket::close_socket() noexcept -{ - do_close_socket(); -} - -inline std::error_code -kqueue_udp_service::open_datagram_socket( - udp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* kq_impl = static_cast(&impl); - kq_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int v6only = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)); - } - - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - // SO_NOSIGPIPE on macOS (where MSG_NOSIGNAL doesn't exist) -#ifdef SO_NOSIGPIPE - { - int one = 1; - ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); - } -#endif - - kq_impl->fd_ = fd; - - kq_impl->desc_state_.fd = fd; - { - std::lock_guard lock(kq_impl->desc_state_.mutex); - kq_impl->desc_state_.read_op = nullptr; - kq_impl->desc_state_.write_op = nullptr; - kq_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &kq_impl->desc_state_); - - return {}; -} - -inline std::error_code -kqueue_udp_service::bind_datagram(udp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_udp_socket.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_udp_socket.hpp deleted file mode 100644 index 8b7d0a149..000000000 --- a/include/boost/corosio/native/detail/kqueue/kqueue_udp_socket.hpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_KQUEUE - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -class kqueue_udp_service; -class kqueue_udp_socket; - -/// kqueue datagram base operation. -struct kqueue_datagram_op : reactor_op -{ - void operator()() override; -}; - -/// kqueue send_to operation. -struct kqueue_send_to_op final : reactor_send_to_op -{ - void cancel() noexcept override; -}; - -/// kqueue recv_from operation. -struct kqueue_recv_from_op final : reactor_recv_from_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// kqueue connect operation for UDP. -struct kqueue_udp_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// kqueue connected send operation. -struct kqueue_send_op final : reactor_send_op -{ - void cancel() noexcept override; -}; - -/// kqueue connected recv operation. -struct kqueue_recv_op final : reactor_recv_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// Datagram socket implementation for kqueue backend. -class kqueue_udp_socket final - : public reactor_datagram_socket< - kqueue_udp_socket, - kqueue_udp_service, - kqueue_udp_connect_op, - kqueue_send_to_op, - kqueue_recv_from_op, - kqueue_send_op, - kqueue_recv_op, - descriptor_state> -{ - friend class kqueue_udp_service; - -public: - explicit kqueue_udp_socket(kqueue_udp_service& svc) noexcept; - ~kqueue_udp_socket() override; - - std::coroutine_handle<> send_to( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv_from( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint*, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> send( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - endpoint remote_endpoint() const noexcept override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_KQUEUE - -#endif // BOOST_COROSIO_NATIVE_DETAIL_KQUEUE_KQUEUE_UDP_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_acceptor.hpp b/include/boost/corosio/native/detail/reactor/reactor_acceptor.hpp index 130921fa2..2a91c4803 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_acceptor.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_acceptor.hpp @@ -39,15 +39,21 @@ namespace boost::corosio::detail { @tparam Op The backend's base op type. @tparam AcceptOp The backend's accept op type. @tparam DescState The backend's descriptor_state type. + @tparam ImplBase The public vtable base + (tcp_acceptor::implementation or + local_stream_acceptor::implementation). + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ template< class Derived, class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase = tcp_acceptor::implementation, + class Endpoint = endpoint> class reactor_acceptor - : public tcp_acceptor::implementation + : public ImplBase , public std::enable_shared_from_this , public intrusive_list::node { @@ -58,7 +64,7 @@ class reactor_acceptor protected: Service& svc_; int fd_ = -1; - endpoint local_endpoint_; + Endpoint local_endpoint_; public: /// Pending accept operation slot. @@ -76,7 +82,7 @@ class reactor_acceptor } /// Return the cached local endpoint. - endpoint local_endpoint() const noexcept override + Endpoint local_endpoint() const noexcept override { return local_endpoint_; } @@ -113,9 +119,20 @@ class reactor_acceptor } /// Cache the local endpoint. - void set_local_endpoint(endpoint ep) noexcept + void set_local_endpoint(Endpoint ep) noexcept { - local_endpoint_ = ep; + local_endpoint_ = std::move(ep); + } + + /// Assign the fd and initialize descriptor state (acceptor: read_op only, no registration). + void init_acceptor_fd(int fd) noexcept + { + fd_ = fd; + desc_state_.fd = fd; + { + std::lock_guard lock(desc_state_.mutex); + desc_state_.read_op = nullptr; + } } /// Return a reference to the owning service. @@ -124,6 +141,14 @@ class reactor_acceptor return svc_; } + // --- Virtual method overrides --- + + void cancel() noexcept override { do_cancel(); } + + void close_socket() noexcept { do_close_socket(); } + + // --- End virtual overrides --- + /** Cancel a single pending operation. Claims the operation from the read_op descriptor slot @@ -133,10 +158,7 @@ class reactor_acceptor */ void cancel_single_op(Op& op) noexcept; - /** Cancel the pending accept operation. - - Invoked by the derived class's cancel() override. - */ + /** Cancel the pending accept operation. */ void do_cancel() noexcept; /** Close the acceptor and cancel pending operations. @@ -147,6 +169,9 @@ class reactor_acceptor */ void do_close_socket() noexcept; + /** Release the acceptor without closing the fd. */ + native_handle_type do_release_socket() noexcept; + /** Bind the acceptor socket to an endpoint. Caches the resolved local endpoint (including ephemeral @@ -155,7 +180,7 @@ class reactor_acceptor @param ep The endpoint to bind to. @return The error code from bind(), or success. */ - std::error_code do_bind(endpoint ep); + std::error_code do_bind(Endpoint const& ep); /** Start listening on the acceptor socket. @@ -173,10 +198,12 @@ template< class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> void -reactor_acceptor::cancel_single_op( - Op& op) noexcept +reactor_acceptor:: + cancel_single_op(Op& op) noexcept { auto self = this->weak_from_this().lock(); if (!self) @@ -203,9 +230,11 @@ template< class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> void -reactor_acceptor:: +reactor_acceptor:: do_cancel() noexcept { cancel_single_op(acc_); @@ -216,9 +245,11 @@ template< class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> void -reactor_acceptor:: +reactor_acceptor:: do_close_socket() noexcept { auto self = this->weak_from_this().lock(); @@ -256,7 +287,60 @@ reactor_acceptor:: desc_state_.fd = -1; desc_state_.registered_events = 0; - local_endpoint_ = endpoint{}; + local_endpoint_ = Endpoint{}; +} + +template< + class Derived, + class Service, + class Op, + class AcceptOp, + class DescState, + class ImplBase, + class Endpoint> +native_handle_type +reactor_acceptor:: + do_release_socket() noexcept +{ + auto self = this->weak_from_this().lock(); + if (self) + { + acc_.request_cancel(); + + reactor_op_base* claimed = nullptr; + { + std::lock_guard lock(desc_state_.mutex); + claimed = std::exchange(desc_state_.read_op, nullptr); + desc_state_.read_ready = false; + desc_state_.write_ready = false; + + if (desc_state_.is_enqueued_.load(std::memory_order_acquire)) + desc_state_.impl_ref_ = self; + } + + if (claimed) + { + acc_.impl_ptr = self; + svc_.post(&acc_); + svc_.work_finished(); + } + } + + native_handle_type released = fd_; + + if (fd_ >= 0) + { + if (desc_state_.registered_events != 0) + svc_.scheduler().deregister_descriptor(fd_); + fd_ = -1; + } + + desc_state_.fd = -1; + desc_state_.registered_events = 0; + + local_endpoint_ = Endpoint{}; + + return released; } template< @@ -264,22 +348,24 @@ template< class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::error_code -reactor_acceptor::do_bind( - endpoint ep) +reactor_acceptor:: + do_bind(Endpoint const& ep) { sockaddr_storage storage{}; socklen_t addrlen = to_sockaddr(ep, storage); if (::bind(fd_, reinterpret_cast(&storage), addrlen) < 0) return make_err(errno); - // Cache local endpoint (resolves ephemeral port) + // Cache local endpoint (resolves ephemeral port / path) sockaddr_storage local{}; socklen_t local_len = sizeof(local); if (::getsockname(fd_, reinterpret_cast(&local), &local_len) == 0) - set_local_endpoint(from_sockaddr(local)); + set_local_endpoint(from_sockaddr_as(local, local_len, Endpoint{})); return {}; } @@ -289,10 +375,12 @@ template< class Service, class Op, class AcceptOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::error_code -reactor_acceptor::do_listen( - int backlog) +reactor_acceptor:: + do_listen(int backlog) { if (::listen(fd_, backlog) < 0) return make_err(errno); diff --git a/include/boost/corosio/native/detail/reactor/reactor_acceptor_service.hpp b/include/boost/corosio/native/detail/reactor/reactor_acceptor_service.hpp new file mode 100644 index 000000000..d81162ade --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_acceptor_service.hpp @@ -0,0 +1,138 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_ACCEPTOR_SERVICE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_ACCEPTOR_SERVICE_HPP + +#include +#include +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +/* CRTP base for reactor-backed acceptor service implementations. + + Provides the shared construct/destroy/shutdown/close/post/work + logic that is identical across all reactor backends and acceptor + types (TCP and local stream). Derived classes add only + protocol-specific open/bind/listen/stream_service. + + @tparam Derived The concrete service type (CRTP). + @tparam ServiceBase The abstract service base + (tcp_acceptor_service or + local_stream_acceptor_service). + @tparam Scheduler The backend's scheduler type. + @tparam Impl The backend's acceptor impl type. + @tparam StreamService The concrete stream service type returned + by stream_service(). +*/ +template< + class Derived, + class ServiceBase, + class Scheduler, + class Impl, + class StreamService> +class reactor_acceptor_service : public ServiceBase +{ + friend Derived; + using state_type = reactor_service_state; + +public: + /// Propagated from Scheduler for register_op's write notification. + static constexpr bool needs_write_notification = + Scheduler::needs_write_notification; + +private: + explicit reactor_acceptor_service(capy::execution_context& ctx) + : ctx_(ctx) + , state_( + std::make_unique( + ctx.template use_service())) + { + } + +public: + ~reactor_acceptor_service() override = default; + + void shutdown() override + { + std::lock_guard lock(state_->mutex_); + + while (auto* impl = state_->impl_list_.pop_front()) + impl->close_socket(); + } + + io_object::implementation* construct() override + { + auto impl = std::make_shared(static_cast(*this)); + auto* raw = impl.get(); + + std::lock_guard lock(state_->mutex_); + state_->impl_ptrs_.emplace(raw, std::move(impl)); + state_->impl_list_.push_back(raw); + + return raw; + } + + void destroy(io_object::implementation* impl) override + { + auto* typed = static_cast(impl); + typed->close_socket(); + std::lock_guard lock(state_->mutex_); + state_->impl_list_.remove(typed); + state_->impl_ptrs_.erase(typed); + } + + void close(io_object::handle& h) override + { + static_cast(h.get())->close_socket(); + } + + Scheduler& scheduler() const noexcept + { + return state_->sched_; + } + + void post(scheduler_op* op) + { + state_->sched_.post(op); + } + + void work_started() noexcept + { + state_->sched_.work_started(); + } + + void work_finished() noexcept + { + state_->sched_.work_finished(); + } + + StreamService* stream_service() const noexcept + { + return stream_svc_; + } + +protected: + capy::execution_context& ctx_; + std::unique_ptr state_; + StreamService* stream_svc_ = nullptr; + +private: + reactor_acceptor_service(reactor_acceptor_service const&) = delete; + reactor_acceptor_service& operator=(reactor_acceptor_service const&) = delete; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_ACCEPTOR_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_backend.hpp b/include/boost/corosio/native/detail/reactor/reactor_backend.hpp new file mode 100644 index 000000000..9f418f9da --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_backend.hpp @@ -0,0 +1,185 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_BACKEND_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_BACKEND_HPP + +/* Parameterized reactor backend. + + Assembles all socket, service, acceptor, and op types for a given + backend Traits type. Includes the accept() implementation (which + needs all types to be complete) and the reactor_types + bundle used by backend.hpp. +*/ + +#include +#include +#include +#include + +#include + +namespace boost::corosio::detail { + +// ============================================================ +// Acceptor accept() implementation +// ============================================================ + +template +std::coroutine_handle<> +reactor_acceptor_final::accept( + std::coroutine_handle<> h, + capy::executor_ref ex, + std::stop_token token, + std::error_code* ec, + io_object::implementation** impl_out) +{ + using socket_final = stream_socket_t; + + auto& op = this->acc_; + op.reset(); + op.h = h; + op.ex = ex; + op.ec_out = ec; + op.impl_out = impl_out; + op.fd = this->fd_; + op.start(token, this); + + sockaddr_storage peer_storage{}; + socklen_t peer_addrlen = 0; + + int accepted = Traits::accept_policy::do_accept( + this->fd_, peer_storage, peer_addrlen); + + if (accepted >= 0) + { + { + std::lock_guard lock(this->desc_state_.mutex); + this->desc_state_.read_ready = false; + } + + if (this->svc_.scheduler().try_consume_inline_budget()) + { + auto* socket_svc = this->svc_.stream_service(); + if (socket_svc) + { + auto& impl = + static_cast(*socket_svc->construct()); + impl.set_socket(accepted); + + impl.desc_state_.fd = accepted; + { + std::lock_guard lock(impl.desc_state_.mutex); + impl.desc_state_.read_op = nullptr; + impl.desc_state_.write_op = nullptr; + impl.desc_state_.connect_op = nullptr; + } + socket_svc->scheduler().register_descriptor( + accepted, &impl.desc_state_); + + impl.set_endpoints( + this->local_endpoint_, + from_sockaddr_as( + peer_storage, peer_addrlen, Endpoint{})); + + *ec = {}; + if (impl_out) + *impl_out = &impl; + } + else + { + ::close(accepted); + *ec = make_err(ENOENT); + if (impl_out) + *impl_out = nullptr; + } + op.cont_op.cont.h = h; + return dispatch_coro(ex, op.cont_op.cont); + } + + op.accepted_fd = accepted; + op.peer_storage = peer_storage; + op.peer_addrlen = peer_addrlen; + op.complete(0, 0); + op.impl_ptr = this->shared_from_this(); + this->svc_.post(&op); + return std::noop_coroutine(); + } + + if (errno == EAGAIN || errno == EWOULDBLOCK) + { + op.impl_ptr = this->shared_from_this(); + this->svc_.work_started(); + + std::lock_guard lock(this->desc_state_.mutex); + bool io_done = false; + if (this->desc_state_.read_ready) + { + this->desc_state_.read_ready = false; + op.perform_io(); + io_done = (op.errn != EAGAIN && op.errn != EWOULDBLOCK); + if (!io_done) + op.errn = 0; + } + + if (io_done || op.cancelled.load(std::memory_order_acquire)) + { + this->svc_.post(&op); + this->svc_.work_finished(); + } + else + { + this->desc_state_.read_op = &op; + } + return std::noop_coroutine(); + } + + op.complete(errno, 0); + op.impl_ptr = this->shared_from_this(); + this->svc_.post(&op); + return std::noop_coroutine(); +} + +// ============================================================ +// Type bundle for backend.hpp +// ============================================================ + +template +struct reactor_types +{ + using tcp_socket_type = stream_socket_t; + using tcp_service_type = reactor_tcp_service_final< + Traits, tcp_socket_type>; + + using udp_socket_type = dgram_socket_t; + using udp_service_type = reactor_udp_service_final< + Traits, udp_socket_type>; + + using tcp_acceptor_type = stream_acceptor_t; + using tcp_acceptor_service_type = reactor_acceptor_service_final< + Traits, tcp_acceptor_service, tcp_acceptor_type, + tcp_service_type, endpoint>; + + using local_stream_socket_type = stream_socket_t; + using local_stream_service_type = reactor_local_stream_service_final< + Traits, local_stream_socket_type>; + + using local_datagram_socket_type = dgram_socket_t; + using local_datagram_service_type = reactor_local_dgram_service_final< + Traits, local_datagram_socket_type>; + + using local_stream_acceptor_type = stream_acceptor_t; + using local_stream_acceptor_service_type = reactor_acceptor_service_final< + Traits, local_stream_acceptor_service, local_stream_acceptor_type, + local_stream_service_type, local_endpoint>; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_BACKEND_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_basic_socket.hpp b/include/boost/corosio/native/detail/reactor/reactor_basic_socket.hpp index 7008ccc76..b7a1e46fa 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_basic_socket.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_basic_socket.hpp @@ -42,8 +42,14 @@ namespace boost::corosio::detail { or udp_socket::implementation). @tparam Service The backend's service type. @tparam DescState The backend's descriptor_state type. + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ -template +template< + class Derived, + class ImplBase, + class Service, + class DescState, + class Endpoint = endpoint> class reactor_basic_socket : public ImplBase , public std::enable_shared_from_this @@ -51,10 +57,10 @@ class reactor_basic_socket { friend Derived; - template + template friend class reactor_stream_socket; - template + template friend class reactor_datagram_socket; explicit reactor_basic_socket(Service& svc) noexcept : svc_(svc) {} @@ -62,7 +68,7 @@ class reactor_basic_socket protected: Service& svc_; int fd_ = -1; - endpoint local_endpoint_; + Endpoint local_endpoint_; public: /// Per-descriptor state for persistent reactor registration. @@ -77,7 +83,7 @@ class reactor_basic_socket } /// Return the cached local endpoint. - endpoint local_endpoint() const noexcept override + Endpoint local_endpoint() const noexcept override { return local_endpoint_; } @@ -119,8 +125,22 @@ class reactor_basic_socket fd_ = fd; } + /// Assign the fd, initialize descriptor state, and register with the reactor. + void init_and_register(int fd) noexcept + { + fd_ = fd; + desc_state_.fd = fd; + { + std::lock_guard lock(desc_state_.mutex); + desc_state_.read_op = nullptr; + desc_state_.write_op = nullptr; + desc_state_.connect_op = nullptr; + } + svc_.scheduler().register_descriptor(fd, &desc_state_); + } + /// Cache the local endpoint. - void set_local_endpoint(endpoint ep) noexcept + void set_local_endpoint(Endpoint ep) noexcept { local_endpoint_ = ep; } @@ -133,7 +153,7 @@ class reactor_basic_socket @param ep The endpoint to bind to. @return Error code on failure, empty on success. */ - std::error_code do_bind(endpoint ep) noexcept + std::error_code do_bind(Endpoint const& ep) noexcept { sockaddr_storage storage{}; socklen_t addrlen = to_sockaddr(ep, socket_family(fd_), storage); @@ -145,7 +165,8 @@ class reactor_basic_socket if (::getsockname( fd_, reinterpret_cast(&local_storage), &local_len) == 0) - local_endpoint_ = from_sockaddr(local_storage); + local_endpoint_ = + from_sockaddr_as(local_storage, local_len, Endpoint{}); return {}; } @@ -161,7 +182,8 @@ class reactor_basic_socket Op& op, reactor_op_base*& desc_slot, bool& ready_flag, - bool& cancel_flag) noexcept; + bool& cancel_flag, + bool is_write_direction = false) noexcept; /** Cancel a single pending operation. @@ -193,16 +215,24 @@ class reactor_basic_socket for_each_desc_entry(auto fn) */ void do_close_socket() noexcept; + + /** Release the socket without closing the fd. + + Like do_close_socket() but does not call ::close(). + Returns the fd so the caller can take ownership. + */ + native_handle_type do_release_socket() noexcept; }; -template +template template void -reactor_basic_socket::register_op( +reactor_basic_socket::register_op( Op& op, reactor_op_base*& desc_slot, bool& ready_flag, - bool& cancel_flag) noexcept + bool& cancel_flag, + bool is_write_direction) noexcept { svc_.work_started(); @@ -231,13 +261,22 @@ reactor_basic_socket::register_op( else { desc_slot = &op; + + // Select must rebuild its fd_sets when a write-direction op + // is parked, so select() watches for writability. Compiled + // away to nothing for epoll and kqueue. + if constexpr (Service::needs_write_notification) + { + if (is_write_direction) + svc_.scheduler().notify_reactor(); + } } } -template +template template void -reactor_basic_socket::cancel_single_op( +reactor_basic_socket::cancel_single_op( Op& op) noexcept { auto self = this->weak_from_this().lock(); @@ -272,9 +311,9 @@ reactor_basic_socket::cancel_single_op( } } -template +template void -reactor_basic_socket:: +reactor_basic_socket:: do_cancel() noexcept { auto self = this->weak_from_this().lock(); @@ -315,9 +354,9 @@ reactor_basic_socket:: } } -template +template void -reactor_basic_socket:: +reactor_basic_socket:: do_close_socket() noexcept { auto self = this->weak_from_this().lock(); @@ -365,8 +404,9 @@ reactor_basic_socket:: if (fd_ >= 0) { - if (desc_state_.registered_events != 0) - svc_.scheduler().deregister_descriptor(fd_); + // init_and_register always registers the descriptor, so any + // live fd is registered. Deregister unconditionally. + svc_.scheduler().deregister_descriptor(fd_); ::close(fd_); fd_ = -1; } @@ -374,7 +414,75 @@ reactor_basic_socket:: desc_state_.fd = -1; desc_state_.registered_events = 0; - local_endpoint_ = endpoint{}; + local_endpoint_ = Endpoint{}; +} + +template +native_handle_type +reactor_basic_socket:: + do_release_socket() noexcept +{ + // Cancel pending ops (same as do_close_socket) + auto self = this->weak_from_this().lock(); + if (self) + { + auto* d = static_cast(this); + + d->for_each_op([](auto& op) { op.request_cancel(); }); + + struct claimed_entry + { + reactor_op_base* base = nullptr; + }; + claimed_entry claimed[3]; + int count = 0; + + { + std::lock_guard lock(desc_state_.mutex); + d->for_each_desc_entry( + [&](auto& /*op*/, reactor_op_base*& desc_slot) { + auto* c = std::exchange(desc_slot, nullptr); + if (c) + { + claimed[count].base = c; + ++count; + } + }); + desc_state_.read_ready = false; + desc_state_.write_ready = false; + desc_state_.read_cancel_pending = false; + desc_state_.write_cancel_pending = false; + desc_state_.connect_cancel_pending = false; + + if (desc_state_.is_enqueued_.load(std::memory_order_acquire)) + desc_state_.impl_ref_ = self; + } + + for (int i = 0; i < count; ++i) + { + claimed[i].base->impl_ptr = self; + svc_.post(claimed[i].base); + svc_.work_finished(); + } + } + + native_handle_type released = fd_; + + if (fd_ >= 0) + { + // init_and_register always registers the descriptor, so any + // live fd is registered. Deregister unconditionally. + svc_.scheduler().deregister_descriptor(fd_); + // Do NOT close -- caller takes ownership + fd_ = -1; + } + + desc_state_.fd = -1; + desc_state_.registered_events = 0; + + local_endpoint_ = Endpoint{}; + + return released; } } // namespace boost::corosio::detail diff --git a/include/boost/corosio/native/detail/reactor/reactor_datagram_ops.hpp b/include/boost/corosio/native/detail/reactor/reactor_datagram_ops.hpp new file mode 100644 index 000000000..e1909e955 --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_datagram_ops.hpp @@ -0,0 +1,117 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DATAGRAM_OPS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DATAGRAM_OPS_HPP + +#include +#include + +namespace boost::corosio::detail { + +/* Parameterized datagram op types for reactor backends. + + @tparam Traits Backend traits (epoll_traits, kqueue_traits, etc.) + @tparam Socket The concrete datagram socket type (forward-declared). + @tparam DummyAcc Acceptor type placeholder (datagrams have no acceptor). + @tparam Endpoint The endpoint type (endpoint or local_endpoint). +*/ + +template +struct reactor_dgram_base_op + : reactor_op +{ + void operator()() override; + void cancel() noexcept override; +}; + +template +struct reactor_dgram_connect_op final + : reactor_connect_op< + reactor_dgram_base_op, + Endpoint> +{ + void operator()() override; +}; + +template +struct reactor_dgram_send_to_op final + : reactor_send_to_op< + reactor_dgram_base_op> +{ +}; + +template +struct reactor_dgram_recv_from_op final + : reactor_recv_from_op< + reactor_dgram_base_op, + Endpoint> +{ + void operator()() override; +}; + +template +struct reactor_dgram_send_op final + : reactor_send_op< + reactor_dgram_base_op> +{ +}; + +template +struct reactor_dgram_recv_op final + : reactor_recv_op< + reactor_dgram_base_op> +{ + void operator()() override; +}; + +// --- Deferred implementations --- + +template +void +reactor_dgram_base_op::operator()() +{ + complete_io_op(*this); +} + +template +void +reactor_dgram_base_op::cancel() noexcept +{ + if (this->socket_impl_) + this->socket_impl_->cancel_single_op(*this); + else + this->request_cancel(); +} + +template +void +reactor_dgram_connect_op::operator()() +{ + complete_connect_op(*this); +} + +template +void +reactor_dgram_recv_from_op::operator()() +{ + complete_datagram_op(*this, this->source_out); +} + +template +void +reactor_dgram_recv_op::operator()() +{ + // Datagram completion: zero-length datagrams are valid, not EOF. + complete_datagram_op(*this); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DATAGRAM_OPS_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_datagram_socket.hpp b/include/boost/corosio/native/detail/reactor/reactor_datagram_socket.hpp index 11b94ae85..ef12dc361 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_datagram_socket.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_datagram_socket.hpp @@ -11,6 +11,7 @@ #define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_DATAGRAM_SOCKET_HPP #include +#include #include #include #include @@ -23,6 +24,17 @@ namespace boost::corosio::detail { +/* Map portable message_flags values to native MSG_* constants. */ +inline int +to_native_msg_flags(int flags) noexcept +{ + int native = 0; + if (flags & 1) native |= MSG_PEEK; + if (flags & 2) native |= MSG_OOB; + if (flags & 4) native |= MSG_DONTROUTE; + return native; +} + /** CRTP base for reactor-backed datagram socket implementations. Inherits shared data members and cancel/close/register logic @@ -38,6 +50,10 @@ namespace boost::corosio::detail { @tparam SendOp The backend's connected send op type. @tparam RecvOp The backend's connected recv op type. @tparam DescState The backend's descriptor_state type. + @tparam ImplBase The public vtable base + (udp_socket::implementation or + local_datagram_socket::implementation). + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ template< class Derived, @@ -47,26 +63,30 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase = udp_socket::implementation, + class Endpoint = endpoint> class reactor_datagram_socket : public reactor_basic_socket< Derived, - udp_socket::implementation, + ImplBase, Service, - DescState> + DescState, + Endpoint> { using base_type = reactor_basic_socket< Derived, - udp_socket::implementation, + ImplBase, Service, - DescState>; + DescState, + Endpoint>; friend base_type; friend Derived; explicit reactor_datagram_socket(Service& svc) noexcept : base_type(svc) {} protected: - endpoint remote_endpoint_; + Endpoint remote_endpoint_; public: /// Pending connect operation slot. @@ -87,16 +107,91 @@ class reactor_datagram_socket ~reactor_datagram_socket() override = default; /// Return the cached remote endpoint. - endpoint remote_endpoint() const noexcept override + Endpoint remote_endpoint() const noexcept override { return remote_endpoint_; } + // --- Virtual method overrides (satisfy ImplBase pure virtuals) --- + + std::coroutine_handle<> send_to( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + Endpoint dest, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_send_to(h, ex, buf, dest, flags, token, ec, bytes_out); + } + + std::coroutine_handle<> recv_from( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + Endpoint* source, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_recv_from(h, ex, buf, source, flags, token, ec, bytes_out); + } + + std::coroutine_handle<> connect( + std::coroutine_handle<> h, + capy::executor_ref ex, + Endpoint ep, + std::stop_token token, + std::error_code* ec) override + { + return do_connect(h, ex, ep, token, ec); + } + + std::coroutine_handle<> send( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_send(h, ex, buf, flags, token, ec, bytes_out); + } + + std::coroutine_handle<> recv( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + int flags, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_recv(h, ex, buf, flags, token, ec, bytes_out); + } + + void cancel() noexcept override + { + this->do_cancel(); + } + + // --- End virtual overrides --- + + /// Close the socket (non-virtual, called by the service). + void close_socket() noexcept + { + do_close_socket(); + } + /// Cache local and remote endpoints. - void set_endpoints(endpoint local, endpoint remote) noexcept + void set_endpoints(Endpoint local, Endpoint remote) noexcept { - this->local_endpoint_ = local; - remote_endpoint_ = remote; + this->local_endpoint_ = std::move(local); + remote_endpoint_ = std::move(remote); } /** Shared send_to dispatch. @@ -109,7 +204,8 @@ class reactor_datagram_socket std::coroutine_handle<>, capy::executor_ref, buffer_param, - endpoint, + Endpoint const&, + int flags, std::stop_token const&, std::error_code*, std::size_t*); @@ -124,7 +220,8 @@ class reactor_datagram_socket std::coroutine_handle<>, capy::executor_ref, buffer_param, - endpoint*, + Endpoint*, + int flags, std::stop_token const&, std::error_code*, std::size_t*); @@ -138,7 +235,7 @@ class reactor_datagram_socket std::coroutine_handle<> do_connect( std::coroutine_handle<>, capy::executor_ref, - endpoint, + Endpoint const&, std::stop_token const&, std::error_code*); @@ -151,6 +248,7 @@ class reactor_datagram_socket std::coroutine_handle<>, capy::executor_ref, buffer_param, + int flags, std::stop_token const&, std::error_code*, std::size_t*); @@ -164,6 +262,7 @@ class reactor_datagram_socket std::coroutine_handle<>, capy::executor_ref, buffer_param, + int flags, std::stop_token const&, std::error_code*, std::size_t*); @@ -176,7 +275,42 @@ class reactor_datagram_socket void do_close_socket() noexcept { base_type::do_close_socket(); - remote_endpoint_ = endpoint{}; + remote_endpoint_ = Endpoint{}; + } + + native_handle_type do_release_socket() noexcept + { + auto fd = base_type::do_release_socket(); + remote_endpoint_ = Endpoint{}; + return fd; + } + + /** Shut down part or all of the full-duplex connection. + + Not an override — concrete backends forward here. + + @param what 0 = receive, 1 = send, 2 = both. + */ + std::error_code do_shutdown(int what) noexcept + { + int how; + switch (what) + { + case 0: + how = SHUT_RD; + break; + case 1: + how = SHUT_WR; + break; + case 2: + how = SHUT_RDWR; + break; + default: + return make_err(EINVAL); + } + if (::shutdown(this->fd_, how) != 0) + return make_err(errno); + return {}; } private: @@ -245,7 +379,9 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> reactor_datagram_socket< Derived, @@ -255,12 +391,15 @@ reactor_datagram_socket< RecvFromOp, SendOp, RecvOp, - DescState>:: + DescState, + ImplBase, + Endpoint>:: do_send_to( std::coroutine_handle<> h, capy::executor_ref ex, buffer_param param, - endpoint dest, + Endpoint const& dest, + int flags, std::stop_token const& token, std::error_code* ec, std::size_t* bytes_out) @@ -279,8 +418,9 @@ reactor_datagram_socket< } // Set up destination address - op.dest_len = to_sockaddr(dest, socket_family(this->fd_), op.dest_storage); - op.fd = this->fd_; + op.dest_len = to_sockaddr(dest, socket_family(this->fd_), op.dest_storage); + op.fd = this->fd_; + op.msg_flags = to_native_msg_flags(flags); // Speculative sendmsg msghdr msg{}; @@ -290,9 +430,9 @@ reactor_datagram_socket< msg.msg_iovlen = static_cast(op.iovec_count); #ifdef MSG_NOSIGNAL - constexpr int send_flags = MSG_NOSIGNAL; + int send_flags = op.msg_flags | MSG_NOSIGNAL; #else - constexpr int send_flags = 0; + int send_flags = op.msg_flags; #endif ssize_t n; @@ -335,7 +475,7 @@ reactor_datagram_socket< this->register_op( op, this->desc_state_.write_op, this->desc_state_.write_ready, - this->desc_state_.write_cancel_pending); + this->desc_state_.write_cancel_pending, true); return std::noop_coroutine(); } @@ -349,7 +489,9 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> reactor_datagram_socket< Derived, @@ -359,12 +501,15 @@ reactor_datagram_socket< RecvFromOp, SendOp, RecvOp, - DescState>:: + DescState, + ImplBase, + Endpoint>:: do_recv_from( std::coroutine_handle<> h, capy::executor_ref ex, buffer_param param, - endpoint* source, + Endpoint* source, + int flags, std::stop_token const& token, std::error_code* ec, std::size_t* bytes_out) @@ -397,6 +542,7 @@ reactor_datagram_socket< op.fd = this->fd_; op.source_out = source; + op.msg_flags = to_native_msg_flags(flags); // Speculative recvmsg msghdr msg{}; @@ -408,7 +554,7 @@ reactor_datagram_socket< ssize_t n; do { - n = ::recvmsg(this->fd_, &msg, 0); + n = ::recvmsg(this->fd_, &msg, op.msg_flags); } while (n < 0 && errno == EINTR); @@ -416,13 +562,18 @@ reactor_datagram_socket< { int err = (n < 0) ? errno : 0; auto bytes = (n > 0) ? static_cast(n) : std::size_t(0); + if (n >= 0) + op.source_addrlen = msg.msg_namelen; if (this->svc_.scheduler().try_consume_inline_budget()) { *ec = err ? make_err(err) : std::error_code{}; *bytes_out = bytes; if (source && !err && n >= 0) - *source = from_sockaddr(op.source_storage); + *source = from_sockaddr_as( + op.source_storage, + op.source_addrlen, + Endpoint{}); op.cont_op.cont.h = h; return dispatch_coro(ex, op.cont_op.cont); } @@ -461,7 +612,9 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> reactor_datagram_socket< Derived, @@ -471,11 +624,13 @@ reactor_datagram_socket< RecvFromOp, SendOp, RecvOp, - DescState>:: + DescState, + ImplBase, + Endpoint>:: do_connect( std::coroutine_handle<> h, capy::executor_ref ex, - endpoint ep, + Endpoint const& ep, std::stop_token const& token, std::error_code* ec) { @@ -493,7 +648,8 @@ reactor_datagram_socket< if (::getsockname( this->fd_, reinterpret_cast(&local_storage), &local_len) == 0) - this->local_endpoint_ = from_sockaddr(local_storage); + this->local_endpoint_ = + from_sockaddr_as(local_storage, local_len, Endpoint{}); remote_endpoint_ = ep; } @@ -531,7 +687,7 @@ reactor_datagram_socket< this->register_op( op, this->desc_state_.connect_op, this->desc_state_.write_ready, - this->desc_state_.connect_cancel_pending); + this->desc_state_.connect_cancel_pending, true); return std::noop_coroutine(); } @@ -545,7 +701,9 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> reactor_datagram_socket< Derived, @@ -555,11 +713,14 @@ reactor_datagram_socket< RecvFromOp, SendOp, RecvOp, - DescState>:: + DescState, + ImplBase, + Endpoint>:: do_send( std::coroutine_handle<> h, capy::executor_ref ex, buffer_param param, + int flags, std::stop_token const& token, std::error_code* ec, std::size_t* bytes_out) @@ -576,7 +737,8 @@ reactor_datagram_socket< op.iovecs[i].iov_len = bufs[i].size(); } - op.fd = this->fd_; + op.fd = this->fd_; + op.msg_flags = to_native_msg_flags(flags); // Speculative sendmsg with no destination (connected mode) msghdr msg{}; @@ -584,9 +746,9 @@ reactor_datagram_socket< msg.msg_iovlen = static_cast(op.iovec_count); #ifdef MSG_NOSIGNAL - constexpr int send_flags = MSG_NOSIGNAL; + int send_flags = op.msg_flags | MSG_NOSIGNAL; #else - constexpr int send_flags = 0; + int send_flags = op.msg_flags; #endif ssize_t n; @@ -629,7 +791,7 @@ reactor_datagram_socket< this->register_op( op, this->desc_state_.write_op, this->desc_state_.write_ready, - this->desc_state_.write_cancel_pending); + this->desc_state_.write_cancel_pending, true); return std::noop_coroutine(); } @@ -643,7 +805,9 @@ template< class RecvFromOp, class SendOp, class RecvOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> reactor_datagram_socket< Derived, @@ -653,11 +817,14 @@ reactor_datagram_socket< RecvFromOp, SendOp, RecvOp, - DescState>:: + DescState, + ImplBase, + Endpoint>:: do_recv( std::coroutine_handle<> h, capy::executor_ref ex, buffer_param param, + int flags, std::stop_token const& token, std::error_code* ec, std::size_t* bytes_out) @@ -687,7 +854,8 @@ reactor_datagram_socket< op.iovecs[i].iov_len = bufs[i].size(); } - op.fd = this->fd_; + op.fd = this->fd_; + op.msg_flags = to_native_msg_flags(flags); // Speculative recvmsg with no source (connected mode) msghdr msg{}; @@ -697,7 +865,7 @@ reactor_datagram_socket< ssize_t n; do { - n = ::recvmsg(this->fd_, &msg, 0); + n = ::recvmsg(this->fd_, &msg, op.msg_flags); } while (n < 0 && errno == EINTR); diff --git a/include/boost/corosio/native/detail/reactor/reactor_op.hpp b/include/boost/corosio/native/detail/reactor/reactor_op.hpp index 69cd95250..80a925a6c 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_op.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_op.hpp @@ -148,18 +148,19 @@ struct reactor_op : reactor_op_base and cancel() are provided by the concrete backend type. @tparam Base The backend's base op type. + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ -template +template struct reactor_connect_op : Base { /// Endpoint to connect to. - endpoint target_endpoint; + Endpoint target_endpoint; /// Reset operation state for reuse. void reset() noexcept { Base::reset(); - target_endpoint = endpoint{}; + target_endpoint = Endpoint{}; } void perform_io() noexcept override @@ -263,11 +264,14 @@ struct reactor_write_op : Base /** Shared accept operation. - Delegates the actual syscall to AcceptPolicy::do_accept(fd, peer_storage), - which returns the accepted fd or -1 with errno set. + Delegates the actual syscall to + AcceptPolicy::do_accept(fd, peer_storage, peer_addrlen), + which returns the accepted fd or -1 with errno set and writes + the real peer address length into peer_addrlen. @tparam Base The backend's base op type. - @tparam AcceptPolicy Provides `static int do_accept(int, sockaddr_storage&)`. + @tparam AcceptPolicy Provides + `static int do_accept(int, sockaddr_storage&, socklen_t&)`. */ template struct reactor_accept_op : Base @@ -284,6 +288,9 @@ struct reactor_accept_op : Base /// Peer address storage filled by accept. sockaddr_storage peer_storage{}; + /// Actual peer address length returned by accept. + socklen_t peer_addrlen = 0; + void reset() noexcept { Base::reset(); @@ -291,11 +298,13 @@ struct reactor_accept_op : Base peer_impl = nullptr; impl_out = nullptr; peer_storage = {}; + peer_addrlen = 0; } void perform_io() noexcept override { - int new_fd = AcceptPolicy::do_accept(this->fd, peer_storage); + int new_fd = + AcceptPolicy::do_accept(this->fd, peer_storage, peer_addrlen); if (new_fd >= 0) { accepted_fd = new_fd; @@ -326,10 +335,14 @@ struct reactor_send_op : Base /// Number of active I/O vectors. int iovec_count = 0; + /// User-supplied message flags. + int msg_flags = 0; + void reset() noexcept { Base::reset(); iovec_count = 0; + msg_flags = 0; } void perform_io() noexcept override @@ -339,9 +352,9 @@ struct reactor_send_op : Base msg.msg_iovlen = static_cast(iovec_count); #ifdef MSG_NOSIGNAL - constexpr int send_flags = MSG_NOSIGNAL; + int send_flags = msg_flags | MSG_NOSIGNAL; #else - constexpr int send_flags = 0; + int send_flags = msg_flags; #endif ssize_t n; @@ -378,6 +391,9 @@ struct reactor_recv_op : Base /// Number of active I/O vectors. int iovec_count = 0; + /// User-supplied message flags. + int msg_flags = 0; + /// Return true (this is a read-direction operation). bool is_read_operation() const noexcept override { @@ -388,6 +404,7 @@ struct reactor_recv_op : Base { Base::reset(); iovec_count = 0; + msg_flags = 0; } void perform_io() noexcept override @@ -399,7 +416,7 @@ struct reactor_recv_op : Base ssize_t n; do { - n = ::recvmsg(this->fd, &msg, 0); + n = ::recvmsg(this->fd, &msg, msg_flags); } while (n < 0 && errno == EINTR); @@ -434,12 +451,16 @@ struct reactor_send_to_op : Base /// Destination address length. socklen_t dest_len = 0; + /// User-supplied message flags. + int msg_flags = 0; + void reset() noexcept { Base::reset(); iovec_count = 0; dest_storage = {}; dest_len = 0; + msg_flags = 0; } void perform_io() noexcept override @@ -451,9 +472,9 @@ struct reactor_send_to_op : Base msg.msg_iovlen = static_cast(iovec_count); #ifdef MSG_NOSIGNAL - constexpr int send_flags = MSG_NOSIGNAL; + int send_flags = msg_flags | MSG_NOSIGNAL; #else - constexpr int send_flags = 0; + int send_flags = msg_flags; #endif ssize_t n; @@ -475,8 +496,9 @@ struct reactor_send_to_op : Base Uses recvmsg() with msg_name to capture the source endpoint. @tparam Base The backend's base op type. + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ -template +template struct reactor_recv_from_op : Base { /// Maximum scatter-gather buffer count. @@ -491,8 +513,14 @@ struct reactor_recv_from_op : Base /// Source address storage filled by recvmsg. sockaddr_storage source_storage{}; + /// Actual source address length returned by recvmsg. + socklen_t source_addrlen = 0; + /// Output pointer for the source endpoint (set by do_recv_from). - endpoint* source_out = nullptr; + Endpoint* source_out = nullptr; + + /// User-supplied message flags. + int msg_flags = 0; /// Return true (this is a read-direction operation). bool is_read_operation() const noexcept override @@ -505,7 +533,9 @@ struct reactor_recv_from_op : Base Base::reset(); iovec_count = 0; source_storage = {}; + source_addrlen = 0; source_out = nullptr; + msg_flags = 0; } void perform_io() noexcept override @@ -519,12 +549,15 @@ struct reactor_recv_from_op : Base ssize_t n; do { - n = ::recvmsg(this->fd, &msg, 0); + n = ::recvmsg(this->fd, &msg, msg_flags); } while (n < 0 && errno == EINTR); if (n >= 0) + { + source_addrlen = msg.msg_namelen; this->complete(0, static_cast(n)); + } else this->complete(errno, 0); } diff --git a/include/boost/corosio/native/detail/reactor/reactor_op_complete.hpp b/include/boost/corosio/native/detail/reactor/reactor_op_complete.hpp index 3864bd19a..209cb701b 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_op_complete.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_op_complete.hpp @@ -79,13 +79,15 @@ complete_connect_op(Op& op) if (success && op.socket_impl_) { - endpoint local_ep; + using ep_type = decltype(op.target_endpoint); + ep_type local_ep; sockaddr_storage local_storage{}; socklen_t local_len = sizeof(local_storage); if (::getsockname( op.fd, reinterpret_cast(&local_storage), &local_len) == 0) - local_ep = from_sockaddr(local_storage); + local_ep = + from_sockaddr_as(local_storage, local_len, ep_type{}); op.socket_impl_->set_endpoints(local_ep, op.target_endpoint); } @@ -113,6 +115,7 @@ complete_connect_op(Op& op) @param acceptor_impl The acceptor that accepted the connection. @param accepted_fd The accepted file descriptor (set to -1 on success). @param peer_storage The peer address from accept(). + @param peer_addrlen The actual peer address length from accept(). @param impl_out Output pointer for the new socket impl. @param ec_out Output pointer for any error. @return True on success, false on failure. @@ -123,10 +126,11 @@ setup_accepted_socket( AcceptorImpl* acceptor_impl, int& accepted_fd, sockaddr_storage const& peer_storage, + socklen_t peer_addrlen, io_object::implementation** impl_out, std::error_code* ec_out) { - auto* socket_svc = acceptor_impl->service().tcp_service(); + auto* socket_svc = acceptor_impl->service().stream_service(); if (!socket_svc) { *ec_out = make_err(ENOENT); @@ -145,8 +149,10 @@ setup_accepted_socket( } socket_svc->scheduler().register_descriptor(accepted_fd, &impl.desc_state_); + using ep_type = decltype(acceptor_impl->local_endpoint()); impl.set_endpoints( - acceptor_impl->local_endpoint(), from_sockaddr(peer_storage)); + acceptor_impl->local_endpoint(), + from_sockaddr_as(peer_storage, peer_addrlen, ep_type{})); if (impl_out) *impl_out = &impl; @@ -183,8 +189,8 @@ complete_accept_op(Op& op) if (success && op.accepted_fd >= 0 && op.acceptor_impl_) { if (!setup_accepted_socket( - op.acceptor_impl_, op.accepted_fd, op.peer_storage, op.impl_out, - op.ec_out)) + op.acceptor_impl_, op.accepted_fd, op.peer_storage, + op.peer_addrlen, op.impl_out, op.ec_out)) success = false; } @@ -205,7 +211,38 @@ complete_accept_op(Op& op) dispatch_coro(saved_ex, op.cont_op.cont).resume(); } -/** Complete a datagram operation (send_to or recv_from). +/** Complete a connected datagram operation (send or recv). + + No source endpoint to capture. Critically, does NOT map + bytes_transferred == 0 to EOF — zero-length datagrams are valid + events on connected datagram sockets. + + @tparam Op The concrete datagram operation type. + @param op The operation to complete. +*/ +template +void +complete_datagram_op(Op& op) +{ + op.stop_cb.reset(); + op.socket_impl_->desc_state_.scheduler_->reset_inline_budget(); + + if (op.cancelled.load(std::memory_order_acquire)) + *op.ec_out = capy::error::canceled; + else if (op.errn != 0) + *op.ec_out = make_err(op.errn); + else + *op.ec_out = {}; + + *op.bytes_out = op.bytes_transferred; + + op.cont_op.cont.h = op.h; + capy::executor_ref saved_ex(op.ex); + auto prevent = std::move(op.impl_ptr); + dispatch_coro(saved_ex, op.cont_op.cont).resume(); +} + +/** Complete a datagram operation with source endpoint capture. For recv_from operations, writes the source endpoint from the recorded sockaddr_storage into the caller's endpoint pointer. @@ -216,9 +253,9 @@ complete_accept_op(Op& op) @param source_out Optional pointer to store source endpoint (non-null for recv_from, null for send_to). */ -template +template void -complete_datagram_op(Op& op, endpoint* source_out) +complete_datagram_op(Op& op, Endpoint* source_out) { op.stop_cb.reset(); op.socket_impl_->desc_state_.scheduler_->reset_inline_budget(); @@ -234,7 +271,10 @@ complete_datagram_op(Op& op, endpoint* source_out) if (source_out && !op.cancelled.load(std::memory_order_acquire) && op.errn == 0) - *source_out = from_sockaddr(op.source_storage); + *source_out = from_sockaddr_as( + op.source_storage, + op.source_addrlen, + Endpoint{}); op.cont_op.cont.h = op.h; capy::executor_ref saved_ex(op.ex); diff --git a/include/boost/corosio/native/detail/reactor/reactor_scheduler.hpp b/include/boost/corosio/native/detail/reactor/reactor_scheduler.hpp index 967630bd9..ae2a1dbf7 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_scheduler.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_scheduler.hpp @@ -148,6 +148,9 @@ class reactor_scheduler using lock_type = mutex_type::scoped_lock; using event_type = conditionally_enabled_event; + /// Epoll and kqueue do not need write-direction notification. + static constexpr bool needs_write_notification = false; + /// Post a coroutine for deferred execution. void post(std::coroutine_handle<> h) const override; diff --git a/include/boost/corosio/native/detail/reactor/reactor_service_finals.hpp b/include/boost/corosio/native/detail/reactor/reactor_service_finals.hpp new file mode 100644 index 000000000..e24e38431 --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_service_finals.hpp @@ -0,0 +1,396 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SERVICE_FINALS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SERVICE_FINALS_HPP + +/* Parameterized final service types for reactor backends. + + One service template per protocol (TCP, local stream, UDP, local + datagram, TCP acceptor, local stream acceptor) because each abstract + service base declares different virtual methods. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +// ============================================================ +// Shared socket creation helpers +// ============================================================ + +template +std::error_code +do_open_socket( + SocketFinal* socket_impl, + int family, int type, int protocol, + bool is_ip) noexcept +{ + socket_impl->close_socket(); + + int fd = Traits::create_socket(family, type, protocol); + if (fd < 0) + return make_err(errno); + + std::error_code ec = is_ip + ? Traits::configure_ip_socket(fd, family) + : Traits::configure_local_socket(fd); + + if (ec) + { + ::close(fd); + return ec; + } + + socket_impl->init_and_register(fd); + return {}; +} + +template +std::error_code +do_assign_fd( + SocketFinal* socket_impl, + int fd, + int expected_type) noexcept +{ + socket_impl->close_socket(); + + // Validate that fd is actually an AF_UNIX socket of the expected + // type BEFORE applying any backend setup. Otherwise we could + // adopt (or worse, mutate flags on) a foreign fd — e.g., an + // AF_INET SOCK_STREAM fd passed into local_stream_socket::assign, + // or a SOCK_DGRAM fd passed into a stream socket. The caller + // retains ownership of fd on any error from this function, so + // doing the checks first leaves a rejected fd unmodified. + { + sockaddr_storage st{}; + socklen_t st_len = sizeof(st); + if (::getsockname( + fd, reinterpret_cast(&st), &st_len) != 0) + return make_err(errno); + if (st.ss_family != AF_UNIX) + return make_err(EAFNOSUPPORT); + + int sock_type = 0; + socklen_t opt_len = sizeof(sock_type); + if (::getsockopt( + fd, SOL_SOCKET, SO_TYPE, &sock_type, &opt_len) != 0) + return make_err(errno); + if (sock_type != expected_type) + return make_err(EPROTOTYPE); + } + + // Apply backend fd setup (O_NONBLOCK, FD_CLOEXEC, SO_NOSIGPIPE on kqueue, + // FD_SETSIZE validation on select). On failure, the caller retains + // ownership of fd — assign_socket reports the error and assign() throws + // without closing. + std::error_code ec = Traits::configure_local_socket(fd); + if (ec) + return ec; + + socket_impl->init_and_register(fd); + + // Best-effort: refresh endpoint caches so local_endpoint() and + // remote_endpoint() reflect the actual fd state. Failures (e.g. + // ENOTCONN from getpeername on an unconnected socket) are ignored; + // for unnamed socketpair() fds the queries succeed but yield empty + // endpoints, which is the correct cached state. + using endpoint_type = std::remove_cvref_t< + decltype(socket_impl->local_endpoint())>; + + endpoint_type local_ep{}; + sockaddr_storage local_storage{}; + socklen_t local_len = sizeof(local_storage); + if (::getsockname( + fd, reinterpret_cast(&local_storage), &local_len) == 0) + local_ep = from_sockaddr_as(local_storage, local_len, endpoint_type{}); + + endpoint_type remote_ep{}; + sockaddr_storage peer_storage{}; + socklen_t peer_len = sizeof(peer_storage); + if (::getpeername( + fd, reinterpret_cast(&peer_storage), &peer_len) == 0) + remote_ep = from_sockaddr_as(peer_storage, peer_len, endpoint_type{}); + + socket_impl->set_endpoints(local_ep, remote_ep); + + return {}; +} + +template +std::error_code +do_open_acceptor( + AccFinal* acc_impl, + int family, int type, int protocol, + bool is_ip) noexcept +{ + acc_impl->close_socket(); + + int fd = Traits::create_socket(family, type, protocol); + if (fd < 0) + return make_err(errno); + + std::error_code ec = is_ip + ? Traits::configure_ip_acceptor(fd, family) + : Traits::configure_local_socket(fd); + + if (ec) + { + ::close(fd); + return ec; + } + + acc_impl->init_acceptor_fd(fd); + return {}; +} + +// ============================================================ +// TCP service +// ============================================================ + +template +class reactor_tcp_service_final final + : public reactor_socket_service< + reactor_tcp_service_final, + tcp_service, + typename Traits::scheduler_type, + SocketFinal> +{ + using base_service = reactor_socket_service< + reactor_tcp_service_final, tcp_service, + typename Traits::scheduler_type, SocketFinal>; + friend base_service; + +public: + explicit reactor_tcp_service_final(capy::execution_context& ctx) + : base_service(ctx) {} + + std::error_code open_socket( + tcp_socket::implementation& impl, + int family, int type, int protocol) override + { + return do_open_socket( + static_cast(&impl), + family, type, protocol, true); + } + + std::error_code bind_socket( + tcp_socket::implementation& impl, endpoint ep) override + { + return static_cast(&impl)->do_bind(ep); + } + + void pre_shutdown(SocketFinal* impl) noexcept + { + impl->hook_.pre_shutdown(impl->native_handle()); + } + + void pre_destroy(SocketFinal* impl) noexcept + { + impl->hook_.pre_destroy(impl->native_handle()); + } +}; + +// ============================================================ +// Local stream service +// ============================================================ + +template +class reactor_local_stream_service_final final + : public reactor_socket_service< + reactor_local_stream_service_final, + local_stream_service, + typename Traits::scheduler_type, + SocketFinal> +{ + using base_service = reactor_socket_service< + reactor_local_stream_service_final, local_stream_service, + typename Traits::scheduler_type, SocketFinal>; + friend base_service; + +public: + explicit reactor_local_stream_service_final(capy::execution_context& ctx) + : base_service(ctx) {} + + std::error_code open_socket( + local_stream_socket::implementation& impl, + int family, int type, int protocol) override + { + return do_open_socket( + static_cast(&impl), + family, type, protocol, false); + } + + std::error_code assign_socket( + local_stream_socket::implementation& impl, int fd) override + { + return do_assign_fd( + static_cast(&impl), fd, SOCK_STREAM); + } +}; + +// ============================================================ +// UDP service +// ============================================================ + +template +class reactor_udp_service_final final + : public reactor_socket_service< + reactor_udp_service_final, + udp_service, + typename Traits::scheduler_type, + SocketFinal> +{ + using base_service = reactor_socket_service< + reactor_udp_service_final, udp_service, + typename Traits::scheduler_type, SocketFinal>; + friend base_service; + +public: + explicit reactor_udp_service_final(capy::execution_context& ctx) + : base_service(ctx) {} + + std::error_code open_datagram_socket( + udp_socket::implementation& impl, + int family, int type, int protocol) override + { + return do_open_socket( + static_cast(&impl), + family, type, protocol, true); + } + + std::error_code bind_datagram( + udp_socket::implementation& impl, endpoint ep) override + { + return static_cast(&impl)->do_bind(ep); + } +}; + +// ============================================================ +// Local datagram service +// ============================================================ + +template +class reactor_local_dgram_service_final final + : public reactor_socket_service< + reactor_local_dgram_service_final, + local_datagram_service, + typename Traits::scheduler_type, + SocketFinal> +{ + using base_service = reactor_socket_service< + reactor_local_dgram_service_final, local_datagram_service, + typename Traits::scheduler_type, SocketFinal>; + friend base_service; + +public: + explicit reactor_local_dgram_service_final(capy::execution_context& ctx) + : base_service(ctx) {} + + std::error_code open_socket( + local_datagram_socket::implementation& impl, + int family, int type, int protocol) override + { + return do_open_socket( + static_cast(&impl), + family, type, protocol, false); + } + + std::error_code assign_socket( + local_datagram_socket::implementation& impl, int fd) override + { + return do_assign_fd( + static_cast(&impl), fd, SOCK_DGRAM); + } + + std::error_code bind_socket( + local_datagram_socket::implementation& impl, + corosio::local_endpoint ep) override + { + return static_cast(&impl)->do_bind(ep); + } +}; + +// ============================================================ +// Acceptor service +// ============================================================ + +template +class reactor_acceptor_service_final final + : public reactor_acceptor_service< + reactor_acceptor_service_final, + ServiceBase, + typename Traits::scheduler_type, + AccFinal, + StreamServiceFinal> +{ + using base_service = reactor_acceptor_service< + reactor_acceptor_service_final, + ServiceBase, + typename Traits::scheduler_type, + AccFinal, + StreamServiceFinal>; + friend base_service; + +public: + explicit reactor_acceptor_service_final(capy::execution_context& ctx) + : base_service(ctx) + { + // Look up the concrete stream service directly by its type. + // Avoids dynamic_cast which can fail across template boundaries + // on some platforms (FreeBSD clang RTTI/visibility). + this->stream_svc_ = + this->ctx_.template find_service(); + } + + std::error_code open_acceptor_socket( + typename AccFinal::impl_base_type& impl, + int family, int type, int protocol) override + { + return do_open_acceptor( + static_cast(&impl), + family, type, protocol, + std::is_same_v); + } + + std::error_code bind_acceptor( + typename AccFinal::impl_base_type& impl, + Endpoint ep) override + { + return static_cast(&impl)->do_bind(ep); + } + + std::error_code listen_acceptor( + typename AccFinal::impl_base_type& impl, + int backlog) override + { + return static_cast(&impl)->do_listen(backlog); + } +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SERVICE_FINALS_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_socket_finals.hpp b/include/boost/corosio/native/detail/reactor/reactor_socket_finals.hpp new file mode 100644 index 000000000..c66038d3e --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_socket_finals.hpp @@ -0,0 +1,350 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SOCKET_FINALS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SOCKET_FINALS_HPP + +/* Parameterized final socket and acceptor types for reactor backends. + + These templates are instantiated per-backend via Traits to produce + the concrete socket types used by the public API. Each final type + is a thin wrapper adding only protocol-specific details (ImplBase, + Endpoint) to the CRTP base classes. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio::detail { + +// ============================================================ +// Forward declarations +// ============================================================ + +template class reactor_stream_socket_final; +template class reactor_dgram_socket_final; +template class reactor_acceptor_final; + +template class reactor_tcp_service_final; +template class reactor_local_stream_service_final; +template class reactor_udp_service_final; +template class reactor_local_dgram_service_final; +template class reactor_acceptor_service_final; + +// ============================================================ +// Op type aliases +// ============================================================ + +template +using stream_socket_t = reactor_stream_socket_final, + tcp_socket::implementation, + local_stream_socket::implementation>, + Endpoint>; + +template +using stream_acceptor_t = reactor_acceptor_final, + tcp_acceptor::implementation, + local_stream_acceptor::implementation>, + Endpoint>; + +template +using stream_base_op = reactor_stream_base_op< + Traits, stream_socket_t, + stream_acceptor_t, Endpoint>; + +template +using stream_connect_op = reactor_stream_connect_op< + Traits, stream_socket_t, + stream_acceptor_t, Endpoint>; + +template +using stream_read_op = reactor_stream_read_op< + Traits, stream_socket_t, + stream_acceptor_t, Endpoint>; + +template +using stream_write_op = reactor_stream_write_op< + Traits, stream_socket_t, + stream_acceptor_t, Endpoint>; + +template +using stream_accept_op = reactor_stream_accept_op< + Traits, stream_socket_t, + stream_acceptor_t, Endpoint>; + +template +using dgram_socket_t = reactor_dgram_socket_final, + udp_socket::implementation, + local_datagram_socket::implementation>, + Endpoint>; + +template +using dgram_connect_op = reactor_dgram_connect_op< + Traits, dgram_socket_t, + stream_acceptor_t, Endpoint>; + +template +using dgram_send_to_op = reactor_dgram_send_to_op< + Traits, dgram_socket_t, + stream_acceptor_t, Endpoint>; + +template +using dgram_recv_from_op = reactor_dgram_recv_from_op< + Traits, dgram_socket_t, + stream_acceptor_t, Endpoint>; + +template +using dgram_send_op = reactor_dgram_send_op< + Traits, dgram_socket_t, + stream_acceptor_t, Endpoint>; + +template +using dgram_recv_op = reactor_dgram_recv_op< + Traits, dgram_socket_t, + stream_acceptor_t, Endpoint>; + +// ============================================================ +// Stream socket final +// ============================================================ + +// release_socket below cannot be marked 'override' unconditionally: it +// overrides a pure virtual only when ImplBase is local_stream_socket:: +// implementation. For the tcp_socket::implementation instantiation the +// base has no such virtual, so 'override' would fail to compile. Scope- +// suppress clang's -Winconsistent-missing-override for the overriding +// instantiation. clang-tidy's modernize-use-override is silenced +// separately via NOLINTNEXTLINE below. +BOOST_COROSIO_CLANG_WARNING_PUSH +BOOST_COROSIO_CLANG_WARNING_DISABLE("-Winconsistent-missing-override") + +template +class reactor_stream_socket_final final + : public reactor_stream_socket< + reactor_stream_socket_final, + std::conditional_t, + reactor_tcp_service_final< + Traits, reactor_stream_socket_final>, + reactor_local_stream_service_final< + Traits, reactor_stream_socket_final>>, + stream_connect_op, + stream_read_op, + stream_write_op, + typename Traits::desc_state_type, + ImplBase, + Endpoint> +{ + using service_type = std::conditional_t, + reactor_tcp_service_final, + reactor_local_stream_service_final>; + friend service_type; + +public: + using impl_base_type = ImplBase; + + /// Per-socket hook state (e.g., kqueue SO_LINGER tracking). + [[no_unique_address]] typename Traits::stream_socket_hook hook_; + + explicit reactor_stream_socket_final(service_type& svc) noexcept + : reactor_stream_socket_final::reactor_stream_socket(svc) + { + } + + ~reactor_stream_socket_final() override = default; + + std::error_code set_option( + int level, int optname, + void const* data, std::size_t size) noexcept override + { + return hook_.on_set_option(this->fd_, level, optname, data, size); + } + + // Shadows reactor_stream_socket::close_socket so the hook fires on + // every fd close path (user close(), service shutdown/destroy, + // do_assign_fd reuse). The hook needs the fd before do_close_socket + // resets it to -1. + void close_socket() noexcept + { + hook_.pre_shutdown(this->fd_); + this->do_close_socket(); + } + + // Overrides local_stream_socket::implementation::release_socket(). + // Cannot use 'override' — tcp_socket::implementation has no such method. + // NOLINTNEXTLINE(modernize-use-override) + native_handle_type release_socket() noexcept + { + hook_ = {}; + return this->do_release_socket(); + } +}; + +BOOST_COROSIO_CLANG_WARNING_POP + +// ============================================================ +// Datagram socket final +// ============================================================ + +// shutdown/bind/release_socket below cannot be marked 'override' +// unconditionally: they override pure virtuals only when ImplBase is +// local_datagram_socket::implementation. For the udp_socket::implementation +// instantiation the base has no such virtuals, so 'override' would fail +// to compile. Scope-suppress clang's -Winconsistent-missing-override for +// the overriding instantiation. clang-tidy's modernize-use-override is +// silenced separately via NOLINTNEXTLINE below. +BOOST_COROSIO_CLANG_WARNING_PUSH +BOOST_COROSIO_CLANG_WARNING_DISABLE("-Winconsistent-missing-override") + +template +class reactor_dgram_socket_final final + : public reactor_datagram_socket< + reactor_dgram_socket_final, + std::conditional_t, + reactor_udp_service_final< + Traits, reactor_dgram_socket_final>, + reactor_local_dgram_service_final< + Traits, reactor_dgram_socket_final>>, + dgram_connect_op, + dgram_send_to_op, + dgram_recv_from_op, + dgram_send_op, + dgram_recv_op, + typename Traits::desc_state_type, + ImplBase, + Endpoint> +{ + using service_type = std::conditional_t, + reactor_udp_service_final, + reactor_local_dgram_service_final>; + friend service_type; + +public: + using impl_base_type = ImplBase; + + explicit reactor_dgram_socket_final(service_type& svc) noexcept + : reactor_dgram_socket_final::reactor_datagram_socket(svc) + { + } + + ~reactor_dgram_socket_final() override = default; + + // Overrides local_datagram_socket pure virtuals. + // Cannot use 'override' — udp_socket::implementation has no such methods. + // NOLINTNEXTLINE(modernize-use-override) + std::error_code shutdown(corosio::shutdown_type what) noexcept + { + return this->do_shutdown(static_cast(what)); + } + + // NOLINTNEXTLINE(modernize-use-override) + std::error_code bind(Endpoint ep) noexcept + { + return this->do_bind(ep); + } + + // NOLINTNEXTLINE(modernize-use-override) + native_handle_type release_socket() noexcept + { + return this->do_release_socket(); + } +}; + +BOOST_COROSIO_CLANG_WARNING_POP + +// ============================================================ +// Acceptor final +// ============================================================ + +template +using stream_service_for = std::conditional_t, + reactor_tcp_service_final>, + reactor_local_stream_service_final>>; + +// release_socket below cannot be marked 'override' unconditionally: it +// overrides a pure virtual only when AccImplBase is +// local_stream_acceptor::implementation. For the tcp_acceptor::implementation +// instantiation the base has no such virtual, so 'override' would fail to +// compile. Scope-suppress clang's -Winconsistent-missing-override for the +// overriding instantiation. clang-tidy's modernize-use-override is silenced +// separately via NOLINTNEXTLINE below. +BOOST_COROSIO_CLANG_WARNING_PUSH +BOOST_COROSIO_CLANG_WARNING_DISABLE("-Winconsistent-missing-override") + +template +class reactor_acceptor_final final + : public reactor_acceptor< + reactor_acceptor_final, + reactor_acceptor_service_final< + Traits, + std::conditional_t, + tcp_acceptor_service, local_stream_acceptor_service>, + reactor_acceptor_final, + stream_service_for, + Endpoint>, + stream_base_op, + stream_accept_op, + typename Traits::desc_state_type, + AccImplBase, + Endpoint> +{ + using acc_service_type = reactor_acceptor_service_final< + Traits, + std::conditional_t, + tcp_acceptor_service, local_stream_acceptor_service>, + reactor_acceptor_final, + stream_service_for, + Endpoint>; + friend acc_service_type; + +public: + explicit reactor_acceptor_final(acc_service_type& svc) noexcept + : reactor_acceptor_final::reactor_acceptor(svc) + { + } + + ~reactor_acceptor_final() override = default; + + using impl_base_type = AccImplBase; + + // NOLINTNEXTLINE(modernize-use-override) + native_handle_type release_socket() noexcept + { + return this->do_release_socket(); + } + + std::coroutine_handle<> accept( + std::coroutine_handle<>, + capy::executor_ref, + std::stop_token, + std::error_code*, + io_object::implementation**) override; +}; + +BOOST_COROSIO_CLANG_WARNING_POP + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SOCKET_FINALS_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_socket_service.hpp b/include/boost/corosio/native/detail/reactor/reactor_socket_service.hpp index f391ee637..33ba9f894 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_socket_service.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_socket_service.hpp @@ -38,6 +38,13 @@ class reactor_socket_service : public ServiceBase friend Derived; using state_type = reactor_service_state; +public: + /// Propagated from Scheduler for register_op's write notification. + static constexpr bool needs_write_notification = + Scheduler::needs_write_notification; + +private: + explicit reactor_socket_service(capy::execution_context& ctx) : state_( std::make_unique( diff --git a/include/boost/corosio/native/detail/reactor/reactor_stream_ops.hpp b/include/boost/corosio/native/detail/reactor/reactor_stream_ops.hpp new file mode 100644 index 000000000..9d2edb61c --- /dev/null +++ b/include/boost/corosio/native/detail/reactor/reactor_stream_ops.hpp @@ -0,0 +1,111 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_STREAM_OPS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_STREAM_OPS_HPP + +#include +#include + +namespace boost::corosio::detail { + +/* Parameterized stream op types for reactor backends. + + Given a Traits type (providing write_policy, accept_policy) and + forward-declared Socket/Acceptor types, generates all the concrete + op types needed for a stream socket. The cancel() and operator()() + bodies reference Socket/Acceptor but are only instantiated when the + vtable of a derived type is emitted — at which point those types + are complete. + + @tparam Traits Backend traits (epoll_traits, kqueue_traits, etc.) + @tparam Socket The concrete stream socket type (forward-declared). + @tparam Acceptor The concrete stream acceptor type (forward-declared). + @tparam Endpoint The endpoint type (endpoint or local_endpoint). +*/ + +template +struct reactor_stream_base_op + : reactor_op +{ + void operator()() override; + void cancel() noexcept override; +}; + +template +struct reactor_stream_connect_op final + : reactor_connect_op< + reactor_stream_base_op, + Endpoint> +{ + void operator()() override; +}; + +template +struct reactor_stream_read_op final + : reactor_read_op< + reactor_stream_base_op> +{ +}; + +template +struct reactor_stream_write_op final + : reactor_write_op< + reactor_stream_base_op, + typename Traits::write_policy> +{ +}; + +template +struct reactor_stream_accept_op final + : reactor_accept_op< + reactor_stream_base_op, + typename Traits::accept_policy> +{ + void operator()() override; +}; + +// --- Deferred implementations (instantiated when Socket/Acceptor are complete) --- + +template +void +reactor_stream_base_op::operator()() +{ + complete_io_op(*this); +} + +template +void +reactor_stream_base_op::cancel() noexcept +{ + if (this->socket_impl_) + this->socket_impl_->cancel_single_op(*this); + else if (this->acceptor_impl_) + this->acceptor_impl_->cancel_single_op(*this); + else + this->request_cancel(); +} + +template +void +reactor_stream_connect_op::operator()() +{ + complete_connect_op(*this); +} + +template +void +reactor_stream_accept_op::operator()() +{ + complete_accept_op(*this); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_STREAM_OPS_HPP diff --git a/include/boost/corosio/native/detail/reactor/reactor_stream_socket.hpp b/include/boost/corosio/native/detail/reactor/reactor_stream_socket.hpp index 93fe92700..c88461efc 100644 --- a/include/boost/corosio/native/detail/reactor/reactor_stream_socket.hpp +++ b/include/boost/corosio/native/detail/reactor/reactor_stream_socket.hpp @@ -26,7 +26,7 @@ namespace boost::corosio::detail { /** CRTP base for reactor-backed stream socket implementations. Inherits shared data members and cancel/close/register logic - from reactor_basic_socket. Adds the TCP-specific remote + from reactor_basic_socket. Adds the stream-specific remote endpoint, shutdown, and I/O dispatch (connect, read, write). @tparam Derived The concrete socket type (CRTP). @@ -35,6 +35,10 @@ namespace boost::corosio::detail { @tparam ReadOp The backend's read op type. @tparam WriteOp The backend's write op type. @tparam DescState The backend's descriptor_state type. + @tparam ImplBase The public vtable base + (tcp_socket::implementation or + local_stream_socket::implementation). + @tparam Endpoint The endpoint type (endpoint or local_endpoint). */ template< class Derived, @@ -42,26 +46,30 @@ template< class ConnOp, class ReadOp, class WriteOp, - class DescState> + class DescState, + class ImplBase = tcp_socket::implementation, + class Endpoint = endpoint> class reactor_stream_socket : public reactor_basic_socket< Derived, - tcp_socket::implementation, + ImplBase, Service, - DescState> + DescState, + Endpoint> { using base_type = reactor_basic_socket< Derived, - tcp_socket::implementation, + ImplBase, Service, - DescState>; + DescState, + Endpoint>; friend base_type; friend Derived; explicit reactor_stream_socket(Service& svc) noexcept : base_type(svc) {} protected: - endpoint remote_endpoint_; + Endpoint remote_endpoint_; public: /// Pending connect operation slot. @@ -76,24 +84,80 @@ class reactor_stream_socket ~reactor_stream_socket() override = default; /// Return the cached remote endpoint. - endpoint remote_endpoint() const noexcept override + Endpoint remote_endpoint() const noexcept override { return remote_endpoint_; } - /// Shut down part or all of the full-duplex connection. - std::error_code shutdown(tcp_socket::shutdown_type what) noexcept override + // --- Virtual method overrides (satisfy ImplBase pure virtuals) --- + + std::coroutine_handle<> connect( + std::coroutine_handle<> h, + capy::executor_ref ex, + Endpoint ep, + std::stop_token token, + std::error_code* ec) override + { + return do_connect(h, ex, ep, token, ec); + } + + std::coroutine_handle<> read_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_read_some(h, ex, param, token, ec, bytes_out); + } + + std::coroutine_handle<> write_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) override + { + return do_write_some(h, ex, param, token, ec, bytes_out); + } + + std::error_code + shutdown(corosio::shutdown_type what) noexcept override + { + return do_shutdown(static_cast(what)); + } + + void cancel() noexcept override + { + this->do_cancel(); + } + + // --- End virtual overrides --- + + /// Close the socket (non-virtual, called by the service). + void close_socket() noexcept + { + this->do_close_socket(); + } + + /** Shut down part or all of the full-duplex connection. + + @param what 0 = receive, 1 = send, 2 = both. + */ + std::error_code do_shutdown(int what) noexcept { int how; switch (what) { - case tcp_socket::shutdown_receive: + case 0: // shutdown_receive how = SHUT_RD; break; - case tcp_socket::shutdown_send: + case 1: // shutdown_send how = SHUT_WR; break; - case tcp_socket::shutdown_both: + case 2: // shutdown_both how = SHUT_RDWR; break; default: @@ -105,10 +169,10 @@ class reactor_stream_socket } /// Cache local and remote endpoints. - void set_endpoints(endpoint local, endpoint remote) noexcept + void set_endpoints(Endpoint local, Endpoint remote) noexcept { - this->local_endpoint_ = local; - remote_endpoint_ = remote; + this->local_endpoint_ = std::move(local); + remote_endpoint_ = std::move(remote); } /** Shared connect dispatch. @@ -120,7 +184,7 @@ class reactor_stream_socket std::coroutine_handle<> do_connect( std::coroutine_handle<>, capy::executor_ref, - endpoint, + Endpoint const&, std::stop_token const&, std::error_code*); @@ -160,7 +224,19 @@ class reactor_stream_socket void do_close_socket() noexcept { base_type::do_close_socket(); - remote_endpoint_ = endpoint{}; + remote_endpoint_ = Endpoint{}; + } + + /** Release the socket without closing the fd. + + Extends the base do_release_socket() to also reset + the remote endpoint. + */ + native_handle_type do_release_socket() noexcept + { + auto fd = base_type::do_release_socket(); + remote_endpoint_ = Endpoint{}; + return fd; } private: @@ -213,13 +289,15 @@ template< class ConnOp, class ReadOp, class WriteOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> -reactor_stream_socket:: +reactor_stream_socket:: do_connect( std::coroutine_handle<> h, capy::executor_ref ex, - endpoint ep, + Endpoint const& ep, std::stop_token const& token, std::error_code* ec) { @@ -237,7 +315,8 @@ reactor_stream_socket:: if (::getsockname( this->fd_, reinterpret_cast(&local_storage), &local_len) == 0) - this->local_endpoint_ = from_sockaddr(local_storage); + this->local_endpoint_ = + from_sockaddr_as(local_storage, local_len, Endpoint{}); remote_endpoint_ = ep; } @@ -275,7 +354,7 @@ reactor_stream_socket:: this->register_op( op, this->desc_state_.connect_op, this->desc_state_.write_ready, - this->desc_state_.connect_cancel_pending); + this->desc_state_.connect_cancel_pending, true); return std::noop_coroutine(); } @@ -285,9 +364,11 @@ template< class ConnOp, class ReadOp, class WriteOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> -reactor_stream_socket:: +reactor_stream_socket:: do_read_some( std::coroutine_handle<> h, capy::executor_ref ex, @@ -379,9 +460,11 @@ template< class ConnOp, class ReadOp, class WriteOp, - class DescState> + class DescState, + class ImplBase, + class Endpoint> std::coroutine_handle<> -reactor_stream_socket:: +reactor_stream_socket:: do_write_some( std::coroutine_handle<> h, capy::executor_ref ex, @@ -454,7 +537,7 @@ reactor_stream_socket:: this->register_op( op, this->desc_state_.write_op, this->desc_state_.write_ready, - this->desc_state_.write_cancel_pending); + this->desc_state_.write_cancel_pending, true); return std::noop_coroutine(); } diff --git a/include/boost/corosio/native/detail/select/select_op.hpp b/include/boost/corosio/native/detail/select/select_op.hpp index be7670156..37ac423e4 100644 --- a/include/boost/corosio/native/detail/select/select_op.hpp +++ b/include/boost/corosio/native/detail/select/select_op.hpp @@ -14,175 +14,14 @@ #if BOOST_COROSIO_HAS_SELECT -#include #include -#include -#include -#include -#include - -/* - File descriptors are registered with the select scheduler once (via - select_descriptor_state) and stay registered until closed. - - select() is level-triggered but the descriptor_state pattern - (designed for edge-triggered) works correctly: is_enqueued_ CAS - prevents double-enqueue, add_ready_events is idempotent, and - EAGAIN ops stay parked until the next select() re-reports readiness. - - cancel() captures shared_from_this() into op.impl_ptr to prevent - use-after-free when the socket is closed with pending ops. - - Writes use sendmsg(MSG_NOSIGNAL) on Linux. On macOS/BSD where - MSG_NOSIGNAL may be absent, SO_NOSIGPIPE is set at socket creation - and accepted-socket setup instead. -*/ - namespace boost::corosio::detail { -// Forward declarations -class select_tcp_socket; -class select_tcp_acceptor; -struct select_op; - -// Forward declaration -class select_scheduler; - /// Per-descriptor state for persistent select registration. struct select_descriptor_state final : reactor_descriptor_state {}; -/// select base operation — thin wrapper over reactor_op. -struct select_op : reactor_op -{ - void operator()() override; -}; - -/// select connect operation. -struct select_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// select scatter-read operation. -struct select_read_op final : reactor_read_op -{ - void cancel() noexcept override; -}; - -/** Provides sendmsg() with EINTR retry for select writes. - - Uses MSG_NOSIGNAL where available (Linux). On platforms without - it (macOS/BSD), SO_NOSIGPIPE is set at socket creation time - and flags=0 is used here. -*/ -struct select_write_policy -{ - static ssize_t write(int fd, iovec* iovecs, int count) noexcept - { - msghdr msg{}; - msg.msg_iov = iovecs; - msg.msg_iovlen = static_cast(count); - -#ifdef MSG_NOSIGNAL - constexpr int send_flags = MSG_NOSIGNAL; -#else - constexpr int send_flags = 0; -#endif - - ssize_t n; - do - { - n = ::sendmsg(fd, &msg, send_flags); - } - while (n < 0 && errno == EINTR); - return n; - } -}; - -/// select gather-write operation. -struct select_write_op final : reactor_write_op -{ - void cancel() noexcept override; -}; - -/** Provides accept() + fcntl(O_NONBLOCK|FD_CLOEXEC) with FD_SETSIZE check. - - Uses accept() instead of accept4() for broader POSIX compatibility. -*/ -struct select_accept_policy -{ - static int do_accept(int fd, sockaddr_storage& peer) noexcept - { - socklen_t addrlen = sizeof(peer); - int new_fd; - do - { - new_fd = ::accept(fd, reinterpret_cast(&peer), &addrlen); - } - while (new_fd < 0 && errno == EINTR); - - if (new_fd < 0) - return new_fd; - - if (new_fd >= FD_SETSIZE) - { - ::close(new_fd); - errno = EINVAL; - return -1; - } - - int flags = ::fcntl(new_fd, F_GETFL, 0); - if (flags == -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - - if (::fcntl(new_fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - - if (::fcntl(new_fd, F_SETFD, FD_CLOEXEC) == -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } - -#ifdef SO_NOSIGPIPE - int one = 1; - if (::setsockopt(new_fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) == - -1) - { - int err = errno; - ::close(new_fd); - errno = err; - return -1; - } -#endif - - return new_fd; - } -}; - -/// select accept operation. -struct select_accept_op final - : reactor_accept_op -{ - void operator()() override; - void cancel() noexcept override; -}; - } // namespace boost::corosio::detail #endif // BOOST_COROSIO_HAS_SELECT diff --git a/include/boost/corosio/native/detail/select/select_scheduler.hpp b/include/boost/corosio/native/detail/select/select_scheduler.hpp index a3ac4461e..d301dcf2e 100644 --- a/include/boost/corosio/native/detail/select/select_scheduler.hpp +++ b/include/boost/corosio/native/detail/select/select_scheduler.hpp @@ -69,6 +69,9 @@ struct select_descriptor_state; class BOOST_COROSIO_DECL select_scheduler final : public reactor_scheduler { public: + /// Select needs write-direction notification to rebuild fd_sets. + static constexpr bool needs_write_notification = true; + /** Construct the scheduler. Creates a self-pipe for reactor interruption. diff --git a/include/boost/corosio/native/detail/select/select_tcp_acceptor.hpp b/include/boost/corosio/native/detail/select/select_tcp_acceptor.hpp deleted file mode 100644 index 683229001..000000000 --- a/include/boost/corosio/native/detail/select/select_tcp_acceptor.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include -#include - -namespace boost::corosio::detail { - -class select_tcp_acceptor_service; - -/// Acceptor implementation for select backend. -class select_tcp_acceptor final - : public reactor_acceptor< - select_tcp_acceptor, - select_tcp_acceptor_service, - select_op, - select_accept_op, - select_descriptor_state> -{ - friend class select_tcp_acceptor_service; - -public: - explicit select_tcp_acceptor(select_tcp_acceptor_service& svc) noexcept; - - std::coroutine_handle<> accept( - std::coroutine_handle<>, - capy::executor_ref, - std::stop_token, - std::error_code*, - io_object::implementation**) override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_HPP diff --git a/include/boost/corosio/native/detail/select/select_tcp_acceptor_service.hpp b/include/boost/corosio/native/detail/select/select_tcp_acceptor_service.hpp deleted file mode 100644 index e60b3160a..000000000 --- a/include/boost/corosio/native/detail/select/select_tcp_acceptor_service.hpp +++ /dev/null @@ -1,437 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/// State for select acceptor service. -using select_tcp_acceptor_state = - reactor_service_state; - -/** select acceptor service implementation. - - Inherits from tcp_acceptor_service to enable runtime polymorphism. - Uses key_type = tcp_acceptor_service for service lookup. -*/ -class BOOST_COROSIO_DECL select_tcp_acceptor_service final - : public tcp_acceptor_service -{ -public: - explicit select_tcp_acceptor_service( - capy::execution_context& ctx, select_tcp_service& tcp_svc); - ~select_tcp_acceptor_service() override; - - select_tcp_acceptor_service(select_tcp_acceptor_service const&) = delete; - select_tcp_acceptor_service& - operator=(select_tcp_acceptor_service const&) = delete; - - void shutdown() override; - - io_object::implementation* construct() override; - void destroy(io_object::implementation*) override; - void close(io_object::handle&) override; - std::error_code open_acceptor_socket( - tcp_acceptor::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_acceptor(tcp_acceptor::implementation& impl, endpoint ep) override; - std::error_code - listen_acceptor(tcp_acceptor::implementation& impl, int backlog) override; - - select_scheduler& scheduler() const noexcept - { - return state_->sched_; - } - void post(scheduler_op* op); - void work_started() noexcept; - void work_finished() noexcept; - - /** Get the TCP service for creating peer sockets during accept. */ - select_tcp_service* tcp_service() const noexcept; - -private: - select_tcp_service* tcp_svc_; - std::unique_ptr state_; -}; - -inline void -select_accept_op::cancel() noexcept -{ - if (acceptor_impl_) - acceptor_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_accept_op::operator()() -{ - complete_accept_op(*this); -} - -inline select_tcp_acceptor::select_tcp_acceptor( - select_tcp_acceptor_service& svc) noexcept - : reactor_acceptor(svc) -{ -} - -inline std::coroutine_handle<> -select_tcp_acceptor::accept( - std::coroutine_handle<> h, - capy::executor_ref ex, - std::stop_token token, - std::error_code* ec, - io_object::implementation** impl_out) -{ - auto& op = acc_; - op.reset(); - op.h = h; - op.ex = ex; - op.ec_out = ec; - op.impl_out = impl_out; - op.fd = fd_; - op.start(token, this); - - sockaddr_storage peer_storage{}; - socklen_t addrlen = sizeof(peer_storage); - int accepted; - do - { - accepted = - ::accept(fd_, reinterpret_cast(&peer_storage), &addrlen); - } - while (accepted < 0 && errno == EINTR); - - if (accepted >= 0) - { - if (accepted >= FD_SETSIZE) - { - ::close(accepted); - op.complete(EINVAL, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - int flags = ::fcntl(accepted, F_GETFL, 0); - if (flags == -1) - { - int err = errno; - ::close(accepted); - op.complete(err, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - if (::fcntl(accepted, F_SETFL, flags | O_NONBLOCK) == -1) - { - int err = errno; - ::close(accepted); - op.complete(err, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - if (::fcntl(accepted, F_SETFD, FD_CLOEXEC) == -1) - { - int err = errno; - ::close(accepted); - op.complete(err, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - { - std::lock_guard lock(desc_state_.mutex); - desc_state_.read_ready = false; - } - - if (svc_.scheduler().try_consume_inline_budget()) - { - auto* socket_svc = svc_.tcp_service(); - if (socket_svc) - { - auto& impl = - static_cast(*socket_svc->construct()); - impl.set_socket(accepted); - - impl.desc_state_.fd = accepted; - { - std::lock_guard lock(impl.desc_state_.mutex); - impl.desc_state_.read_op = nullptr; - impl.desc_state_.write_op = nullptr; - impl.desc_state_.connect_op = nullptr; - } - socket_svc->scheduler().register_descriptor( - accepted, &impl.desc_state_); - - impl.set_endpoints( - local_endpoint_, from_sockaddr(peer_storage)); - - *ec = {}; - if (impl_out) - *impl_out = &impl; - } - else - { - ::close(accepted); - *ec = make_err(ENOENT); - if (impl_out) - *impl_out = nullptr; - } - op.cont_op.cont.h = h; - return dispatch_coro(ex, op.cont_op.cont); - } - - op.accepted_fd = accepted; - op.peer_storage = peer_storage; - op.complete(0, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); - } - - if (errno == EAGAIN || errno == EWOULDBLOCK) - { - op.impl_ptr = shared_from_this(); - svc_.work_started(); - - std::lock_guard lock(desc_state_.mutex); - bool io_done = false; - if (desc_state_.read_ready) - { - desc_state_.read_ready = false; - op.perform_io(); - io_done = (op.errn != EAGAIN && op.errn != EWOULDBLOCK); - if (!io_done) - op.errn = 0; - } - - if (io_done || op.cancelled.load(std::memory_order_acquire)) - { - svc_.post(&op); - svc_.work_finished(); - } - else - { - desc_state_.read_op = &op; - } - return std::noop_coroutine(); - } - - op.complete(errno, 0); - op.impl_ptr = shared_from_this(); - svc_.post(&op); - return std::noop_coroutine(); -} - -inline void -select_tcp_acceptor::cancel() noexcept -{ - do_cancel(); -} - -inline void -select_tcp_acceptor::close_socket() noexcept -{ - do_close_socket(); -} - -inline select_tcp_acceptor_service::select_tcp_acceptor_service( - capy::execution_context& ctx, select_tcp_service& tcp_svc) - : tcp_svc_(&tcp_svc) - , state_( - std::make_unique( - ctx.use_service())) -{ -} - -inline select_tcp_acceptor_service::~select_tcp_acceptor_service() {} - -inline void -select_tcp_acceptor_service::shutdown() -{ - std::lock_guard lock(state_->mutex_); - - while (auto* impl = state_->impl_list_.pop_front()) - impl->close_socket(); - - // Don't clear impl_ptrs_ here — same rationale as - // select_tcp_service::shutdown(). Let ~state_ release ptrs - // after scheduler shutdown has drained all queued ops. -} - -inline io_object::implementation* -select_tcp_acceptor_service::construct() -{ - auto impl = std::make_shared(*this); - auto* raw = impl.get(); - - std::lock_guard lock(state_->mutex_); - state_->impl_ptrs_.emplace(raw, std::move(impl)); - state_->impl_list_.push_back(raw); - - return raw; -} - -inline void -select_tcp_acceptor_service::destroy(io_object::implementation* impl) -{ - auto* select_impl = static_cast(impl); - select_impl->close_socket(); - std::lock_guard lock(state_->mutex_); - state_->impl_list_.remove(select_impl); - state_->impl_ptrs_.erase(select_impl); -} - -inline void -select_tcp_acceptor_service::close(io_object::handle& h) -{ - static_cast(h.get())->close_socket(); -} - -inline std::error_code -select_tcp_acceptor_service::open_acceptor_socket( - tcp_acceptor::implementation& impl, int family, int type, int protocol) -{ - auto* select_impl = static_cast(&impl); - select_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - if (fd >= FD_SETSIZE) - { - ::close(fd); - return make_err(EMFILE); - } - - if (family == AF_INET6) - { - int val = 0; // dual-stack default - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)); - } - -#ifdef SO_NOSIGPIPE - { - int nosig = 1; - ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &nosig, sizeof(nosig)); - } -#endif - - select_impl->fd_ = fd; - - // Set up descriptor state but do NOT register with reactor yet - // (registration happens in do_listen via reactor_acceptor base) - select_impl->desc_state_.fd = fd; - { - std::lock_guard lock(select_impl->desc_state_.mutex); - select_impl->desc_state_.read_op = nullptr; - } - - return {}; -} - -inline std::error_code -select_tcp_acceptor_service::bind_acceptor( - tcp_acceptor::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -inline std::error_code -select_tcp_acceptor_service::listen_acceptor( - tcp_acceptor::implementation& impl, int backlog) -{ - return static_cast(&impl)->do_listen(backlog); -} - -inline void -select_tcp_acceptor_service::post(scheduler_op* op) -{ - state_->sched_.post(op); -} - -inline void -select_tcp_acceptor_service::work_started() noexcept -{ - state_->sched_.work_started(); -} - -inline void -select_tcp_acceptor_service::work_finished() noexcept -{ - state_->sched_.work_finished(); -} - -inline select_tcp_service* -select_tcp_acceptor_service::tcp_service() const noexcept -{ - return tcp_svc_; -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_ACCEPTOR_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/select/select_tcp_service.hpp b/include/boost/corosio/native/detail/select/select_tcp_service.hpp deleted file mode 100644 index d384e40af..000000000 --- a/include/boost/corosio/native/detail/select/select_tcp_service.hpp +++ /dev/null @@ -1,251 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include - -#include -#include -#include - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -/* - Each I/O op tries the syscall speculatively; only registers with - the reactor on EAGAIN. Fd is registered once at open time and - stays registered until close. The reactor only marks ready_events_; - actual I/O happens in invoke_deferred_io(). cancel() captures - shared_from_this() into op.impl_ptr to keep the impl alive. -*/ - -namespace boost::corosio::detail { - -/** select TCP service implementation. - - Inherits from tcp_service to enable runtime polymorphism. - Uses key_type = tcp_service for service lookup. -*/ -class BOOST_COROSIO_DECL select_tcp_service final - : public reactor_socket_service< - select_tcp_service, - tcp_service, - select_scheduler, - select_tcp_socket> -{ -public: - explicit select_tcp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_socket( - tcp_socket::implementation& impl, - int family, - int type, - int protocol) override; - - std::error_code - bind_socket(tcp_socket::implementation& impl, endpoint ep) override; -}; - -inline void -select_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_read_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_write_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_op::operator()() -{ - complete_io_op(*this); -} - -inline void -select_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline select_tcp_socket::select_tcp_socket(select_tcp_service& svc) noexcept - : reactor_stream_socket(svc) -{ -} - -inline select_tcp_socket::~select_tcp_socket() = default; - -inline std::coroutine_handle<> -select_tcp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - auto result = do_connect(h, ex, ep, token, ec); - // Rebuild fd_sets so select() watches for writability - if (result == std::noop_coroutine()) - svc_.scheduler().notify_reactor(); - return result; -} - -inline std::coroutine_handle<> -select_tcp_socket::read_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_read_some(h, ex, param, token, ec, bytes_out); -} - -inline std::coroutine_handle<> -select_tcp_socket::write_some( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param param, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - auto result = do_write_some(h, ex, param, token, ec, bytes_out); - // Rebuild fd_sets so select() watches for writability - if (result == std::noop_coroutine()) - svc_.scheduler().notify_reactor(); - return result; -} - -inline void -select_tcp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -select_tcp_socket::close_socket() noexcept -{ - do_close_socket(); -} - -inline std::error_code -select_tcp_service::open_socket( - tcp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* select_impl = static_cast(&impl); - select_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int one = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)); - } - - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - if (fd >= FD_SETSIZE) - { - ::close(fd); - return make_err(EMFILE); - } - -#ifdef SO_NOSIGPIPE - { - int one = 1; - ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); - } -#endif - - select_impl->fd_ = fd; - - select_impl->desc_state_.fd = fd; - { - std::lock_guard lock(select_impl->desc_state_.mutex); - select_impl->desc_state_.read_op = nullptr; - select_impl->desc_state_.write_op = nullptr; - select_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &select_impl->desc_state_); - - return {}; -} - -inline std::error_code -select_tcp_service::bind_socket( - tcp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/select/select_tcp_socket.hpp b/include/boost/corosio/native/detail/select/select_tcp_socket.hpp deleted file mode 100644 index 943157358..000000000 --- a/include/boost/corosio/native/detail/select/select_tcp_socket.hpp +++ /dev/null @@ -1,72 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include -#include - -namespace boost::corosio::detail { - -class select_tcp_service; - -/// Stream socket implementation for select backend. -class select_tcp_socket final - : public reactor_stream_socket< - select_tcp_socket, - select_tcp_service, - select_connect_op, - select_read_op, - select_write_op, - select_descriptor_state> -{ - friend class select_tcp_service; - -public: - explicit select_tcp_socket(select_tcp_service& svc) noexcept; - ~select_tcp_socket() override; - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> read_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> write_some( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TCP_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/select/select_traits.hpp b/include/boost/corosio/native/detail/select/select_traits.hpp new file mode 100644 index 000000000..1ddf7d29f --- /dev/null +++ b/include/boost/corosio/native/detail/select/select_traits.hpp @@ -0,0 +1,237 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TRAITS_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TRAITS_HPP + +#include + +#if BOOST_COROSIO_HAS_SELECT + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +/* select backend traits. + + Captures the platform-specific behavior of the portable select() backend: + manual fcntl for O_NONBLOCK/FD_CLOEXEC, FD_SETSIZE validation, + conditional SO_NOSIGPIPE, sendmsg(MSG_NOSIGNAL) where available, + and accept()+fcntl for accepted connections. +*/ + +namespace boost::corosio::detail { + +class select_scheduler; +struct select_descriptor_state; + +struct select_traits +{ + using scheduler_type = select_scheduler; + using desc_state_type = select_descriptor_state; + + static constexpr bool needs_write_notification = true; + + /// No extra per-socket state or lifecycle hooks needed for select. + struct stream_socket_hook + { + std::error_code on_set_option( + int fd, int level, int optname, + void const* data, std::size_t size) noexcept + { + if (::setsockopt( + fd, level, optname, data, + static_cast(size)) != 0) + return make_err(errno); + return {}; + } + static void pre_shutdown(int) noexcept {} + static void pre_destroy(int) noexcept {} + }; + + struct write_policy + { + static ssize_t write(int fd, iovec* iovecs, int count) noexcept + { + msghdr msg{}; + msg.msg_iov = iovecs; + msg.msg_iovlen = static_cast(count); + +#ifdef MSG_NOSIGNAL + constexpr int send_flags = MSG_NOSIGNAL; +#else + constexpr int send_flags = 0; +#endif + + ssize_t n; + do + { + n = ::sendmsg(fd, &msg, send_flags); + } + while (n < 0 && errno == EINTR); + return n; + } + }; + + struct accept_policy + { + static int do_accept( + int fd, sockaddr_storage& peer, socklen_t& addrlen) noexcept + { + addrlen = sizeof(peer); + int new_fd; + do + { + new_fd = ::accept( + fd, reinterpret_cast(&peer), &addrlen); + } + while (new_fd < 0 && errno == EINTR); + + if (new_fd < 0) + return new_fd; + + if (new_fd >= FD_SETSIZE) + { + ::close(new_fd); + errno = EINVAL; + return -1; + } + + int flags = ::fcntl(new_fd, F_GETFL, 0); + if (flags == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } + + if (::fcntl(new_fd, F_SETFL, flags | O_NONBLOCK) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } + + if (::fcntl(new_fd, F_SETFD, FD_CLOEXEC) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } + +#ifdef SO_NOSIGPIPE + int one = 1; + if (::setsockopt( + new_fd, SOL_SOCKET, SO_NOSIGPIPE, + &one, sizeof(one)) == -1) + { + int err = errno; + ::close(new_fd); + errno = err; + return -1; + } +#endif + + return new_fd; + } + }; + + /// Create a plain socket (no atomic flags — select is POSIX-portable). + static int create_socket(int family, int type, int protocol) noexcept + { + return ::socket(family, type, protocol); + } + + /// Set O_NONBLOCK, FD_CLOEXEC; check FD_SETSIZE; optionally SO_NOSIGPIPE. + /// Caller is responsible for closing fd on error. + static std::error_code set_fd_options(int fd) noexcept + { + int flags = ::fcntl(fd, F_GETFL, 0); + if (flags == -1) + return make_err(errno); + if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) + return make_err(errno); + if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) + return make_err(errno); + + if (fd >= FD_SETSIZE) + return make_err(EINVAL); + +#ifdef SO_NOSIGPIPE + // SO_NOSIGPIPE is the primary defense against SIGPIPE on + // platforms that don't support MSG_NOSIGNAL. A silent failure + // here would leave the fd vulnerable to crashing the process + // on a closed-peer write, so propagate the error. + { + int one = 1; + if (::setsockopt( + fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)) != 0) + return make_err(errno); + } +#endif + + return {}; + } + + /// Apply protocol-specific options after socket creation. + /// For IP sockets, sets IPV6_V6ONLY on AF_INET6. + static std::error_code + configure_ip_socket(int fd, int family) noexcept + { + if (family == AF_INET6) + { + int one = 1; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)) != 0) + return make_err(errno); + } + + return set_fd_options(fd); + } + + /// Apply protocol-specific options for acceptor sockets. + /// For IP acceptors, sets IPV6_V6ONLY=0 (dual-stack). + static std::error_code + configure_ip_acceptor(int fd, int family) noexcept + { + if (family == AF_INET6) + { + int val = 0; + if (::setsockopt( + fd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val)) != 0) + return make_err(errno); + } + + return set_fd_options(fd); + } + + /// Apply options for local (unix) sockets. + static std::error_code + configure_local_socket(int fd) noexcept + { + return set_fd_options(fd); + } +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_SELECT + +#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_TRAITS_HPP diff --git a/include/boost/corosio/native/detail/select/select_udp_service.hpp b/include/boost/corosio/native/detail/select/select_udp_service.hpp deleted file mode 100644 index 7d254ce8e..000000000 --- a/include/boost/corosio/native/detail/select/select_udp_service.hpp +++ /dev/null @@ -1,315 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include - -#include -#include -#include - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -/** select UDP service implementation. - - Inherits from udp_service to enable runtime polymorphism. - Uses key_type = udp_service for service lookup. -*/ -class BOOST_COROSIO_DECL select_udp_service final - : public reactor_socket_service< - select_udp_service, - udp_service, - select_scheduler, - select_udp_socket> -{ -public: - explicit select_udp_service(capy::execution_context& ctx) - : reactor_socket_service(ctx) - { - } - - std::error_code open_datagram_socket( - udp_socket::implementation& impl, - int family, - int type, - int protocol) override; - std::error_code - bind_datagram(udp_socket::implementation& impl, endpoint ep) override; -}; - -// Cancellation for connectionless ops - -inline void -select_send_to_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_recv_from_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Cancellation for connected-mode ops - -inline void -select_udp_connect_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_send_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -inline void -select_recv_op::cancel() noexcept -{ - if (socket_impl_) - socket_impl_->cancel_single_op(*this); - else - request_cancel(); -} - -// Completion handlers - -inline void -select_datagram_op::operator()() -{ - complete_io_op(*this); -} - -inline void -select_recv_from_op::operator()() -{ - complete_datagram_op(*this, this->source_out); -} - -inline void -select_udp_connect_op::operator()() -{ - complete_connect_op(*this); -} - -inline void -select_recv_op::operator()() -{ - complete_io_op(*this); -} - -// Socket construction/destruction - -inline select_udp_socket::select_udp_socket(select_udp_service& svc) noexcept - : reactor_datagram_socket(svc) -{ -} - -inline select_udp_socket::~select_udp_socket() = default; - -// Connectionless I/O - -inline std::coroutine_handle<> -select_udp_socket::send_to( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint dest, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - auto result = do_send_to(h, ex, buf, dest, token, ec, bytes_out); - if (result == std::noop_coroutine()) - svc_.scheduler().notify_reactor(); - return result; -} - -inline std::coroutine_handle<> -select_udp_socket::recv_from( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - endpoint* source, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv_from(h, ex, buf, source, token, ec, bytes_out); -} - -// Connected-mode I/O - -inline std::coroutine_handle<> -select_udp_socket::connect( - std::coroutine_handle<> h, - capy::executor_ref ex, - endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - auto result = do_connect(h, ex, ep, token, ec); - if (result == std::noop_coroutine()) - svc_.scheduler().notify_reactor(); - return result; -} - -inline std::coroutine_handle<> -select_udp_socket::send( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - auto result = do_send(h, ex, buf, token, ec, bytes_out); - if (result == std::noop_coroutine()) - svc_.scheduler().notify_reactor(); - return result; -} - -inline std::coroutine_handle<> -select_udp_socket::recv( - std::coroutine_handle<> h, - capy::executor_ref ex, - buffer_param buf, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - return do_recv(h, ex, buf, token, ec, bytes_out); -} - -inline endpoint -select_udp_socket::remote_endpoint() const noexcept -{ - return reactor_datagram_socket::remote_endpoint(); -} - -inline void -select_udp_socket::cancel() noexcept -{ - do_cancel(); -} - -inline void -select_udp_socket::close_socket() noexcept -{ - do_close_socket(); -} - -inline std::error_code -select_udp_service::open_datagram_socket( - udp_socket::implementation& impl, int family, int type, int protocol) -{ - auto* select_impl = static_cast(&impl); - select_impl->close_socket(); - - int fd = ::socket(family, type, protocol); - if (fd < 0) - return make_err(errno); - - if (family == AF_INET6) - { - int one = 1; - ::setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)); - } - - int flags = ::fcntl(fd, F_GETFL, 0); - if (flags == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) - { - int errn = errno; - ::close(fd); - return make_err(errn); - } - - if (fd >= FD_SETSIZE) - { - ::close(fd); - return make_err(EMFILE); - } - -#ifdef SO_NOSIGPIPE - { - int one = 1; - ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one)); - } -#endif - - select_impl->fd_ = fd; - - select_impl->desc_state_.fd = fd; - { - std::lock_guard lock(select_impl->desc_state_.mutex); - select_impl->desc_state_.read_op = nullptr; - select_impl->desc_state_.write_op = nullptr; - select_impl->desc_state_.connect_op = nullptr; - } - scheduler().register_descriptor(fd, &select_impl->desc_state_); - - return {}; -} - -inline std::error_code -select_udp_service::bind_datagram(udp_socket::implementation& impl, endpoint ep) -{ - return static_cast(&impl)->do_bind(ep); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/select/select_udp_socket.hpp b/include/boost/corosio/native/detail/select/select_udp_socket.hpp deleted file mode 100644 index c1146d0b9..000000000 --- a/include/boost/corosio/native/detail/select/select_udp_socket.hpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_SELECT - -#include -#include -#include -#include -#include - -namespace boost::corosio::detail { - -class select_udp_service; -class select_udp_socket; - -/// select datagram base operation. -struct select_datagram_op : reactor_op -{ - void operator()() override; -}; - -/// select send_to operation. -struct select_send_to_op final : reactor_send_to_op -{ - void cancel() noexcept override; -}; - -/// select recv_from operation. -struct select_recv_from_op final : reactor_recv_from_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// select connect operation for UDP. -struct select_udp_connect_op final : reactor_connect_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// select connected send operation. -struct select_send_op final : reactor_send_op -{ - void cancel() noexcept override; -}; - -/// select connected recv operation. -struct select_recv_op final : reactor_recv_op -{ - void operator()() override; - void cancel() noexcept override; -}; - -/// Datagram socket implementation for select backend. -class select_udp_socket final - : public reactor_datagram_socket< - select_udp_socket, - select_udp_service, - select_udp_connect_op, - select_send_to_op, - select_recv_from_op, - select_send_op, - select_recv_op, - select_descriptor_state> -{ - friend class select_udp_service; - -public: - explicit select_udp_socket(select_udp_service& svc) noexcept; - ~select_udp_socket() override; - - std::coroutine_handle<> send_to( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv_from( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - endpoint*, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - endpoint, - std::stop_token, - std::error_code*) override; - - std::coroutine_handle<> send( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - std::coroutine_handle<> recv( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - std::stop_token, - std::error_code*, - std::size_t*) override; - - endpoint remote_endpoint() const noexcept override; - - void cancel() noexcept override; - void close_socket() noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_SELECT - -#endif // BOOST_COROSIO_NATIVE_DETAIL_SELECT_SELECT_UDP_SOCKET_HPP diff --git a/include/boost/corosio/native/native_tcp_acceptor.hpp b/include/boost/corosio/native/native_tcp_acceptor.hpp index 4ff586756..539b0823a 100644 --- a/include/boost/corosio/native/native_tcp_acceptor.hpp +++ b/include/boost/corosio/native/native_tcp_acceptor.hpp @@ -14,18 +14,6 @@ #include #ifndef BOOST_COROSIO_MRDOCS -#if BOOST_COROSIO_HAS_EPOLL -#include -#endif - -#if BOOST_COROSIO_HAS_SELECT -#include -#endif - -#if BOOST_COROSIO_HAS_KQUEUE -#include -#endif - #if BOOST_COROSIO_HAS_IOCP #include #endif diff --git a/include/boost/corosio/native/native_tcp_socket.hpp b/include/boost/corosio/native/native_tcp_socket.hpp index c25f7507c..07f7a8aaa 100644 --- a/include/boost/corosio/native/native_tcp_socket.hpp +++ b/include/boost/corosio/native/native_tcp_socket.hpp @@ -14,18 +14,6 @@ #include #ifndef BOOST_COROSIO_MRDOCS -#if BOOST_COROSIO_HAS_EPOLL -#include -#endif - -#if BOOST_COROSIO_HAS_SELECT -#include -#endif - -#if BOOST_COROSIO_HAS_KQUEUE -#include -#endif - #if BOOST_COROSIO_HAS_IOCP #include #endif diff --git a/include/boost/corosio/native/native_udp_socket.hpp b/include/boost/corosio/native/native_udp_socket.hpp index 7deb0e159..c48d16a91 100644 --- a/include/boost/corosio/native/native_udp_socket.hpp +++ b/include/boost/corosio/native/native_udp_socket.hpp @@ -14,18 +14,6 @@ #include #ifndef BOOST_COROSIO_MRDOCS -#if BOOST_COROSIO_HAS_EPOLL -#include -#endif - -#if BOOST_COROSIO_HAS_SELECT -#include -#endif - -#if BOOST_COROSIO_HAS_KQUEUE -#include -#endif - #if BOOST_COROSIO_HAS_IOCP #include #endif @@ -119,8 +107,8 @@ class native_udp_socket : public udp_socket { token_ = env->stop_token; return self_.get_impl().send_to( - h, env->executor, buffers_, dest_, token_, &ec_, - &bytes_transferred_); + h, env->executor, buffers_, dest_, 0, + token_, &ec_, &bytes_transferred_); } }; @@ -161,8 +149,8 @@ class native_udp_socket : public udp_socket { token_ = env->stop_token; return self_.get_impl().recv_from( - h, env->executor, buffers_, &source_, token_, &ec_, - &bytes_transferred_); + h, env->executor, buffers_, &source_, 0, + token_, &ec_, &bytes_transferred_); } }; diff --git a/include/boost/corosio/shutdown_type.hpp b/include/boost/corosio/shutdown_type.hpp new file mode 100644 index 000000000..275d56726 --- /dev/null +++ b/include/boost/corosio/shutdown_type.hpp @@ -0,0 +1,36 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_SHUTDOWN_TYPE_HPP +#define BOOST_COROSIO_SHUTDOWN_TYPE_HPP + +namespace boost::corosio { + +/** Different ways a socket may be shutdown. + + Used by tcp_socket, local_stream_socket, and + local_datagram_socket to specify the direction of + communication to disable. + + The enumerator values match the POSIX SHUT_RD / SHUT_WR / + SHUT_RDWR convention (0, 1, 2). +*/ +enum shutdown_type +{ + /// Disable further receive/read operations. + shutdown_receive, + /// Disable further send/write operations. + shutdown_send, + /// Disable both send and receive operations. + shutdown_both +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_SHUTDOWN_TYPE_HPP diff --git a/include/boost/corosio/socket_option.hpp b/include/boost/corosio/socket_option.hpp index 1cb12741a..431156094 100644 --- a/include/boost/corosio/socket_option.hpp +++ b/include/boost/corosio/socket_option.hpp @@ -743,7 +743,7 @@ class BOOST_COROSIO_DECL multicast_interface_v4 alignas(4) unsigned char storage_[max_storage_]{}; public: - /// Construct with default values (INADDR_ANY). + /// Construct with default values (wildcard interface). multicast_interface_v4() noexcept = default; /** Construct with an interface address. diff --git a/include/boost/corosio/tcp.hpp b/include/boost/corosio/tcp.hpp index cfb53965d..8130e3c1c 100644 --- a/include/boost/corosio/tcp.hpp +++ b/include/boost/corosio/tcp.hpp @@ -63,13 +63,13 @@ class BOOST_COROSIO_DECL tcp return v6_; } - /// Return the address family (AF_INET or AF_INET6). + /// Return the internet address family (IPv4 or IPv6). int family() const noexcept; - /// Return the socket type (SOCK_STREAM). + /// Return the stream socket type. static int type() noexcept; - /// Return the IP protocol (IPPROTO_TCP). + /// Return the TCP protocol number. static int protocol() noexcept; /// The associated socket type. diff --git a/include/boost/corosio/tcp_socket.hpp b/include/boost/corosio/tcp_socket.hpp index 5a0e86381..ad0a6f71a 100644 --- a/include/boost/corosio/tcp_socket.hpp +++ b/include/boost/corosio/tcp_socket.hpp @@ -15,10 +15,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -76,13 +78,8 @@ namespace boost::corosio { class BOOST_COROSIO_DECL tcp_socket : public io_stream { public: - /** Different ways a socket may be shutdown. */ - enum shutdown_type - { - shutdown_receive, - shutdown_send, - shutdown_both - }; + using shutdown_type = corosio::shutdown_type; + using enum corosio::shutdown_type; /** Define backend hooks for TCP socket operations. @@ -162,35 +159,18 @@ class BOOST_COROSIO_DECL tcp_socket : public io_stream /// Represent the awaitable returned by @ref connect. struct connect_awaitable + : detail::void_op_base { tcp_socket& s_; endpoint endpoint_; - std::stop_token token_; - mutable std::error_code ec_; connect_awaitable(tcp_socket& s, endpoint ep) noexcept - : s_(s) - , endpoint_(ep) - { - } + : s_(s), endpoint_(ep) {} - bool await_ready() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - return token_.stop_requested(); - } - - capy::io_result<> await_resume() const noexcept - { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled)}; - return {ec_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; - return s_.get().connect(h, env->executor, endpoint_, token_, &ec_); + return s_.get().connect(h, ex, endpoint_, token_, &ec_); } }; @@ -416,12 +396,23 @@ class BOOST_COROSIO_DECL tcp_socket : public io_stream @endcode Any error from the underlying system call is silently discarded - because it is unlikely to be helpful. + because it is unlikely to be helpful. To observe errors, use the + @ref shutdown(shutdown_type,std::error_code&) overload. @param what Determines what operations will no longer be allowed. */ void shutdown(shutdown_type what); + /** Shut down part or all of the socket (non-throwing). + + Same semantics as @ref shutdown(shutdown_type) but reports + syscall errors via @p ec instead of swallowing them. + + @param what Determines what operations will no longer be allowed. + @param ec Set to the error code on failure, cleared on success. + */ + void shutdown(shutdown_type what, std::error_code& ec) noexcept; + /** Set a socket option. Applies a type-safe socket option to the underlying socket. diff --git a/include/boost/corosio/udp.hpp b/include/boost/corosio/udp.hpp index 94a1a4374..c482275f1 100644 --- a/include/boost/corosio/udp.hpp +++ b/include/boost/corosio/udp.hpp @@ -60,13 +60,13 @@ class BOOST_COROSIO_DECL udp return v6_; } - /// Return the address family (AF_INET or AF_INET6). + /// Return the internet address family (IPv4 or IPv6). int family() const noexcept; - /// Return the socket type (SOCK_DGRAM). + /// Return the datagram socket type. static int type() noexcept; - /// Return the IP protocol (IPPROTO_UDP). + /// Return the UDP protocol number. static int protocol() noexcept; /// The associated socket type. diff --git a/include/boost/corosio/udp_socket.hpp b/include/boost/corosio/udp_socket.hpp index 88a9290b0..86c08b534 100644 --- a/include/boost/corosio/udp_socket.hpp +++ b/include/boost/corosio/udp_socket.hpp @@ -14,10 +14,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -110,6 +112,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object capy::executor_ref ex, buffer_param buf, endpoint dest, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) = 0; @@ -131,6 +134,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object capy::executor_ref ex, buffer_param buf, endpoint* source, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) = 0; @@ -210,6 +214,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object std::coroutine_handle<> h, capy::executor_ref ex, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) = 0; @@ -229,6 +234,7 @@ class BOOST_COROSIO_DECL udp_socket : public io_object std::coroutine_handle<> h, capy::executor_ref ex, buffer_param buf, + int flags, std::stop_token token, std::error_code* ec, std::size_t* bytes_out) = 0; @@ -240,201 +246,100 @@ class BOOST_COROSIO_DECL udp_socket : public io_object to the backend implementation on suspension. */ struct send_to_awaitable + : detail::bytes_op_base { udp_socket& s_; buffer_param buf_; endpoint dest_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_ = 0; + int flags_; send_to_awaitable( - udp_socket& s, buffer_param buf, endpoint dest) noexcept - : s_(s) - , buf_(buf) - , dest_(dest) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + udp_socket& s, buffer_param buf, + endpoint dest, int flags = 0) noexcept + : s_(s), buf_(buf), dest_(dest), flags_(flags) {} - capy::io_result await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; return s_.get().send_to( - h, env->executor, buf_, dest_, token_, &ec_, &bytes_); + h, ex, buf_, dest_, flags_, token_, &ec_, &bytes_); } }; - /** Represent the awaitable returned by @ref recv_from. - - Captures the receive buffer and source endpoint reference, - then dispatches to the backend implementation on suspension. - */ struct recv_from_awaitable + : detail::bytes_op_base { udp_socket& s_; buffer_param buf_; endpoint& source_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_ = 0; + int flags_; recv_from_awaitable( - udp_socket& s, buffer_param buf, endpoint& source) noexcept - : s_(s) - , buf_(buf) - , source_(source) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + udp_socket& s, buffer_param buf, + endpoint& source, int flags = 0) noexcept + : s_(s), buf_(buf), source_(source), flags_(flags) {} - capy::io_result await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; return s_.get().recv_from( - h, env->executor, buf_, &source_, token_, &ec_, &bytes_); + h, ex, buf_, &source_, flags_, token_, &ec_, &bytes_); } }; - /** Represent the awaitable returned by @ref connect. - - Captures the target endpoint, then dispatches to the backend - implementation on suspension. - */ struct connect_awaitable + : detail::void_op_base { udp_socket& s_; endpoint endpoint_; - std::stop_token token_; - mutable std::error_code ec_; connect_awaitable(udp_socket& s, endpoint ep) noexcept - : s_(s) - , endpoint_(ep) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + : s_(s), endpoint_(ep) {} - capy::io_result<> await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled)}; - return {ec_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; - return s_.get().connect(h, env->executor, endpoint_, token_, &ec_); + return s_.get().connect(h, ex, endpoint_, token_, &ec_); } }; - /** Represent the awaitable returned by @ref send. - - Captures the buffer, then dispatches to the backend - implementation on suspension. No endpoint argument - (uses the connected peer). - */ struct send_awaitable + : detail::bytes_op_base { udp_socket& s_; buffer_param buf_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_ = 0; + int flags_; - send_awaitable(udp_socket& s, buffer_param buf) noexcept - : s_(s) - , buf_(buf) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + send_awaitable( + udp_socket& s, buffer_param buf, + int flags = 0) noexcept + : s_(s), buf_(buf), flags_(flags) {} - capy::io_result await_resume() const noexcept + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_}; - } - - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> - { - token_ = env->stop_token; - return s_.get().send(h, env->executor, buf_, token_, &ec_, &bytes_); + return s_.get().send( + h, ex, buf_, flags_, token_, &ec_, &bytes_); } }; - /** Represent the awaitable returned by @ref recv. - - Captures the receive buffer, then dispatches to the backend - implementation on suspension. No source endpoint (connected - mode filters at the kernel level). - */ struct recv_awaitable + : detail::bytes_op_base { udp_socket& s_; buffer_param buf_; - std::stop_token token_; - mutable std::error_code ec_; - mutable std::size_t bytes_ = 0; - - recv_awaitable(udp_socket& s, buffer_param buf) noexcept - : s_(s) - , buf_(buf) - { - } - - bool await_ready() const noexcept - { - return token_.stop_requested(); - } + int flags_; - capy::io_result await_resume() const noexcept - { - if (token_.stop_requested()) - return {make_error_code(std::errc::operation_canceled), 0}; - return {ec_, bytes_}; - } + recv_awaitable( + udp_socket& s, buffer_param buf, + int flags = 0) noexcept + : s_(s), buf_(buf), flags_(flags) {} - auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) - -> std::coroutine_handle<> + std::coroutine_handle<> dispatch( + std::coroutine_handle<> h, capy::executor_ref ex) const { - token_ = env->stop_token; - return s_.get().recv(h, env->executor, buf_, token_, &ec_, &bytes_); + return s_.get().recv( + h, ex, buf_, flags_, token_, &ec_, &bytes_); } }; @@ -601,6 +506,12 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @param buf The buffer containing data to send. @param dest The destination endpoint. + @param flags Message flags (e.g. message_flags::dont_route). + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. @return An awaitable that completes with `io_result`. @@ -608,11 +519,22 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @throws std::logic_error if the socket is not open. */ template - auto send_to(Buffers const& buf, endpoint dest) + auto send_to( + Buffers const& buf, + endpoint dest, + corosio::message_flags flags) { if (!is_open()) detail::throw_logic_error("send_to: socket not open"); - return send_to_awaitable(*this, buf, dest); + return send_to_awaitable( + *this, buf, dest, static_cast(flags)); + } + + /// @overload + template + auto send_to(Buffers const& buf, endpoint dest) + { + return send_to(buf, dest, corosio::message_flags::none); } /** Receive a datagram and capture the sender's endpoint. @@ -620,6 +542,12 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @param buf The buffer to receive data into. @param source Reference to an endpoint that will be set to the sender's address on successful completion. + @param flags Message flags (e.g. message_flags::peek). + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. @return An awaitable that completes with `io_result`. @@ -627,11 +555,22 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @throws std::logic_error if the socket is not open. */ template - auto recv_from(Buffers const& buf, endpoint& source) + auto recv_from( + Buffers const& buf, + endpoint& source, + corosio::message_flags flags) { if (!is_open()) detail::throw_logic_error("recv_from: socket not open"); - return recv_from_awaitable(*this, buf, source); + return recv_from_awaitable( + *this, buf, source, static_cast(flags)); + } + + /// @overload + template + auto recv_from(Buffers const& buf, endpoint& source) + { + return recv_from(buf, source, corosio::message_flags::none); } /** Initiate an asynchronous connect to set the default peer. @@ -641,6 +580,11 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @param ep The remote endpoint to connect to. + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. + @return An awaitable that completes with `io_result<>`. @throws std::system_error if the socket needs to be opened @@ -656,6 +600,12 @@ class BOOST_COROSIO_DECL udp_socket : public io_object /** Send a datagram to the connected peer. @param buf The buffer containing data to send. + @param flags Message flags. + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. @return An awaitable that completes with `io_result`. @@ -663,16 +613,30 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @throws std::logic_error if the socket is not open. */ template - auto send(Buffers const& buf) + auto send(Buffers const& buf, corosio::message_flags flags) { if (!is_open()) detail::throw_logic_error("send: socket not open"); - return send_awaitable(*this, buf); + return send_awaitable( + *this, buf, static_cast(flags)); + } + + /// @overload + template + auto send(Buffers const& buf) + { + return send(buf, corosio::message_flags::none); } /** Receive a datagram from the connected peer. @param buf The buffer to receive data into. + @param flags Message flags (e.g. message_flags::peek). + + @par Cancellation + Supports cancellation via the awaitable's stop_token or by + calling @ref cancel. On cancellation, yields + `errc::operation_canceled`. @return An awaitable that completes with `io_result`. @@ -680,11 +644,19 @@ class BOOST_COROSIO_DECL udp_socket : public io_object @throws std::logic_error if the socket is not open. */ template - auto recv(Buffers const& buf) + auto recv(Buffers const& buf, corosio::message_flags flags) { if (!is_open()) detail::throw_logic_error("recv: socket not open"); - return recv_awaitable(*this, buf); + return recv_awaitable( + *this, buf, static_cast(flags)); + } + + /// @overload + template + auto recv(Buffers const& buf) + { + return recv(buf, corosio::message_flags::none); } /** Get the remote endpoint of the socket. diff --git a/perf/bench/CMakeLists.txt b/perf/bench/CMakeLists.txt index 4b31796d7..d820c556b 100644 --- a/perf/bench/CMakeLists.txt +++ b/perf/bench/CMakeLists.txt @@ -29,7 +29,9 @@ add_executable(corosio_bench corosio/http_server_bench.cpp corosio/timer_bench.cpp corosio/accept_churn_bench.cpp - corosio/fan_out_bench.cpp) + corosio/fan_out_bench.cpp + corosio/unix_socket_throughput_bench.cpp + corosio/unix_socket_latency_bench.cpp) target_link_libraries(corosio_bench PRIVATE @@ -60,7 +62,11 @@ if (TARGET Boost::asio) ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/fan_out_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/timer_bench.cpp ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/accept_churn_bench.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/fan_out_bench.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/fan_out_bench.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/unix_socket_throughput_bench.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/asio/coroutine/unix_socket_latency_bench.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/unix_socket_throughput_bench.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/asio/callback/unix_socket_latency_bench.cpp) target_link_libraries(corosio_bench PRIVATE Boost::asio) target_compile_definitions(corosio_bench PRIVATE BOOST_COROSIO_BENCH_HAS_ASIO=1) endif () diff --git a/perf/bench/asio/callback/benchmarks.hpp b/perf/bench/asio/callback/benchmarks.hpp index cd18cfa8a..b1e9c1f5e 100644 --- a/perf/bench/asio/callback/benchmarks.hpp +++ b/perf/bench/asio/callback/benchmarks.hpp @@ -35,6 +35,12 @@ bench::benchmark_suite make_accept_churn_suite(); /// Create the fan-out/fan-in benchmark suite. bench::benchmark_suite make_fan_out_suite(); +/// Create the Unix socket throughput benchmark suite. +bench::benchmark_suite make_unix_socket_throughput_suite(); + +/// Create the Unix socket latency benchmark suite. +bench::benchmark_suite make_unix_socket_latency_suite(); + } // namespace asio_callback_bench #endif diff --git a/perf/bench/asio/callback/unix_socket_latency_bench.cpp b/perf/bench/asio/callback/unix_socket_latency_bench.cpp new file mode 100644 index 000000000..bf5f4b993 --- /dev/null +++ b/perf/bench/asio/callback/unix_socket_latency_bench.cpp @@ -0,0 +1,238 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include "../unix_socket_utils.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace asio = boost::asio; +using asio_bench::local_socket; + +namespace asio_callback_bench { +namespace { + +struct unix_pingpong_op +{ + enum phase + { + write_client, + read_server, + write_server, + read_client + }; + + local_socket& client; + local_socket& server; + std::vector send_buf; + std::vector recv_buf; + bench::state& state; + perf::stopwatch sw; + phase phase_; + + unix_pingpong_op( + local_socket& c, + local_socket& s, + std::size_t message_size, + bench::state& st) + : client(c) + , server(s) + , send_buf(message_size, 'P') + , recv_buf(message_size) + , state(st) + , phase_(write_client) + { + } + + void start() + { + if (!state.running()) + { + client.shutdown(local_socket::shutdown_send); + return; + } + sw.reset(); + phase_ = write_client; + do_step(); + } + + void do_step() + { + switch (phase_) + { + case write_client: + asio::async_write( + client, asio::buffer(send_buf), + [this](boost::system::error_code ec, std::size_t) { + if (ec) + return; + phase_ = read_server; + do_step(); + }); + break; + + case read_server: + asio::async_read( + server, asio::buffer(recv_buf), + [this](boost::system::error_code ec, std::size_t) { + if (ec) + return; + phase_ = write_server; + do_step(); + }); + break; + + case write_server: + asio::async_write( + server, asio::buffer(recv_buf), + [this](boost::system::error_code ec, std::size_t) { + if (ec) + return; + phase_ = read_client; + do_step(); + }); + break; + + case read_client: + asio::async_read( + client, asio::buffer(recv_buf), + [this](boost::system::error_code ec, std::size_t) { + if (ec) + return; + state.latency().add(sw.elapsed_ns()); + state.ops().fetch_add(1, std::memory_order_relaxed); + start(); + }); + break; + } + } +}; + +void +bench_pingpong_latency_impl(bench::state& state, bool lockless) +{ + auto message_size = static_cast(state.range(0)); + state.counters["message_size"] = static_cast(message_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [client, server] = asio_bench::make_unix_socket_pair(ioc); + + unix_pingpong_op op(client, server, message_size, state); + + op.start(); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + client.close(); + server.close(); +} + +void bench_pingpong_latency(bench::state& s) { bench_pingpong_latency_impl(s, false); } +void bench_pingpong_latency_lockless(bench::state& s) { bench_pingpong_latency_impl(s, true); } + +void +bench_concurrent_latency_impl(bench::state& state, bool lockless) +{ + int num_pairs = static_cast(state.range(0)); + state.counters["num_pairs"] = num_pairs; + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + + std::vector clients; + std::vector servers; + + clients.reserve(num_pairs); + servers.reserve(num_pairs); + + for (int i = 0; i < num_pairs; ++i) + { + auto [c, s] = asio_bench::make_unix_socket_pair(ioc); + clients.push_back(std::move(c)); + servers.push_back(std::move(s)); + } + + std::vector> ops; + ops.reserve(num_pairs); + for (int p = 0; p < num_pairs; ++p) + { + ops.push_back( + std::make_unique( + clients[p], servers[p], 64, state)); + ops.back()->start(); + } + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + + for (auto& c : clients) + c.close(); + for (auto& s : servers) + s.close(); +} + +void bench_concurrent_latency(bench::state& s) { bench_concurrent_latency_impl(s, false); } +void bench_concurrent_latency_lockless(bench::state& s) { bench_concurrent_latency_impl(s, true); } + +} // anonymous namespace + +bench::benchmark_suite +make_unix_socket_latency_suite() +{ + using F = bench::bench_flags; + return bench::benchmark_suite("unix_socket_latency", F::none) + .set_warmup([] { + asio::io_context ioc; + auto [c, s] = asio_bench::make_unix_socket_pair(ioc); + char buf[64] = {}; + for (int i = 0; i < 100; ++i) + { + asio::write(c, asio::buffer(buf)); + asio::read(s, asio::buffer(buf)); + } + c.close(); + s.close(); + }) + .add("pingpong", bench_pingpong_latency) + .args({1, 64, 1024}) + .add("pingpong_lockless", bench_pingpong_latency_lockless) + .args({1, 64, 1024}) + .add("concurrent", bench_concurrent_latency) + .args({1, 4, 16}) + .add("concurrent_lockless", bench_concurrent_latency_lockless) + .args({1, 4, 16}); +} + +} // namespace asio_callback_bench diff --git a/perf/bench/asio/callback/unix_socket_throughput_bench.cpp b/perf/bench/asio/callback/unix_socket_throughput_bench.cpp new file mode 100644 index 000000000..eecd861b1 --- /dev/null +++ b/perf/bench/asio/callback/unix_socket_throughput_bench.cpp @@ -0,0 +1,190 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include "../unix_socket_utils.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace asio = boost::asio; +using asio_bench::local_socket; + +namespace asio_callback_bench { +namespace { + +struct unix_write_op +{ + local_socket& sock; + std::vector& buf; + std::size_t chunk_size; + std::atomic& running; + + void start() + { + if (!running.load(std::memory_order_relaxed)) + { + sock.shutdown(local_socket::shutdown_send); + return; + } + sock.async_write_some( + asio::buffer(buf.data(), chunk_size), + [this](boost::system::error_code ec, std::size_t) { + if (ec) + return; + start(); + }); + } +}; + +struct unix_read_op +{ + local_socket& sock; + std::vector& buf; + std::size_t& total_read; + + void start() + { + sock.async_read_some( + asio::buffer(buf.data(), buf.size()), + [this](boost::system::error_code ec, std::size_t n) { + if (ec || n == 0) + return; + total_read += n; + start(); + }); + } +}; + +void +bench_throughput_impl(bench::state& state, bool lockless) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [writer, reader] = asio_bench::make_unix_socket_pair(ioc); + + std::vector write_buf(chunk_size, 'x'); + std::vector read_buf(chunk_size); + + std::atomic running{true}; + std::size_t total_read = 0; + + unix_write_op wop{writer, write_buf, chunk_size, running}; + unix_read_op rop{reader, read_buf, total_read}; + + perf::stopwatch sw; + + wop.start(); + rop.start(); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + state.add_bytes(static_cast(total_read)); + + writer.close(); + reader.close(); +} + +void bench_throughput(bench::state& s) { bench_throughput_impl(s, false); } +void bench_throughput_lockless(bench::state& s) { bench_throughput_impl(s, true); } + +void +bench_bidirectional_throughput_impl(bench::state& state, bool lockless) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [sock1, sock2] = asio_bench::make_unix_socket_pair(ioc); + + std::vector buf1(chunk_size, 'a'); + std::vector buf2(chunk_size, 'b'); + std::vector rbuf1(chunk_size); + std::vector rbuf2(chunk_size); + + std::atomic running{true}; + std::size_t read1 = 0; + std::size_t read2 = 0; + + unix_write_op wop1{sock1, buf1, chunk_size, running}; + unix_read_op rop1{sock2, rbuf1, read1}; + + unix_write_op wop2{sock2, buf2, chunk_size, running}; + unix_read_op rop2{sock1, rbuf2, read2}; + + perf::stopwatch sw; + + wop1.start(); + rop1.start(); + wop2.start(); + rop2.start(); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + state.add_bytes(static_cast(read1 + read2)); + + sock1.close(); + sock2.close(); +} + +void bench_bidirectional_throughput(bench::state& s) { bench_bidirectional_throughput_impl(s, false); } +void bench_bidirectional_throughput_lockless(bench::state& s) { bench_bidirectional_throughput_impl(s, true); } + +} // anonymous namespace + +bench::benchmark_suite +make_unix_socket_throughput_suite() +{ + using F = bench::bench_flags; + return bench::benchmark_suite("unix_socket_throughput", F::none) + .set_warmup([] { + asio::io_context ioc; + auto [w, r] = asio_bench::make_unix_socket_pair(ioc); + std::vector buf(4096, 'w'); + asio::write(w, asio::buffer(buf)); + asio::read(r, asio::buffer(buf)); + w.close(); + r.close(); + }) + .add("unidirectional", bench_throughput) + .range(1024, 1048576, 4) + .add("unidirectional_lockless", bench_throughput_lockless) + .range(1024, 1048576, 4) + .add("bidirectional", bench_bidirectional_throughput) + .range(1024, 1048576, 4) + .add("bidirectional_lockless", bench_bidirectional_throughput_lockless) + .range(1024, 1048576, 4); +} + +} // namespace asio_callback_bench diff --git a/perf/bench/asio/coroutine/benchmarks.hpp b/perf/bench/asio/coroutine/benchmarks.hpp index 1421080e9..ff98e6c5a 100644 --- a/perf/bench/asio/coroutine/benchmarks.hpp +++ b/perf/bench/asio/coroutine/benchmarks.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -35,6 +36,12 @@ bench::benchmark_suite make_accept_churn_suite(); /// Create the fan-out/fan-in benchmark suite. bench::benchmark_suite make_fan_out_suite(); +/// Create the Unix socket throughput benchmark suite. +bench::benchmark_suite make_unix_socket_throughput_suite(); + +/// Create the Unix socket latency benchmark suite. +bench::benchmark_suite make_unix_socket_latency_suite(); + } // namespace asio_bench #endif diff --git a/perf/bench/asio/coroutine/unix_socket_latency_bench.cpp b/perf/bench/asio/coroutine/unix_socket_latency_bench.cpp new file mode 100644 index 000000000..5cceabad0 --- /dev/null +++ b/perf/bench/asio/coroutine/unix_socket_latency_bench.cpp @@ -0,0 +1,187 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include "../unix_socket_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace asio_bench { +namespace { + +asio::awaitable +pingpong_client_task( + local_socket& client, + local_socket& server, + std::size_t message_size, + std::atomic& running, + bench::state& state) +{ + std::vector send_buf(message_size, 'P'); + std::vector recv_buf(message_size); + + try + { + while (running.load(std::memory_order_relaxed)) + { + auto lp = state.lap(); + + co_await asio::async_write( + client, asio::buffer(send_buf.data(), send_buf.size()), + asio::deferred); + + co_await asio::async_read( + server, asio::buffer(recv_buf.data(), recv_buf.size()), + asio::deferred); + + co_await asio::async_write( + server, asio::buffer(recv_buf.data(), recv_buf.size()), + asio::deferred); + + co_await asio::async_read( + client, asio::buffer(recv_buf.data(), recv_buf.size()), + asio::deferred); + } + + client.shutdown(local_socket::shutdown_send); + } + catch (std::exception const&) + { + } +} + +void +bench_pingpong_latency_impl(bench::state& state, bool lockless) +{ + auto message_size = static_cast(state.range(0)); + state.counters["message_size"] = static_cast(message_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [client, server] = make_unix_socket_pair(ioc); + + std::atomic running{true}; + + asio::co_spawn( + ioc, + pingpong_client_task( + client, server, message_size, running, state), + asio::detached); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + client.close(); + server.close(); +} + +void bench_pingpong_latency(bench::state& s) { bench_pingpong_latency_impl(s, false); } +void bench_pingpong_latency_lockless(bench::state& s) { bench_pingpong_latency_impl(s, true); } + +void +bench_concurrent_latency_impl(bench::state& state, bool lockless) +{ + int num_pairs = static_cast(state.range(0)); + state.counters["num_pairs"] = num_pairs; + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + + std::vector clients; + std::vector servers; + + clients.reserve(num_pairs); + servers.reserve(num_pairs); + + for (int i = 0; i < num_pairs; ++i) + { + auto [c, s] = make_unix_socket_pair(ioc); + clients.push_back(std::move(c)); + servers.push_back(std::move(s)); + } + + std::atomic running{true}; + + for (int p = 0; p < num_pairs; ++p) + { + asio::co_spawn( + ioc, + pingpong_client_task( + clients[p], servers[p], 64, running, state), + asio::detached); + } + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + + for (auto& c : clients) + c.close(); + for (auto& s : servers) + s.close(); +} + +void bench_concurrent_latency(bench::state& s) { bench_concurrent_latency_impl(s, false); } +void bench_concurrent_latency_lockless(bench::state& s) { bench_concurrent_latency_impl(s, true); } + +} // anonymous namespace + +bench::benchmark_suite +make_unix_socket_latency_suite() +{ + return bench::benchmark_suite("unix_socket_latency") + .set_warmup([] { + asio::io_context ioc; + auto [c, s] = make_unix_socket_pair(ioc); + char buf[64] = {}; + for (int i = 0; i < 100; ++i) + { + asio::write(c, asio::buffer(buf)); + asio::read(s, asio::buffer(buf)); + } + c.close(); + s.close(); + }) + .add("pingpong", bench_pingpong_latency) + .args({1, 64, 1024}) + .add("pingpong_lockless", bench_pingpong_latency_lockless) + .args({1, 64, 1024}) + .add("concurrent", bench_concurrent_latency) + .args({1, 4, 16}) + .add("concurrent_lockless", bench_concurrent_latency_lockless) + .args({1, 4, 16}); +} + +} // namespace asio_bench diff --git a/perf/bench/asio/coroutine/unix_socket_throughput_bench.cpp b/perf/bench/asio/coroutine/unix_socket_throughput_bench.cpp new file mode 100644 index 000000000..6a11f157e --- /dev/null +++ b/perf/bench/asio/coroutine/unix_socket_throughput_bench.cpp @@ -0,0 +1,228 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include "../unix_socket_utils.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace asio_bench { +namespace { + +void +bench_throughput_impl(bench::state& state, bool lockless) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [writer, reader] = make_unix_socket_pair(ioc); + + std::vector write_buf(chunk_size, 'x'); + std::vector read_buf(chunk_size); + + std::atomic running{true}; + + auto write_task = [&]() -> asio::awaitable { + try + { + while (running.load(std::memory_order_relaxed)) + { + co_await writer.async_write_some( + asio::buffer(write_buf.data(), chunk_size), asio::deferred); + } + writer.shutdown(local_socket::shutdown_send); + } + catch (std::exception const&) + { + } + }; + + auto read_task = [&]() -> asio::awaitable { + try + { + for (;;) + { + auto n = co_await reader.async_read_some( + asio::buffer(read_buf.data(), read_buf.size()), + asio::deferred); + if (n == 0) + break; + state.add_bytes(static_cast(n)); + } + } + catch (std::exception const&) + { + } + }; + + perf::stopwatch sw; + + asio::co_spawn(ioc, write_task(), asio::detached); + asio::co_spawn(ioc, read_task(), asio::detached); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + writer.close(); + reader.close(); +} + +void bench_throughput(bench::state& s) { bench_throughput_impl(s, false); } +void bench_throughput_lockless(bench::state& s) { bench_throughput_impl(s, true); } + +void +bench_bidirectional_throughput_impl(bench::state& state, bool lockless) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + asio::io_context ioc(lockless ? BOOST_ASIO_CONCURRENCY_HINT_UNSAFE : 1); + auto [sock1, sock2] = make_unix_socket_pair(ioc); + + std::vector buf1(chunk_size, 'a'); + std::vector buf2(chunk_size, 'b'); + + std::atomic running{true}; + + auto write1_task = [&]() -> asio::awaitable { + try + { + while (running.load(std::memory_order_relaxed)) + { + co_await sock1.async_write_some( + asio::buffer(buf1.data(), chunk_size), asio::deferred); + } + sock1.shutdown(local_socket::shutdown_send); + } + catch (std::exception const&) + { + } + }; + + auto read1_task = [&]() -> asio::awaitable { + try + { + std::vector rbuf(chunk_size); + for (;;) + { + auto n = co_await sock2.async_read_some( + asio::buffer(rbuf.data(), rbuf.size()), asio::deferred); + if (n == 0) + break; + state.add_bytes(static_cast(n)); + } + } + catch (std::exception const&) + { + } + }; + + auto write2_task = [&]() -> asio::awaitable { + try + { + while (running.load(std::memory_order_relaxed)) + { + co_await sock2.async_write_some( + asio::buffer(buf2.data(), chunk_size), asio::deferred); + } + sock2.shutdown(local_socket::shutdown_send); + } + catch (std::exception const&) + { + } + }; + + auto read2_task = [&]() -> asio::awaitable { + try + { + std::vector rbuf(chunk_size); + for (;;) + { + auto n = co_await sock1.async_read_some( + asio::buffer(rbuf.data(), rbuf.size()), asio::deferred); + if (n == 0) + break; + state.add_bytes(static_cast(n)); + } + } + catch (std::exception const&) + { + } + }; + + perf::stopwatch sw; + + asio::co_spawn(ioc, write1_task(), asio::detached); + asio::co_spawn(ioc, read1_task(), asio::detached); + asio::co_spawn(ioc, write2_task(), asio::detached); + asio::co_spawn(ioc, read2_task(), asio::detached); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + sock1.close(); + sock2.close(); +} + +void bench_bidirectional_throughput(bench::state& s) { bench_bidirectional_throughput_impl(s, false); } +void bench_bidirectional_throughput_lockless(bench::state& s) { bench_bidirectional_throughput_impl(s, true); } + +} // anonymous namespace + +bench::benchmark_suite +make_unix_socket_throughput_suite() +{ + return bench::benchmark_suite("unix_socket_throughput") + .set_warmup([] { + asio::io_context ioc; + auto [w, r] = make_unix_socket_pair(ioc); + std::vector buf(4096, 'w'); + asio::write(w, asio::buffer(buf)); + asio::read(r, asio::buffer(buf)); + w.close(); + r.close(); + }) + .add("unidirectional", bench_throughput) + .range(1024, 1048576, 4) + .add("unidirectional_lockless", bench_throughput_lockless) + .range(1024, 1048576, 4) + .add("bidirectional", bench_bidirectional_throughput) + .range(1024, 1048576, 4) + .add("bidirectional_lockless", bench_bidirectional_throughput_lockless) + .range(1024, 1048576, 4); +} + +} // namespace asio_bench diff --git a/perf/bench/asio/unix_socket_utils.hpp b/perf/bench/asio/unix_socket_utils.hpp new file mode 100644 index 000000000..f42315a37 --- /dev/null +++ b/perf/bench/asio/unix_socket_utils.hpp @@ -0,0 +1,39 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef ASIO_BENCH_UNIX_SOCKET_UTILS_HPP +#define ASIO_BENCH_UNIX_SOCKET_UTILS_HPP + +#include +#include +#include + +#include + +namespace asio_bench { + +namespace asio = boost::asio; + +using executor_type = asio::io_context::executor_type; +using local_protocol = asio::local::stream_protocol; +using local_socket = asio::basic_stream_socket; + +/** Create a connected pair of Unix domain sockets for benchmarking. */ +inline std::pair +make_unix_socket_pair(asio::io_context& ioc) +{ + local_socket s1(ioc.get_executor()); + local_socket s2(ioc.get_executor()); + asio::local::connect_pair(s1, s2); + return {std::move(s1), std::move(s2)}; +} + +} // namespace asio_bench + +#endif diff --git a/perf/bench/corosio/benchmarks.hpp b/perf/bench/corosio/benchmarks.hpp index eb1c3f9b3..cbe5408a9 100644 --- a/perf/bench/corosio/benchmarks.hpp +++ b/perf/bench/corosio/benchmarks.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -63,6 +64,20 @@ bench::benchmark_suite make_accept_churn_suite(); template bench::benchmark_suite make_fan_out_suite(); +/** Create the Unix socket throughput benchmark suite. + + @tparam Backend A backend tag value (e.g., `epoll`). +*/ +template +bench::benchmark_suite make_unix_socket_throughput_suite(); + +/** Create the Unix socket latency benchmark suite. + + @tparam Backend A backend tag value (e.g., `epoll`). +*/ +template +bench::benchmark_suite make_unix_socket_latency_suite(); + } // namespace corosio_bench #endif diff --git a/perf/bench/corosio/unix_socket_latency_bench.cpp b/perf/bench/corosio/unix_socket_latency_bench.cpp new file mode 100644 index 000000000..33ad440f4 --- /dev/null +++ b/perf/bench/corosio/unix_socket_latency_bench.cpp @@ -0,0 +1,268 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include +#include "../../common/native_includes.hpp" + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace corosio_bench { +namespace { + +template +capy::task<> +unix_pingpong_client_task( + corosio::local_stream_socket& client, + corosio::local_stream_socket& server, + std::size_t message_size, + bench::state& state) +{ + std::vector send_buf(message_size, 'P'); + std::vector recv_buf(message_size); + + while (state.running()) + { + auto lp = state.lap(); + + auto [ec1, n1] = co_await capy::write( + client, capy::const_buffer(send_buf.data(), send_buf.size())); + if (ec1) + co_return; + + auto [ec2, n2] = co_await capy::read( + server, capy::mutable_buffer(recv_buf.data(), recv_buf.size())); + if (ec2) + co_return; + + auto [ec3, n3] = co_await capy::write( + server, capy::const_buffer(recv_buf.data(), n2)); + if (ec3) + co_return; + + auto [ec4, n4] = co_await capy::read( + client, capy::mutable_buffer(recv_buf.data(), recv_buf.size())); + if (ec4) + co_return; + } + + client.shutdown(corosio::local_stream_socket::shutdown_send); +} + +template +void +bench_unix_pingpong_latency(bench::state& state) +{ + auto message_size = static_cast(state.range(0)); + state.counters["message_size"] = static_cast(message_size); + + corosio::native_io_context ioc; + auto [client, server] = corosio::make_local_stream_pair(ioc); + + capy::run_async(ioc.get_executor())( + unix_pingpong_client_task(client, server, message_size, state)); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + client.close(); + server.close(); +} + +template +void +bench_unix_concurrent_latency(bench::state& state) +{ + int num_pairs = static_cast(state.range(0)); + state.counters["num_pairs"] = num_pairs; + + corosio::native_io_context ioc; + + std::vector clients; + std::vector servers; + + clients.reserve(num_pairs); + servers.reserve(num_pairs); + + for (int i = 0; i < num_pairs; ++i) + { + auto [c, s] = corosio::make_local_stream_pair(ioc); + clients.push_back(std::move(c)); + servers.push_back(std::move(s)); + } + + for (int p = 0; p < num_pairs; ++p) + { + capy::run_async(ioc.get_executor())( + unix_pingpong_client_task(clients[p], servers[p], 64, state)); + } + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + + for (auto& c : clients) + c.close(); + for (auto& s : servers) + s.close(); +} + +template +void +bench_unix_pingpong_latency_lockless(bench::state& state) +{ + auto message_size = static_cast(state.range(0)); + state.counters["message_size"] = static_cast(message_size); + + corosio::io_context_options opts; + opts.single_threaded = true; + corosio::native_io_context ioc(opts); + auto [client, server] = corosio::make_local_stream_pair(ioc); + + capy::run_async(ioc.get_executor())( + unix_pingpong_client_task(client, server, message_size, state)); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + client.close(); + server.close(); +} + +template +void +bench_unix_concurrent_latency_lockless(bench::state& state) +{ + int num_pairs = static_cast(state.range(0)); + state.counters["num_pairs"] = num_pairs; + + corosio::io_context_options opts; + opts.single_threaded = true; + corosio::native_io_context ioc(opts); + + std::vector clients; + std::vector servers; + + clients.reserve(num_pairs); + servers.reserve(num_pairs); + + for (int i = 0; i < num_pairs; ++i) + { + auto [c, s] = corosio::make_local_stream_pair(ioc); + clients.push_back(std::move(c)); + servers.push_back(std::move(s)); + } + + for (int p = 0; p < num_pairs; ++p) + { + capy::run_async(ioc.get_executor())( + unix_pingpong_client_task(clients[p], servers[p], 64, state)); + } + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + state.stop(); + }); + + perf::stopwatch sw; + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + + for (auto& c : clients) + c.close(); + for (auto& s : servers) + s.close(); +} + +} // anonymous namespace + +template +bench::benchmark_suite +make_unix_socket_latency_suite() +{ + using F = bench::bench_flags; + + return bench::benchmark_suite("unix_socket_latency", F::none) + .set_warmup([]{ + corosio::native_io_context ioc; + auto [c, s] = corosio::make_local_stream_pair(ioc); + char buf[64] = {}; + auto task = [&]() -> capy::task<> { + for (int i = 0; i < 100; ++i) + { + (void)co_await c.write_some( + capy::const_buffer(buf, sizeof(buf))); + (void)co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + } + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + c.close(); + s.close(); + }) + .add("pingpong", bench_unix_pingpong_latency) + .args({1, 64, 1024}) + .add("pingpong_lockless", bench_unix_pingpong_latency_lockless) + .args({1, 64, 1024}) + .add("concurrent", bench_unix_concurrent_latency) + .args({1, 4, 16}) + .add("concurrent_lockless", bench_unix_concurrent_latency_lockless) + .args({1, 4, 16}); +} + +} // namespace corosio_bench + +COROSIO_SUITE_INSTANTIATE_POSIX(corosio_bench::make_unix_socket_latency_suite) + +#endif // BOOST_COROSIO_POSIX diff --git a/perf/bench/corosio/unix_socket_throughput_bench.cpp b/perf/bench/corosio/unix_socket_throughput_bench.cpp new file mode 100644 index 000000000..60aea0087 --- /dev/null +++ b/perf/bench/corosio/unix_socket_throughput_bench.cpp @@ -0,0 +1,352 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include "benchmarks.hpp" +#include +#include "../../common/native_includes.hpp" + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace corosio_bench { +namespace { + +template +void +bench_unix_throughput(bench::state& state) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + corosio::native_io_context ioc; + auto [writer, reader] = corosio::make_local_stream_pair(ioc); + + std::vector write_buf(chunk_size, 'x'); + std::vector read_buf(chunk_size); + + std::atomic running{true}; + + auto write_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await writer.write_some( + capy::const_buffer(write_buf.data(), chunk_size)); + if (ec) + break; + } + writer.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read_task = [&]() -> capy::task<> { + for (;;) + { + auto [ec, n] = co_await reader.read_some( + capy::mutable_buffer(read_buf.data(), read_buf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + perf::stopwatch sw; + + capy::run_async(ioc.get_executor())(write_task()); + capy::run_async(ioc.get_executor())(read_task()); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + writer.close(); + reader.close(); +} + +template +void +bench_unix_bidirectional_throughput(bench::state& state) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + corosio::native_io_context ioc; + auto [sock1, sock2] = corosio::make_local_stream_pair(ioc); + + std::vector buf1(chunk_size, 'a'); + std::vector buf2(chunk_size, 'b'); + + std::atomic running{true}; + + auto write1_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await sock1.write_some( + capy::const_buffer(buf1.data(), chunk_size)); + if (ec) + break; + } + sock1.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read1_task = [&]() -> capy::task<> { + std::vector rbuf(chunk_size); + for (;;) + { + auto [ec, n] = co_await sock2.read_some( + capy::mutable_buffer(rbuf.data(), rbuf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + auto write2_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await sock2.write_some( + capy::const_buffer(buf2.data(), chunk_size)); + if (ec) + break; + } + sock2.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read2_task = [&]() -> capy::task<> { + std::vector rbuf(chunk_size); + for (;;) + { + auto [ec, n] = co_await sock1.read_some( + capy::mutable_buffer(rbuf.data(), rbuf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + perf::stopwatch sw; + + capy::run_async(ioc.get_executor())(write1_task()); + capy::run_async(ioc.get_executor())(read1_task()); + capy::run_async(ioc.get_executor())(write2_task()); + capy::run_async(ioc.get_executor())(read2_task()); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + sock1.close(); + sock2.close(); +} + +template +void +bench_unix_throughput_lockless(bench::state& state) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + corosio::io_context_options opts; + opts.single_threaded = true; + corosio::native_io_context ioc(opts); + auto [writer, reader] = corosio::make_local_stream_pair(ioc); + + std::vector write_buf(chunk_size, 'x'); + std::vector read_buf(chunk_size); + + std::atomic running{true}; + + auto write_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await writer.write_some( + capy::const_buffer(write_buf.data(), chunk_size)); + if (ec) + break; + } + writer.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read_task = [&]() -> capy::task<> { + for (;;) + { + auto [ec, n] = co_await reader.read_some( + capy::mutable_buffer(read_buf.data(), read_buf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + perf::stopwatch sw; + + capy::run_async(ioc.get_executor())(write_task()); + capy::run_async(ioc.get_executor())(read_task()); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + writer.close(); + reader.close(); +} + +template +void +bench_unix_bidirectional_throughput_lockless(bench::state& state) +{ + auto chunk_size = static_cast(state.range(0)); + state.counters["chunk_size"] = static_cast(chunk_size); + + corosio::io_context_options opts; + opts.single_threaded = true; + corosio::native_io_context ioc(opts); + auto [sock1, sock2] = corosio::make_local_stream_pair(ioc); + + std::vector buf1(chunk_size, 'a'); + std::vector buf2(chunk_size, 'b'); + + std::atomic running{true}; + + auto write1_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await sock1.write_some( + capy::const_buffer(buf1.data(), chunk_size)); + if (ec) + break; + } + sock1.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read1_task = [&]() -> capy::task<> { + std::vector rbuf(chunk_size); + for (;;) + { + auto [ec, n] = co_await sock2.read_some( + capy::mutable_buffer(rbuf.data(), rbuf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + auto write2_task = [&]() -> capy::task<> { + while (running.load(std::memory_order_relaxed)) + { + auto [ec, n] = co_await sock2.write_some( + capy::const_buffer(buf2.data(), chunk_size)); + if (ec) + break; + } + sock2.shutdown(corosio::local_stream_socket::shutdown_send); + }; + + auto read2_task = [&]() -> capy::task<> { + std::vector rbuf(chunk_size); + for (;;) + { + auto [ec, n] = co_await sock1.read_some( + capy::mutable_buffer(rbuf.data(), rbuf.size())); + if (ec || n == 0) + break; + state.add_bytes(static_cast(n)); + } + }; + + perf::stopwatch sw; + + capy::run_async(ioc.get_executor())(write1_task()); + capy::run_async(ioc.get_executor())(read1_task()); + capy::run_async(ioc.get_executor())(write2_task()); + capy::run_async(ioc.get_executor())(read2_task()); + + std::thread timer([&]() { + std::this_thread::sleep_for( + std::chrono::duration(state.duration())); + running.store(false, std::memory_order_relaxed); + }); + + ioc.run(); + timer.join(); + + state.set_elapsed(sw.elapsed_seconds()); + sock1.close(); + sock2.close(); +} + +} // anonymous namespace + +template +bench::benchmark_suite +make_unix_socket_throughput_suite() +{ + using F = bench::bench_flags; + + return bench::benchmark_suite("unix_socket_throughput", F::none) + .set_warmup([]{ + corosio::native_io_context ioc; + auto [w, r] = corosio::make_local_stream_pair(ioc); + std::vector buf(4096, 'w'); + auto task = [&]() -> capy::task<> { + (void)co_await w.write_some( + capy::const_buffer(buf.data(), buf.size())); + (void)co_await r.read_some( + capy::mutable_buffer(buf.data(), buf.size())); + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + w.close(); + r.close(); + }) + .add("unidirectional", bench_unix_throughput) + .range(1024, 1048576, 4) + .add("unidirectional_lockless", bench_unix_throughput_lockless) + .range(1024, 1048576, 4) + .add("bidirectional", bench_unix_bidirectional_throughput) + .range(1024, 1048576, 4) + .add("bidirectional_lockless", bench_unix_bidirectional_throughput_lockless) + .range(1024, 1048576, 4); +} + +} // namespace corosio_bench + +COROSIO_SUITE_INSTANTIATE_POSIX(corosio_bench::make_unix_socket_throughput_suite) + +#endif // BOOST_COROSIO_POSIX diff --git a/perf/bench/main.cpp b/perf/bench/main.cpp index 486230e06..3c217434e 100644 --- a/perf/bench/main.cpp +++ b/perf/bench/main.cpp @@ -76,6 +76,10 @@ add_corosio_suites(bench::benchmark_runner& runner, BackendTag) runner.add_suite("corosio", corosio_bench::make_timer_suite()); runner.add_suite("corosio", corosio_bench::make_accept_churn_suite()); runner.add_suite("corosio", corosio_bench::make_fan_out_suite()); +#if BOOST_COROSIO_POSIX + runner.add_suite("corosio", corosio_bench::make_unix_socket_throughput_suite()); + runner.add_suite("corosio", corosio_bench::make_unix_socket_latency_suite()); +#endif } #ifdef BOOST_COROSIO_BENCH_HAS_ASIO @@ -89,6 +93,8 @@ add_asio_suites(bench::benchmark_runner& runner) runner.add_suite("asio", asio_bench::make_timer_suite()); runner.add_suite("asio", asio_bench::make_accept_churn_suite()); runner.add_suite("asio", asio_bench::make_fan_out_suite()); + runner.add_suite("asio", asio_bench::make_unix_socket_throughput_suite()); + runner.add_suite("asio", asio_bench::make_unix_socket_latency_suite()); } void @@ -101,6 +107,8 @@ add_asio_callback_suites(bench::benchmark_runner& runner) runner.add_suite("asio_callback", asio_callback_bench::make_timer_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_accept_churn_suite()); runner.add_suite("asio_callback", asio_callback_bench::make_fan_out_suite()); + runner.add_suite("asio_callback", asio_callback_bench::make_unix_socket_throughput_suite()); + runner.add_suite("asio_callback", asio_callback_bench::make_unix_socket_latency_suite()); } #endif diff --git a/perf/common/native_includes.hpp b/perf/common/native_includes.hpp index 97488530c..c28248fcc 100644 --- a/perf/common/native_includes.hpp +++ b/perf/common/native_includes.hpp @@ -50,4 +50,10 @@ COROSIO_SUITE_INSTANTIATE_SELECT(decl) \ COROSIO_SUITE_INSTANTIATE_IOCP(decl) +// POSIX-only instantiation (no IOCP) for Unix domain socket benchmarks +#define COROSIO_SUITE_INSTANTIATE_POSIX(decl) \ + COROSIO_SUITE_INSTANTIATE_EPOLL(decl) \ + COROSIO_SUITE_INSTANTIATE_KQUEUE(decl) \ + COROSIO_SUITE_INSTANTIATE_SELECT(decl) + #endif // BOOST_COROSIO_PERF_NATIVE_INCLUDES_HPP diff --git a/src/corosio/src/io_context.cpp b/src/corosio/src/io_context.cpp index 7dfea384c..f03241301 100644 --- a/src/corosio/src/io_context.cpp +++ b/src/corosio/src/io_context.cpp @@ -15,26 +15,8 @@ #include #include -#if BOOST_COROSIO_HAS_EPOLL -#include -#include -#include -#include -#endif - -#if BOOST_COROSIO_HAS_SELECT -#include -#include -#include -#include -#endif - -#if BOOST_COROSIO_HAS_KQUEUE -#include -#include -#include -#include -#endif +// Reactor backend types come from backend.hpp via reactor_backend.hpp. +// Only IOCP needs additional includes. #if BOOST_COROSIO_HAS_IOCP #include @@ -54,9 +36,12 @@ epoll_t::construct(capy::execution_context& ctx, unsigned concurrency_hint) auto& sched = ctx.make_service( static_cast(concurrency_hint)); - auto& tcp_svc = ctx.make_service(); - ctx.make_service(tcp_svc); - ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); return sched; } @@ -69,9 +54,12 @@ select_t::construct(capy::execution_context& ctx, unsigned concurrency_hint) auto& sched = ctx.make_service( static_cast(concurrency_hint)); - auto& tcp_svc = ctx.make_service(); - ctx.make_service(tcp_svc); - ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); return sched; } @@ -84,9 +72,12 @@ kqueue_t::construct(capy::execution_context& ctx, unsigned concurrency_hint) auto& sched = ctx.make_service( static_cast(concurrency_hint)); - auto& tcp_svc = ctx.make_service(); - ctx.make_service(tcp_svc); - ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); + ctx.make_service(); return sched; } diff --git a/src/corosio/src/local_datagram.cpp b/src/corosio/src/local_datagram.cpp new file mode 100644 index 000000000..b671f08b7 --- /dev/null +++ b/src/corosio/src/local_datagram.cpp @@ -0,0 +1,46 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#endif + +namespace boost::corosio { + +int +local_datagram::family() noexcept +{ +#if BOOST_COROSIO_POSIX + return AF_UNIX; +#else + return 0; +#endif +} + +int +local_datagram::type() noexcept +{ +#if BOOST_COROSIO_POSIX + return SOCK_DGRAM; +#else + return 0; +#endif +} + +int +local_datagram::protocol() noexcept +{ + return 0; +} + +} // namespace boost::corosio diff --git a/src/corosio/src/local_datagram_socket.cpp b/src/corosio/src/local_datagram_socket.cpp new file mode 100644 index 000000000..513a5ef15 --- /dev/null +++ b/src/corosio/src/local_datagram_socket.cpp @@ -0,0 +1,155 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include + +namespace boost::corosio { + +local_datagram_socket::~local_datagram_socket() +{ + close(); +} + +local_datagram_socket::local_datagram_socket(capy::execution_context& ctx) + : io_object(create_handle(ctx)) +{ +} + +void +local_datagram_socket::open(local_datagram proto) +{ + if (is_open()) + return; + open_for_family(proto.family(), proto.type(), proto.protocol()); +} + +void +local_datagram_socket::open_for_family(int family, int type, int protocol) +{ + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.open_socket( + static_cast(*h_.get()), + family, type, protocol); + if (ec) + detail::throw_system_error(ec, "local_datagram_socket::open"); +} + +void +local_datagram_socket::close() +{ + if (!is_open()) + return; + h_.service().close(h_); +} + +std::error_code +local_datagram_socket::bind(corosio::local_endpoint ep) +{ + if (!is_open()) + detail::throw_logic_error("bind: socket not open"); + auto& svc = static_cast(h_.service()); + return svc.bind_socket( + static_cast(*h_.get()), + ep); +} + +void +local_datagram_socket::cancel() +{ + if (!is_open()) + return; + get().cancel(); +} + +void +local_datagram_socket::shutdown(shutdown_type what) +{ + if (is_open()) + { + // Best-effort: errors like ENOTCONN are expected and unhelpful + [[maybe_unused]] auto ec = get().shutdown(what); + } +} + +void +local_datagram_socket::shutdown(shutdown_type what, std::error_code& ec) noexcept +{ + ec = {}; + if (is_open()) + ec = get().shutdown(what); +} + +void +local_datagram_socket::assign(int fd) +{ + if (is_open()) + detail::throw_logic_error("assign: socket already open"); + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.assign_socket( + static_cast(*h_.get()), fd); + if (ec) + detail::throw_system_error(ec, "local_datagram_socket::assign"); +} + +native_handle_type +local_datagram_socket::native_handle() const noexcept +{ + if (!is_open()) + return -1; + return get().native_handle(); +} + +native_handle_type +local_datagram_socket::release() +{ + if (!is_open()) + detail::throw_logic_error("release: socket not open"); + return get().release_socket(); +} + +std::size_t +local_datagram_socket::available() const +{ + if (!is_open()) + detail::throw_logic_error("available: socket not open"); + int value = 0; + if (::ioctl(native_handle(), FIONREAD, &value) < 0) + detail::throw_system_error( + std::error_code(errno, std::system_category()), + "local_datagram_socket::available"); + return static_cast(value); +} + +local_endpoint +local_datagram_socket::local_endpoint() const noexcept +{ + if (!is_open()) + return corosio::local_endpoint{}; + return get().local_endpoint(); +} + +local_endpoint +local_datagram_socket::remote_endpoint() const noexcept +{ + if (!is_open()) + return corosio::local_endpoint{}; + return get().remote_endpoint(); +} + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/local_endpoint.cpp b/src/corosio/src/local_endpoint.cpp new file mode 100644 index 000000000..c09025b15 --- /dev/null +++ b/src/corosio/src/local_endpoint.cpp @@ -0,0 +1,61 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include + +#include +#include +#include + +namespace boost::corosio { + +local_endpoint::local_endpoint(std::string_view path) +{ + if (path.size() > max_path_length) + detail::throw_system_error( + std::make_error_code(std::errc::filename_too_long), + "local_endpoint"); + std::memcpy(path_, path.data(), path.size()); + len_ = static_cast(path.size()); +} + +local_endpoint::local_endpoint( + std::string_view path, std::error_code& ec) noexcept +{ + if (path.size() > max_path_length) + { + ec = std::make_error_code(std::errc::filename_too_long); + return; + } + ec = {}; + std::memcpy(path_, path.data(), path.size()); + len_ = static_cast(path.size()); +} + +std::ostream& +operator<<(std::ostream& os, local_endpoint const& ep) +{ + if (ep.empty()) + return os; + if (ep.is_abstract()) + { + // Skip the leading null byte; print the rest as the name + os << "[abstract:" + << std::string_view(ep.path_ + 1, ep.len_ - 1) + << ']'; + } + else + { + os << ep.path(); + } + return os; +} + +} // namespace boost::corosio diff --git a/src/corosio/src/local_socket_pair.cpp b/src/corosio/src/local_socket_pair.cpp new file mode 100644 index 000000000..3ccdea8eb --- /dev/null +++ b/src/corosio/src/local_socket_pair.cpp @@ -0,0 +1,134 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio { + +namespace { + +#ifndef SOCK_NONBLOCK +// Only needed on platforms lacking SOCK_NONBLOCK/SOCK_CLOEXEC (e.g. older BSDs). +// On Linux/glibc SOCK_NONBLOCK is always defined, so omitting this definition +// avoids a -Wunused-function diagnostic. +// +// Returns 0 on success or errno on failure. Caller owns fd on error. +int +make_nonblock_cloexec(int fd) +{ + int fl = ::fcntl(fd, F_GETFL, 0); + if (fl == -1) + return errno; + if (::fcntl(fd, F_SETFL, fl | O_NONBLOCK) == -1) + return errno; + if (::fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) + return errno; + return 0; +} +#endif + +void +create_pair(int type, int fds[2]) +{ + int flags = type; +#ifdef SOCK_NONBLOCK + flags |= SOCK_NONBLOCK | SOCK_CLOEXEC; +#endif + if (::socketpair(AF_UNIX, flags, 0, fds) != 0) + throw std::system_error( + std::error_code(errno, std::system_category()), + "socketpair"); +#ifndef SOCK_NONBLOCK + int err = make_nonblock_cloexec(fds[0]); + if (err == 0) + err = make_nonblock_cloexec(fds[1]); + if (err != 0) + { + ::close(fds[0]); + ::close(fds[1]); + throw std::system_error( + std::error_code(err, std::system_category()), + "socketpair (fcntl setup)"); + } +#endif +} + +} // namespace + +std::pair +make_local_stream_pair(io_context& ctx) +{ + int fds[2]; + create_pair(SOCK_STREAM, fds); + + try + { + local_stream_socket s1(ctx); + local_stream_socket s2(ctx); + + s1.assign(fds[0]); + fds[0] = -1; + s2.assign(fds[1]); + fds[1] = -1; + + return {std::move(s1), std::move(s2)}; + } + catch (...) + { + if (fds[0] >= 0) + ::close(fds[0]); + if (fds[1] >= 0) + ::close(fds[1]); + throw; + } +} + +std::pair +make_local_datagram_pair(io_context& ctx) +{ + int fds[2]; + create_pair(SOCK_DGRAM, fds); + + try + { + local_datagram_socket s1(ctx); + local_datagram_socket s2(ctx); + + s1.assign(fds[0]); + fds[0] = -1; + s2.assign(fds[1]); + fds[1] = -1; + + return {std::move(s1), std::move(s2)}; + } + catch (...) + { + if (fds[0] >= 0) + ::close(fds[0]); + if (fds[1] >= 0) + ::close(fds[1]); + throw; + } +} + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/local_stream.cpp b/src/corosio/src/local_stream.cpp new file mode 100644 index 000000000..cf4754bbb --- /dev/null +++ b/src/corosio/src/local_stream.cpp @@ -0,0 +1,46 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#endif + +namespace boost::corosio { + +int +local_stream::family() noexcept +{ +#if BOOST_COROSIO_POSIX + return AF_UNIX; +#else + return 0; +#endif +} + +int +local_stream::type() noexcept +{ +#if BOOST_COROSIO_POSIX + return SOCK_STREAM; +#else + return 0; +#endif +} + +int +local_stream::protocol() noexcept +{ + return 0; +} + +} // namespace boost::corosio diff --git a/src/corosio/src/local_stream_acceptor.cpp b/src/corosio/src/local_stream_acceptor.cpp new file mode 100644 index 000000000..786cb305f --- /dev/null +++ b/src/corosio/src/local_stream_acceptor.cpp @@ -0,0 +1,131 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include + +#include +#include + +namespace boost::corosio { + +local_stream_acceptor::~local_stream_acceptor() +{ + close(); +} + +local_stream_acceptor::local_stream_acceptor(capy::execution_context& ctx) + : io_object(create_handle(ctx)) + , ctx_(ctx) +{ +} + +void +local_stream_acceptor::open(local_stream proto) +{ + if (is_open()) + return; + auto& svc = + static_cast(h_.service()); + auto ec = svc.open_acceptor_socket( + static_cast(*h_.get()), + proto.family(), proto.type(), proto.protocol()); + if (ec) + detail::throw_system_error(ec, "local_stream_acceptor::open"); +} + +std::error_code +local_stream_acceptor::bind(corosio::local_endpoint ep, bind_option opt) +{ + if (!is_open()) + detail::throw_logic_error("bind: acceptor not open"); + + if (opt == bind_option::unlink_existing && + !ep.empty() && !ep.is_abstract()) + { + auto p = ep.path(); + // path() is not null-terminated for the fixed buffer, + // so copy to a local array for syscalls. + char buf[local_endpoint::max_path_length + 1]; + std::memcpy(buf, p.data(), p.size()); + buf[p.size()] = '\0'; + + // Only remove the path if it actually points to a socket + // file. Use lstat (NOT stat) so a symlink at the path is + // left alone — otherwise this option could be abused via + // symlink races to delete arbitrary files. Any lstat + // failure (ENOENT or otherwise) is treated as "nothing to + // remove" and we let bind() handle whatever's actually + // there. + struct stat st; + if (::lstat(buf, &st) == 0 && S_ISSOCK(st.st_mode)) + ::unlink(buf); + } + + auto& svc = + static_cast(h_.service()); + return svc.bind_acceptor( + static_cast(*h_.get()), + ep); +} + +std::error_code +local_stream_acceptor::listen(int backlog) +{ + if (!is_open()) + detail::throw_logic_error("listen: acceptor not open"); + auto& svc = + static_cast(h_.service()); + return svc.listen_acceptor( + static_cast(*h_.get()), + backlog); +} + +void +local_stream_acceptor::close() +{ + if (!is_open()) + return; + h_.service().close(h_); +} + +native_handle_type +local_stream_acceptor::release() +{ + if (!is_open()) + detail::throw_logic_error("release: acceptor not open"); + return get().release_socket(); +} + +void +local_stream_acceptor::cancel() +{ + if (!is_open()) + return; + get().cancel(); +} + +local_endpoint +local_stream_acceptor::local_endpoint() const noexcept +{ + if (!is_open()) + return corosio::local_endpoint{}; + return get().local_endpoint(); +} + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/local_stream_socket.cpp b/src/corosio/src/local_stream_socket.cpp new file mode 100644 index 000000000..41a6ec195 --- /dev/null +++ b/src/corosio/src/local_stream_socket.cpp @@ -0,0 +1,144 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include + +namespace boost::corosio { + +local_stream_socket::~local_stream_socket() +{ + close(); +} + +local_stream_socket::local_stream_socket(capy::execution_context& ctx) + : io_object(create_handle(ctx)) +{ +} + +void +local_stream_socket::open(local_stream proto) +{ + if (is_open()) + return; + open_for_family(proto.family(), proto.type(), proto.protocol()); +} + +void +local_stream_socket::open_for_family(int family, int type, int protocol) +{ + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.open_socket( + static_cast(*h_.get()), + family, type, protocol); + if (ec) + detail::throw_system_error(ec, "local_stream_socket::open"); +} + +void +local_stream_socket::close() +{ + if (!is_open()) + return; + h_.service().close(h_); +} + +void +local_stream_socket::cancel() +{ + if (!is_open()) + return; + get().cancel(); +} + +void +local_stream_socket::shutdown(shutdown_type what) +{ + if (is_open()) + { + // Best-effort: errors like ENOTCONN are expected and unhelpful + [[maybe_unused]] auto ec = get().shutdown(what); + } +} + +void +local_stream_socket::shutdown(shutdown_type what, std::error_code& ec) noexcept +{ + ec = {}; + if (is_open()) + ec = get().shutdown(what); +} + +void +local_stream_socket::assign(int fd) +{ + if (is_open()) + detail::throw_logic_error("assign: socket already open"); + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.assign_socket( + static_cast(*h_.get()), fd); + if (ec) + detail::throw_system_error(ec, "local_stream_socket::assign"); +} + +native_handle_type +local_stream_socket::native_handle() const noexcept +{ + if (!is_open()) + return -1; + return get().native_handle(); +} + +native_handle_type +local_stream_socket::release() +{ + if (!is_open()) + detail::throw_logic_error("release: socket not open"); + return get().release_socket(); +} + +std::size_t +local_stream_socket::available() const +{ + if (!is_open()) + detail::throw_logic_error("available: socket not open"); + int value = 0; + if (::ioctl(native_handle(), FIONREAD, &value) < 0) + detail::throw_system_error( + std::error_code(errno, std::system_category()), + "local_stream_socket::available"); + return static_cast(value); +} + +local_endpoint +local_stream_socket::local_endpoint() const noexcept +{ + if (!is_open()) + return corosio::local_endpoint{}; + return get().local_endpoint(); +} + +local_endpoint +local_stream_socket::remote_endpoint() const noexcept +{ + if (!is_open()) + return corosio::local_endpoint{}; + return get().remote_endpoint(); +} + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/tcp_socket.cpp b/src/corosio/src/tcp_socket.cpp index 34c351ee7..1ab56f77a 100644 --- a/src/corosio/src/tcp_socket.cpp +++ b/src/corosio/src/tcp_socket.cpp @@ -104,6 +104,14 @@ tcp_socket::shutdown(shutdown_type what) } } +void +tcp_socket::shutdown(shutdown_type what, std::error_code& ec) noexcept +{ + ec = {}; + if (is_open()) + ec = get().shutdown(what); +} + native_handle_type tcp_socket::native_handle() const noexcept { diff --git a/test/unit/local_datagram_socket.cpp b/test/unit/local_datagram_socket.cpp new file mode 100644 index 000000000..fd07285ca --- /dev/null +++ b/test/unit/local_datagram_socket.cpp @@ -0,0 +1,608 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +namespace { + +std::string +make_temp_socket_path() +{ + char tmpl[] = "/tmp/corosio_test_XXXXXX"; + if (!::mkdtemp(tmpl)) + throw std::runtime_error("mkdtemp failed"); + std::string path(tmpl); + path += "/sock"; + return path; +} + +void +cleanup_path(std::string const& path) +{ + ::unlink(path.c_str()); + auto dir = path.substr(0, path.rfind('/')); + ::rmdir(dir.c_str()); +} + +} // namespace + +template +struct local_datagram_socket_test +{ + void testConstruction() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testOpen() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + sock.open(); + BOOST_TEST_EQ(sock.is_open(), true); + + sock.close(); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testMove() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc); + s1.open(); + BOOST_TEST_EQ(s1.is_open(), true); + + local_datagram_socket s2(std::move(s1)); + BOOST_TEST_EQ(s2.is_open(), true); + BOOST_TEST_EQ(s1.is_open(), false); + } + + void testSendRecvConnected() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_datagram_pair(ioc); + + auto ex = ioc.get_executor(); + + char const msg[] = "dgram test"; + char buf[64] = {}; + std::error_code send_ec, recv_ec; + std::size_t sent = 0, recvd = 0; + bool send_done = false, recv_done = false; + + capy::run_async(ex)( + [](local_datagram_socket& s, char const* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.send(capy::const_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(s1, msg, std::strlen(msg), send_ec, sent, send_done)); + + capy::run_async(ex)( + [](local_datagram_socket& s, char* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.recv(capy::mutable_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(s2, buf, sizeof(buf), recv_ec, recvd, recv_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(send_done, true); + BOOST_TEST_EQ(!send_ec, true); + BOOST_TEST_EQ(sent, std::strlen(msg)); + BOOST_TEST_EQ(recv_done, true); + BOOST_TEST_EQ(!recv_ec, true); + BOOST_TEST_EQ(recvd, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf, recvd), std::string(msg)); + } + + void testZeroLengthDatagram() + { + // A zero-length datagram is a legitimate event for connected + // datagram sockets and must NOT be reported as EOF (cond::eof). + // Regression test for the reactor backends, where + // reactor_dgram_recv_op previously inherited complete_io_op's + // stream-style "is_read_operation && bytes==0 -> EOF" mapping. + io_context ioc(Backend); + auto [s1, s2] = make_local_datagram_pair(ioc); + + auto ex = ioc.get_executor(); + + char buf[64] = {'\xCC'}; + std::error_code send_ec, recv_ec; + std::size_t sent = 0, recvd = 0; + bool send_done = false, recv_done = false; + + capy::run_async(ex)( + [](local_datagram_socket& s, std::error_code& ec_out, + std::size_t& n_out, bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.send(capy::const_buffer(nullptr, 0)); + ec_out = ec; + n_out = n; + done = true; + }(s1, send_ec, sent, send_done)); + + capy::run_async(ex)( + [](local_datagram_socket& s, char* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.recv(capy::mutable_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(s2, buf, sizeof(buf), recv_ec, recvd, recv_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(send_done, true); + BOOST_TEST_EQ(!send_ec, true); + BOOST_TEST_EQ(sent, 0u); + + BOOST_TEST_EQ(recv_done, true); + // Critical: zero-length datagram is success, not EOF. + BOOST_TEST_EQ(!recv_ec, true); + BOOST_TEST(recv_ec != capy::cond::eof); + BOOST_TEST_EQ(recvd, 0u); + } + + void testExplicitBind() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + sock.open(); + + auto path = make_temp_socket_path(); + auto ec = sock.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + + cleanup_path(path); + } + + void testSendToRecvFrom() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + auto path1 = make_temp_socket_path(); + auto path2 = make_temp_socket_path(); + + local_datagram_socket s1(ioc); + local_datagram_socket s2(ioc); + s1.open(); + s2.open(); + + auto ec1 = s1.bind(local_endpoint(path1)); + auto ec2 = s2.bind(local_endpoint(path2)); + BOOST_TEST_EQ(!ec1, true); + BOOST_TEST_EQ(!ec2, true); + + char const msg[] = "sendto test"; + char buf[64] = {}; + std::error_code send_ec, recv_ec; + std::size_t sent = 0, recvd = 0; + bool send_done = false, recv_done = false; + local_endpoint source; + + capy::run_async(ex)( + [](local_datagram_socket& s, char const* data, std::size_t len, + local_endpoint dest, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await s.send_to( + capy::const_buffer(data, len), dest); + ec_out = ec; + n_out = n; + done = true; + }(s1, msg, std::strlen(msg), local_endpoint(path2), + send_ec, sent, send_done)); + + capy::run_async(ex)( + [](local_datagram_socket& s, char* data, std::size_t len, + local_endpoint& source_out, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await s.recv_from( + capy::mutable_buffer(data, len), source_out); + ec_out = ec; + n_out = n; + done = true; + }(s2, buf, sizeof(buf), source, recv_ec, recvd, recv_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(send_done, true); + BOOST_TEST_EQ(!send_ec, true); + BOOST_TEST_EQ(sent, std::strlen(msg)); + BOOST_TEST_EQ(recv_done, true); + BOOST_TEST_EQ(!recv_ec, true); + BOOST_TEST_EQ(recvd, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf, recvd), std::string(msg)); + + // Source endpoint should be the sender's bound path + BOOST_TEST_EQ(source.path(), path1); + + cleanup_path(path1); + cleanup_path(path2); + } + + void testBindFailure() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + sock.open(); + + // Bind to a path under a nonexistent directory + auto ec = sock.bind(local_endpoint("/tmp/nonexistent_dir_corosio/sock")); + BOOST_TEST_EQ(!!ec, true); + } + + void testDatagramBoundary() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_datagram_pair(ioc); + auto ex = ioc.get_executor(); + + // Send two messages of different sizes, verify they + // arrive as distinct datagrams (not merged like a stream). + // Use a single coroutine per socket to avoid concurrent + // same-type operations (documented as unsafe). + char const msg1[] = "short"; + char const msg2[] = "a longer message"; + char buf1[64] = {}; + char buf2[64] = {}; + std::error_code send_ec1, send_ec2, recv_ec1, recv_ec2; + std::size_t sent1 = 0, sent2 = 0, recvd1 = 0, recvd2 = 0; + bool done = false; + + capy::run_async(ex)( + [](local_datagram_socket& sender, + local_datagram_socket& receiver, + char const* m1, std::size_t m1_len, + char const* m2, std::size_t m2_len, + char* b1, std::size_t b1_len, + char* b2, std::size_t b2_len, + std::error_code& se1, std::size_t& sn1, + std::error_code& se2, std::size_t& sn2, + std::error_code& re1, std::size_t& rn1, + std::error_code& re2, std::size_t& rn2, + bool& d) -> capy::task<> { + // Send both messages sequentially + { + auto [ec, n] = co_await sender.send( + capy::const_buffer(m1, m1_len)); + se1 = ec; sn1 = n; + } + { + auto [ec, n] = co_await sender.send( + capy::const_buffer(m2, m2_len)); + se2 = ec; sn2 = n; + } + // Receive both messages sequentially + { + auto [ec, n] = co_await receiver.recv( + capy::mutable_buffer(b1, b1_len)); + re1 = ec; rn1 = n; + } + { + auto [ec, n] = co_await receiver.recv( + capy::mutable_buffer(b2, b2_len)); + re2 = ec; rn2 = n; + } + d = true; + }(s1, s2, + msg1, std::strlen(msg1), msg2, std::strlen(msg2), + buf1, sizeof(buf1), buf2, sizeof(buf2), + send_ec1, sent1, send_ec2, sent2, + recv_ec1, recvd1, recv_ec2, recvd2, done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(done, true); + BOOST_TEST_EQ(!send_ec1, true); + BOOST_TEST_EQ(!send_ec2, true); + BOOST_TEST_EQ(!recv_ec1, true); + BOOST_TEST_EQ(!recv_ec2, true); + + // Each recv returns exactly one datagram -- not a merged stream + BOOST_TEST_EQ(recvd1, std::strlen(msg1)); + BOOST_TEST_EQ(recvd2, std::strlen(msg2)); + BOOST_TEST_EQ(std::string(buf1, recvd1), std::string(msg1)); + BOOST_TEST_EQ(std::string(buf2, recvd2), std::string(msg2)); + } + +#ifdef __linux__ + void testAbstractSocket() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + // Abstract socket: null byte prefix, no filesystem entry + std::string abs_path1(1, '\0'); + abs_path1 += "corosio_test_abstract_dgram_1"; + std::string abs_path2(1, '\0'); + abs_path2 += "corosio_test_abstract_dgram_2"; + + local_datagram_socket s1(ioc); + local_datagram_socket s2(ioc); + s1.open(); + s2.open(); + + auto ec1 = s1.bind(local_endpoint(abs_path1)); + auto ec2 = s2.bind(local_endpoint(abs_path2)); + BOOST_TEST_EQ(!ec1, true); + BOOST_TEST_EQ(!ec2, true); + + char const msg[] = "abstract dgram"; + char buf[64] = {}; + std::error_code send_ec, recv_ec; + std::size_t sent = 0, recvd = 0; + bool send_done = false, recv_done = false; + local_endpoint source; + + capy::run_async(ex)( + [](local_datagram_socket& s, char const* data, std::size_t len, + local_endpoint dest, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await s.send_to( + capy::const_buffer(data, len), dest); + ec_out = ec; + n_out = n; + done = true; + }(s1, msg, std::strlen(msg), local_endpoint(abs_path2), + send_ec, sent, send_done)); + + capy::run_async(ex)( + [](local_datagram_socket& s, char* data, std::size_t len, + local_endpoint& source_out, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await s.recv_from( + capy::mutable_buffer(data, len), source_out); + ec_out = ec; + n_out = n; + done = true; + }(s2, buf, sizeof(buf), source, recv_ec, recvd, recv_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(send_done, true); + BOOST_TEST_EQ(!send_ec, true); + BOOST_TEST_EQ(recv_done, true); + BOOST_TEST_EQ(!recv_ec, true); + BOOST_TEST_EQ(recvd, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf, recvd), std::string(msg)); + + // Source should be the sender's abstract path + BOOST_TEST_EQ(source.path(), abs_path1); + BOOST_TEST_EQ(source.is_abstract(), true); + } +#endif // __linux__ + + void testRecvPeek() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_datagram_pair(ioc); + auto ex = ioc.get_executor(); + + // Send a message, peek at it, then consume it. + // Peek should not remove the message from the queue. + char const msg[] = "peek test"; + char buf1[64] = {}; + char buf2[64] = {}; + std::error_code se, re1, re2; + std::size_t sn = 0, rn1 = 0, rn2 = 0; + bool done = false; + + capy::run_async(ex)( + [](local_datagram_socket& sender, + local_datagram_socket& receiver, + char const* data, std::size_t len, + char* b1, std::size_t b1_len, + char* b2, std::size_t b2_len, + std::error_code& se_out, std::size_t& sn_out, + std::error_code& re1_out, std::size_t& rn1_out, + std::error_code& re2_out, std::size_t& rn2_out, + bool& d) -> capy::task<> { + { + auto [ec, n] = co_await sender.send( + capy::const_buffer(data, len)); + se_out = ec; sn_out = n; + } + // Peek -- should not consume + { + auto [ec, n] = co_await receiver.recv( + capy::mutable_buffer(b1, b1_len), + message_flags::peek); + re1_out = ec; rn1_out = n; + } + // Normal recv -- should get same data + { + auto [ec, n] = co_await receiver.recv( + capy::mutable_buffer(b2, b2_len)); + re2_out = ec; rn2_out = n; + } + d = true; + }(s1, s2, msg, std::strlen(msg), + buf1, sizeof(buf1), buf2, sizeof(buf2), + se, sn, re1, rn1, re2, rn2, done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(done, true); + BOOST_TEST_EQ(!se, true); + BOOST_TEST_EQ(!re1, true); + BOOST_TEST_EQ(!re2, true); + + // Both reads should return the same data + BOOST_TEST_EQ(rn1, std::strlen(msg)); + BOOST_TEST_EQ(rn2, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf1, rn1), std::string(msg)); + BOOST_TEST_EQ(std::string(buf2, rn2), std::string(msg)); + } + + void testRecvFromPeek() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + auto path1 = make_temp_socket_path(); + auto path2 = make_temp_socket_path(); + + local_datagram_socket s1(ioc); + local_datagram_socket s2(ioc); + s1.open(); + s2.open(); + + auto ec1 = s1.bind(local_endpoint(path1)); + auto ec2 = s2.bind(local_endpoint(path2)); + BOOST_TEST_EQ(!ec1, true); + BOOST_TEST_EQ(!ec2, true); + + // Send a message via send_to, peek with recv_from, then + // consume with recv_from. Exercises the connectionless + // recv_from path with message_flags::peek. + char const msg[] = "recv_from peek"; + char buf1[64] = {}; + char buf2[64] = {}; + std::error_code se, re1, re2; + std::size_t sn = 0, rn1 = 0, rn2 = 0; + local_endpoint src1, src2; + bool done = false; + + capy::run_async(ex)( + [](local_datagram_socket& sender, + local_datagram_socket& receiver, + char const* data, std::size_t len, + local_endpoint dest, + char* b1, std::size_t b1_len, + char* b2, std::size_t b2_len, + std::error_code& se_out, std::size_t& sn_out, + std::error_code& re1_out, std::size_t& rn1_out, + local_endpoint& src1_out, + std::error_code& re2_out, std::size_t& rn2_out, + local_endpoint& src2_out, + bool& d) -> capy::task<> { + { + auto [ec, n] = co_await sender.send_to( + capy::const_buffer(data, len), dest); + se_out = ec; sn_out = n; + } + // Peek via recv_from -- should not consume + { + auto [ec, n] = co_await receiver.recv_from( + capy::mutable_buffer(b1, b1_len), + src1_out, + message_flags::peek); + re1_out = ec; rn1_out = n; + } + // Normal recv_from -- should get same data + { + auto [ec, n] = co_await receiver.recv_from( + capy::mutable_buffer(b2, b2_len), + src2_out); + re2_out = ec; rn2_out = n; + } + d = true; + }(s1, s2, msg, std::strlen(msg), local_endpoint(path2), + buf1, sizeof(buf1), buf2, sizeof(buf2), + se, sn, re1, rn1, src1, re2, rn2, src2, done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(done, true); + BOOST_TEST_EQ(!se, true); + BOOST_TEST_EQ(!re1, true); + BOOST_TEST_EQ(!re2, true); + + // Both reads should return the same data + BOOST_TEST_EQ(rn1, std::strlen(msg)); + BOOST_TEST_EQ(rn2, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf1, rn1), std::string(msg)); + BOOST_TEST_EQ(std::string(buf2, rn2), std::string(msg)); + + // Source should be the sender's bound path + BOOST_TEST_EQ(src1.path(), path1); + BOOST_TEST_EQ(src2.path(), path1); + + cleanup_path(path1); + cleanup_path(path2); + } + + void run() + { + testConstruction(); + testOpen(); + testMove(); + testSendRecvConnected(); + testZeroLengthDatagram(); + testExplicitBind(); + testSendToRecvFrom(); + testBindFailure(); + testDatagramBoundary(); + testRecvPeek(); + testRecvFromPeek(); +#ifdef __linux__ + testAbstractSocket(); +#endif + } +}; + +COROSIO_BACKEND_TESTS( + local_datagram_socket_test, "boost.corosio.local_datagram_socket") + +} // namespace boost::corosio + +#else // !BOOST_COROSIO_POSIX + +// Empty on non-POSIX platforms + +#endif diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp new file mode 100644 index 000000000..8ee995255 --- /dev/null +++ b/test/unit/local_stream_socket.cpp @@ -0,0 +1,437 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +// Verify local_stream_socket satisfies stream concepts + +static_assert(capy::ReadStream); +static_assert(capy::WriteStream); + +namespace { + +std::string +make_temp_socket_path() +{ + char tmpl[] = "/tmp/corosio_test_XXXXXX"; + if (!::mkdtemp(tmpl)) + throw std::runtime_error("mkdtemp failed"); + std::string path(tmpl); + path += "/sock"; + return path; +} + +void +cleanup_path(std::string const& path) +{ + ::unlink(path.c_str()); + auto dir = path.substr(0, path.rfind('/')); + ::rmdir(dir.c_str()); +} + +} // namespace + +template +struct local_stream_socket_test +{ + void testConstruction() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testOpen() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + + sock.open(); + BOOST_TEST_EQ(sock.is_open(), true); + + sock.close(); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testMove() + { + io_context ioc(Backend); + local_stream_socket s1(ioc); + s1.open(); + BOOST_TEST_EQ(s1.is_open(), true); + + local_stream_socket s2(std::move(s1)); + BOOST_TEST_EQ(s2.is_open(), true); + BOOST_TEST_EQ(s1.is_open(), false); + } + + void testConnectAccept() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto path = make_temp_socket_path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + ec = acc.listen(); + BOOST_TEST_EQ(!ec, true); + + std::error_code accept_ec, connect_ec; + bool accept_done = false, connect_done = false; + + local_stream_socket server(ioc); + local_stream_socket client(ioc); + client.open(); + + capy::run_async(ex)( + [](local_stream_acceptor& a, local_stream_socket& s, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec] = co_await a.accept(s); + ec_out = ec; + done = true; + }(acc, server, accept_ec, accept_done)); + + capy::run_async(ex)( + [](local_stream_socket& s, local_endpoint ep, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec] = co_await s.connect(ep); + ec_out = ec; + done = true; + }(client, local_endpoint(path), connect_ec, connect_done)); + + ioc.run(); + ioc.restart(); + + cleanup_path(path); + + BOOST_TEST_EQ(accept_done, true); + BOOST_TEST_EQ(!accept_ec, true); + BOOST_TEST_EQ(connect_done, true); + BOOST_TEST_EQ(!connect_ec, true); + BOOST_TEST_EQ(server.is_open(), true); + BOOST_TEST_EQ(client.is_open(), true); + } + + void testMoveAccept() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto path = make_temp_socket_path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + ec = acc.listen(); + BOOST_TEST_EQ(!ec, true); + + std::error_code accept_ec, connect_ec; + bool accept_done = false, connect_done = false; + bool server_open = false; + + local_stream_socket client(ioc); + client.open(); + + capy::run_async(ex)( + [](local_stream_acceptor& a, + std::error_code& ec_out, bool& open_out, + bool& done) -> capy::task<> { + auto [ec, peer] = co_await a.accept(); + ec_out = ec; + open_out = peer.is_open(); + done = true; + }(acc, accept_ec, server_open, accept_done)); + + capy::run_async(ex)( + [](local_stream_socket& s, local_endpoint ep, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec] = co_await s.connect(ep); + ec_out = ec; + done = true; + }(client, local_endpoint(path), connect_ec, connect_done)); + + ioc.run(); + ioc.restart(); + + cleanup_path(path); + + BOOST_TEST_EQ(accept_done, true); + BOOST_TEST_EQ(!accept_ec, true); + BOOST_TEST_EQ(server_open, true); + BOOST_TEST_EQ(connect_done, true); + BOOST_TEST_EQ(!connect_ec, true); + } + + void testReadWrite() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_stream_pair(ioc); + + auto ex = ioc.get_executor(); + + char const msg[] = "hello unix sockets"; + char buf[64] = {}; + std::error_code write_ec, read_ec; + std::size_t written = 0, read_n = 0; + bool write_done = false, read_done = false; + + capy::run_async(ex)( + [](local_stream_socket& s, char const* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await capy::write( + s, capy::const_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(s1, msg, std::strlen(msg), write_ec, written, write_done)); + + capy::run_async(ex)( + [](local_stream_socket& s, char* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await s.read_some( + capy::mutable_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(s2, buf, sizeof(buf), read_ec, read_n, read_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(write_done, true); + BOOST_TEST_EQ(!write_ec, true); + BOOST_TEST_EQ(written, std::strlen(msg)); + BOOST_TEST_EQ(read_done, true); + BOOST_TEST_EQ(!read_ec, true); + BOOST_TEST_EQ(read_n, std::strlen(msg)); + BOOST_TEST_EQ(std::string(buf, read_n), std::string(msg)); + } + + void testSocketPair() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_stream_pair(ioc); + + BOOST_TEST_EQ(s1.is_open(), true); + BOOST_TEST_EQ(s2.is_open(), true); + } + + void testUnlinkExisting() + { + io_context ioc(Backend); + auto path = make_temp_socket_path(); + + // First bind creates the socket file + { + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + } + + // Second bind without unlink_existing should fail + { + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!!ec, true); + } + + // Third bind with unlink_existing should succeed + { + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind( + local_endpoint(path), bind_option::unlink_existing); + BOOST_TEST_EQ(!ec, true); + } + + cleanup_path(path); + } + + void testUnlinkNonexistent() + { + // unlink_existing on a path that doesn't exist should + // succeed (unlink silently fails with ENOENT). + io_context ioc(Backend); + auto path = make_temp_socket_path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind( + local_endpoint(path), bind_option::unlink_existing); + BOOST_TEST_EQ(!ec, true); + + cleanup_path(path); + } + + void testEndpointOrdering() + { + local_endpoint a("/tmp/a"); + local_endpoint b("/tmp/b"); + local_endpoint a2("/tmp/a"); + local_endpoint prefix("/tmp"); + local_endpoint empty; + + // Equality + BOOST_TEST_EQ(a == a2, true); + BOOST_TEST_EQ(a != b, true); + + // Ordering + BOOST_TEST_EQ(a < b, true); + BOOST_TEST_EQ(b > a, true); + BOOST_TEST_EQ(a <= a2, true); + BOOST_TEST_EQ(a >= a2, true); + + // Prefix is less than full path + BOOST_TEST_EQ(prefix < a, true); + + // Empty is less than everything + BOOST_TEST_EQ(empty < a, true); + BOOST_TEST_EQ(empty < prefix, true); + + // Spaceship + BOOST_TEST_EQ((a <=> a2) == std::strong_ordering::equal, true); + BOOST_TEST_EQ((a <=> b) == std::strong_ordering::less, true); + } + + void run() + { + testConstruction(); + testOpen(); + testMove(); + testConnectAccept(); + testMoveAccept(); + testReadWrite(); + testSocketPair(); + testUnlinkExisting(); + testUnlinkNonexistent(); + testEndpointOrdering(); + testEndpointStreamOutput(); + testAvailable(); + testRelease(); + } + + void testAvailable() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_stream_pair(ioc); + + // Nothing written yet + BOOST_TEST_EQ(s2.available(), std::size_t(0)); + + // Write some data synchronously through the pair + char const msg[] = "available test"; + auto ex = ioc.get_executor(); + bool done = false; + + capy::run_async(ex)( + [](local_stream_socket& s, char const* data, std::size_t len, + bool& d) -> capy::task<> { + (void)co_await capy::write(s, capy::const_buffer(data, len)); + d = true; + }(s1, msg, std::strlen(msg), done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(done, true); + BOOST_TEST_EQ(s2.available(), std::strlen(msg)); + } + + void testRelease() + { + io_context ioc(Backend); + auto [s1, s2] = make_local_stream_pair(ioc); + + BOOST_TEST_EQ(s1.is_open(), true); + + int fd = s1.release(); + BOOST_TEST_EQ(fd >= 0, true); + BOOST_TEST_EQ(s1.is_open(), false); + + // The released fd is still valid -- write through it + char const msg[] = "released"; + BOOST_TEST_EQ(::write(fd, msg, std::strlen(msg)) > 0, true); + ::close(fd); + } + + void testEndpointStreamOutput() + { + // Non-abstract path + { + std::ostringstream os; + os << local_endpoint("/tmp/sock"); + BOOST_TEST_EQ(os.str(), std::string("/tmp/sock")); + } + + // Empty endpoint + { + std::ostringstream os; + os << local_endpoint(); + BOOST_TEST_EQ(os.str(), std::string("")); + } + +#ifdef __linux__ + // Abstract socket + { + std::string abs_path(1, '\0'); + abs_path += "test_name"; + std::ostringstream os; + os << local_endpoint(abs_path); + BOOST_TEST_EQ(os.str(), std::string("[abstract:test_name]")); + } +#endif + } +}; + +COROSIO_BACKEND_TESTS( + local_stream_socket_test, "boost.corosio.local_stream_socket") + +} // namespace boost::corosio + +#else // !BOOST_COROSIO_POSIX + +// Empty on non-POSIX platforms + +#endif diff --git a/test/unit/tcp_socket.cpp b/test/unit/tcp_socket.cpp index 6fd70679d..14df74e34 100644 --- a/test/unit/tcp_socket.cpp +++ b/test/unit/tcp_socket.cpp @@ -897,7 +897,10 @@ struct tcp_socket_test auto task = [](tcp_socket& a, tcp_socket& b) -> capy::task<> { // Write data then shutdown send (void)co_await a.write_some(capy::const_buffer("hello", 5)); - a.shutdown(tcp_socket::shutdown_send); + // Note: unqualified shutdown_send (not tcp_socket::shutdown_send) + // to work around a GCC 11 ICE in tsubst_copy when a class-template + // using-enum enumerator is referenced inside a lambda-coroutine. + a.shutdown(shutdown_send); // Read the data char buf[32] = {}; @@ -925,8 +928,8 @@ struct tcp_socket_test test::make_socket_pair(ioc); auto task = [](tcp_socket& a, tcp_socket& b) -> capy::task<> { - // Shutdown receive on b - b.shutdown(tcp_socket::shutdown_receive); + // Shutdown receive on b (unqualified; see GCC 11 note above). + b.shutdown(shutdown_receive); // b can still send (void)co_await b.write_some(capy::const_buffer("from_b", 6)); @@ -949,10 +952,11 @@ struct tcp_socket_test io_context ioc(Backend); tcp_socket sock(ioc); - // Shutdown on closed tcp_socket should not crash - sock.shutdown(tcp_socket::shutdown_send); - sock.shutdown(tcp_socket::shutdown_receive); - sock.shutdown(tcp_socket::shutdown_both); + // Shutdown on closed tcp_socket should not crash. + // Unqualified enumerators; see GCC 11 note above. + sock.shutdown(shutdown_send); + sock.shutdown(shutdown_receive); + sock.shutdown(shutdown_both); } void testShutdownBothSendDirection() @@ -962,9 +966,9 @@ struct tcp_socket_test test::make_socket_pair(ioc); auto task = [](tcp_socket& a, tcp_socket& b) -> capy::task<> { - // Write data then shutdown both + // Write data then shutdown both (unqualified; see GCC 11 note above). (void)co_await a.write_some(capy::const_buffer("goodbye", 7)); - a.shutdown(tcp_socket::shutdown_both); + a.shutdown(shutdown_both); // Peer should receive the data char buf[32] = {}; diff --git a/test/unit/udp_socket.cpp b/test/unit/udp_socket.cpp index 7d550d926..04783c371 100644 --- a/test/unit/udp_socket.cpp +++ b/test/unit/udp_socket.cpp @@ -811,6 +811,92 @@ struct udp_socket_test ioc.run(); } + void testZeroLengthDatagram() + { + // A zero-length datagram is a legitimate UDP event and must + // NOT be reported as EOF (cond::eof). Regression test for the + // reactor backends, where reactor_dgram_recv_op previously + // inherited complete_io_op's stream-style "is_read_operation + // && bytes==0 -> EOF" mapping. IOCP was unaffected (its UDP + // recv ops never set is_read_), but this test runs across all + // backends to lock the contract in place. + // + // The recv and send run in parallel coroutines so the recv + // hits the reactor-parked path (EAGAIN -> register -> wake) + // rather than completing inline. The inline path bypasses + // complete_io_op and wouldn't exercise the bug. + io_context ioc(Backend); + + udp_socket a(ioc); + udp_socket b(ioc); + + // Bind both so connect(ep) works without auto-open picking an + // ephemeral that's hard to discover before the connect completes. + a.open(); + b.open(); + BOOST_TEST_EQ(a.bind(endpoint(ipv4_address::loopback(), 0)), + std::error_code{}); + BOOST_TEST_EQ(b.bind(endpoint(ipv4_address::loopback(), 0)), + std::error_code{}); + auto a_ep = a.local_endpoint(); + auto b_ep = b.local_endpoint(); + + // Connect both directions synchronously before running the + // parallel send/recv tasks. + auto ex = ioc.get_executor(); + capy::run_async(ex)( + [](udp_socket& s, endpoint dest) -> capy::task<> { + auto [ec] = co_await s.connect(dest); + BOOST_TEST_EQ(ec, std::error_code{}); + }(a, b_ep)); + capy::run_async(ex)( + [](udp_socket& s, endpoint dest) -> capy::task<> { + auto [ec] = co_await s.connect(dest); + BOOST_TEST_EQ(ec, std::error_code{}); + }(b, a_ep)); + ioc.run(); + ioc.restart(); + + char buf[64] = {'\xCC'}; + std::error_code send_ec, recv_ec; + std::size_t sent = 0, recvd = 0; + bool send_done = false, recv_done = false; + + capy::run_async(ex)( + [](udp_socket& s, std::error_code& ec_out, + std::size_t& n_out, bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.send(capy::const_buffer(nullptr, 0)); + ec_out = ec; + n_out = n; + done = true; + }(a, send_ec, sent, send_done)); + + capy::run_async(ex)( + [](udp_socket& s, char* data, std::size_t len, + std::error_code& ec_out, std::size_t& n_out, + bool& done) -> capy::task<> { + auto [ec, n] = + co_await s.recv(capy::mutable_buffer(data, len)); + ec_out = ec; + n_out = n; + done = true; + }(b, buf, sizeof(buf), recv_ec, recvd, recv_done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST_EQ(send_done, true); + BOOST_TEST_EQ(!send_ec, true); + BOOST_TEST_EQ(sent, 0u); + + BOOST_TEST_EQ(recv_done, true); + // Critical: zero-length datagram is success, not EOF. + BOOST_TEST_EQ(!recv_ec, true); + BOOST_TEST(recv_ec != capy::cond::eof); + BOOST_TEST_EQ(recvd, 0u); + } + void testCancelConnectedRecv() { io_context ioc(Backend); @@ -939,6 +1025,7 @@ struct udp_socket_test testConnectAutoOpen(); testSendRecvConnected(); testSendRecvConnectedV6(); + testZeroLengthDatagram(); testCancelConnectedRecv(); testMulticastLoopHops(); testMulticastLoopHopsV6();