From 6bcaf93ceaa3a36b055dd99e7a5d05c0320c298f Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 23 Jul 2026 21:51:10 +0200 Subject: [PATCH] docs: compile every documentation code block Replace all ~375 hand-typed [source,cpp] blocks across the 30 doc pages with includes of compiled sources, mirroring Boost.Capy, so an example that breaks can no longer render on the site. - test/doc/snippets: one TU per page; fragments compile and run as test suites, exercising loopback socket pairs, ephemeral ports, in-process signals, real timers, temp-path file I/O, and genuine error paths (connection refused, FIN, RST, cancellation, timeout) - test/doc/programs: complete programs; servers that would run forever are compiled and linked but not executed (run_examples.py covers the real examples' runtime behavior) - tutorial pages excerpt the canonical example programs via tag regions; tls_context_examples.cpp joins the example build so its excerpts cannot rot - page preambles ("Code snippets assume") render from the tagged tops of the TUs that compile them, so stated assumptions stay honest - @antora/collector-extension, registered in the library's own playbook, scans example/ and test/doc into the Antora examples family; pages use include::example$...[tag=...] - blocks that intentionally do not compile are explicit: role=pseudocode (6) and role=external (2) - snippet_check.py is retired: extraction-based checking is obsolete when the docs render from sources the test suite compiles and runs --- .github/workflows/ci.yml | 2 +- doc/antora.yml | 8 + doc/local-playbook.yml | 1 + .../pages/3.tutorials/3a.echo-server.adoc | 120 +-- .../pages/3.tutorials/3b.http-client.adoc | 175 +---- .../ROOT/pages/3.tutorials/3c.dns-lookup.adoc | 120 +-- .../pages/3.tutorials/3d.tls-context.adoc | 252 ++---- .../pages/3.tutorials/3e.hash-server.adoc | 106 +-- .../ROOT/pages/3.tutorials/3f.reconnect.adoc | 172 +---- .../ROOT/pages/4.guide/4a.tcp-networking.adoc | 17 +- .../4.guide/4b.concurrent-programming.adoc | 138 +--- .../ROOT/pages/4.guide/4c.io-context.adoc | 104 +-- .../ROOT/pages/4.guide/4c2.configuration.adoc | 21 +- .../ROOT/pages/4.guide/4d.sockets.adoc | 144 +--- .../ROOT/pages/4.guide/4e.tcp-acceptor.adoc | 129 +--- .../ROOT/pages/4.guide/4f.endpoints.adoc | 114 +-- .../pages/4.guide/4g.composed-operations.adoc | 179 +---- doc/modules/ROOT/pages/4.guide/4h.timers.adoc | 65 +- .../ROOT/pages/4.guide/4i.signals.adoc | 174 +---- .../ROOT/pages/4.guide/4j.resolver.adoc | 161 +--- .../ROOT/pages/4.guide/4k.tcp-server.adoc | 240 +----- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 342 +-------- .../ROOT/pages/4.guide/4m.error-handling.adoc | 151 +--- .../ROOT/pages/4.guide/4n.buffers.adoc | 167 +--- .../ROOT/pages/4.guide/4o.file-io.adoc | 88 +-- .../ROOT/pages/4.guide/4p.unix-sockets.adoc | 96 +-- doc/modules/ROOT/pages/4.guide/4q.udp.adoc | 98 +-- doc/modules/ROOT/pages/4.guide/4r.wait.adoc | 38 +- .../ROOT/pages/5.testing/5a.mocket.adoc | 62 +- .../ROOT/pages/5.testing/5b.socket-pair.adoc | 36 +- .../ROOT/pages/5.testing/5c.patterns.adoc | 70 +- doc/modules/ROOT/pages/index.adoc | 46 +- doc/modules/ROOT/pages/quick-start.adoc | 340 +++----- doc/package-lock.json | 28 +- doc/package.json | 1 + example/CMakeLists.txt | 7 +- example/client/http_client.cpp | 31 +- example/echo-server/echo_server.cpp | 40 +- example/hash-server/hash_server.cpp | 14 + example/https-client/https_client.cpp | 26 +- example/nslookup/nslookup.cpp | 8 + example/reconnect/reconnect.cpp | 12 + example/tls_context_examples.cpp | 11 + test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/doc/CMakeLists.txt | 76 ++ test/doc/Jamfile | 43 ++ test/doc/README.md | 62 ++ test/doc/WriteProgramTest.cmake | 15 + test/doc/programs/4c_io_context_typical.cpp | 42 + test/doc/programs/4k_tcp_server_echo.cpp | 109 +++ test/doc/programs/index_page_connect.cpp | 53 ++ test/doc/programs/quick_start_context.cpp | 26 + test/doc/programs/quick_start_echo.cpp | 124 +++ test/doc/run_examples.py | 167 ++++ test/doc/snippets/3a_echo_server.cpp | 230 ++++++ test/doc/snippets/3b_http_client.cpp | 131 ++++ test/doc/snippets/3c_dns_lookup.cpp | 187 +++++ test/doc/snippets/3d_tls_context.cpp | 488 ++++++++++++ test/doc/snippets/3f_reconnect.cpp | 109 +++ test/doc/snippets/4a_tcp_networking.cpp | 151 ++++ .../snippets/4b_concurrent_programming.cpp | 436 +++++++++++ test/doc/snippets/4c2_configuration.cpp | 105 +++ test/doc/snippets/4c_io_context.cpp | 332 ++++++++ test/doc/snippets/4d_sockets.cpp | 570 ++++++++++++++ test/doc/snippets/4e_tcp_acceptor.cpp | 425 ++++++++++ test/doc/snippets/4f_endpoints.cpp | 362 +++++++++ test/doc/snippets/4g_composed_operations.cpp | 416 ++++++++++ test/doc/snippets/4h_timers.cpp | 222 ++++++ test/doc/snippets/4i_signals.cpp | 407 ++++++++++ test/doc/snippets/4j_resolver.cpp | 400 ++++++++++ test/doc/snippets/4k_tcp_server.cpp | 316 ++++++++ test/doc/snippets/4l_tls.cpp | 668 ++++++++++++++++ test/doc/snippets/4m_error_handling.cpp | 723 ++++++++++++++++++ test/doc/snippets/4n_buffers.cpp | 606 +++++++++++++++ test/doc/snippets/4o_file_io.cpp | 400 ++++++++++ test/doc/snippets/4p_unix_sockets.cpp | 357 +++++++++ test/doc/snippets/4q_udp.cpp | 408 ++++++++++ test/doc/snippets/4r_wait.cpp | 231 ++++++ test/doc/snippets/5a_mocket.cpp | 249 ++++++ test/doc/snippets/5b_socket_pair.cpp | 131 ++++ test/doc/snippets/5c_patterns.cpp | 235 ++++++ test/doc/snippets/index_page.cpp | 38 + test/doc/snippets/quick_start.cpp | 87 +++ 84 files changed, 10885 insertions(+), 3408 deletions(-) create mode 100644 test/doc/CMakeLists.txt create mode 100644 test/doc/Jamfile create mode 100644 test/doc/README.md create mode 100644 test/doc/WriteProgramTest.cmake create mode 100644 test/doc/programs/4c_io_context_typical.cpp create mode 100644 test/doc/programs/4k_tcp_server_echo.cpp create mode 100644 test/doc/programs/index_page_connect.cpp create mode 100644 test/doc/programs/quick_start_context.cpp create mode 100644 test/doc/programs/quick_start_echo.cpp create mode 100644 test/doc/run_examples.py create mode 100644 test/doc/snippets/3a_echo_server.cpp create mode 100644 test/doc/snippets/3b_http_client.cpp create mode 100644 test/doc/snippets/3c_dns_lookup.cpp create mode 100644 test/doc/snippets/3d_tls_context.cpp create mode 100644 test/doc/snippets/3f_reconnect.cpp create mode 100644 test/doc/snippets/4a_tcp_networking.cpp create mode 100644 test/doc/snippets/4b_concurrent_programming.cpp create mode 100644 test/doc/snippets/4c2_configuration.cpp create mode 100644 test/doc/snippets/4c_io_context.cpp create mode 100644 test/doc/snippets/4d_sockets.cpp create mode 100644 test/doc/snippets/4e_tcp_acceptor.cpp create mode 100644 test/doc/snippets/4f_endpoints.cpp create mode 100644 test/doc/snippets/4g_composed_operations.cpp create mode 100644 test/doc/snippets/4h_timers.cpp create mode 100644 test/doc/snippets/4i_signals.cpp create mode 100644 test/doc/snippets/4j_resolver.cpp create mode 100644 test/doc/snippets/4k_tcp_server.cpp create mode 100644 test/doc/snippets/4l_tls.cpp create mode 100644 test/doc/snippets/4m_error_handling.cpp create mode 100644 test/doc/snippets/4n_buffers.cpp create mode 100644 test/doc/snippets/4o_file_io.cpp create mode 100644 test/doc/snippets/4p_unix_sockets.cpp create mode 100644 test/doc/snippets/4q_udp.cpp create mode 100644 test/doc/snippets/4r_wait.cpp create mode 100644 test/doc/snippets/5a_mocket.cpp create mode 100644 test/doc/snippets/5b_socket_pair.cpp create mode 100644 test/doc/snippets/5c_patterns.cpp create mode 100644 test/doc/snippets/index_page.cpp create mode 100644 test/doc/snippets/quick_start.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1260ca51..f1bca8054 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -516,7 +516,7 @@ jobs: generator: ${{ matrix.generator }} generator-toolset: ${{ matrix.generator-toolset }} build-type: ${{ matrix.build-type }} - build-target: boost_corosio_tests + build-target: tests run-tests: true cxxstd: ${{ matrix.cmake-cxxstd || matrix.cxxstd }} cc: ${{ steps.setup-cpp.outputs.cc || matrix.cc }} diff --git a/doc/antora.yml b/doc/antora.yml index fcedd3aa1..db9aedbf7 100644 --- a/doc/antora.yml +++ b/doc/antora.yml @@ -18,6 +18,14 @@ asciidoc: nav: - modules/ROOT/nav.adoc ext: + collector: + - scan: + - dir: example + into: modules/ROOT/examples + - dir: test/doc/snippets + into: modules/ROOT/examples/snippets + - dir: test/doc/programs + into: modules/ROOT/examples/programs cpp-reference: config: doc/mrdocs.yml cpp-tagfiles: diff --git a/doc/local-playbook.yml b/doc/local-playbook.yml index 208b0718b..74fef7b9b 100644 --- a/doc/local-playbook.yml +++ b/doc/local-playbook.yml @@ -19,6 +19,7 @@ ui: antora: extensions: + - require: '@antora/collector-extension' - require: '@cppalliance/antora-cpp-tagfiles-extension' cpp-tagfiles: using-namespaces: diff --git a/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc index 4df95dcd1..beb6a7668 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3a.echo-server.adoc @@ -18,13 +18,7 @@ pattern. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$echo-server/echo_server.cpp[tag=assume] ---- ==== @@ -51,37 +45,11 @@ This avoids allocation during operation and limits resource usage. == Worker Implementation -Workers derive from `worker_base` and implement two methods: +Workers derive from `tcp_server::worker_base` and implement two methods: [source,cpp] ---- -class echo_server : public corosio::tcp_server -{ - class worker : public worker_base - { - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - char buf_[4096]; - - public: - explicit worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - } - - corosio::tcp_socket& socket() override - { - return sock_; - } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } - - capy::task<> do_session(); - }; +include::example$echo-server/echo_server.cpp[tag=worker_class] ---- Each worker: @@ -97,22 +65,7 @@ The session coroutine handles one connection: [source,cpp] ---- -capy::task<> echo_server::worker::do_session() -{ - for (;;) - { - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf_, sizeof buf_)); - - auto [wec, wn] = co_await capy::write( - sock_, capy::const_buffer(buf_, n)); - - if (wec || ec) - break; - } - - sock_.close(); -} +include::example$echo-server/echo_server.cpp[tag=session] ---- Notice: @@ -130,23 +83,7 @@ A helper builds the worker pool, then the constructor hands it to [source,cpp] ---- - static std::vector> - make_workers(corosio::io_context& ctx, int n) - { - std::vector> v; - v.reserve(n); - for (int i = 0; i < n; ++i) - v.push_back(std::make_unique(ctx)); - return v; - } - -public: - echo_server(corosio::io_context& ctx, int max_workers) - : tcp_server(ctx, ctx.get_executor()) - { - set_workers(make_workers(ctx, max_workers)); - } -}; +include::example$echo-server/echo_server.cpp[tag=server] ---- Workers are owned polymorphically through `std::unique_ptr`, so @@ -156,34 +93,7 @@ Workers are owned polymorphically through `std::unique_ptr`, so [source,cpp] ---- -int main(int argc, char* argv[]) -{ - if (argc != 3) - { - std::cerr << "Usage: echo_server \n"; - return 1; - } - - auto port = static_cast(std::atoi(argv[1])); - int max_workers = std::atoi(argv[2]); - - corosio::io_context ioc; - - echo_server server(ioc, max_workers); - - auto ec = server.bind(corosio::endpoint(port)); - if (ec) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - return 1; - } - - std::cout << "Echo server listening on port " << port - << " with " << max_workers << " workers\n"; - - server.start(); - ioc.run(); -} +include::example$echo-server/echo_server.cpp[tag=main] ---- == Key Design Decisions @@ -208,11 +118,7 @@ The `capy::write()` free function ensures all data is sent: [source,cpp] ---- -// write_some: may write partial data -auto [ec, n] = co_await sock.write_some(buf); // n might be < buf.size() - -// write: writes all data or fails -auto [ec, n] = co_await capy::write(sock, buf); // n == buf.size() or error +include::example$snippets/3a_echo_server.cpp[tag=composed_write,indent=0] ---- For echo servers, we want complete message delivery. @@ -224,22 +130,14 @@ with advance-then-check, we always act on `n` before inspecting `ec`: [source,cpp] ---- -auto [ec, n] = co_await sock.read_some(buf); -auto [wec, wn] = co_await capy::write(sock, const_buffer(buf.data(), n)); -if (wec || ec) - break; // Normal termination path +include::example$snippets/3a_echo_server.cpp[tag=advance_then_check,indent=0] ---- With exceptions, EOF would require a try-catch: [source,cpp] ---- -try { - auto [ec, n] = co_await sock.read_some(buf); - if (ec) throw std::system_error(ec); -} catch (...) { - // EOF is an exception here -} +include::example$snippets/3a_echo_server.cpp[tag=exceptions_eof,indent=0] ---- == Testing diff --git a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc index 39f58c4ec..f8790c587 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3b.http-client.adoc @@ -18,18 +18,7 @@ composed I/O operations, and the exception-based error handling pattern. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$client/http_client.cpp[tag=assume] ---- ==== @@ -52,13 +41,7 @@ HTTP/1.1 requests have a simple text format: [source,cpp] ---- -std::string build_request(std::string_view host) -{ - return "GET / HTTP/1.1\r\n" - "Host: " + std::string(host) + "\r\n" - "Connection: close\r\n" - "\r\n"; -} +include::example$client/http_client.cpp[tag=build_request] ---- The `Connection: close` header tells the server to close the connection @@ -69,56 +52,23 @@ marks the end of the response. [source,cpp] ---- -capy::task do_request( - corosio::io_stream& stream, - std::string_view host) -{ - // Build and send the request - std::string request = build_request(host); - if (auto [ec, n] = co_await capy::write( - stream, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - // Read the entire response - std::string response; - auto [ec, n] = co_await capy::read( - stream, capy::string_dynamic_buffer(&response)); - - // Reading into a dynamic buffer completes with success at EOF - if (ec && ec != capy::cond::eof) - throw std::system_error(ec); - - std::cout << response << std::endl; -} +include::example$client/http_client.cpp[tag=do_request] ---- Key points: * The write throws if writing fails -* `capy::read(stream, capy::string_dynamic_buffer(&response))` reads until EOF -* Reading into a dynamic buffer completes with *success* at end-of-stream — it - returns no error and reports the total bytes read. The `!= capy::cond::eof` - check is a harmless defensive guard, not the thing that signals - end-of-response +* Each `capy::read` fills the chunk completely unless the stream ends or + fails first; the loop appends whatever arrived and goes around again +* EOF (`capy::error::eof`) is the expected way the loop ends: the server + closes the connection after the response, so it is not treated as a + failure == The Connection Coroutine [source,cpp] ---- -capy::task run_client( - corosio::io_context& ioc, - corosio::ipv4_address addr, - std::uint16_t port) -{ - corosio::tcp_socket s(ioc); - s.open(); - - // Connect (throws on error) - if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec) - throw std::system_error(ec); - - co_await do_request(s, addr.to_string()); -} +include::example$client/http_client.cpp[tag=run_client] ---- The socket must be opened before connecting. We pass the socket as an @@ -130,50 +80,25 @@ shown below. [source,cpp] ---- -int main(int argc, char* argv[]) -{ - if (argc != 3) - { - std::cerr << "Usage: http_client \n" - << "Example: http_client 35.190.118.110 80\n"; - return 1; - } - - // Parse IP address - corosio::ipv4_address addr; - if (auto ec = corosio::parse_ipv4_address(argv[1], addr); ec) - { - std::cerr << "Invalid IP address: " << argv[1] << "\n"; - return 1; - } - - auto port = static_cast(std::atoi(argv[2])); - - corosio::io_context ioc; - capy::run_async(ioc.get_executor())( - run_client(ioc, addr, port)); - ioc.run(); -} +include::example$client/http_client.cpp[tag=main] ---- == Reading Until EOF -Wrapping a `std::string` in `capy::string_dynamic_buffer` lets -`capy::read` grow it as data arrives, reading until EOF: +Reading a fixed-size chunk at a time and appending to a `std::string` +accumulates the whole response, however large it is: [source,cpp] ---- -std::string response; -auto [ec, n] = co_await capy::read( - stream, capy::string_dynamic_buffer(&response)); +include::example$client/http_client.cpp[tag=read_loop,indent=0] ---- This: -* Automatically grows the string as needed -* Completes with *success* (no error) when the connection closes, since the - dynamic-buffer read treats EOF as the natural end of the stream -* Returns the total bytes read in `n` +* Grows `response` by whatever each read delivers +* Ends when the connection closes: `capy::read` reports + `capy::error::eof` once the server has sent everything +* Treats any other error as a genuine failure and throws == Error vs. Exception Patterns @@ -186,20 +111,14 @@ Compare structured bindings: [source,cpp] ---- -auto [ec] = co_await s.connect(ep); -if (ec) -{ - std::cerr << "Connect failed: " << ec.message() << "\n"; - co_return; -} +include::example$snippets/3b_http_client.cpp[tag=error_bindings,indent=0] ---- With exceptions: [source,cpp] ---- -if (auto [ec] = co_await s.connect(ep); ec) // Throw on error - throw std::system_error(ec); +include::example$snippets/3b_http_client.cpp[tag=error_exceptions,indent=0] ---- Both are valid. Use exceptions when errors are exceptional; use structured @@ -238,59 +157,9 @@ A `wolfssl_stream` is not an `io_stream`, so it needs its own [source,cpp] ---- -#include - -capy::task do_request( - corosio::tls_stream& stream, - std::string_view host) -{ - std::string request = build_request(host); - if (auto [ec, n] = co_await capy::write( - stream, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - std::string response; - auto [ec, n] = co_await capy::read( - stream, capy::string_dynamic_buffer(&response)); - // As with the plain stream, the dynamic-buffer read completes with - // success at EOF; this check is a defensive guard. - if (ec && ec != capy::cond::eof) - throw std::system_error(ec); - - std::cout << response << std::endl; -} - -capy::task run_https_client( - corosio::io_context& ioc, - corosio::ipv4_address addr, - std::uint16_t port, - std::string_view hostname) -{ - corosio::tcp_socket s(ioc); - s.open(); - - if (auto [ec] = co_await s.connect(corosio::endpoint(addr, port)); ec) - throw std::system_error(ec); - - // Configure the TLS context - corosio::tls_context ctx; - if (auto ec = ctx.set_default_verify_paths(); ec) - throw std::system_error(ec); - if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec) - throw std::system_error(ec); - - // Wrap the connected socket without taking ownership (pointer form) - corosio::wolfssl_stream secure(&s, ctx); - secure.set_hostname(hostname); - if (auto [ec] = co_await secure.handshake( - corosio::tls_role::client); ec) - throw std::system_error(ec); - - co_await do_request(secure, hostname); - - if (auto [ec] = co_await secure.shutdown(); ec) - throw std::system_error(ec); -} +include::example$https-client/https_client.cpp[tag=tls_include] + +include::example$https-client/https_client.cpp[tag=tls_client] ---- The TLS overload mirrors the plain one: `capy::read` and `capy::write` diff --git a/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc index 0f9e9d102..1fb24a74f 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3c.dns-lookup.adoc @@ -18,12 +18,7 @@ addresses. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$nslookup/nslookup.cpp[tag=assume] ---- ==== @@ -34,8 +29,7 @@ addresses. The `resolver` class performs this asynchronously: [source,cpp] ---- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "https"); +include::example$snippets/3c_dns_lookup.cpp[tag=resolver_overview,indent=0] ---- The second argument is the service name (or port number as a string). It @@ -45,42 +39,7 @@ determines the port in the returned endpoints. [source,cpp] ---- -capy::task do_lookup( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - - auto [ec, results] = co_await r.resolve(host, service); - if (ec) - { - std::cerr << "Resolve failed: " << ec.message() << "\n"; - co_return; - } - - std::cout << "Results for " << host; - if (!service.empty()) - std::cout << ":" << service; - std::cout << "\n"; - - for (auto const& entry : results) - { - auto ep = entry.get_endpoint(); - if (ep.is_v4()) - { - std::cout << " IPv4: " << ep.v4_address().to_string() - << ":" << ep.port() << "\n"; - } - else - { - std::cout << " IPv6: " << ep.v6_address().to_string() - << ":" << ep.port() << "\n"; - } - } - - std::cout << "\nTotal: " << results.size() << " addresses\n"; -} +include::example$nslookup/nslookup.cpp[tag=lookup] ---- == Understanding Results @@ -97,45 +56,14 @@ The `endpoint` class supports both IPv4 and IPv6: [source,cpp] ---- -auto ep = entry.get_endpoint(); - -if (ep.is_v4()) -{ - // IPv4 address - corosio::ipv4_address addr = ep.v4_address(); -} -else -{ - // IPv6 address - corosio::ipv6_address addr = ep.v6_address(); -} - -std::uint16_t port = ep.port(); +include::example$snippets/3c_dns_lookup.cpp[tag=entry_endpoint,indent=0] ---- == Main Function [source,cpp] ---- -int main(int argc, char* argv[]) -{ - if (argc < 2 || argc > 3) - { - std::cerr << "Usage: nslookup [service]\n" - << "Examples:\n" - << " nslookup www.google.com\n" - << " nslookup www.google.com https\n" - << " nslookup localhost 8080\n"; - return 1; - } - - std::string_view host = argv[1]; - std::string_view service = (argc == 3) ? argv[2] : ""; - - corosio::io_context ioc; - capy::run_async(ioc.get_executor())(do_lookup(ioc, host, service)); - ioc.run(); -} +include::example$nslookup/nslookup.cpp[tag=main] ---- == Resolver Flags @@ -144,10 +72,7 @@ The resolver accepts optional flags to control behavior: [source,cpp] ---- -auto [ec, results] = co_await r.resolve( - host, service, - corosio::resolve_flags::numeric_host | - corosio::resolve_flags::numeric_service); +include::example$snippets/3c_dns_lookup.cpp[tag=resolve_with_flags,indent=0] ---- Available flags: @@ -181,34 +106,7 @@ After resolving, iterate through results to find a working connection: [source,cpp] ---- -capy::task connect_to_host( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(host, service); - if (resolve_ec) - throw std::system_error(resolve_ec); - - corosio::tcp_socket sock(ioc); - sock.open(); - - // Try each address until one works - std::error_code last_ec; - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry.get_endpoint()); - if (!ec) - { - std::cout << "Connected to " << host << "\n"; - co_return; - } - last_ec = ec; - } - - throw std::system_error(last_ec, "all addresses failed"); -} +include::example$snippets/3c_dns_lookup.cpp[tag=connect_to_host] ---- == Running the Lookup Tool @@ -238,14 +136,14 @@ Resolver operations support cancellation via `std::stop_token`: [source,cpp] ---- -r.cancel(); // Cancel pending operation +include::example$snippets/3c_dns_lookup.cpp[tag=resolver_cancel,indent=0] ---- A cancelled resolution completes with `capy::cond::canceled` (the underlying value is `capy::error::canceled`), so test for it with `ec == capy::cond::canceled`. -Or through the affine awaitable protocol when using `capy::jcancellable_task`. +Or through the affine awaitable protocol, which propagates the stop token from `capy::run_async` to every awaitable in the chain. == Next Steps diff --git a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index fc35802d2..6c179c23c 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -19,10 +19,7 @@ TLS connections are established and verified. Code snippets assume: [source,cpp] ---- -#include - -namespace corosio = boost::corosio; -using namespace boost::corosio; +include::example$snippets/3d_tls_context.cpp[tag=assume] ---- ==== @@ -59,15 +56,7 @@ them across multiple TLS streams. [source,cpp] ---- -// Create a default context -tls_context ctx; - -// Copy shares the same underlying state -tls_context ctx2 = ctx; // ctx and ctx2 share state - -// Move transfers ownership -tls_context ctx3 = std::move( ctx ); -// ctx is now empty +include::example$snippets/3d_tls_context.cpp[tag=construction,indent=0] ---- The default constructor creates a context ready for TLS 1.2 and TLS 1.3 @@ -80,20 +69,7 @@ Most applications follow this pattern: [source,cpp] ---- -tls_context ctx; - -// 1. Load credentials (for servers, or clients using client certs) -ctx.use_certificate_chain_file( "server.crt" ); -ctx.use_private_key_file( "server.key", tls_file_format::pem ); - -// 2. Configure trust anchors (for verifying peer certificates) -ctx.set_default_verify_paths(); // Use system CA store - -// 3. Set verification mode -ctx.set_verify_mode( tls_verify_mode::peer ); - -// 4. Configure protocol options (optional) -ctx.set_min_protocol_version( tls_version::tls_1_2 ); +include::example$snippets/3d_tls_context.cpp[tag=typical_setup,indent=0] ---- == Credential Loading @@ -109,19 +85,14 @@ from separate PEM files: [source,cpp] ---- -// Load certificate chain (leaf + intermediates) -ctx.use_certificate_chain_file( "fullchain.pem" ); - -// Load the matching private key -ctx.use_private_key_file( "privkey.key", tls_file_format::pem ); +include::example$snippets/3d_tls_context.cpp[tag=load_separate,indent=0] ---- For a single certificate without intermediates: [source,cpp] ---- -ctx.use_certificate_file( "server.crt", tls_file_format::pem ); -ctx.use_private_key_file( "server.key", tls_file_format::pem ); +include::example$snippets/3d_tls_context.cpp[tag=load_single,indent=0] ---- === Loading from PKCS#12 Bundles @@ -131,7 +102,7 @@ into a single password-protected file: [source,cpp] ---- -ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ); +include::example$snippets/3d_tls_context.cpp[tag=pkcs12_file,indent=0] ---- NOTE: The bundle is decoded when the first stream is created; a wrong @@ -146,11 +117,7 @@ manager), use the non-file variants: [source,cpp] ---- -std::string cert_pem = fetch_certificate_from_vault(); -std::string key_pem = fetch_key_from_vault(); - -ctx.use_certificate_chain( cert_pem ); -ctx.use_private_key( key_pem, tls_file_format::pem ); +include::example$snippets/3d_tls_context.cpp[tag=load_memory,indent=0] ---- === DER Format @@ -159,8 +126,7 @@ For binary DER-encoded files (common in embedded systems): [source,cpp] ---- -ctx.use_certificate_file( "server.der", tls_file_format::der ); -ctx.use_private_key_file( "server.key.der", tls_file_format::der ); +include::example$snippets/3d_tls_context.cpp[tag=der_files,indent=0] ---- === Encrypted Private Keys @@ -179,7 +145,7 @@ For HTTPS clients connecting to public servers, use the system's CA store: [source,cpp] ---- -ctx.set_default_verify_paths(); +include::example$snippets/3d_tls_context.cpp[tag=system_trust,indent=0] ---- This uses the operating system's trusted certificates: @@ -199,8 +165,7 @@ For internal PKI or testing, load a custom CA bundle: [source,cpp] ---- -// Load CA bundle file (may contain multiple CAs) -ctx.load_verify_file( "/path/to/ca-bundle.crt" ); +include::example$snippets/3d_tls_context.cpp[tag=ca_bundle,indent=0] ---- NOTE: With OpenSSL, `load_verify_file()` registers only the *first* @@ -213,7 +178,7 @@ Add a directory of CA certificates: [source,cpp] ---- -ctx.add_verify_path( "/etc/ssl/certs" ); +include::example$snippets/3d_tls_context.cpp[tag=ca_directory,indent=0] ---- NOTE: With OpenSSL the directory must use the hashed-filename layout @@ -227,13 +192,7 @@ Add CA certificates one at a time: [source,cpp] ---- -// From memory -std::string internal_ca = load_ca_from_config(); -ctx.add_certificate_authority( internal_ca ); - -// Multiple CAs -ctx.add_certificate_authority( root_ca_pem ); -ctx.add_certificate_authority( intermediate_ca_pem ); +include::example$snippets/3d_tls_context.cpp[tag=ca_individual,indent=0] ---- === Combining Trust Sources @@ -242,11 +201,7 @@ You can combine multiple trust sources: [source,cpp] ---- -// Start with system trust store -ctx.set_default_verify_paths(); - -// Add an internal CA for corporate servers -ctx.add_certificate_authority( corporate_ca_pem ); +include::example$snippets/3d_tls_context.cpp[tag=combine_trust,indent=0] ---- NOTE: The `set_default_verify_paths()` call above contributes nothing in @@ -262,12 +217,7 @@ Set minimum and/or maximum TLS versions: [source,cpp] ---- -// Require TLS 1.2 or newer (default) -ctx.set_min_protocol_version( tls_version::tls_1_2 ); - -// Require TLS 1.3 only -ctx.set_min_protocol_version( tls_version::tls_1_3 ); -ctx.set_max_protocol_version( tls_version::tls_1_3 ); +include::example$snippets/3d_tls_context.cpp[tag=version_bounds,indent=0] ---- NOTE: On WolfSSL the ceiling is enforced by selecting a version-specific @@ -281,11 +231,7 @@ are configured separately: [source,cpp] ---- -// TLS 1.2 and below -ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ); - -// TLS 1.3 (distinct API and suite names) -ctx.set_ciphersuites_tls13( "TLS_AES_256_GCM_SHA384" ); +include::example$snippets/3d_tls_context.cpp[tag=cipher_suites,indent=0] ---- NOTE: Both backends apply these (WolfSSL merges the two into a single @@ -300,18 +246,14 @@ ALPN negotiates the application protocol over TLS. Common uses: [source,cpp] ---- -// HTTP/2 with HTTP/1.1 fallback -ctx.set_alpn( { "h2", "http/1.1" } ); - -// gRPC -ctx.set_alpn( { "h2" } ); +include::example$snippets/3d_tls_context.cpp[tag=alpn_offer,indent=0] ---- Read the negotiated protocol from the stream after the handshake: [source,cpp] ---- -std::string_view proto = stream.alpn_protocol(); // "h2", or empty if none +include::example$snippets/3d_tls_context.cpp[tag=alpn_read,indent=0] ---- NOTE: On WolfSSL, ALPN requires a `HAVE_ALPN` build; without it, offering @@ -328,14 +270,7 @@ Configure how peer certificates are verified during the TLS handshake. [source,cpp] ---- -// Don't verify peer (not recommended for production) -ctx.set_verify_mode( tls_verify_mode::none ); - -// Verify peer if certificate is presented -ctx.set_verify_mode( tls_verify_mode::peer ); - -// Require and verify peer certificate (mTLS server-side) -ctx.set_verify_mode( tls_verify_mode::require_peer ); +include::example$snippets/3d_tls_context.cpp[tag=verify_modes,indent=0] ---- Typical usage: @@ -350,7 +285,7 @@ For clients, set the expected server hostname on the stream before the handshake [source,cpp] ---- -secure.set_hostname( "api.example.com" ); +include::example$snippets/3d_tls_context.cpp[tag=set_hostname,indent=0] ---- This enables: @@ -365,8 +300,7 @@ Limit how many intermediate certificates are allowed: [source,cpp] ---- -// Allow up to 3 intermediates (leaf -> 3 intermediates -> root) -ctx.set_verify_depth( 3 ); +include::example$snippets/3d_tls_context.cpp[tag=verify_depth,indent=0] ---- The default (typically 100) is sufficient for most chains. @@ -381,16 +315,7 @@ the backend's `X509_STORE_CTX*`. [source,cpp] ---- -ctx.set_verify_callback( - []( bool preverified, corosio::verify_context& verify_ctx ) -> bool - { - if( !preverified ) - return false; // chain did not verify - - auto der = verify_ctx.certificate(); // DER of the current cert - return der.size() == expected_pin.size() && - std::equal( der.begin(), der.end(), expected_pin.begin() ); - }); +include::example$snippets/3d_tls_context.cpp[tag=verify_callback,indent=0] ---- [NOTE] @@ -423,14 +348,7 @@ the policy is not `disabled`: [source,cpp] ---- -// Don't check revocation (default) -ctx.set_revocation_policy( tls_revocation_policy::disabled ); - -// Accept unknown status, reject a listed (revoked) certificate -ctx.set_revocation_policy( tls_revocation_policy::soft_fail ); - -// Also reject when status can't be determined (strict) -ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); +include::example$snippets/3d_tls_context.cpp[tag=revocation_policy,indent=0] ---- === CRL (Certificate Revocation Lists) @@ -439,14 +357,7 @@ Load CRLs from the CA that issued the certificates you're verifying: [source,cpp] ---- -// From file -ctx.add_crl_file( "/path/to/issuer.crl" ); - -// From memory (e.g., fetched via HTTP) -std::string crl_data = fetch_crl_from_url( crl_url ); -ctx.add_crl( crl_data ); - -ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); +include::example$snippets/3d_tls_context.cpp[tag=crl_load,indent=0] ---- CRLs must be refreshed periodically as they expire. @@ -469,18 +380,7 @@ is applied on OpenSSL and on WolfSSL builds with `HAVE_CRL`. [source,cpp] ---- -// Bootstrap context: for fetching revocation data -tls_context bootstrap_ctx; -bootstrap_ctx.set_default_verify_paths(); -bootstrap_ctx.set_verify_mode( tls_verify_mode::peer ); -bootstrap_ctx.set_revocation_policy( tls_revocation_policy::disabled ); - -// Hardened context: for sensitive connections -tls_context hardened_ctx; -hardened_ctx.set_default_verify_paths(); -hardened_ctx.set_verify_mode( tls_verify_mode::peer ); -hardened_ctx.add_crl_file( "cached.crl" ); -hardened_ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); +include::example$snippets/3d_tls_context.cpp[tag=bootstrap_hardened,indent=0] ---- == Password Handling @@ -492,16 +392,7 @@ Set a password callback before loading encrypted material. [source,cpp] ---- -// Set callback before loading encrypted key -ctx.set_password_callback( - []( std::size_t max_length, tls_password_purpose purpose ) - { - // purpose: for_reading (decrypt) or for_writing (encrypt) - return std::string( "my-secret-password" ); - }); - -// Now load encrypted private key -ctx.use_private_key_file( "encrypted.key", tls_file_format::pem ); +include::example$snippets/3d_tls_context.cpp[tag=password_callback,indent=0] ---- === Secure Password Handling @@ -510,16 +401,7 @@ In production, don't hardcode passwords: [source,cpp] ---- -ctx.set_password_callback( - []( std::size_t max_length, tls_password_purpose purpose ) - { - // Read from environment - if( auto* pw = std::getenv( "TLS_KEY_PASSWORD" ) ) - return std::string( pw ); - - // Or prompt user - return prompt_user_for_password(); - }); +include::example$snippets/3d_tls_context.cpp[tag=password_env,indent=0] ---- === PKCS#12 Passwords @@ -528,20 +410,27 @@ PKCS#12 files take the password directly (no callback needed): [source,cpp] ---- -ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ); +include::example$snippets/3d_tls_context.cpp[tag=pkcs12_file,indent=0] ---- For PKCS#12 loaded from memory: [source,cpp] ---- -ctx.use_pkcs12( pkcs12_data, "bundle-password" ); +include::example$snippets/3d_tls_context.cpp[tag=pkcs12_memory,indent=0] ---- == Complete Examples This section demonstrates complete, practical configurations for common -scenarios. +scenarios, excerpted from the compiled `tls_context_examples` example. +The examples share a small `must()` helper that turns a reported error +into an exception: + +[source,cpp] +---- +include::example$tls_context_examples.cpp[tag=must] +---- === HTTPS Client Context @@ -552,68 +441,30 @@ instead, for example [source,cpp] ---- -tls_context make_https_client_context() -{ - tls_context ctx; - - // Trust system CAs for public websites - ctx.set_default_verify_paths(); - - // Verify server certificates - ctx.set_verify_mode( tls_verify_mode::peer ); - - // Modern TLS only - ctx.set_min_protocol_version( tls_version::tls_1_2 ); - - return ctx; -} +include::example$tls_context_examples.cpp[tag=https_client] +---- -// Usage with TLS stream -tls_context ctx = make_https_client_context(); +To use the context, wrap a connected socket in a TLS stream, set the +expected hostname, and perform the handshake, as the `https-client` +example does: -// Pointer form wraps the connected socket without taking ownership -corosio::wolfssl_stream secure( &sock, ctx ); -secure.set_hostname( "api.example.com" ); -co_await secure.handshake( corosio::tls_role::client ); +[source,cpp] +---- +include::example$https-client/https_client.cpp[tag=tls_wrap,indent=0] ---- === TLS Server Context [source,cpp] ---- -tls_context make_server_context() -{ - tls_context ctx; - - // Load server credentials - ctx.use_certificate_chain_file( "fullchain.pem" ); - ctx.use_private_key_file( "privkey.pem", tls_file_format::pem ); - - // Don't verify client certificates (no mTLS) - ctx.set_verify_mode( tls_verify_mode::none ); - - return ctx; -} +include::example$tls_context_examples.cpp[tag=basic_server] ---- === Mutual TLS (mTLS) [source,cpp] ---- -tls_context make_mtls_client_context() -{ - tls_context ctx; - - // Client credentials for mTLS - ctx.use_certificate_chain_file( "client.crt" ); - ctx.use_private_key_file( "client.key", tls_file_format::pem ); - - // Trust specific CA for server verification - ctx.load_verify_file( "server-ca.crt" ); - ctx.set_verify_mode( tls_verify_mode::peer ); - - return ctx; -} +include::example$tls_context_examples.cpp[tag=mtls_client] ---- == Error Handling @@ -623,16 +474,7 @@ to detect errors: [source,cpp] ---- -// Throw on error -if( auto ec = ctx.use_certificate_file( "cert.pem", tls_file_format::pem ) ) - throw std::system_error(ec); - -// Check error explicitly -if( auto ec = ctx.load_verify_file( "ca.crt" ) ) -{ - std::cerr << "Failed to load CA: " << ec.message() << "\n"; - return; -} +include::example$snippets/3d_tls_context.cpp[tag=error_handling,indent=0] ---- Common errors include: diff --git a/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc b/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc index 7199b52e4..2d0dfb946 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3e.hash-server.adoc @@ -19,18 +19,7 @@ switching between them mid-coroutine with `capy::run()`. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$hash-server/hash_server.cpp[tag=assume] ---- ==== @@ -62,20 +51,7 @@ work justifies leaving the event loop. [source,cpp] ---- -capy::task -compute_fnv1a( char const* data, std::size_t len ) -{ - constexpr std::uint64_t basis = 14695981039346656037ULL; - constexpr std::uint64_t prime = 1099511628211ULL; - - std::uint64_t h = basis; - for (std::size_t i = 0; i < len; ++i) - { - h ^= static_cast( data[i] ); - h *= prime; - } - co_return h; -} +include::example$hash-server/hash_server.cpp[tag=hash_function] ---- This is a `capy::task` — a lazy coroutine that doesn't start until someone @@ -88,38 +64,7 @@ Each client connection is handled by a single coroutine: [source,cpp] ---- -capy::task<> -do_session( - corosio::tcp_socket sock, - capy::thread_pool& pool ) -{ - char buf[4096]; - - // 1. Read data from client (on io_context) - auto [ec, n] = co_await sock.read_some( - capy::mutable_buffer( buf, sizeof( buf ) ) ); - - if (ec) - { - sock.close(); - co_return; - } - - // 2. Switch to thread pool for CPU-bound hash computation, - // then automatically resume on io_context when done - auto hash = co_await capy::run( pool.get_executor() )( - compute_fnv1a( buf, n ) ); - - // 3. Send hex result back to client (on io_context) - auto result = to_hex( hash ) + "\n"; - auto [wec, wn] = co_await capy::write( - sock, - capy::const_buffer( result.data(), result.size() ) ); - (void)wec; - (void)wn; - - sock.close(); -} +include::example$hash-server/hash_server.cpp[tag=session] ---- Three things happen in sequence, but on two different executors: @@ -142,8 +87,7 @@ When you write: [source,cpp] ---- -auto hash = co_await capy::run( pool.get_executor() )( - compute_fnv1a( buf, n ) ); +include::example$hash-server/hash_server.cpp[tag=run_switch,indent=0] ---- Behind the scenes: @@ -169,23 +113,7 @@ The accept loop creates a socket per connection and spawns a session: [source,cpp] ---- -capy::task<> -do_accept( - corosio::io_context& ioc, - corosio::tcp_acceptor& acc, - capy::thread_pool& pool ) -{ - for (;;) - { - corosio::tcp_socket peer( ioc ); - auto [ec] = co_await acc.accept( peer ); - if (ec) - break; - - capy::run_async( ioc.get_executor() )( - do_session( std::move( peer ), pool ) ); - } -} +include::example$hash-server/hash_server.cpp[tag=accept] ---- `run_async` is fire-and-forget — each session runs independently on the @@ -196,29 +124,7 @@ connection. [source,cpp] ---- -int main( int argc, char* argv[] ) -{ - if (argc != 2) - { - std::cerr << "Usage: hash_server \n"; - return 1; - } - - auto port = static_cast( std::atoi( argv[1] ) ); - - corosio::io_context ioc; - capy::thread_pool pool( 4 ); - - corosio::tcp_acceptor acc( ioc, corosio::endpoint( port ) ); - - std::cout << "Hash server listening on port " << port << "\n"; - - capy::run_async( ioc.get_executor() )( - do_accept( ioc, acc, pool ) ); - - ioc.run(); - pool.join(); -} +include::example$hash-server/hash_server.cpp[tag=main] ---- The `io_context` drives all network I/O on the main thread. The thread pool diff --git a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc index bd448a801..ce34c1120 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3f.reconnect.adoc @@ -19,20 +19,7 @@ for graceful shutdown. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$reconnect/reconnect.cpp[tag=assume] ---- ==== @@ -64,37 +51,7 @@ configured maximum: [source,cpp] ---- -struct exponential_backoff -{ - using duration = std::chrono::milliseconds; - -private: - duration initial_; - duration delay_; - duration max_; - -public: - exponential_backoff(duration initial, duration max) noexcept - : initial_(initial) - , delay_(initial) - , max_(max) - { - } - - /// Return the current delay and advance to the next. - duration next() noexcept - { - auto current = (std::min)(delay_, max_); - delay_ = (std::min)(delay_ * 2, max_); - return current; - } - - /// Restart the sequence from the initial delay. - void reset() noexcept - { - delay_ = initial_; - } -}; +include::example$reconnect/reconnect.cpp[tag=backoff] ---- With an initial delay of 500ms and a 30s cap, calling `next()` produces: @@ -110,20 +67,7 @@ Once connected, the client reads data until the peer disconnects: [source,cpp] ---- -capy::task<> -do_session(corosio::tcp_socket& sock) -{ - char buf[4096]; - for (;;) - { - auto [ec, n] = - co_await sock.read_some(capy::mutable_buffer(buf, sizeof buf)); - if (ec) - break; - std::cout.write(buf, static_cast(n)); - std::cout.flush(); - } -} +include::example$reconnect/reconnect.cpp[tag=session] ---- This is the same read loop you would find in any echo client. The interesting @@ -136,58 +80,7 @@ backoff policy for the next delay, calls `delay()`, and tries again: [source,cpp] ---- -capy::task<> -connect_with_backoff( - corosio::io_context& ioc, - corosio::endpoint ep, - exponential_backoff backoff, - int max_attempts) -{ - corosio::tcp_socket sock(ioc); - int attempt = 0; - - for (;;) - { - ++attempt; - - auto [ec] = co_await sock.connect(ep); - if (!ec) - { - std::cout << "Connected on attempt " << attempt << std::endl; - co_await do_session(sock); - - // Peer disconnected, restart the retry sequence - sock.close(); - backoff.reset(); - attempt = 0; - std::cout << "Disconnected, reconnecting..." << std::endl; - continue; - } - - sock.close(); - - std::cout << "Attempt " << attempt << " failed: " << ec.message() - << std::endl; - - if (max_attempts > 0 && attempt >= max_attempts) - { - std::cout << "Giving up after " << attempt << " attempts" - << std::endl; - co_return; - } - - auto wait_for = backoff.next(); - - std::cout << "Retrying in " << wait_for.count() << "ms" << std::endl; - - auto [delay_ec] = co_await corosio::delay(wait_for); - if (delay_ec == capy::cond::canceled) - { - std::cout << "Retry cancelled" << std::endl; - co_return; - } - } -} +include::example$reconnect/reconnect.cpp[tag=reconnect_loop] ---- There are two exit conditions: @@ -212,13 +105,7 @@ use a stop token: [source,cpp] ---- -std::stop_source stop_src; - -capy::run_async(ioc.get_executor(), stop_src.get_token())( - connect_with_backoff(ioc, ep, backoff, 10)); - -// Later, from any thread: -stop_src.request_stop(); +include::example$snippets/3f_reconnect.cpp[tag=stop_token_shutdown,indent=0] ---- When the stop source is signaled: @@ -264,54 +151,7 @@ executes the coroutine's own cleanup code. [source,cpp] ---- -int -main(int argc, char* argv[]) -{ - if (argc != 3) - { - std::cerr << "Usage: reconnect \n"; - return EXIT_FAILURE; - } - - corosio::ipv4_address addr; - if (auto ec = corosio::parse_ipv4_address(argv[1], addr); ec) - { - std::cerr << "Invalid IP address: " << argv[1] << "\n"; - return EXIT_FAILURE; - } - - auto port = static_cast(std::atoi(argv[2])); - - corosio::io_context ioc; - - using namespace std::chrono_literals; - exponential_backoff backoff(500ms, 30s); - - // The stop_source lets us cancel the coroutine gracefully from any - // thread. When signaled, pending delay and connect operations return - // cond::canceled, the coroutine's own loop breaks, and it unwinds - // through normal control flow. - // - // Contrast with io_context::stop(), which yanks the event loop out - // from under suspended coroutines without giving them a chance to - // observe cancellation. stop() is safe (pending operations are - // cleaned up during destruction), but coroutine-internal cleanup - // logic is bypassed. - std::stop_source stop_src; - - capy::run_async(ioc.get_executor(), stop_src.get_token())( - connect_with_backoff(ioc, corosio::endpoint(addr, port), backoff, 10)); - - // Run the event loop on a background thread so main - // can signal cancellation after a timeout. - std::thread worker([&ioc] { ioc.run(); }); - - std::this_thread::sleep_for(5s); - stop_src.request_stop(); - worker.join(); - - return EXIT_SUCCESS; -} +include::example$reconnect/reconnect.cpp[tag=main] ---- The event loop runs on a background thread. After five seconds the main 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 cec210845..c703d8584 100644 --- a/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc +++ b/doc/modules/ROOT/pages/4.guide/4a.tcp-networking.adoc @@ -620,14 +620,7 @@ Corosio provides the `resolver` class for asynchronous DNS lookups: [source,cpp] ---- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "https"); - -for (auto const& entry : results) -{ - auto ep = entry.get_endpoint(); - // Try connecting to ep... -} +include::example$snippets/4a_tcp_networking.cpp[tag=resolver_lookup,indent=0] ---- == Practical Considerations @@ -680,7 +673,7 @@ Corosio exposes this through `socket_option::no_delay`, set via [source,cpp] ---- -sock.set_option(corosio::socket_option::no_delay(true)); +include::example$snippets/4a_tcp_networking.cpp[tag=no_delay,indent=0] ---- === SO_REUSEADDR @@ -728,11 +721,7 @@ exact amounts: [source,cpp] ---- -// Wrong: might read less than buffer size -auto [ec, n] = co_await sock.read_some(buf); - -// Right: reads until buffer is full or EOF -auto [ec, n] = co_await capy::read(sock, buf); +include::example$snippets/4a_tcp_networking.cpp[tag=read_partial,indent=0] ---- === Connection Refused diff --git a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc index cfb213504..ecbe7118e 100644 --- a/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc +++ b/doc/modules/ROOT/pages/4.guide/4b.concurrent-programming.adoc @@ -65,12 +65,7 @@ operations: [source,cpp] ---- -int counter = 0; - -// Task 1 // Task 2 -++counter; ++counter; -// Both read 0, both write 1 -// Expected: 2, Actual: 1 (data race) +include::example$snippets/4b_concurrent_programming.cpp[tag=race_condition,indent=0] ---- The `++counter` operation isn't atomic—it reads, modifies, then writes. If two @@ -82,11 +77,7 @@ The pattern read → modify → write is a classic source of races: [source,cpp] ---- -if (resource_available) // Read -{ - resource_available = false; // Write - use_resource(); -} +include::example$snippets/4b_concurrent_programming.cpp[tag=read_modify_write,indent=0] ---- If two tasks check `resource_available` simultaneously, both may see `true` @@ -130,14 +121,7 @@ hold it at a time: [source,cpp] ---- -std::mutex m; -int counter = 0; - -void increment() -{ - std::lock_guard lock(m); - ++counter; // Safe: only one thread at a time -} +include::example$snippets/4b_concurrent_programming.cpp[tag=mutex_protection] ---- The region between lock acquisition and release is a _critical section_. @@ -146,7 +130,7 @@ The region between lock acquisition and release is a _critical section_. When tasks acquire multiple locks, they risk deadlock: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Thread 1 // Thread 2 lock(mutex_a); lock(mutex_b); @@ -179,11 +163,7 @@ An event loop processes one event at a time: [source,cpp] ---- -while (!stopped) -{ - wait_for_event(); // Blocks until I/O completes - handle_event(); // Run the handler -} +include::example$snippets/4b_concurrent_programming.cpp[tag=event_loop,indent=0] ---- Events might be: "data arrived on socket X," "timer expired," "new connection @@ -248,19 +228,7 @@ the coroutine resumes—possibly on a different thread. [source,cpp] ---- -capy::task handle_client(corosio::tcp_socket sock) -{ - char buf[1024]; - - auto [ec, n] = co_await sock.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - // Suspends here until data arrives - - if (ec) - co_return; // Exit on error - - // Process data... -} +include::example$snippets/4b_concurrent_programming.cpp[tag=suspension_points] ---- Between `co_await` and resumption, no code in this coroutine runs. Other @@ -314,8 +282,7 @@ necessarily the thread that started the operation. [source,cpp] ---- -capy::run_async(ioc.get_executor())(my_coroutine()); -// my_coroutine resumes through ioc's executor +include::example$snippets/4b_concurrent_programming.cpp[tag=executor_affinity,indent=0] ---- If `io_context::run()` is called from one thread, resumptions happen on that @@ -359,7 +326,7 @@ waits. This provides mutual exclusion without explicit locks. With mutexes, synchronization is explicit—you lock before accessing shared data. With strands, synchronization is structural—all access goes through the strand. -[source,cpp] +[source,cpp,role=external] ---- // Mutex approach: explicit locking at every access std::mutex m; @@ -401,15 +368,7 @@ sequential `co_await`s naturally serialize: [source,cpp] ---- -capy::task session(corosio::tcp_socket sock) -{ - // All code in this coroutine runs sequentially - auto [ec, n] = co_await sock.read_some(buf); - // No other code in this coroutine runs until above completes - - co_await sock.write_some(response); - // Still sequential -} +include::example$snippets/4b_concurrent_programming.cpp[tag=strand_session] ---- With a single-threaded `io_context`, coroutines sharing that executor can @@ -443,14 +402,7 @@ For CPU utilization or higher throughput: [source,cpp] ---- -corosio::io_context ioc(4); // Hint: 4 threads - -std::vector threads; -for (int i = 0; i < 4; ++i) - threads.emplace_back([&ioc] { ioc.run(); }); - -for (auto& t : threads) - t.join(); +include::example$snippets/4b_concurrent_programming.cpp[tag=multi_threaded_run,indent=0] ---- With multiple threads: @@ -483,21 +435,7 @@ The simplest pattern: each client gets a coroutine. [source,cpp] ---- -capy::task accept_loop( - corosio::io_context& ioc, - corosio::tcp_acceptor& acc) -{ - for (;;) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - if (ec) break; - - // Spawn independent coroutine for this client - capy::run_async(ioc.get_executor())( - handle_client(std::move(peer))); - } -} +include::example$snippets/4b_concurrent_programming.cpp[tag=accept_loop] ---- Each `handle_client` coroutine runs independently. The accept loop continues @@ -515,22 +453,9 @@ For bounded resource usage, use a fixed pool of workers: [source,cpp] ---- -struct worker -{ - corosio::tcp_socket sock; - std::string buf; - bool in_use = false; - - explicit worker(corosio::io_context& ioc) : sock(ioc) {} -}; +include::example$snippets/4b_concurrent_programming.cpp[tag=worker_pool] -// Preallocate workers -std::vector workers; -workers.reserve(max_workers); -for (int i = 0; i < max_workers; ++i) - workers.emplace_back(ioc); - -// Assign connections to free workers +include::example$snippets/4b_concurrent_programming.cpp[tag=worker_pool_use,indent=0] ---- Corosio's `tcp_server` class implements this pattern—see @@ -542,12 +467,7 @@ For multi-stage processing, chain coroutines: [source,cpp] ---- -capy::task pipeline(corosio::tcp_socket sock) -{ - auto message = co_await read_message(sock); - auto result = co_await process(message); - co_await write_response(sock, result); -} +include::example$snippets/4b_concurrent_programming.cpp[tag=pipeline] ---- Each stage suspends independently, allowing other coroutines to run. @@ -560,17 +480,7 @@ Never block inside a coroutine: [source,cpp] ---- -// WRONG: blocks the entire io_context -capy::task bad() -{ - std::this_thread::sleep_for(1s); // Don't do this! -} - -// RIGHT: suspend with an async delay -capy::task good() -{ - co_await corosio::delay(1s); -} +include::example$snippets/4b_concurrent_programming.cpp[tag=blocking] ---- Blocking calls (sleep, mutex lock, synchronous I/O) prevent other coroutines @@ -582,17 +492,7 @@ Spawned coroutines must not hold references to destroyed objects: [source,cpp] ---- -// WRONG: socket destroyed while coroutine runs -{ - corosio::tcp_socket sock(ioc); - capy::run_async(ex)(use_socket(sock)); // Takes reference! -} // sock destroyed here, coroutine still running - -// RIGHT: move socket into coroutine -{ - corosio::tcp_socket sock(ioc); - capy::run_async(ex)(use_socket(std::move(sock))); -} // OK, coroutine owns the socket +include::example$snippets/4b_concurrent_programming.cpp[tag=dangling_reference,indent=0] ---- A coroutine may outlive the scope that spawned it. Ensure captured data lives @@ -604,11 +504,7 @@ Don't access an object from a coroutine with different executor affinity: [source,cpp] ---- -// Dangerous: socket created on ctx1, used from ex2 -corosio::tcp_socket sock(ctx1); -capy::run_async(ex2)([&sock, ep]() -> capy::task { - co_await sock.connect(ep); // Wrong executor! -}); +include::example$snippets/4b_concurrent_programming.cpp[tag=cross_executor,indent=0] ---- Keep I/O objects with the coroutines that use them. diff --git a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc index e74dd8368..6ce8f629d 100644 --- a/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c.io-context.adoc @@ -18,8 +18,7 @@ coroutine execution. Code snippets assume: [source,cpp] ---- -#include -namespace corosio = boost::corosio; +include::example$snippets/4c_io_context.cpp[tag=assume] ---- ==== @@ -29,11 +28,7 @@ Every Corosio program needs at least one `io_context`: [source,cpp] ---- -corosio::io_context ioc; - -// ... create I/O objects and launch coroutines ... - -ioc.run(); // Process events until all work completes +include::example$snippets/4c_io_context.cpp[tag=overview,indent=0] ---- The `io_context`: @@ -49,7 +44,7 @@ The `io_context`: [source,cpp] ---- -corosio::io_context ioc; +include::example$snippets/4c_io_context.cpp[tag=construct_default,indent=0] ---- Creates an `io_context` with a concurrency hint of @@ -61,8 +56,7 @@ thread-safe locking tier. Thread safety is governed by the `locking` option [source,cpp] ---- -corosio::io_context ioc(1); // one thread expected to call run() -corosio::io_context ioc(4); // up to 4 threads expected to call run() +include::example$snippets/4c_io_context.cpp[tag=construct_hint,indent=0] ---- The concurrency hint is a performance tuning value indicating how many threads @@ -89,8 +83,7 @@ Processes all pending work until stopped: [source,cpp] ---- -std::size_t n = ioc.run(); -std::cout << "Processed " << n << " handlers\n"; +include::example$snippets/4c_io_context.cpp[tag=run,indent=0] ---- This function: @@ -106,7 +99,7 @@ Processes at most one work item: [source,cpp] ---- -std::size_t n = ioc.run_one(); // Returns 0 or 1 +include::example$snippets/4c_io_context.cpp[tag=run_one,indent=0] ---- Useful for manual event loop control or interleaving with other work. @@ -117,10 +110,7 @@ Process work for a limited time: [source,cpp] ---- -using namespace std::chrono_literals; - -auto n = ioc.run_for(100ms); // Run for 100 milliseconds -auto m = ioc.run_until(deadline); // Run until time point +include::example$snippets/4c_io_context.cpp[tag=run_for,indent=0] ---- These return the number of handlers executed within the time limit. @@ -131,8 +121,7 @@ Process ready work without blocking: [source,cpp] ---- -std::size_t n = ioc.poll(); // All ready handlers -std::size_t m = ioc.poll_one(); // At most one ready handler +include::example$snippets/4c_io_context.cpp[tag=poll,indent=0] ---- These never block waiting for I/O. Useful for games or GUI applications @@ -146,7 +135,7 @@ Signal the event loop to stop: [source,cpp] ---- -ioc.stop(); +include::example$snippets/4c_io_context.cpp[tag=stop,indent=0] ---- This causes `run()` to return as soon as possible. Pending work remains @@ -158,8 +147,7 @@ Check if the context has been stopped: [source,cpp] ---- -if (ioc.stopped()) - std::cout << "Event loop stopped\n"; +include::example$snippets/4c_io_context.cpp[tag=stopped,indent=0] ---- === restart() @@ -168,10 +156,7 @@ Reset the stopped state: [source,cpp] ---- -ioc.stop(); -// ... do something ... -ioc.restart(); -ioc.run(); // Can run again +include::example$snippets/4c_io_context.cpp[tag=restart,indent=0] ---- You must call `restart()` before calling `run()` again after it returns. @@ -182,34 +167,14 @@ The `io_context::executor_type` provides the interface for dispatching work: [source,cpp] ---- -auto ex = ioc.get_executor(); - -// Launch a coroutine -capy::run_async(ex)(my_coroutine()); - -// Access the context -corosio::io_context& ctx = ex.context(); - -// Check if running inside the event loop -if (ex.running_in_this_thread()) - std::cout << "Inside run()\n"; +include::example$snippets/4c_io_context.cpp[tag=executor,indent=0] ---- === Executor Operations [source,cpp] ---- -auto ex = ioc.get_executor(); - -// Dispatch a continuation: symmetric transfer if inside run(), -// otherwise post. Returns a handle to resume. -std::coroutine_handle<> next = ex.dispatch(cont); - -// Post a continuation: always queue for later execution -ex.post(cont); - -// Post a bare coroutine handle for later execution -ex.post(handle); +include::example$snippets/4c_io_context.cpp[tag=executor_ops,indent=0] ---- The dispatch operation `ex.dispatch(cont)` enables symmetric transfer when @@ -222,8 +187,7 @@ efficiently. [source,cpp] ---- -ex.on_work_started(); // Increment work count -ex.on_work_finished(); // Decrement work count +include::example$snippets/4c_io_context.cpp[tag=work_tracking,indent=0] ---- The event loop runs while the work count is non-zero. I/O objects and @@ -233,19 +197,7 @@ coroutines track work automatically. [source,cpp] ---- -int main() -{ - corosio::io_context ioc; - - // Create I/O objects - corosio::tcp_socket sock(ioc); - - // Launch initial coroutine - capy::run_async(ioc.get_executor())(main_coroutine(sock)); - - // Run until all work completes - ioc.run(); -} +include::example$programs/4c_io_context_typical.cpp[tag=full] ---- == Thread Safety @@ -256,14 +208,7 @@ into it. [source,cpp] ---- -corosio::io_context ioc(4); - -std::vector threads; -for (int i = 0; i < 4; ++i) - threads.emplace_back([&ioc] { ioc.run(); }); - -for (auto& t : threads) - t.join(); +include::example$snippets/4c_io_context.cpp[tag=multithreaded,indent=0] ---- Multiple threads can call `run()` concurrently. The `io_context` distributes @@ -289,15 +234,7 @@ that ran the loop, and only then destroy the context. [source,cpp] ---- -corosio::io_context ioc(4); - -std::vector threads; -for (int i = 0; i < 4; ++i) - threads.emplace_back([&ioc] { ioc.run(); }); - -// Join every run() thread before ioc leaves scope. -for (auto& t : threads) - t.join(); +include::example$snippets/4c_io_context.cpp[tag=teardown,indent=0] ---- WARNING: `stop()` abandons pending work rather than draining it. Destroying the @@ -311,12 +248,7 @@ management: [source,cpp] ---- -// Create or get a service -my_service& svc = ioc.use_service(); - -// Check if service exists -if (ioc.has_service()) - // ... +include::example$snippets/4c_io_context.cpp[tag=services,indent=0] ---- Services are destroyed when the `io_context` is destroyed. diff --git a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc index 35e8d575e..a1df49366 100644 --- a/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc +++ b/doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc @@ -20,25 +20,18 @@ enabled in multi-threaded mode. [source,cpp] ---- -#include +include::example$snippets/4c2_configuration.cpp[tag=options_basic_include] -corosio::io_context_options opts; -opts.max_events_per_poll = 256; -opts.inline_budget_max = 32; - -corosio::io_context ioc(opts); +include::example$snippets/4c2_configuration.cpp[tag=options_basic,indent=0] ---- Both `io_context` and `native_io_context` accept options: [source,cpp] ---- -#include - -corosio::io_context_options opts; -opts.max_events_per_poll = 512; +include::example$snippets/4c2_configuration.cpp[tag=options_native_include] -corosio::native_io_context ioc(opts); +include::example$snippets/4c2_configuration.cpp[tag=options_native,indent=0] ---- == Available Options @@ -174,11 +167,7 @@ independently of the `concurrency_hint` (see [source,cpp] ---- -corosio::io_context_options opts; -opts.locking = corosio::locking_mode::unsafe; - -corosio::io_context ioc(opts); -ioc.run(); // only one thread may call this +include::example$snippets/4c2_configuration.cpp[tag=locking_unsafe,indent=0] ---- WARNING: The `unsafe` and `unsafe_io` tiers impose hard restrictions. diff --git a/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc index 527eb929d..cb4df7b0a 100644 --- a/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4d.sockets.adoc @@ -18,12 +18,7 @@ management. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/4d_sockets.cpp[tag=assume] ---- ==== @@ -33,15 +28,7 @@ A socket represents one end of a TCP connection: [source,cpp] ---- -corosio::tcp_socket s(ioc); -s.open(); - -auto [ec] = co_await s.connect( - corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); - -char buf[1024]; -auto [read_ec, n] = co_await s.read_some( - capy::mutable_buffer(buf, sizeof(buf))); +include::example$snippets/4d_sockets.cpp[tag=overview,indent=0] ---- == Construction @@ -50,12 +37,7 @@ Sockets are constructed from an execution context or executor: [source,cpp] ---- -// From io_context -corosio::tcp_socket s1(ioc); - -// From executor -auto ex = ioc.get_executor(); -corosio::tcp_socket s2(ex); +include::example$snippets/4d_sockets.cpp[tag=construct,indent=0] ---- The socket doesn't own system resources until `open()` is called. @@ -68,8 +50,7 @@ Creates the underlying TCP socket: [source,cpp] ---- -s.open(); // Creates IPv4 TCP socket, associates with the platform - // reactor (IOCP on Windows, epoll/kqueue/select on POSIX) +include::example$snippets/4d_sockets.cpp[tag=open,indent=0] ---- This allocates a socket handle and registers it with the I/O backend. @@ -81,7 +62,7 @@ Releases socket resources: [source,cpp] ---- -s.close(); // Cancels pending ops, closes socket +include::example$snippets/4d_sockets.cpp[tag=close,indent=0] ---- Any pending operations complete with `capy::error::canceled`; test for it @@ -93,8 +74,7 @@ Check if the socket is open: [source,cpp] ---- -if (s.is_open()) - // Socket ready for I/O +include::example$snippets/4d_sockets.cpp[tag=is_open,indent=0] ---- == Connecting @@ -103,7 +83,7 @@ The `connect()` operation initiates a TCP connection: [source,cpp] ---- -auto [ec] = co_await s.connect(endpoint); +include::example$snippets/4d_sockets.cpp[tag=connect,indent=0] ---- This returns an `io_result<>` that you can unpack with structured bindings. @@ -135,8 +115,7 @@ For simpler code when errors are fatal: [source,cpp] ---- -if (auto [ec] = co_await s.connect(endpoint); ec) - throw std::system_error(ec); // Throws on error +include::example$snippets/4d_sockets.cpp[tag=connect_throw,indent=0] ---- === Range-Based Connect @@ -147,18 +126,7 @@ each in order, returning on the first success: [source,cpp] ---- -#include - -corosio::resolver r(ioc); -auto [rec, results] = co_await r.resolve("www.boost.org", "80"); -if (rec) - co_return; - -corosio::tcp_socket s(ioc); -auto [cec, ep] = co_await corosio::connect(s, results); -if (cec) - co_return; -// `ep` is the endpoint that accepted the connection. +include::example$snippets/4d_sockets.cpp[tag=connect_range,indent=0] ---- Between attempts the socket is closed so the next `connect()` auto-opens with @@ -170,12 +138,7 @@ socket's endpoint type: [source,cpp] ---- -template - requires std::convertible_to< - std::ranges::range_reference_t, - typename Socket::endpoint_type> -capy::task> -connect(Socket& s, Range endpoints); +include::example$snippets/4d_sockets.cpp[tag=connect_signature] ---- On success, returns the connected endpoint. On all-fail, returns the error @@ -189,12 +152,7 @@ each attempt. Returning `false` skips the candidate: [source,cpp] ---- -auto [ec, ep] = co_await corosio::connect( - s, - results, - [](std::error_code const&, corosio::endpoint const& e) { - return e.is_v4(); // IPv4 only. - }); +include::example$snippets/4d_sockets.cpp[tag=connect_condition,indent=0] ---- ==== Iterator Overload @@ -204,9 +162,7 @@ success, or `end` on failure: [source,cpp] ---- -auto [ec, it] = co_await corosio::connect(s, v.begin(), v.end()); -if (!ec) - std::cout << "connected to index " << (it - v.begin()) << "\n"; +include::example$snippets/4d_sockets.cpp[tag=connect_iterator,indent=0] ---- Both overloads accept an optional connect condition as a trailing argument. @@ -219,9 +175,7 @@ Reads available data: [source,cpp] ---- -char buf[1024]; -auto [ec, n] = co_await s.read_some( - capy::mutable_buffer(buf, sizeof(buf))); +include::example$snippets/4d_sockets.cpp[tag=read_some,indent=0] ---- This completes when _any_ data is available. The returned `n` may be less @@ -233,10 +187,7 @@ When the peer closes the connection: [source,cpp] ---- -auto [ec, n] = co_await s.read_some(buf); - -if (ec == capy::cond::eof) - // Connection closed normally +include::example$snippets/4d_sockets.cpp[tag=read_eof,indent=0] ---- EOF is signaled only by `ec == capy::cond::eof`. A successful read with @@ -249,8 +200,7 @@ Use the `capy::read()` free function to fill a buffer completely: [source,cpp] ---- -auto [ec, n] = co_await capy::read(s, buf); -// n == buffer_size(buf) or error occurred +include::example$snippets/4d_sockets.cpp[tag=read_all,indent=0] ---- See xref:4.guide/4g.composed-operations.adoc[Composed Operations] for details. @@ -263,9 +213,7 @@ Writes some data: [source,cpp] ---- -std::string msg = "Hello"; -auto [ec, n] = co_await s.write_some( - capy::const_buffer(msg.data(), msg.size())); +include::example$snippets/4d_sockets.cpp[tag=write_some,indent=0] ---- This completes when _any_ data is written. The returned `n` may be less @@ -277,8 +225,7 @@ Use the `capy::write()` free function: [source,cpp] ---- -auto [ec, n] = co_await capy::write(s, buf); -// n == buffer_size(buf) or error occurred +include::example$snippets/4d_sockets.cpp[tag=write_all,indent=0] ---- == Cancellation @@ -289,7 +236,7 @@ Cancel pending operations: [source,cpp] ---- -s.cancel(); +include::example$snippets/4d_sockets.cpp[tag=cancel,indent=0] ---- All outstanding operations complete with `capy::error::canceled`; test for it @@ -301,16 +248,14 @@ Operations support `std::stop_token` through the affine protocol: [source,cpp] ---- -// Inside a capy::jcancellable_task: -auto [ec, n] = co_await s.read_some(buf); -// Automatically cancelled if stop is requested +include::example$snippets/4d_sockets.cpp[tag=stop_token,indent=0] ---- == Move Semantics Sockets are move-only: -[source,cpp] +[source,cpp,role=pseudocode] ---- corosio::tcp_socket s1(ioc); corosio::tcp_socket s2 = std::move(s1); // OK @@ -322,16 +267,16 @@ Move assignment closes any existing socket: [source,cpp] ---- -s1 = std::move(s2); // Closes s1's socket if open, then moves s2 +include::example$snippets/4d_sockets.cpp[tag=move_assign,indent=0] ---- NOTE: After a move, the destination uses the source's execution context. == The io_stream Interface -`socket` inherits from `io_stream`, which provides: +`tcp_socket` inherits from `io_stream`, which provides: -[source,cpp] +[source,cpp,role=pseudocode] ---- class io_stream : public io_object { @@ -348,14 +293,7 @@ This enables polymorphic use: [source,cpp] ---- -capy::task send_data(corosio::io_stream& stream) -{ - co_await capy::write(stream, some_buffer); -} - -// Works with socket, wolfssl_stream, or any io_stream -corosio::tcp_socket sock(ioc); -co_await send_data(sock); +include::example$snippets/4d_sockets.cpp[tag=io_stream_poly,indent=0] ---- == Buffer Sequences @@ -364,16 +302,7 @@ Read and write operations accept buffer sequences: [source,cpp] ---- -// Single buffer -capy::mutable_buffer buf(data, size); -co_await s.read_some(buf); - -// Multiple buffers (scatter/gather I/O) -std::array bufs = { - capy::mutable_buffer(header, header_size), - capy::mutable_buffer(body, body_size) -}; -co_await s.read_some(bufs); +include::example$snippets/4d_sockets.cpp[tag=buffer_sequences,indent=0] ---- See xref:4.guide/4n.buffers.adoc[Buffer Sequences] for details. @@ -398,28 +327,7 @@ socket. But don't start two reads or two writes concurrently. [source,cpp] ---- -capy::task echo_client(corosio::io_context& ioc) -{ - corosio::tcp_socket s(ioc); - s.open(); - - if (auto [ec] = co_await s.connect( - corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); ec) - throw std::system_error(ec); - - std::string msg = "Hello, server!"; - if (auto [ec, n] = co_await capy::write( - s, capy::const_buffer(msg.data(), msg.size())); ec) - throw std::system_error(ec); - - char buf[1024]; - auto [ec, n] = co_await s.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - - if (!ec) - std::cout << "Server replied: " - << std::string_view(buf, n) << "\n"; -} +include::example$snippets/4d_sockets.cpp[tag=echo_client] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc index 7b0802f74..2cee07fd5 100644 --- a/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc +++ b/doc/modules/ROOT/pages/4.guide/4e.tcp-acceptor.adoc @@ -17,11 +17,7 @@ into socket objects. It's the foundation for building TCP servers. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; +include::example$snippets/4e_tcp_acceptor.cpp[tag=assume] ---- ==== @@ -31,16 +27,7 @@ A tcp_acceptor binds to a local endpoint and waits for clients to connect: [source,cpp] ---- -// Convenience constructor: open + SO_REUSEADDR + bind + listen on port 8080 -corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); - -corosio::tcp_socket peer(ioc); -auto [ec] = co_await acc.accept(peer); - -if (!ec) -{ - // peer is now a connected socket -} +include::example$snippets/4e_tcp_acceptor.cpp[tag=overview_accept,indent=0] ---- == Construction @@ -49,12 +36,7 @@ Acceptors are constructed from an execution context or executor: [source,cpp] ---- -// From io_context -corosio::tcp_acceptor acc1(ioc); - -// From executor -auto ex = ioc.get_executor(); -corosio::tcp_acceptor acc2(ex); +include::example$snippets/4e_tcp_acceptor.cpp[tag=construction,indent=0] ---- A default-constructed acceptor doesn't own system resources until it is @@ -74,8 +56,7 @@ which opens, sets `SO_REUSEADDR`, binds, and listens in a single step: [source,cpp] ---- -// open + SO_REUSEADDR + bind + listen; address family deduced from the endpoint -corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); +include::example$snippets/4e_tcp_acceptor.cpp[tag=convenience_ctor,indent=0] ---- This throws `std::system_error` if binding or listening fails. Unlike the @@ -90,27 +71,14 @@ to prevent accidentally ignoring errors: [source,cpp] ---- -corosio::tcp_acceptor acc(ioc); -acc.open(); // create an IPv4 TCP socket - -if (auto ec = acc.bind(corosio::endpoint(8080))) -{ - std::cerr << "Bind failed: " << ec.message() << "\n"; - return ec; -} - -if (auto ec = acc.listen()) -{ - std::cerr << "Listen failed: " << ec.message() << "\n"; - return ec; -} +include::example$snippets/4e_tcp_acceptor.cpp[tag=bind_listen,indent=0] ---- The `listen()` method accepts an optional `backlog` parameter: [source,cpp] ---- -[[nodiscard]] std::error_code listen(int backlog = 128); +include::example$snippets/4e_tcp_acceptor.cpp[tag=listen_signature,indent=0] ---- The `backlog` parameter specifies the maximum queue length for pending @@ -124,8 +92,7 @@ To accept connections on any network interface, bind to port only, which uses [source,cpp] ---- -// Port only - binds to 0.0.0.0 (all IPv4 interfaces) -corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); +include::example$snippets/4e_tcp_acceptor.cpp[tag=bind_all_interfaces,indent=0] ---- === Binding to a Specific Interface @@ -134,9 +101,7 @@ To accept connections only on a specific interface: [source,cpp] ---- -// Localhost only -corosio::tcp_acceptor acc(ioc, corosio::endpoint( - corosio::ipv4_address::loopback(), 8080)); +include::example$snippets/4e_tcp_acceptor.cpp[tag=bind_loopback,indent=0] ---- == Accepting Connections @@ -147,8 +112,7 @@ The `accept()` operation waits for and accepts an incoming connection: [source,cpp] ---- -corosio::tcp_socket peer(ioc); -auto [ec] = co_await acc.accept(peer); +include::example$snippets/4e_tcp_acceptor.cpp[tag=accept_into_peer,indent=0] ---- On success, `peer` is initialized with the new connection. Any existing @@ -162,7 +126,7 @@ associated with the acceptor's execution context: [source,cpp] ---- -auto [ec, peer] = co_await acc.accept(); +include::example$snippets/4e_tcp_acceptor.cpp[tag=accept_returning,indent=0] ---- Prefer the returning overload for the common case: it is simpler and guarantees @@ -203,7 +167,7 @@ Cancel pending accept operations: [source,cpp] ---- -acc.cancel(); +include::example$snippets/4e_tcp_acceptor.cpp[tag=cancel,indent=0] ---- All outstanding `accept()` operations complete with `operation_canceled`. @@ -215,10 +179,7 @@ protocol: [source,cpp] ---- -// Inside a cancellable task: -auto [ec] = co_await acc.accept(peer); -if (ec == std::errc::operation_canceled) - std::cout << "Accept cancelled\n"; +include::example$snippets/4e_tcp_acceptor.cpp[tag=accept_stop_token,indent=0] ---- == Closing @@ -229,7 +190,7 @@ Release tcp_acceptor resources: [source,cpp] ---- -acc.close(); +include::example$snippets/4e_tcp_acceptor.cpp[tag=close,indent=0] ---- Pending accept operations complete with `operation_canceled`. @@ -240,8 +201,7 @@ Check if the tcp_acceptor is listening: [source,cpp] ---- -if (acc.is_open()) - // Ready to accept +include::example$snippets/4e_tcp_acceptor.cpp[tag=is_open,indent=0] ---- == Move Semantics @@ -250,17 +210,14 @@ Acceptors are move-only: [source,cpp] ---- -corosio::tcp_acceptor acc1(ioc); -corosio::tcp_acceptor acc2 = std::move(acc1); // OK - -corosio::tcp_acceptor acc3 = acc2; // Error: deleted copy constructor +include::example$snippets/4e_tcp_acceptor.cpp[tag=move_only,indent=0] ---- Move assignment closes any existing tcp_acceptor: [source,cpp] ---- -acc1 = std::move(acc2); // Closes acc1's socket if open, then moves acc2 +include::example$snippets/4e_tcp_acceptor.cpp[tag=move_assign,indent=0] ---- NOTE: After a move, the destination uses the source's execution context. @@ -286,29 +243,7 @@ A typical server accept loop: [source,cpp] ---- -capy::task accept_loop( - corosio::io_context& ioc, - corosio::tcp_acceptor& acc) -{ - for (;;) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - - if (ec) - { - if (ec == std::errc::operation_canceled) - break; // Shutdown requested - - std::cerr << "Accept error: " << ec.message() << "\n"; - continue; // Try again - } - - // Spawn a coroutine to handle this connection - capy::run_async(ioc.get_executor())( - handle_connection(std::move(peer))); - } -} +include::example$snippets/4e_tcp_acceptor.cpp[tag=accept_loop] ---- Key points: @@ -324,35 +259,7 @@ Coordinate shutdown with signal handling: [source,cpp] ---- -capy::task run_server(corosio::io_context& ioc) -{ - corosio::tcp_acceptor acc(ioc); - acc.open(); - if (auto ec = acc.bind(corosio::endpoint(8080))) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - co_return; - } - if (auto ec = acc.listen()) - { - std::cerr << "Listen failed: " << ec.message() << "\n"; - co_return; - } - - corosio::signal_set signals(ioc, SIGINT, SIGTERM); - - // Spawn accept loop - capy::run_async(ioc.get_executor())(accept_loop(ioc, acc)); - - // Wait for shutdown signal - auto [ec, signum] = co_await signals.wait(); - if (!ec) - { - std::cout << "Received signal " << signum << ", shutting down\n"; - acc.cancel(); // Stop accepting - // Existing connections continue until complete - } -} +include::example$snippets/4e_tcp_acceptor.cpp[tag=graceful_shutdown] ---- == Relationship to tcp_server diff --git a/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc index cc4c6aa28..175fff91d 100644 --- a/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc +++ b/doc/modules/ROOT/pages/4.guide/4f.endpoints.adoc @@ -18,11 +18,7 @@ and binding acceptors. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; +include::example$snippets/4f_endpoints.cpp[tag=assume] ---- ==== @@ -32,14 +28,7 @@ An endpoint combines an address and port: [source,cpp] ---- -// IPv4 endpoint -corosio::endpoint ep4(corosio::ipv4_address::loopback(), 8080); - -// IPv6 endpoint -corosio::endpoint ep6(corosio::ipv6_address::loopback(), 8080); - -// Port only (binds to all interfaces) -corosio::endpoint bind_ep(8080); +include::example$snippets/4f_endpoints.cpp[tag=overview,indent=0] ---- == Construction @@ -48,23 +37,21 @@ corosio::endpoint bind_ep(8080); [source,cpp] ---- -auto addr = corosio::ipv4_address::loopback(); // 127.0.0.1 -corosio::endpoint ep(addr, 8080); +include::example$snippets/4f_endpoints.cpp[tag=from_v4,indent=0] ---- === From IPv6 Address and Port [source,cpp] ---- -auto addr = corosio::ipv6_address::loopback(); // ::1 -corosio::endpoint ep(addr, 8080); +include::example$snippets/4f_endpoints.cpp[tag=from_v6,indent=0] ---- === From Port Only [source,cpp] ---- -corosio::endpoint ep(8080); // IPv4 any address (0.0.0.0) +include::example$snippets/4f_endpoints.cpp[tag=port_only,indent=0] ---- This binds to all network interfaces, useful for servers. @@ -73,18 +60,14 @@ This binds to all network interfaces, useful for servers. [source,cpp] ---- -corosio::endpoint ep; // IPv4 any address, port 0 +include::example$snippets/4f_endpoints.cpp[tag=default_ctor,indent=0] ---- == Querying Address Type [source,cpp] ---- -if (ep.is_v4()) - std::cout << "IPv4 address\n"; - -if (ep.is_v6()) - std::cout << "IPv6 address\n"; +include::example$snippets/4f_endpoints.cpp[tag=query_type,indent=0] ---- == Accessing Components @@ -93,29 +76,21 @@ if (ep.is_v6()) [source,cpp] ---- -std::uint16_t port = ep.port(); // Host byte order +include::example$snippets/4f_endpoints.cpp[tag=port,indent=0] ---- === IPv4 Address [source,cpp] ---- -if (ep.is_v4()) -{ - corosio::ipv4_address addr = ep.v4_address(); - std::cout << addr.to_string() << "\n"; -} +include::example$snippets/4f_endpoints.cpp[tag=v4_address,indent=0] ---- === IPv6 Address [source,cpp] ---- -if (ep.is_v6()) -{ - corosio::ipv6_address addr = ep.v6_address(); - std::cout << addr.to_string() << "\n"; -} +include::example$snippets/4f_endpoints.cpp[tag=v6_address,indent=0] ---- == Common Address Constants @@ -124,30 +99,21 @@ if (ep.is_v6()) [source,cpp] ---- -// IPv4 loopback: 127.0.0.1 -auto v4_loop = corosio::ipv4_address::loopback(); - -// IPv6 loopback: ::1 -auto v6_loop = corosio::ipv6_address::loopback(); +include::example$snippets/4f_endpoints.cpp[tag=loopback,indent=0] ---- === Any Address [source,cpp] ---- -// IPv4 any: 0.0.0.0 (all interfaces) -auto v4_any = corosio::ipv4_address::any(); - -// IPv6 any: :: (all interfaces) -auto v6_any = corosio::ipv6_address::any(); +include::example$snippets/4f_endpoints.cpp[tag=any,indent=0] ---- === Broadcast [source,cpp] ---- -// IPv4 broadcast: 255.255.255.255 -auto v4_bcast = corosio::ipv4_address::broadcast(); +include::example$snippets/4f_endpoints.cpp[tag=broadcast,indent=0] ---- == Parsing Addresses @@ -157,19 +123,7 @@ Parse addresses from strings. Parsing reports failure through an [source,cpp] ---- -// IPv4 -corosio::ipv4_address addr; -if (auto ec = corosio::parse_ipv4_address("192.168.1.1", addr); !ec) -{ - corosio::endpoint ep(addr, 8080); -} - -// IPv6 -corosio::ipv6_address addr6; -if (auto ec = corosio::parse_ipv6_address("2001:db8::1", addr6); !ec) -{ - corosio::endpoint ep(addr6, 8080); -} +include::example$snippets/4f_endpoints.cpp[tag=parse_addresses,indent=0] ---- You can also parse a full `address:port` string directly into an endpoint @@ -178,11 +132,7 @@ using `parse_endpoint()`, or the `endpoint` constructor that accepts a [source,cpp] ---- -corosio::endpoint ep; -if (auto ec = corosio::parse_endpoint("192.168.1.1:8080", ep); !ec) -{ - // Use ep... -} +include::example$snippets/4f_endpoints.cpp[tag=parse_endpoint,indent=0] ---- == Comparison @@ -191,12 +141,7 @@ Endpoints support equality comparison: [source,cpp] ---- -corosio::endpoint ep1(corosio::ipv4_address::loopback(), 8080); -corosio::endpoint ep2(corosio::ipv4_address::loopback(), 8080); -corosio::endpoint ep3(corosio::ipv4_address::loopback(), 9090); - -assert(ep1 == ep2); // Same address and port -assert(ep1 != ep3); // Different port +include::example$snippets/4f_endpoints.cpp[tag=comparison,indent=0] ---- == Usage with Sockets @@ -205,21 +150,14 @@ assert(ep1 != ep3); // Different port [source,cpp] ---- -corosio::tcp_socket s(ioc); -s.open(); - -corosio::endpoint target( - corosio::ipv4_address::loopback(), 8080); - -auto [ec] = co_await s.connect(target); +include::example$snippets/4f_endpoints.cpp[tag=connecting,indent=0] ---- === Accepting [source,cpp] ---- -// Convenience constructor: open + SO_REUSEADDR + bind + listen -corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); // bind to all interfaces +include::example$snippets/4f_endpoints.cpp[tag=accepting,indent=0] ---- == From Resolver Results @@ -228,26 +166,14 @@ The resolver returns endpoints: [source,cpp] ---- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "80"); - -for (auto const& entry : results) -{ - corosio::endpoint ep = entry.get_endpoint(); - // Try connecting to ep... -} +include::example$snippets/4f_endpoints.cpp[tag=resolver_results,indent=0] ---- Resolver entries implicitly convert to endpoints: [source,cpp] ---- -for (corosio::endpoint ep : results) -{ - auto [ec] = co_await s.connect(ep); - if (!ec) - break; -} +include::example$snippets/4f_endpoints.cpp[tag=implicit_conversion,indent=0] ---- == Internal Representation diff --git a/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc index f915e4004..a7cca443d 100644 --- a/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc +++ b/doc/modules/ROOT/pages/4.guide/4g.composed-operations.adoc @@ -17,13 +17,7 @@ and `write_some()` functions to provide higher-level guarantees. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/4g_composed_operations.cpp[tag=assume] ---- ==== @@ -34,10 +28,7 @@ about how much data is transferred: [source,cpp] ---- -char buf[1024]; -auto [ec, n] = co_await s.read_some( - capy::mutable_buffer(buf, sizeof(buf))); -// n could be 1, 100, 500, or 1024 - no guarantee +include::example$snippets/4g_composed_operations.cpp[tag=primitives,indent=0] ---- For many use cases, you need to transfer a specific amount of data. Composed @@ -49,67 +40,39 @@ The `read()` function reads until the buffer is full or an error occurs: [source,cpp] ---- -char buf[1024]; -auto [ec, n] = co_await capy::read( - stream, capy::mutable_buffer(buf, sizeof(buf))); - -// Either: -// - n == 1024 and ec is default (success) -// - ec == capy::cond::eof and n < 1024 (reached end of stream) -// - ec is some other error +include::example$snippets/4g_composed_operations.cpp[tag=read_full,indent=0] ---- === Signature [source,cpp] ---- -auto -read( - ReadStream auto& stream, - MutableBufferSequence auto const& buffers) -> - capy::io_task; +include::example$snippets/4g_composed_operations.cpp[tag=read_signature] ---- === Behavior 1. Calls `read_some()` repeatedly until all buffers are filled -2. If `read_some()` returns an error (including `cond::eof`), returns - immediately with bytes read so far +2. If `read_some()` reports an error (including `cond::eof`) before the + buffers are filled, returns immediately with bytes read so far 3. On success, returns total bytes (equals `buffer_size(buffers)`) -== capy::read() into a dynamic buffer - -A second overload reads until EOF, growing the buffer as needed. Use -`capy::string_dynamic_buffer` to wrap a `std::string`: - -[source,cpp] ----- -std::string content; -auto [ec, n] = co_await capy::read( - stream, capy::string_dynamic_buffer(&content)); - -// On success (EOF reached): ec is default, n is total bytes read -// On error: ec is the error, n is bytes read before the error ----- +== Reading Until EOF -=== Signature +To consume a stream of unknown length, loop over `read_some()` into a +fixed-size chunk and grow a `std::string` as data arrives: [source,cpp] ---- -auto -read( - ReadStream auto& stream, - DynamicBufferParam auto&& buffers, - std::size_t initial_amount = 2048) -> - capy::io_task; +include::example$snippets/4g_composed_operations.cpp[tag=read_until_eof,indent=0] ---- === Behavior -1. Prepares `initial_amount` bytes via `buffers.prepare()`, then reads -2. Grows 1.5x when the prepared buffer fills completely -3. On `cond::eof`: returns success with total bytes read -4. On any other error: returns immediately with bytes read so far +1. Each `read_some()` returns as soon as any data is available +2. On `cond::eof`: the loop ends with `content` holding the entire stream +3. On any other error: the error is reported with the bytes read so far + already appended == capy::write() @@ -117,24 +80,14 @@ The `write()` function writes all data or fails: [source,cpp] ---- -std::string msg = "Hello, World!"; -auto [ec, n] = co_await capy::write( - stream, capy::const_buffer(msg.data(), msg.size())); - -// Either: -// - n == msg.size() and ec is default (all data written) -// - ec is an error +include::example$snippets/4g_composed_operations.cpp[tag=write_full,indent=0] ---- === Signature [source,cpp] ---- -auto -write( - WriteStream auto& stream, - ConstBufferSequence auto const& buffers) -> - capy::io_task; +include::example$snippets/4g_composed_operations.cpp[tag=write_signature] ---- === Behavior @@ -145,50 +98,34 @@ write( == buffer_slice Helper -Both `read()` and `write()` use `capy::buffer_slice` internally to track -progress through a buffer sequence. `buffer_slice` returns a view over a -byte range of the underlying sequence. It exposes the bytes not yet -transferred via `data()` and advances past completed bytes with -`remove_prefix(n)`: +Both `read()` and `write()` use `capy::consuming_buffers` internally to +track progress through a buffer sequence. A related helper, +`capy::buffer_slice`, returns a view over a byte range of the underlying +sequence. The result is itself a buffer sequence covering only that +range, so it can be passed directly to any I/O operation: [source,cpp] ---- -#include - -std::array bufs = { - capy::mutable_buffer(header, 16), - capy::mutable_buffer(body, 1024) -}; - -auto slice = capy::buffer_slice(bufs); - -// After reading 20 bytes: -slice.remove_prefix(20); -// slice.data() now represents: 4 bytes of header remaining + full body +include::example$snippets/4g_composed_operations.cpp[tag=slice_helper,indent=0] ---- === Interface [source,cpp] ---- -template - requires MutableBufferSequence - || ConstBufferSequence -auto -buffer_slice( - BufferSequence const& seq, - std::size_t offset = 0, - std::size_t length = std::numeric_limits::max()); +include::example$snippets/4g_composed_operations.cpp[tag=slice_interface] ---- -The returned object satisfies the `Slice` concept (and `MutableSlice` -when `seq` is a mutable buffer sequence). Its `data()` member returns a -buffer sequence covering the remaining bytes, and `remove_prefix(n)` -advances the slice past `n` consumed bytes. +The returned `slice_type` models the same buffer-sequence concept as +`seq` (mutable when `seq` is a mutable buffer sequence): slicing a +single buffer yields an adjusted buffer of the same kind, while any +other sequence yields a borrowed view. `offset` is clamped to the total +size, and `length` defaults to the end of the sequence. -NOTE: `seq` must outlive the slice and any buffer sequence obtained from -its `data()`. Passing a temporary buffer sequence is rejected at compile -time; hoist it into a named variable first. +NOTE: Except for the single-buffer case, the result borrows `seq`: it +stores iterators into the sequence, not a copy, so `seq` must outlive +it. Passing a temporary buffer sequence is rejected at compile time; +hoist it into a named variable first. == Error Handling Patterns @@ -196,29 +133,14 @@ time; hoist it into a named variable first. [source,cpp] ---- -auto [ec, n] = co_await capy::read(stream, buf); -if (ec) -{ - if (ec == capy::cond::eof) - std::cout << "End of stream, read " << n << " bytes\n"; - else - std::cerr << "Error: " << ec.message() << "\n"; -} +include::example$snippets/4g_composed_operations.cpp[tag=error_bindings,indent=0] ---- === Exception Pattern [source,cpp] ---- -// For write (EOF doesn't apply) -auto [wec, n] = co_await capy::write(stream, buf); -if (wec) - capy::detail::throw_system_error(wec); - -// For read (need to handle EOF) -auto [rec, rn] = co_await capy::read(stream, buf); -if (rec && rec != capy::cond::eof) - capy::detail::throw_system_error(rec); +include::example$snippets/4g_composed_operations.cpp[tag=error_exceptions,indent=0] ---- == Cancellation @@ -228,9 +150,7 @@ cancelled, they return with `cond::canceled` and the partial byte count. [source,cpp] ---- -auto [ec, n] = co_await capy::read(stream, large_buffer); -if (ec == capy::cond::canceled) - std::cout << "Cancelled after reading " << n << " bytes\n"; +include::example$snippets/4g_composed_operations.cpp[tag=cancellation,indent=0] ---- == Performance Considerations @@ -241,13 +161,7 @@ For optimal performance with multiple buffers: [source,cpp] ---- -// Efficient: single system call per read_some() -std::array bufs = {...}; -co_await capy::read(stream, bufs); - -// Less efficient: may require more system calls -co_await capy::read(stream, buf1); -co_await capy::read(stream, buf2); +include::example$snippets/4g_composed_operations.cpp[tag=multiple_buffers,indent=0] ---- === Buffer Sizing @@ -257,29 +171,14 @@ Choose buffer sizes that match your expected data: * Too small: More system calls * Too large: Memory waste -For unknown-length data (like HTTP responses), use the dynamic buffer overload: - -[source,cpp] ----- -std::string response; -co_await capy::read(stream, capy::string_dynamic_buffer(&response)); ----- +For unknown-length data (like HTTP responses), use the read-until-EOF +loop shown earlier. == Example: HTTP Response Reading [source,cpp] ---- -capy::task read_http_response(corosio::io_stream& stream) -{ - std::string response; - auto [ec, n] = co_await capy::read( - stream, capy::string_dynamic_buffer(&response)); - - if (ec) - capy::detail::throw_system_error(ec); - - co_return response; -} +include::example$snippets/4g_composed_operations.cpp[tag=http_response] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc index 66d64fa26..e2c3bddf1 100644 --- a/doc/modules/ROOT/pages/4.guide/4h.timers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4h.timers.adoc @@ -24,13 +24,7 @@ long-lived object's expiry. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; -using namespace std::chrono_literals; +include::example$snippets/4h_timers.cpp[tag=assume] ---- ==== @@ -41,9 +35,7 @@ duration elapses: [source,cpp] ---- -auto [ec] = co_await corosio::delay(500ms); -if (!ec) - std::cout << "500ms elapsed\n"; +include::example$snippets/4h_timers.cpp[tag=delay_duration,indent=0] ---- A zero or negative duration completes synchronously without @@ -58,15 +50,7 @@ duration from "now": [source,cpp] ---- -auto next = std::chrono::steady_clock::now(); -for (int i = 0; i < 10; ++i) -{ - next += 100ms; - auto [ec] = co_await corosio::delay(next); - if (ec) - break; - std::cout << "Tick " << i << "\n"; -} +include::example$snippets/4h_timers.cpp[tag=delay_timepoint,indent=0] ---- A time point already in the past also completes synchronously. @@ -80,9 +64,7 @@ stopped while it is waiting, the wait ends early and resumes with [source,cpp] ---- -auto [ec] = co_await corosio::delay(10s); -if (ec == capy::cond::canceled) - std::cout << "Delay was cancelled\n"; +include::example$snippets/4h_timers.cpp[tag=delay_cancel,indent=0] ---- There is no `cancel()` to call directly; cancellation flows entirely @@ -96,13 +78,7 @@ Whichever finishes first decides the outcome: [source,cpp] ---- -auto [ec, n] = co_await corosio::timeout( - sock.read_some(buffer), 200ms); - -if (ec == capy::cond::timeout) - std::cout << "No data within 200ms\n"; -else if (!ec) - std::cout << "Read " << n << " bytes\n"; +include::example$snippets/4h_timers.cpp[tag=timeout_read,indent=0] ---- If the inner operation wins, its `io_result` is returned unchanged, @@ -118,8 +94,7 @@ default constructible. [source,cpp] ---- -auto deadline = std::chrono::steady_clock::now() + 5s; -auto [ec] = co_await corosio::timeout(sock.connect(ep), deadline); +include::example$snippets/4h_timers.cpp[tag=timeout_deadline,indent=0] ---- === Distinguishing Timeout from Cancellation @@ -143,11 +118,7 @@ cancelled and `timeout()` reports `capy::cond::canceled`, not [source,cpp] ---- -auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); -if (ec == capy::cond::timeout) - std::cout << "Connect attempt timed out\n"; -else if (ec == capy::cond::canceled) - std::cout << "Connect attempt cancelled by caller\n"; +include::example$snippets/4h_timers.cpp[tag=timeout_vs_cancel,indent=0] ---- == Requires an io_context @@ -167,27 +138,7 @@ with `delay()` for the pause between attempts: [source,cpp] ---- -capy::task<> -connect_with_deadline( - corosio::tcp_socket& sock, - corosio::endpoint ep, - int max_attempts) -{ - for (int attempt = 0; attempt < max_attempts; ++attempt) - { - auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); - if (!ec) - co_return; - - if (ec == capy::cond::canceled) - co_return; - - sock.close(); - auto [dec] = co_await corosio::delay(500ms); - if (dec == capy::cond::canceled) - co_return; - } -} +include::example$snippets/4h_timers.cpp[tag=connect_retry] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc index 224dc0e59..08b1a649b 100644 --- a/doc/modules/ROOT/pages/4.guide/4i.signals.adoc +++ b/doc/modules/ROOT/pages/4.guide/4i.signals.adoc @@ -18,10 +18,7 @@ SIGTERM. Code snippets assume: [source,cpp] ---- -#include -#include - -namespace corosio = boost::corosio; +include::example$snippets/4i_signals.cpp[tag=assume] ---- ==== @@ -29,11 +26,7 @@ namespace corosio = boost::corosio; [source,cpp] ---- -corosio::signal_set signals(ioc, SIGINT, SIGTERM); - -auto [ec, signum] = co_await signals.wait(); -if (!ec) - std::cout << "Received signal " << signum << "\n"; +include::example$snippets/4i_signals.cpp[tag=overview,indent=0] ---- == Construction @@ -42,23 +35,14 @@ if (!ec) [source,cpp] ---- -corosio::signal_set signals(ioc); -signals.add(SIGINT); -signals.add(SIGTERM); +include::example$snippets/4i_signals.cpp[tag=construct_empty,indent=0] ---- === With Initial Signals [source,cpp] ---- -// One signal -corosio::signal_set s1(ioc, SIGINT); - -// Two signals -corosio::signal_set s2(ioc, SIGINT, SIGTERM); - -// Three signals -corosio::signal_set s3(ioc, SIGINT, SIGTERM, SIGHUP); +include::example$snippets/4i_signals.cpp[tag=construct_initial,indent=0] ---- == Supported Signals @@ -102,7 +86,7 @@ Add a signal to the set: [source,cpp] ---- -signals.add(SIGUSR1); +include::example$snippets/4i_signals.cpp[tag=add_signal,indent=0] ---- Adding a signal that's already in the set has no effect. @@ -113,13 +97,7 @@ On POSIX systems, you can specify signal flags when adding a signal: [source,cpp] ---- -using flags = corosio::signal_set; - -// Restart interrupted system calls automatically -signals.add(SIGCHLD, flags::restart); - -// Multiple flags can be combined -signals.add(SIGCHLD, flags::restart | flags::no_child_stop); +include::example$snippets/4i_signals.cpp[tag=add_flags,indent=0] ---- Available flags: @@ -161,15 +139,7 @@ compatible flags: [source,cpp] ---- -corosio::signal_set s1(ioc); -corosio::signal_set s2(ioc); - -s1.add(SIGINT, flags::restart); // OK - first registration -s2.add(SIGINT, flags::restart); // OK - same flags -s2.add(SIGINT, flags::no_defer); // Error! - different flags - -// Use dont_care to accept existing flags -s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags +include::example$snippets/4i_signals.cpp[tag=flag_compat,indent=0] ---- === remove() @@ -178,10 +148,7 @@ Remove a signal from the set: [source,cpp] ---- -signals.remove(SIGINT); - -// remove() returns an error_code instead of throwing -std::error_code ec = signals.remove(SIGINT); +include::example$snippets/4i_signals.cpp[tag=remove_signal,indent=0] ---- Removing a signal that's not in the set has no effect. @@ -192,10 +159,7 @@ Remove all signals from the set: [source,cpp] ---- -signals.clear(); - -// clear() returns an error_code instead of throwing -std::error_code ec = signals.clear(); +include::example$snippets/4i_signals.cpp[tag=clear_signals,indent=0] ---- == Waiting for Signals @@ -204,20 +168,7 @@ The `wait()` operation waits for any signal in the set: [source,cpp] ---- -auto [ec, signum] = co_await signals.wait(); - -if (!ec) -{ - switch (signum) - { - case SIGINT: - std::cout << "Interrupt received\n"; - break; - case SIGTERM: - std::cout << "Termination requested\n"; - break; - } -} +include::example$snippets/4i_signals.cpp[tag=wait_switch,indent=0] ---- == Cancellation @@ -228,16 +179,14 @@ Cancel pending wait operations: [source,cpp] ---- -signals.cancel(); +include::example$snippets/4i_signals.cpp[tag=cancel_call,indent=0] ---- The wait completes with `capy::cond::canceled`: [source,cpp] ---- -auto [ec, signum] = co_await signals.wait(); -if (ec == capy::cond::canceled) - std::cout << "Wait was cancelled\n"; +include::example$snippets/4i_signals.cpp[tag=cancel_result,indent=0] ---- Cancellation does NOT remove signals from the set. The signal set remains @@ -253,20 +202,7 @@ Signal waits support stop token cancellation through the affine protocol. [source,cpp] ---- -capy::task shutdown_handler( - corosio::io_context& ioc, - std::atomic& running) -{ - corosio::signal_set signals(ioc, SIGINT, SIGTERM); - - auto [ec, signum] = co_await signals.wait(); - if (!ec) - { - std::cout << "Shutdown signal received\n"; - running = false; - ioc.stop(); - } -} +include::example$snippets/4i_signals.cpp[tag=graceful_shutdown] ---- === Multiple Signal Waits @@ -275,75 +211,28 @@ You can wait for signals multiple times: [source,cpp] ---- -capy::task signal_loop(corosio::io_context& ioc) -{ - corosio::signal_set signals(ioc, SIGUSR1); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - std::cout << "Received USR1, doing work...\n"; - // Handle signal - } -} +include::example$snippets/4i_signals.cpp[tag=signal_loop] ---- === Reload Configuration [source,cpp] ---- -capy::task config_reloader( - corosio::io_context& ioc, - Config& config) -{ - corosio::signal_set signals(ioc, SIGHUP); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - std::cout << "Reloading configuration...\n"; - config.reload(); - } -} +include::example$snippets/4i_signals.cpp[tag=config_reload] ---- === Child Process Management (POSIX) [source,cpp] ---- -capy::task child_reaper(corosio::io_context& ioc) -{ - using flags = corosio::signal_set; - - corosio::signal_set signals(ioc); - - // Only notify on child termination, not stop/continue - // Prevent zombie processes automatically - signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait); - - for (;;) - { - auto [ec, signum] = co_await signals.wait(); - if (ec) - break; - - // With no_child_wait, children are reaped automatically - std::cout << "Child process terminated\n"; - } -} +include::example$snippets/4i_signals.cpp[tag=child_reaper] ---- == Move Semantics Signal sets are move-only: -[source,cpp] +[source,cpp,role=pseudocode] ---- corosio::signal_set s1(ioc, SIGINT); corosio::signal_set s2 = std::move(s1); // OK @@ -373,34 +262,7 @@ concurrently on the same signal_set. [source,cpp] ---- -capy::task run_server(corosio::io_context& ioc) -{ - std::atomic running{true}; - - // Start signal handler - capy::run_async(ioc.get_executor())( - [](corosio::io_context& ioc, std::atomic& running) - -> capy::task - { - corosio::signal_set signals(ioc, SIGINT, SIGTERM); - co_await signals.wait(); - running = false; - ioc.stop(); - }(ioc, running)); - - // Accept loop - corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); - - while (running) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - if (ec) - break; - - // Handle connection... - } -} +include::example$snippets/4i_signals.cpp[tag=server_shutdown] ---- == Platform Notes diff --git a/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc index 8116aea9f..f8ac79132 100644 --- a/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc +++ b/doc/modules/ROOT/pages/4.guide/4j.resolver.adoc @@ -18,10 +18,7 @@ asynchronous interface. Code snippets assume: [source,cpp] ---- -#include -#include -#include -namespace corosio = boost::corosio; +include::example$snippets/4j_resolver.cpp[tag=assume] ---- ==== @@ -29,22 +26,14 @@ namespace corosio = boost::corosio; [source,cpp] ---- -corosio::resolver r(ioc); -auto [ec, results] = co_await r.resolve("www.example.com", "https"); - -for (auto const& entry : results) -{ - auto ep = entry.get_endpoint(); - std::cout << ep.v4_address().to_string() << ":" << ep.port() << "\n"; -} +include::example$snippets/4j_resolver.cpp[tag=overview,indent=0] ---- == Construction [source,cpp] ---- -corosio::io_context ioc; -corosio::resolver r(ioc); // From execution context +include::example$snippets/4j_resolver.cpp[tag=construction,indent=0] ---- == Resolving Names @@ -53,7 +42,7 @@ corosio::resolver r(ioc); // From execution context [source,cpp] ---- -auto [ec, results] = co_await r.resolve("www.example.com", "80"); +include::example$snippets/4j_resolver.cpp[tag=basic_resolution,indent=0] ---- The host can be: @@ -72,10 +61,7 @@ The service can be: [source,cpp] ---- -auto [ec, results] = co_await r.resolve( - "www.example.com", - "https", - corosio::resolve_flags::address_configured); +include::example$snippets/4j_resolver.cpp[tag=with_flags,indent=0] ---- == Resolve Flags @@ -119,11 +105,7 @@ Flags can be combined: [source,cpp] ---- -auto flags = - corosio::resolve_flags::numeric_host | - corosio::resolve_flags::numeric_service; - -auto [ec, results] = co_await r.resolve("127.0.0.1", "8080", flags); +include::example$snippets/4j_resolver.cpp[tag=combined_flags,indent=0] ---- == Working with Results @@ -134,22 +116,12 @@ indexing, and so on): [source,cpp] ---- -using resolver_results = std::vector; +include::example$snippets/4j_resolver.cpp[tag=results_alias,indent=0] ---- [source,cpp] ---- -for (auto const& entry : results) -{ - corosio::endpoint ep = entry.get_endpoint(); - - if (ep.is_v4()) - std::cout << "IPv4: " << ep.v4_address().to_string(); - else - std::cout << "IPv6: " << ep.v6_address().to_string(); - - std::cout << ":" << ep.port() << "\n"; -} +include::example$snippets/4j_resolver.cpp[tag=iterate_results,indent=0] ---- [NOTE] @@ -165,18 +137,7 @@ takes the range by value — such as `corosio::connect` — pass an rvalue [source,cpp] ---- -class resolver_entry -{ -public: - corosio::endpoint get_endpoint() const; - - // Implicit conversion to endpoint - operator corosio::endpoint() const; - - // Query strings used in the resolution - std::string const& host_name() const; - std::string const& service_name() const; -}; +include::example$snippets/4j_resolver.cpp[tag=entry_synopsis,indent=0] ---- == Connecting to Resolved Addresses @@ -185,37 +146,7 @@ Try each address until one works: [source,cpp] ---- -capy::task connect_to_service( - corosio::io_context& ioc, - std::string_view host, - std::string_view service) -{ - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(host, service); - - if (resolve_ec) - throw std::system_error(resolve_ec); - - if (results.empty()) - throw std::runtime_error("No addresses found"); - - corosio::tcp_socket sock(ioc); - sock.open(); - - std::error_code last_error; - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry.get_endpoint()); - if (!ec) - co_return; // Connected successfully - - last_error = ec; - sock.close(); - sock.open(); - } - - throw std::system_error(last_error); -} +include::example$snippets/4j_resolver.cpp[tag=connect_to_service] ---- == Cancellation @@ -226,7 +157,7 @@ Cancel pending resolution: [source,cpp] ---- -r.cancel(); +include::example$snippets/4j_resolver.cpp[tag=cancel,indent=0] ---- The in-flight resolution completes with `capy::cond::canceled` (the @@ -264,17 +195,14 @@ Match it with: [source,cpp] ---- -if (ec == capy::cond::canceled) -{ - // resolution was cancelled -} +include::example$snippets/4j_resolver.cpp[tag=match_canceled,indent=0] ---- == Move Semantics Resolvers are move-only: -[source,cpp] +[source,cpp,role=pseudocode] ---- corosio::resolver r1(ioc); corosio::resolver r2 = std::move(r1); // OK @@ -305,19 +233,7 @@ undefined behavior. [source,cpp] ---- -// CORRECT: Sequential resolves on same resolver -auto [ec1, r1] = co_await resolver.resolve("host1", "80"); -auto [ec2, r2] = co_await resolver.resolve("host2", "80"); - -// CORRECT: Parallel resolves with separate resolver instances -corosio::resolver r1(ioc), r2(ioc); -// In separate coroutines: -auto [ec1, res1] = co_await r1.resolve("host1", "80"); -auto [ec2, res2] = co_await r2.resolve("host2", "80"); - -// WRONG: Concurrent resolves on same resolver - UNDEFINED BEHAVIOR -auto f1 = resolver.resolve("host1", "80"); -auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1 +include::example$snippets/4j_resolver.cpp[tag=single_inflight,indent=0] ---- If you need to resolve multiple hostnames concurrently, create a separate @@ -327,54 +243,7 @@ resolver instance for each. [source,cpp] ---- -capy::task http_get( - corosio::io_context& ioc, - std::string_view hostname) -{ - // Resolve hostname - corosio::resolver r(ioc); - auto [resolve_ec, results] = co_await r.resolve(hostname, "80"); - - if (resolve_ec) - { - std::cerr << "Resolution failed: " << resolve_ec.message() << "\n"; - co_return; - } - - // Connect to first address - corosio::tcp_socket sock(ioc); - sock.open(); - - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry); - if (!ec) - break; - } - - if (!sock.is_open()) - { - std::cerr << "Failed to connect\n"; - co_return; - } - - // Send HTTP request - std::string request = - "GET / HTTP/1.1\r\n" - "Host: " + std::string(hostname) + "\r\n" - "Connection: close\r\n" - "\r\n"; - - if (auto [ec, n] = co_await capy::write( - sock, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - // Read response - std::string response; - co_await capy::read(sock, response); - - std::cout << response << "\n"; -} +include::example$snippets/4j_resolver.cpp[tag=http_get] ---- == Platform Notes diff --git a/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc index 81738dc4d..a5145dea1 100644 --- a/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc +++ b/doc/modules/ROOT/pages/4.guide/4k.tcp-server.adoc @@ -18,12 +18,7 @@ lifecycle automatically. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/4k_tcp_server.cpp[tag=assume] ---- ==== @@ -40,49 +35,7 @@ framework handles: [source,cpp] ---- -class echo_worker : public corosio::tcp_server::worker_base -{ - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; - -public: - explicit echo_worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_echo()); - } - - capy::task do_echo(); -}; - -// Build the worker pool as a range of pointer-like objects. -auto make_echo_workers(corosio::io_context& ctx, int n) -{ - std::vector> v; - v.reserve(n); - for (int i = 0; i < n; ++i) - v.push_back(std::make_unique(ctx)); - return v; -} - -class echo_server : public corosio::tcp_server -{ -public: - echo_server(corosio::io_context& ioc, int max_workers) - : tcp_server(ioc, ioc.get_executor()) - { - set_workers(make_echo_workers(ioc, max_workers)); - } -}; +include::example$snippets/4k_tcp_server.cpp[tag=overview] ---- == The Worker Pattern @@ -99,16 +52,7 @@ override: [source,cpp] ---- -class worker_base -{ - // Private list/bookkeeping members managed by tcp_server. -public: - worker_base(); - virtual ~worker_base(); - - virtual void run(launcher launch) = 0; - virtual corosio::tcp_socket& socket() = 0; -}; +include::example$snippets/4k_tcp_server.cpp[tag=worker_base] ---- Your worker inherits from `worker_base`, owns its socket, and implements the @@ -116,32 +60,7 @@ required methods: [source,cpp] ---- -class my_worker : public corosio::tcp_server::worker_base -{ - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string request_buf; - std::string response_buf; - -public: - explicit my_worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - {} - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), handle_connection()); - } - - capy::task handle_connection() - { - // Handle the connection using sock_ - // Worker is automatically returned to pool when coroutine ends - } -}; +include::example$snippets/4k_tcp_server.cpp[tag=my_worker] ---- === Providing Workers @@ -153,32 +72,14 @@ range and reuses each worker across connections: [source,cpp] ---- -template -void set_workers(Range&& workers); +include::example$snippets/4k_tcp_server.cpp[tag=set_workers_signature] ---- A small helper that builds the range keeps construction tidy: [source,cpp] ---- -auto make_workers(corosio::io_context& ctx, int n) -{ - std::vector> v; - v.reserve(n); - for (int i = 0; i < n; ++i) - v.push_back(std::make_unique(ctx)); - return v; -} - -class my_server : public corosio::tcp_server -{ -public: - my_server(corosio::io_context& ioc, int max_workers) - : tcp_server(ioc, ioc.get_executor()) - { - set_workers(make_workers(ioc, max_workers)); - } -}; +include::example$snippets/4k_tcp_server.cpp[tag=make_workers] ---- Because the range holds `unique_ptr`, workers are stored @@ -191,11 +92,7 @@ method with a `launcher` object. The launcher manages the coroutine lifecycle: [source,cpp] ---- -void run(launcher launch) override -{ - // Create and launch the session coroutine - launch(executor, my_coroutine()); -} +include::example$snippets/4k_tcp_server.cpp[tag=launcher_run,indent=0] ---- The launcher: @@ -211,8 +108,7 @@ worker immediately. Calling it multiple times throws `std::logic_error`. [source,cpp] ---- -template -void operator()(Executor const& ex, capy::task task); +include::example$snippets/4k_tcp_server.cpp[tag=launcher_signature] ---- The executor determines where the coroutine runs. Typically you use the @@ -220,7 +116,7 @@ I/O context's executor: [source,cpp] ---- -launch(ctx_.get_executor(), handle_connection()); +include::example$snippets/4k_tcp_server.cpp[tag=launch_call,indent=0] ---- == Binding and Starting @@ -231,17 +127,14 @@ Bind to a local endpoint: [source,cpp] ---- -auto ec = server.bind(corosio::endpoint(8080)); -if (ec) - std::cerr << "Bind failed: " << ec.message() << "\n"; +include::example$snippets/4k_tcp_server.cpp[tag=bind,indent=0] ---- You can bind to multiple ports: [source,cpp] ---- -server.bind(corosio::endpoint(80)); -server.bind(corosio::endpoint(443)); +include::example$snippets/4k_tcp_server.cpp[tag=bind_many,indent=0] ---- === start() @@ -250,7 +143,7 @@ Begin accepting connections: [source,cpp] ---- -server.start(); +include::example$snippets/4k_tcp_server.cpp[tag=start,indent=0] ---- Workers must have been provided via `set_workers()` before calling `start()`, @@ -271,99 +164,7 @@ suspended accept completes once more before the loop exits. [source,cpp] ---- -#include -#include -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; - -class echo_worker : public corosio::tcp_server::worker_base -{ - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf; - -public: - explicit echo_worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf.reserve(4096); - } - - corosio::tcp_socket& socket() override { return sock_; } - - void run(launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } - - capy::task do_session() - { - for (;;) - { - buf.resize(4096); - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf.data(), buf.size())); - - if (ec || n == 0) - break; - - buf.resize(n); - auto [wec, wn] = co_await capy::write( - sock_, capy::const_buffer(buf.data(), buf.size())); - - if (wec) - break; - } - - sock_.close(); - } -}; - -auto make_echo_workers(corosio::io_context& ctx, int n) -{ - std::vector> v; - v.reserve(n); - for (int i = 0; i < n; ++i) - v.push_back(std::make_unique(ctx)); - return v; -} - -class echo_server : public corosio::tcp_server -{ -public: - echo_server(corosio::io_context& ctx, int max_workers) - : tcp_server(ctx, ctx.get_executor()) - { - set_workers(make_echo_workers(ctx, max_workers)); - } -}; - -int main() -{ - corosio::io_context ioc; - - echo_server server(ioc, 100); - - auto ec = server.bind(corosio::endpoint(8080)); - if (ec) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - return 1; - } - - std::cout << "Echo server listening on port 8080\n"; - - server.start(); - ioc.run(); -} +include::example$programs/4k_tcp_server_echo.cpp[tag=full] ---- == Design Considerations @@ -384,16 +185,7 @@ worker's state is properly reset between connections: [source,cpp] ---- -capy::task do_session() -{ - // Reset state at session start - request_.clear(); - response_.clear(); - - // ... handle connection ... - - // Socket closed, worker returns to pool -} +include::example$snippets/4k_tcp_server.cpp[tag=worker_reuse,indent=0] ---- === Multiple Ports @@ -403,9 +195,7 @@ the same worker pool: [source,cpp] ---- -server.bind(corosio::endpoint(80)); // HTTP -server.bind(corosio::endpoint(443)); // HTTPS -server.start(); +include::example$snippets/4k_tcp_server.cpp[tag=multiple_ports,indent=0] ---- === Connection Rejection diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 53f0ab407..8fb9ce0ea 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -19,15 +19,7 @@ covers context configuration, stream usage, and common TLS patterns. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; -using namespace boost::corosio; +include::example$snippets/4l_tls.cpp[tag=assume] ---- ==== @@ -46,13 +38,7 @@ and checks the hostname: [source,cpp] ---- -tls_context ctx; -ctx.set_default_verify_paths(); // trust the system CAs -ctx.set_verify_mode(tls_verify_mode::peer); // require + verify the peer - -corosio::tcp_socket sock(ioc); -corosio::wolfssl_stream secure(&sock, ctx); -secure.set_hostname("example.com"); // SNI + hostname check +include::example$snippets/4l_tls.cpp[tag=verified_client,indent=0] ---- Trust anchors can also be supplied explicitly with @@ -81,27 +67,7 @@ The typical flow: [source,cpp] ---- -// 1. Configure a context -tls_context ctx; -if (auto ec = ctx.set_default_verify_paths(); ec) - throw std::system_error(ec); -if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) - throw std::system_error(ec); - -// 2. Connect a socket -corosio::tcp_socket sock(ioc); -sock.open(); -if (auto [ec] = co_await sock.connect(endpoint); ec) - throw std::system_error(ec); - -// 3. Wrap the connected socket (pointer form; does not take ownership) -corosio::wolfssl_stream secure(&sock, ctx); -secure.set_hostname("api.example.com"); -if (auto [ec] = co_await secure.handshake(tls_role::client); ec) - throw std::system_error(ec); - -// 4. Use encrypted I/O -auto [ec, n] = co_await secure.read_some(buffer); +include::example$snippets/4l_tls.cpp[tag=typical_flow,indent=0] ---- == tls_context @@ -117,8 +83,7 @@ configuration is captured when the first stream is constructed. [source,cpp] ---- -// Default context (TLS 1.2+ enabled) -tls_context ctx; +include::example$snippets/4l_tls.cpp[tag=default_context,indent=0] ---- The default context has no certificates loaded and doesn't verify peers. You'll @@ -133,37 +98,21 @@ key. [source,cpp] ---- -// From file -ctx.use_certificate_file("server.crt", tls_file_format::pem); - -// From memory -std::string cert_data = /* ... */; -ctx.use_certificate(cert_data, tls_file_format::pem); - -// Certificate chain (cert + intermediates) -ctx.use_certificate_chain_file("fullchain.pem"); +include::example$snippets/4l_tls.cpp[tag=load_certificates,indent=0] ---- ==== Loading Private Keys [source,cpp] ---- -// From file -ctx.use_private_key_file("server.key", tls_file_format::pem); - -// From memory -ctx.use_private_key(key_data, tls_file_format::pem); +include::example$snippets/4l_tls.cpp[tag=load_private_keys,indent=0] ---- For encrypted private keys, set a password callback first: [source,cpp] ---- -ctx.set_password_callback( - [](std::size_t max_len, tls_password_purpose purpose) { - return std::string("my-key-password"); - }); -ctx.use_private_key_file("encrypted.key", tls_file_format::pem); +include::example$snippets/4l_tls.cpp[tag=password_callback,indent=0] ---- ==== PKCS#12 Bundles @@ -172,7 +121,7 @@ PKCS#12 (`.pfx` or `.p12`) files bundle certificate, key, and chain together: [source,cpp] ---- -ctx.use_pkcs12_file("credentials.pfx", "bundle-password"); +include::example$snippets/4l_tls.cpp[tag=pkcs12,indent=0] ---- NOTE: The bundle is decoded when the first stream is created; a malformed @@ -190,7 +139,7 @@ Use the operating system's default CA certificates: [source,cpp] ---- -ctx.set_default_verify_paths(); +include::example$snippets/4l_tls.cpp[tag=system_trust,indent=0] ---- On OpenSSL the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are @@ -202,14 +151,7 @@ explicitly via `load_verify_file()` or `add_certificate_authority()`. [source,cpp] ---- -// Single CA from memory -ctx.add_certificate_authority(ca_pem); - -// CA file (may contain multiple certs) -ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); - -// Directory of hashed CA files -ctx.add_verify_path("/etc/ssl/certs"); +include::example$snippets/4l_tls.cpp[tag=custom_cas,indent=0] ---- NOTE: With OpenSSL, `load_verify_file()` registers only the *first* @@ -224,11 +166,7 @@ by `openssl rehash`, while WolfSSL loads every file in the directory. [source,cpp] ---- -// Require TLS 1.3 minimum -ctx.set_min_protocol_version(tls_version::tls_1_3); - -// Cap at TLS 1.2 (unusual, but possible) -ctx.set_max_protocol_version(tls_version::tls_1_2); +include::example$snippets/4l_tls.cpp[tag=protocol_versions,indent=0] ---- NOTE: On WolfSSL the ceiling is applied by selecting a version-specific @@ -252,10 +190,7 @@ Available versions: [source,cpp] ---- -// TLS 1.2-and-below cipher list (OpenSSL syntax) -ctx.set_ciphersuites("ECDHE+AESGCM:ECDHE+CHACHA20"); -// TLS 1.3 cipher suites (configured separately) -ctx.set_ciphersuites_tls13("TLS_AES_256_GCM_SHA384"); +include::example$snippets/4l_tls.cpp[tag=cipher_suites,indent=0] ---- NOTE: `set_ciphersuites()` covers TLS 1.2 and below; TLS 1.3 suites use @@ -273,10 +208,7 @@ Read the negotiated protocol after the handshake with [source,cpp] ---- -// Prefer HTTP/2, fall back to HTTP/1.1 -ctx.set_alpn({"h2", "http/1.1"}); -// ... after handshake: -std::string_view proto = stream.alpn_protocol(); // e.g. "h2", or empty +include::example$snippets/4l_tls.cpp[tag=alpn_config,indent=0] ---- NOTE: On WolfSSL, ALPN requires a `HAVE_ALPN` build; without it, offering @@ -289,14 +221,7 @@ rather than negotiate nothing silently. [source,cpp] ---- -// Don't verify peer (not recommended for clients) -ctx.set_verify_mode(tls_verify_mode::none); - -// Verify if peer presents certificate -ctx.set_verify_mode(tls_verify_mode::peer); - -// Require peer certificate (fail if not presented) -ctx.set_verify_mode(tls_verify_mode::require_peer); +include::example$snippets/4l_tls.cpp[tag=verify_modes,indent=0] ---- For HTTPS clients, use `peer`. For servers requiring client certificates @@ -308,7 +233,7 @@ For clients, set the expected server hostname on the stream: [source,cpp] ---- -secure.set_hostname("api.example.com"); +include::example$snippets/4l_tls.cpp[tag=set_hostname,indent=0] ---- This does two things: @@ -327,7 +252,7 @@ Limit the certificate chain depth: [source,cpp] ---- -ctx.set_verify_depth(10); // Max 10 intermediate certs +include::example$snippets/4l_tls.cpp[tag=verify_depth,indent=0] ---- ==== Custom Verification Callback @@ -340,15 +265,7 @@ exposes the backend's `X509_STORE_CTX*` for backend-specific use.) [source,cpp] ---- -ctx.set_verify_callback( - [](bool preverified, corosio::verify_context& ctx) { - if (!preverified) - return false; // reject if the chain didn't verify - - auto der = ctx.certificate(); // DER of the current certificate - return der.size() == expected_pin.size() && - std::equal(der.begin(), der.end(), expected_pin.begin()); - }); +include::example$snippets/4l_tls.cpp[tag=verify_callback,indent=0] ---- [NOTE] @@ -375,12 +292,7 @@ are consulted only when a revocation policy other than `disabled` is set. [source,cpp] ---- -// Load a CRL (PEM or DER), from file or memory -ctx.add_crl_file("issuer.crl"); -ctx.add_crl(crl_data); - -// Choose how strict to be -ctx.set_revocation_policy(tls_revocation_policy::hard_fail); +include::example$snippets/4l_tls.cpp[tag=revocation,indent=0] ---- Under `soft_fail`, a certificate whose status cannot be determined @@ -408,22 +320,7 @@ the `capy::Stream` concept, so composed operations like `capy::read` and [source,cpp] ---- -enum class tls_role { client, server }; - -class tls_stream -{ -public: - template - auto read_some(B const& buffers); // Decrypt and read - - template - auto write_some(B const& buffers); // Encrypt and write - - virtual capy::io_task<> handshake(tls_role role) = 0; - virtual capy::io_task<> shutdown() = 0; - - virtual capy::any_stream& next_layer() noexcept = 0; // Underlying stream -}; +include::example$snippets/4l_tls.cpp[tag=tls_stream_interface,indent=0] ---- === wolfssl_stream @@ -435,26 +332,16 @@ it. To wrap an already-connected socket, use the pointer form: [source,cpp] ---- -#include - -corosio::tcp_socket sock(ioc); -// ... connect sock ... - -tls_context ctx; -// ... configure ctx ... - -// Reference form: sock must outlive secure -corosio::wolfssl_stream secure(&sock, ctx); - -// Or owning form: secure takes ownership of the socket -corosio::wolfssl_stream owned(std::move(sock), ctx); +include::example$snippets/4l_tls.cpp[tag=wolfssl_construct,indent=0] ---- === openssl_stream The OpenSSL-based implementation, with the same construction modes: -[source,cpp] +// The docs CI builds the WolfSSL backend only, so this fragment is +// not compiled as a test. +[source,cpp,role=external] ---- #include @@ -471,19 +358,14 @@ Before encrypted communication, perform the TLS handshake: [source,cpp] ---- -auto [ec] = co_await secure.handshake(tls_role::client); -if (ec) -{ - std::cerr << "Handshake failed: " << ec.message() << "\n"; - co_return; -} +include::example$snippets/4l_tls.cpp[tag=client_handshake,indent=0] ---- === Server Handshake [source,cpp] ---- -auto [ec] = co_await secure.handshake(tls_role::server); +include::example$snippets/4l_tls.cpp[tag=server_handshake,indent=0] ---- === Handshake Errors @@ -514,15 +396,7 @@ any `capy::Stream`: [source,cpp] ---- -// Read encrypted data -char buf[1024]; -auto [ec, n] = co_await secure.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - -// Write encrypted data -std::string msg = "Hello, TLS!"; -auto [wec, wn] = co_await secure.write_some( - capy::const_buffer(msg.data(), msg.size())); +include::example$snippets/4l_tls.cpp[tag=encrypted_read_write,indent=0] ---- === Composed Operations @@ -531,11 +405,7 @@ The `capy::read()` and `capy::write()` free functions work with TLS streams: [source,cpp] ---- -// Read until buffer full -auto [ec, n] = co_await capy::read(secure, large_buffer); - -// Write all data -auto [wec, wn] = co_await capy::write(secure, data_buffer); +include::example$snippets/4l_tls.cpp[tag=composed_ops,indent=0] ---- == Shutdown @@ -544,9 +414,7 @@ Graceful TLS shutdown sends a close_notify alert: [source,cpp] ---- -auto [ec] = co_await secure.shutdown(); -// Then close the underlying socket -sock.close(); +include::example$snippets/4l_tls.cpp[tag=graceful_shutdown,indent=0] ---- Shutdown is optional but recommended. Without it, the peer can't distinguish @@ -561,35 +429,7 @@ will not accept a TLS stream. Provide a separate overload taking [source,cpp] ---- -capy::task send_request(corosio::io_stream& stream) -{ - std::string request = "GET / HTTP/1.1\r\n\r\n"; - if (auto [ec, n] = co_await capy::write( - stream, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - std::string response; - co_await capy::read(stream, capy::string_dynamic_buffer(&response)); -} - -capy::task send_request(corosio::tls_stream& stream) -{ - std::string request = "GET / HTTP/1.1\r\n\r\n"; - if (auto [ec, n] = co_await capy::write( - stream, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - std::string response; - co_await capy::read(stream, capy::string_dynamic_buffer(&response)); -} - -// Plain socket uses the io_stream overload -corosio::tcp_socket sock(ioc); -co_await send_request(sock); - -// TLS stream uses the tls_stream overload -corosio::wolfssl_stream secure(&sock, ctx); -co_await send_request(secure); +include::example$snippets/4l_tls.cpp[tag=stream_overloads,indent=0] ---- == HTTPS Client Example @@ -605,73 +445,13 @@ for example: [source,cpp] ---- -ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); +include::example$snippets/4l_tls.cpp[tag=ca_bundle,indent=0] ---- ==== [source,cpp] ---- -capy::task https_get( - corosio::io_context& ioc, - std::string_view hostname, - std::uint16_t port) -{ - // Resolve hostname - corosio::resolver resolver(ioc); - auto [resolve_ec, results] = co_await resolver.resolve( - hostname, std::to_string(port)); - if (resolve_ec) - throw std::system_error(resolve_ec); - - // Connect TCP socket - corosio::tcp_socket sock(ioc); - sock.open(); - - for (auto const& entry : results) - { - auto [ec] = co_await sock.connect(entry.get_endpoint()); - if (!ec) - break; - } - - // Configure TLS - tls_context ctx; - if (auto ec = ctx.set_default_verify_paths(); ec) - throw std::system_error(ec); - if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) - throw std::system_error(ec); - - // Wrap the connected socket (pointer form) and handshake - corosio::wolfssl_stream secure(&sock, ctx); - secure.set_hostname(hostname); - if (auto [ec] = co_await secure.handshake(tls_role::client); ec) - throw std::system_error(ec); - - // Send HTTP request - std::string request = - "GET / HTTP/1.1\r\n" - "Host: " + std::string(hostname) + "\r\n" - "Connection: close\r\n" - "\r\n"; - - if (auto [ec, n] = co_await capy::write( - secure, capy::const_buffer(request.data(), request.size())); ec) - throw std::system_error(ec); - - // Read response - std::string response; - auto [ec, n] = co_await capy::read( - secure, capy::string_dynamic_buffer(&response)); - - // EOF expected when server closes - if (ec && ec != capy::cond::eof) - throw std::system_error(ec); - - std::cout << response << "\n"; - - // Graceful shutdown - co_await secure.shutdown(); -} +include::example$snippets/4l_tls.cpp[tag=https_get,indent=0] ---- == TLS Server Example @@ -680,49 +460,7 @@ Server with certificate and key: [source,cpp] ---- -capy::task tls_server( - corosio::io_context& ioc, - std::uint16_t port) -{ - // Configure server TLS context - tls_context ctx; - ctx.use_certificate_chain_file("server-fullchain.pem"); - ctx.use_private_key_file("server.key", tls_file_format::pem); - - // Set up acceptor - corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); - - for (;;) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - if (ec) break; - - // Spawn handler - capy::run_async(ioc.get_executor())( - handle_tls_client(std::move(peer), ctx)); - } -} - -capy::task handle_tls_client( - corosio::tcp_socket sock, - tls_context ctx) -{ - // Owning form: the handler owns the socket, so move it in - corosio::wolfssl_stream secure(std::move(sock), ctx); - - auto [ec] = co_await secure.handshake(tls_role::server); - if (ec) - co_return; - - // Handle encrypted connection... - char buf[1024]; - auto [read_ec, n] = co_await secure.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - - // Graceful shutdown - co_await secure.shutdown(); -} +include::example$snippets/4l_tls.cpp[tag=tls_server,indent=0] ---- == Mutual TLS (mTLS) @@ -733,13 +471,7 @@ For client certificate authentication: [source,cpp] ---- -tls_context server_ctx; -server_ctx.use_certificate_chain_file("server.pem"); -server_ctx.use_private_key_file("server.key", tls_file_format::pem); - -// Require client certificate -server_ctx.set_verify_mode(tls_verify_mode::require_peer); -server_ctx.load_verify_file("client-ca.pem"); +include::example$snippets/4l_tls.cpp[tag=mtls_server,indent=0] ---- === Client Side @@ -750,13 +482,7 @@ server's CA explicitly with `load_verify_file()` instead. [source,cpp] ---- -tls_context client_ctx; -client_ctx.set_default_verify_paths(); -client_ctx.set_verify_mode(tls_verify_mode::peer); - -// Provide client certificate -client_ctx.use_certificate_file("client.crt", tls_file_format::pem); -client_ctx.use_private_key_file("client.key", tls_file_format::pem); +include::example$snippets/4l_tls.cpp[tag=mtls_client,indent=0] ---- == Thread Safety diff --git a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc index 3d3504eaf..71b2739ae 100644 --- a/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc +++ b/doc/modules/ROOT/pages/4.guide/4m.error-handling.adoc @@ -17,13 +17,7 @@ error code alongside any values produced by the operation. Code snippets assume: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; -using namespace std::chrono_literals; +include::example$snippets/4m_error_handling.cpp[tag=assume] ---- ==== @@ -40,14 +34,7 @@ check for errors by testing `ec`. [source,cpp] ---- -// Void result (connect, handshake) -io_result<> // Contains: ec - -// Single value (read_some, write_some) -io_result // Contains: ec, n (bytes transferred) - -// Typed result (resolve) -io_result // Contains: ec, results +include::example$snippets/4m_error_handling.cpp[tag=result_shapes,indent=0] ---- == Structured Bindings Pattern @@ -56,17 +43,7 @@ Use structured bindings to extract results: [source,cpp] ---- -// Void result -auto [ec] = co_await sock.connect(endpoint); -if (ec) - std::cerr << "Connect failed: " << ec.message() << "\n"; - -// Value result -auto [ec, n] = co_await sock.read_some(buffer); -if (ec) - std::cerr << "Read failed: " << ec.message() << "\n"; -else - std::cout << "Read " << n << " bytes\n"; +include::example$snippets/4m_error_handling.cpp[tag=structured_bindings,indent=0] ---- This pattern gives you full control over error handling. @@ -77,11 +54,7 @@ You can also bind the whole result and read its members: [source,cpp] ---- -auto result = co_await sock.connect(endpoint); -if (!result.ec) - std::cout << "Connected successfully\n"; -else - std::cerr << "Failed: " << result.ec.message() << "\n"; +include::example$snippets/4m_error_handling.cpp[tag=direct_members,indent=0] ---- The payload lives in `result.values`; for single-value results, prefer @@ -94,10 +67,7 @@ test `ec` and throw explicitly: [source,cpp] ---- -auto [ec, n] = co_await sock.read_some(buffer); -if (ec) - throw std::system_error(ec); -// 'n' bytes were read +include::example$snippets/4m_error_handling.cpp[tag=throw_explicit,indent=0] ---- This keeps error handling explicit and avoids hidden control flow. @@ -113,12 +83,7 @@ This keeps error handling explicit and avoids hidden control flow. [source,cpp] ---- -auto [ec, n] = co_await sock.read_some(buf); -if (ec == capy::cond::eof) -{ - std::cout << "End of stream after " << n << " bytes\n"; - // Not an exceptional condition -} +include::example$snippets/4m_error_handling.cpp[tag=inspect_eof,indent=0] ---- === Throw When: @@ -130,15 +95,7 @@ if (ec == capy::cond::eof) [source,cpp] ---- -auto throw_on_error = [](auto result) { - if (result.ec) - throw std::system_error(result.ec); - return result; -}; - -throw_on_error(co_await sock.connect(endpoint)); -throw_on_error(co_await capy::write(sock, request)); -auto [ec, n] = throw_on_error(co_await capy::read(sock, buffer)); +include::example$snippets/4m_error_handling.cpp[tag=throw_style,indent=0] ---- == Common Error Codes @@ -185,8 +142,7 @@ condition, which matches both: [source,cpp] ---- -if (ec == capy::cond::canceled) - std::cout << "Operation was cancelled\n"; +include::example$snippets/4m_error_handling.cpp[tag=canceled_condition,indent=0] ---- === Timeout vs. Cancellation @@ -199,11 +155,7 @@ own cancellation, independent of the deadline) produces [source,cpp] ---- -auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); -if (ec == capy::cond::timeout) - std::cout << "Deadline elapsed before connecting\n"; -else if (ec == capy::cond::canceled) - std::cout << "Cancelled before the deadline\n"; +include::example$snippets/4m_error_handling.cpp[tag=timeout_vs_cancel,indent=0] ---- Never infer a timeout from `capy::cond::canceled`, and never infer a @@ -216,26 +168,14 @@ End-of-stream is signaled by the `capy::cond::eof` condition: [source,cpp] ---- -auto [ec, n] = co_await capy::read(stream, buffer); -if (ec == capy::cond::eof) -{ - std::cout << "Stream ended, read " << n << " bytes total\n"; - // This is often expected, not an error -} -else if (ec) -{ - std::cerr << "Unexpected error: " << ec.message() << "\n"; -} +include::example$snippets/4m_error_handling.cpp[tag=eof_expected,indent=0] ---- When you throw on read errors, filter out EOF if it is expected: [source,cpp] ---- -auto [ec, n] = co_await capy::read(stream, response); -if (ec && ec != capy::cond::eof) - throw std::system_error(ec); -// EOF is expected when server closes connection +include::example$snippets/4m_error_handling.cpp[tag=eof_filtered,indent=0] ---- == Partial Success @@ -244,13 +184,7 @@ Some operations may partially succeed before an error: [source,cpp] ---- -auto [ec, n] = co_await capy::write(stream, large_buffer); -if (ec) -{ - std::cerr << "Error after writing " << n << " of " - << buffer_size(large_buffer) << " bytes\n"; - // Can potentially resume from here -} +include::example$snippets/4m_error_handling.cpp[tag=partial_success,indent=0] ---- The composed operations (`read()`, `write()`) return the total bytes @@ -262,11 +196,7 @@ Corosio uses `std::error_code`, which supports categories: [source,cpp] ---- -if (ec.category() == std::system_category()) - // Operating system error - -if (ec.category() == std::generic_category()) - // Portable POSIX-style error +include::example$snippets/4m_error_handling.cpp[tag=error_categories,indent=0] ---- Capy's own errors (`eof`, `canceled`) don't expose a public category accessor; @@ -278,16 +208,7 @@ Use error conditions for portable comparison: [source,cpp] ---- -// Specific error (platform-dependent) -if (ec == std::errc::connection_refused) - // ... - -// Error condition (portable) -if (ec == capy::cond::canceled) - // Matches any cancellation error - -if (ec == capy::cond::eof) - // Matches end-of-stream +include::example$snippets/4m_error_handling.cpp[tag=compare_errors,indent=0] ---- == Exception Safety in Coroutines @@ -296,20 +217,7 @@ When using exceptions in coroutines, caught exceptions don't leak: [source,cpp] ---- -capy::task safe_operation() -{ - try - { - auto [ec] = co_await sock.connect(endpoint); - if (ec) - throw std::system_error(ec); - } - catch (std::system_error const& e) - { - std::cerr << "Connect failed: " << e.what() << "\n"; - // Exception handled here, doesn't propagate - } -} +include::example$snippets/4m_error_handling.cpp[tag=exception_safety,indent=0] ---- Uncaught exceptions in a task are stored and rethrown when the task is @@ -319,34 +227,7 @@ awaited. [source,cpp] ---- -capy::task connect_with_retry( - corosio::io_context& ioc, - corosio::endpoint ep, - int max_retries) -{ - corosio::tcp_socket sock(ioc); - - for (int attempt = 0; attempt < max_retries; ++attempt) - { - sock.open(); - auto [ec] = co_await sock.connect(ep); - - if (!ec) - co_return; // Success - - std::cerr << "Attempt " << (attempt + 1) - << " failed: " << ec.message() << "\n"; - - sock.close(); - - // Wait before retry (exponential backoff) - auto [dec] = co_await corosio::delay(std::chrono::seconds(1 << attempt)); - if (dec == capy::cond::canceled) - co_return; // Cancellation aborts the retry loop - } - - throw std::runtime_error("Failed to connect after retries"); -} +include::example$snippets/4m_error_handling.cpp[tag=robust_connect] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc index d17b40066..486af5c2d 100644 --- a/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc +++ b/doc/modules/ROOT/pages/4.guide/4n.buffers.adoc @@ -17,8 +17,7 @@ explains how to use buffers effectively. Code snippets assume: [source,cpp] ---- -#include -namespace capy = boost::capy; +include::example$snippets/4n_buffers.cpp[tag=assume] ---- ==== @@ -30,8 +29,7 @@ A writable region of memory: [source,cpp] ---- -char data[1024]; -capy::mutable_buffer buf(data, sizeof(data)); +include::example$snippets/4n_buffers.cpp[tag=mutable_buffer,indent=0] ---- === const_buffer @@ -40,8 +38,7 @@ A read-only region of memory: [source,cpp] ---- -std::string msg = "Hello"; -capy::const_buffer buf(msg.data(), msg.size()); +include::example$snippets/4n_buffers.cpp[tag=const_buffer,indent=0] ---- == Creating Buffers @@ -50,32 +47,21 @@ capy::const_buffer buf(msg.data(), msg.size()); [source,cpp] ---- -char data[1024]; -capy::mutable_buffer mbuf(data, sizeof(data)); - -const char* str = "Hello"; -capy::const_buffer cbuf(str, 5); +include::example$snippets/4n_buffers.cpp[tag=from_raw_arrays,indent=0] ---- === From std::string [source,cpp] ---- -std::string s = "Hello, World!"; - -// Writable (be careful with string invalidation) -capy::mutable_buffer mbuf(s.data(), s.size()); - -// Read-only -capy::const_buffer cbuf(s.data(), s.size()); +include::example$snippets/4n_buffers.cpp[tag=from_string,indent=0] ---- === From std::vector [source,cpp] ---- -std::vector vec(1024); -capy::mutable_buffer buf(vec.data(), vec.size()); +include::example$snippets/4n_buffers.cpp[tag=from_vector,indent=0] ---- == Buffer Sequences @@ -88,8 +74,7 @@ A single buffer is a valid buffer sequence: [source,cpp] ---- -capy::mutable_buffer buf(data, size); -co_await sock.read_some(buf); // Works directly +include::example$snippets/4n_buffers.cpp[tag=single_buffer,indent=0] ---- === Multiple Buffers @@ -98,18 +83,7 @@ Use arrays or vectors of buffers: [source,cpp] ---- -// Array of buffers -std::array bufs = { - capy::mutable_buffer(header, header_size), - capy::mutable_buffer(body, body_size) -}; -co_await sock.read_some(bufs); - -// Vector of buffers -std::vector send_bufs; -send_bufs.push_back(capy::const_buffer(header.data(), header.size())); -send_bufs.push_back(capy::const_buffer(body.data(), body.size())); -co_await sock.write_some(send_bufs); +include::example$snippets/4n_buffers.cpp[tag=multi_buffer,indent=0] ---- == Buffer Sequence Concepts @@ -118,13 +92,7 @@ Corosio uses concepts from Capy: [source,cpp] ---- -// Readable buffers (for writing to sockets) -template -auto write_some(ConstBufferSequence const& buffers); - -// Writable buffers (for reading from sockets) -template -auto read_some(MutableBufferSequence const& buffers); +include::example$snippets/4n_buffers.cpp[tag=sequence_concepts] ---- A type satisfies these concepts if it's iterable and yields buffer types. @@ -135,36 +103,39 @@ Get the total size of a buffer sequence: [source,cpp] ---- -std::array bufs = {...}; -std::size_t total = capy::buffer_size(bufs); +include::example$snippets/4n_buffers.cpp[tag=buffer_size,indent=0] ---- == buffer_slice -`capy::buffer_slice` returns a view over a byte range of a buffer sequence. -It exposes the bytes not yet transferred via `data()` and advances past -completed bytes with `remove_prefix(n)`: +`capy::buffer_slice` returns a byte sub-range of a buffer sequence, as a +value. The result is itself a buffer sequence, so it can be passed directly +to any I/O operation: [source,cpp] ---- -#include +include::example$snippets/4n_buffers.cpp[tag=buffer_slice_include] +include::example$snippets/4n_buffers.cpp[tag=buffer_slice,indent=0] +---- -std::array bufs = { - capy::mutable_buffer(header, 16), - capy::mutable_buffer(body, 1024) -}; +Except for the single-buffer case, the result borrows the underlying +sequence, which must outlive the slice. -auto slice = capy::buffer_slice(bufs); +== consuming_buffers -// After reading 20 bytes: -auto [ec, n] = co_await sock.read_some(slice.data()); -slice.remove_prefix(n); // Advance past the bytes read +When transferring data incrementally, `capy::consuming_buffers` is a cursor +that tracks progress through a sequence. It exposes the bytes not yet +transferred via `data()` and advances past completed bytes with +`consume(n)`: -// slice.data() now represents the remaining unread portion +[source,cpp] +---- +include::example$snippets/4n_buffers.cpp[tag=consuming_buffers_include] +include::example$snippets/4n_buffers.cpp[tag=consuming_buffers,indent=0] ---- This is used internally by `read()` and `write()` but can be used directly. -The underlying sequence must outlive the slice and any sequence obtained from +The underlying sequence must outlive the cursor and any view obtained from its `data()`. == buffer_param @@ -173,18 +144,9 @@ The `buffer_param` class type-erases buffer sequences: [source,cpp] ---- -#include - -void accept_any_buffer(corosio::buffer_param buffers) -{ - capy::mutable_buffer temp[8]; - std::size_t n = buffers.copy_to(temp, 8); - // Use temp[0..n-1] -} - -// Works with any buffer sequence (implicit conversion) -std::array bufs = {...}; -accept_any_buffer(bufs); +include::example$snippets/4n_buffers.cpp[tag=buffer_param_include] +include::example$snippets/4n_buffers.cpp[tag=buffer_param] +include::example$snippets/4n_buffers.cpp[tag=buffer_param_call,indent=0] ---- This enables non-templated code to work with any buffer type. @@ -198,25 +160,7 @@ operation: [source,cpp] ---- -// WRONG: buffer outlives string -capy::task bad_example(corosio::tcp_socket& sock) -{ - capy::const_buffer buf; - { - std::string temp = "Hello"; - buf = capy::const_buffer(temp.data(), temp.size()); - } // temp destroyed here! - - co_await sock.write_some(buf); // Undefined behavior -} - -// CORRECT: keep storage alive -capy::task good_example(corosio::tcp_socket& sock) -{ - std::string msg = "Hello"; - co_await sock.write_some( - capy::const_buffer(msg.data(), msg.size())); -} +include::example$snippets/4n_buffers.cpp[tag=lifetime] ---- === String Invalidation @@ -225,12 +169,7 @@ Be careful when using `std::string` as buffer storage: [source,cpp] ---- -std::string s = "Hello"; -capy::mutable_buffer buf(s.data(), s.size()); - -s += " World"; // May reallocate, invalidating buf! - -// Use buf here: UNDEFINED BEHAVIOR +include::example$snippets/4n_buffers.cpp[tag=string_invalidation,indent=0] ---- Either: @@ -247,55 +186,21 @@ Multiple buffers can be used for efficient scatter/gather operations: [source,cpp] ---- -struct message_header { ... }; -char body[1024]; - -std::array read_bufs = { - capy::mutable_buffer(&header, sizeof(header)), - capy::mutable_buffer(body, sizeof(body)) -}; - -auto [ec, n] = co_await sock.read_some(read_bufs); -// Data fills header first, then body +include::example$snippets/4n_buffers.cpp[tag=scatter_read,indent=0] ---- === Writing from Multiple Buffers (Gather) [source,cpp] ---- -std::string header = "HTTP/1.1 200 OK\r\n\r\n"; -std::string body = "Hello, World!"; - -std::array write_bufs = { - capy::const_buffer(header.data(), header.size()), - capy::const_buffer(body.data(), body.size()) -}; - -auto [ec, n] = co_await sock.write_some(write_bufs); -// Sends header followed by body in a single operation +include::example$snippets/4n_buffers.cpp[tag=gather_write,indent=0] ---- == Example: Reading a Fixed-Size Header [source,cpp] ---- -struct packet_header -{ - std::uint32_t magic; - std::uint32_t length; -}; - -capy::task read_header(corosio::io_stream& stream) -{ - packet_header header; - auto [ec, n] = co_await capy::read( - stream, capy::mutable_buffer(&header, sizeof(header))); - - if (ec) - throw std::system_error(ec); - - return header; -} +include::example$snippets/4n_buffers.cpp[tag=read_header] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc b/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc index eaa4bf22b..093423faa 100644 --- a/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc +++ b/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc @@ -19,9 +19,7 @@ platforms and use native overlapped I/O on Windows. Code snippets assume: [source,cpp] ---- -#include -#include -namespace corosio = boost::corosio; +include::example$snippets/4o_file_io.cpp[tag=assume] ---- ==== @@ -35,30 +33,14 @@ so it works with any algorithm that accepts an `io_stream&`. [source,cpp] ---- -corosio::stream_file f(ioc); -f.open("data.bin", corosio::file_base::read_only); - -char buf[4096]; -auto [ec, n] = co_await f.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - -if (ec == capy::cond::eof) - // reached end of file +include::example$snippets/4o_file_io.cpp[tag=stream_read,indent=0] ---- === Writing a File [source,cpp] ---- -corosio::stream_file f(ioc); -f.open("output.bin", - corosio::file_base::write_only - | corosio::file_base::create - | corosio::file_base::truncate); - -std::string data = "hello world"; -auto [ec, n] = co_await f.write_some( - capy::const_buffer(data.data(), data.size())); +include::example$snippets/4o_file_io.cpp[tag=stream_write,indent=0] ---- === Seeking @@ -67,9 +49,7 @@ The file position can be moved with `seek()`: [source,cpp] ---- -f.seek(0, corosio::file_base::seek_set); // beginning -f.seek(100, corosio::file_base::seek_cur); // forward 100 bytes -f.seek(-10, corosio::file_base::seek_end); // 10 bytes before end +include::example$snippets/4o_file_io.cpp[tag=seek,indent=0] ---- == Random Access File @@ -83,24 +63,14 @@ regions of a file. [source,cpp] ---- -corosio::random_access_file f(ioc); -f.open("data.bin", corosio::file_base::read_only); - -char buf[256]; -auto [ec, n] = co_await f.read_some_at( - 1024, // byte offset - capy::mutable_buffer(buf, sizeof(buf))); +include::example$snippets/4o_file_io.cpp[tag=read_at,indent=0] ---- === Writing at an Offset [source,cpp] ---- -corosio::random_access_file f(ioc); -f.open("data.bin", corosio::file_base::read_write); - -auto [ec, n] = co_await f.write_some_at( - 512, capy::const_buffer("patched", 7)); +include::example$snippets/4o_file_io.cpp[tag=write_at,indent=0] ---- == Open Flags @@ -125,10 +95,7 @@ Flags are combined with `|`: [source,cpp] ---- -f.open("log.txt", - corosio::file_base::write_only - | corosio::file_base::create - | corosio::file_base::append); +include::example$snippets/4o_file_io.cpp[tag=open_flags,indent=0] ---- == File Metadata @@ -137,29 +104,38 @@ Both file types provide synchronous metadata operations: [source,cpp] ---- -auto bytes = f.size(); // file size in bytes -f.resize(1024); // truncate or extend -f.sync_data(); // flush data to stable storage -f.sync_all(); // flush data and metadata +include::example$snippets/4o_file_io.cpp[tag=metadata,indent=0] ---- `stream_file` additionally provides `seek()` for repositioning. == Native Handle Access -Both file types support adopting and releasing native handles: +Both file types support releasing and adopting native handles. +`release()` transfers ownership out of the file object; the caller +becomes responsible for closing the handle: [source,cpp] ---- -// Release ownership — caller must close the handle -auto handle = f.release(); -assert(!f.is_open()); +include::example$snippets/4o_file_io.cpp[tag=native_release,indent=0] +---- + +`assign()` adopts a handle obtained from the platform's file API, such +as `open()` or `CreateFile`: -// Adopt an existing handle — file takes ownership -corosio::random_access_file f2(ioc); -f2.assign(handle); +[source,cpp] +---- +include::example$snippets/4o_file_io.cpp[tag=native_adopt,indent=0] ---- +[NOTE] +==== +On Windows, `assign` requires a handle that has never been associated +with an I/O completion port. A handle released from another Corosio +file object is already associated and cannot be re-adopted there; on +POSIX platforms no such restriction exists. +==== + == Error Handling File operations follow the same error model as sockets. Reads past @@ -167,15 +143,7 @@ end-of-file return `capy::cond::eof`: [source,cpp] ---- -auto [ec, n] = co_await f.read_some(buf); -if (ec == capy::cond::eof) -{ - // no more data -} -else if (ec) -{ - // I/O error -} +include::example$snippets/4o_file_io.cpp[tag=error_handling,indent=0] ---- Opening a nonexistent file with `read_only` throws `std::system_error`. diff --git a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc index 866c3fb65..20b6e487a 100644 --- a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc @@ -19,15 +19,7 @@ throughput than loopback TCP. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/4p_unix_sockets.cpp[tag=assume] ---- ==== @@ -69,26 +61,7 @@ connect, and both sides read and write byte streams. [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))); -} +include::example$snippets/4p_unix_sockets.cpp[tag=server] ---- The acceptor does **not** automatically remove the socket file on close. @@ -97,27 +70,14 @@ are done: [source,cpp] ---- -::unlink("/tmp/my_app.sock"); // remove stale socket -acc.bind(corosio::local_endpoint("/tmp/my_app.sock")); +include::example$snippets/4p_unix_sockets.cpp[tag=unlink_bind,indent=0] ---- === 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))); -} +include::example$snippets/4p_unix_sockets.cpp[tag=client] ---- === Socket Pairs @@ -129,17 +89,7 @@ on a worker thread. [source,cpp] ---- -corosio::local_stream_socket s1(ioc), s2(ioc); -if (auto ec = corosio::connect_pair(s1, s2)) - throw std::system_error(ec, "connect_pair"); - -// 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" +include::example$snippets/4p_unix_sockets.cpp[tag=stream_pair,indent=0] ---- == Datagram Sockets @@ -153,19 +103,7 @@ 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); +include::example$snippets/4p_unix_sockets.cpp[tag=datagram_connectionless,indent=0] ---- === Connected Mode @@ -174,14 +112,7 @@ After calling `connect()`, use `send`/`recv` without specifying the peer: [source,cpp] ---- -corosio::local_datagram_socket s1(ioc), s2(ioc); -if (auto ec = corosio::connect_pair(s1, s2)) - throw std::system_error(ec, "connect_pair"); - -co_await s1.send(capy::const_buffer("msg", 3)); - -auto [ec, n] = co_await s2.recv( - capy::mutable_buffer(buf, sizeof(buf))); +include::example$snippets/4p_unix_sockets.cpp[tag=datagram_pair,indent=0] ---- == Local Endpoints @@ -190,14 +121,7 @@ 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(); +include::example$snippets/4p_unix_sockets.cpp[tag=endpoints,indent=0] ---- The maximum path length is 103 bytes. This is the portable minimum across @@ -215,9 +139,7 @@ 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()); +include::example$snippets/4p_unix_sockets.cpp[tag=abstract,indent=0] ---- == Comparison with TCP diff --git a/doc/modules/ROOT/pages/4.guide/4q.udp.adoc b/doc/modules/ROOT/pages/4.guide/4q.udp.adoc index d4c0bb500..0d89e8a49 100644 --- a/doc/modules/ROOT/pages/4.guide/4q.udp.adoc +++ b/doc/modules/ROOT/pages/4.guide/4q.udp.adoc @@ -23,15 +23,7 @@ Simple, Unreliable]. This page covers the Corosio API: `udp_socket`, the Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/4q_udp.cpp[tag=assume] ---- ==== @@ -42,10 +34,7 @@ factory functions `udp::v4()` and `udp::v6()` return the IPv4 and IPv6 forms: [source,cpp] ---- -corosio::udp_socket sock(ioc); -sock.open(corosio::udp::v4()); // SOCK_DGRAM, AF_INET -// or -sock.open(corosio::udp::v6()); // SOCK_DGRAM, AF_INET6 +include::example$snippets/4q_udp.cpp[tag=protocol,indent=0] ---- `open()` defaults to `udp::v4()` when no protocol is supplied. The tag is also @@ -59,12 +48,7 @@ bound to a local endpoint: [source,cpp] ---- -corosio::udp_socket sock(ioc); -sock.open(corosio::udp::v4()); - -auto ec = sock.bind( - corosio::endpoint(corosio::ipv4_address::any(), 9000)); -if (ec) /* handle bind failure */; +include::example$snippets/4q_udp.cpp[tag=open_bind,indent=0] ---- `bind()` returns `std::error_code` rather than throwing — port-already-in-use @@ -86,11 +70,7 @@ receive captures the sender's address. [source,cpp] ---- -char const msg[] = "hello"; -corosio::endpoint dest(corosio::ipv4_address::loopback(), 9000); - -auto [ec, n] = co_await sock.send_to( - capy::const_buffer(msg, sizeof(msg)), dest); +include::example$snippets/4q_udp.cpp[tag=send_to,indent=0] ---- `send_to` either delivers the entire datagram to the network or fails — there @@ -104,15 +84,7 @@ address in the endpoint reference you pass in: [source,cpp] ---- -char buf[1500]; -corosio::endpoint sender; - -auto [ec, n] = co_await sock.recv_from( - capy::mutable_buffer(buf, sizeof(buf)), sender); -if (!ec) -{ - // buf[0..n) holds the datagram; sender holds the source address. -} +include::example$snippets/4q_udp.cpp[tag=recv_from,indent=0] ---- If the buffer is smaller than the incoming datagram, the excess is silently @@ -123,26 +95,7 @@ covers a typical Ethernet MTU; 65535 covers any IPv4/IPv6 datagram. [source,cpp] ---- -capy::task<> echo(corosio::io_context& ioc) -{ - corosio::udp_socket sock(ioc); - sock.open(corosio::udp::v4()); - auto ec = sock.bind( - corosio::endpoint(corosio::ipv4_address::any(), 9000)); - if (ec) co_return; - - char buf[1500]; - for (;;) - { - corosio::endpoint sender; - auto [rec, n] = co_await sock.recv_from( - capy::mutable_buffer(buf, sizeof(buf)), sender); - if (rec) co_return; - - co_await sock.send_to( - capy::const_buffer(buf, n), sender); - } -} +include::example$snippets/4q_udp.cpp[tag=echo] ---- Notice that one socket serves every client. UDP has no per-connection state, @@ -157,16 +110,7 @@ become available without endpoint arguments: [source,cpp] ---- -corosio::udp_socket sock(ioc); -auto [cec] = co_await sock.connect( - corosio::endpoint(corosio::ipv4_address::loopback(), 9000)); -if (cec) co_return; - -co_await sock.send(capy::const_buffer("ping", 4)); - -char buf[64]; -auto [rec, n] = co_await sock.recv( - capy::mutable_buffer(buf, sizeof(buf))); +include::example$snippets/4q_udp.cpp[tag=connected,indent=0] ---- If the socket is not yet open when `connect()` is called, it is opened @@ -205,9 +149,7 @@ Connected mode is useful for two reasons: [source,cpp] ---- -auto [ec, n] = co_await sock.recv_from( - capy::mutable_buffer(buf, sizeof(buf)), sender, - corosio::message_flags::peek); +include::example$snippets/4q_udp.cpp[tag=peek,indent=0] ---- == Socket Options @@ -216,11 +158,7 @@ Options are set with the typed wrappers in ``: [source,cpp] ---- -sock.set_option(corosio::socket_option::reuse_address(true)); -sock.set_option(corosio::socket_option::broadcast(true)); -sock.set_option(corosio::socket_option::receive_buffer_size(1 << 20)); - -auto bcast = sock.get_option(); +include::example$snippets/4q_udp.cpp[tag=options,indent=0] ---- Options commonly relevant to UDP: @@ -257,16 +195,7 @@ address. To receive them, bind to the multicast port and join the group: [source,cpp] ---- -corosio::udp_socket sock(ioc); -sock.open(corosio::udp::v4()); -sock.set_option(corosio::socket_option::reuse_address(true)); - -auto ec = sock.bind( - corosio::endpoint(corosio::ipv4_address::any(), 30001)); -if (ec) co_return; - -sock.set_option(corosio::socket_option::join_group_v4( - corosio::ipv4_address("239.255.0.1"))); +include::example$snippets/4q_udp.cpp[tag=multicast,indent=0] ---- Related options: @@ -299,7 +228,7 @@ with `errc::operation_canceled`: [source,cpp] ---- -sock.cancel(); +include::example$snippets/4q_udp.cpp[tag=cancel,indent=0] ---- Coroutine-scoped cancellation flows through the awaiting task's environment. @@ -308,10 +237,7 @@ completes with the canceled error when stop is requested: [source,cpp] ---- -std::stop_source ss; -capy::run_async(ioc.get_executor(), ss.get_token())(my_task()); -// ... -ss.request_stop(); // unblocks any in-flight recv_from inside my_task +include::example$snippets/4q_udp.cpp[tag=stop_cancel,indent=0] ---- For portable error comparison, check against `capy::cond::canceled` rather diff --git a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc index aaf21aaef..09842d4d0 100644 --- a/doc/modules/ROOT/pages/4.guide/4r.wait.adoc +++ b/doc/modules/ROOT/pages/4.guide/4r.wait.adoc @@ -20,14 +20,7 @@ that data is available or that the descriptor is writable. Code snippets assume: [source,cpp] ---- -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; -using namespace std::chrono_literals; +include::example$snippets/4r_wait.cpp[tag=assume] ---- ==== @@ -37,7 +30,7 @@ Three directions are exposed via the `wait_type` enum: [source,cpp] ---- -enum class wait_type { read, write, error }; +include::example$snippets/4r_wait.cpp[tag=wait_type_enum,indent=0] ---- The awaitable yields an `error_code` with no `bytes_transferred`. On @@ -46,11 +39,7 @@ from it. [source,cpp] ---- -auto [ec] = co_await sock.wait(corosio::wait_type::read); -if (!ec) { - // sock is readable: a subsequent read_some will return data - // without blocking. -} +include::example$snippets/4r_wait.cpp[tag=wait_read,indent=0] ---- == Wrapping a Nonblocking C API @@ -60,9 +49,10 @@ manage their own buffers on an `O_NONBLOCK` fd. They need a "tell me when the fd is ready" primitive that does not steal bytes from the stream. -The typical pattern: +The typical pattern (sketched against a hypothetical libpq +integration): -[source,cpp] +[source,cpp,role=pseudocode] ---- // pq is some PG connection holding a nonblocking socket fd. corosio::tcp_socket sock = adopt_fd(ioc, PQsocket(pq)); @@ -88,11 +78,7 @@ blocking: [source,cpp] ---- -auto [wec] = co_await acceptor.wait(corosio::wait_type::read); -if (wec) co_return; - -corosio::tcp_socket peer(ioc); -auto [aec] = co_await acceptor.accept(peer); +include::example$snippets/4r_wait.cpp[tag=acceptor_wait,indent=0] ---- This is useful when application-level conditions must be checked @@ -107,10 +93,7 @@ signaling) without holding an `accept()` call open. [source,cpp] ---- -auto waiter = [&]() -> capy::task<> { - auto [ec] = co_await sock.wait(corosio::wait_type::read); - // ec == capy::cond::canceled if sock.cancel() was invoked -}; +include::example$snippets/4r_wait.cpp[tag=wait_cancel,indent=0] ---- xref:4.guide/4h.timers.adoc[`timeout()`] composes with `wait()` the @@ -119,10 +102,7 @@ yields an `io_result` like any other awaitable: [source,cpp] ---- -auto [ec] = co_await corosio::timeout( - sock.wait(corosio::wait_type::read), 200ms); -if (ec == capy::cond::timeout) - std::cout << "No readiness within 200ms\n"; +include::example$snippets/4r_wait.cpp[tag=wait_timeout,indent=0] ---- == `wait_type::write` Semantics diff --git a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc index bc3996e1a..938cf76fa 100644 --- a/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc +++ b/doc/modules/ROOT/pages/5.testing/5a.mocket.adoc @@ -20,10 +20,7 @@ for byte-level deterministic tests of corosio I/O code. Code snippets assume: [source,cpp] ---- -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/5a_mocket.cpp[tag=assume] ---- ==== @@ -46,9 +43,7 @@ Mockets are created paired with a peer socket, connected via loopback: [source,cpp] ---- -corosio::io_context ioc; - -auto [m, peer] = corosio::test::make_mocket_pair(ioc); +include::example$snippets/5a_mocket.cpp[tag=creating,indent=0] ---- `make_mocket_pair` returns `std::pair, Socket>`. The @@ -62,13 +57,7 @@ Use `provide()` to stage bytes that the mocket itself will hand back from [source,cpp] ---- -m.provide("HTTP/1.1 200 OK\r\n\r\nHello"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - char buf[64] = {}; - auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf)); - // buf[0..n] == "HTTP/1.1 200 OK\r\n\r\nHello" -}; +include::example$snippets/5a_mocket.cpp[tag=provide,indent=0] ---- Multiple `provide()` calls append. Reads consume from the front of the @@ -81,13 +70,7 @@ Use `expect()` to declare bytes that the code under test must write next: [source,cpp] ---- -m.expect("GET / HTTP/1.1\r\n\r\n"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - auto [ec, n] = co_await m_ref.write_some( - capy::const_buffer("GET / HTTP/1.1\r\n\r\n", 18)); - // ec is empty; n == 18 -}; +include::example$snippets/5a_mocket.cpp[tag=expect,indent=0] ---- `write_some` matches the leading bytes of the buffer sequence against the @@ -107,21 +90,7 @@ transfers: [source,cpp] ---- -// max_read_size = 4, max_write_size = 3 force short transfers. -auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, 4, 3); - -m.provide("0123456789"); -m.expect("abcdef"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - char buf[16] = {}; - auto [rec, rn] = co_await m_ref.read_some(capy::make_buffer(buf)); - // rn == 4 ("0123") - - auto [wec, wn] = co_await m_ref.write_some( - capy::const_buffer("abcdef", 6)); - // wn == 3 (matched "abc") -}; +include::example$snippets/5a_mocket.cpp[tag=chunked,indent=0] ---- A value of `0` for either size parameter throws `std::logic_error` from @@ -134,12 +103,7 @@ buffers are empty: [source,cpp] ---- -auto ec = m.close(); -if (ec == capy::error::test_failure) -{ - // Either provide() data was never read, - // or expect() data was never written. -} +include::example$snippets/5a_mocket.cpp[tag=close_check,indent=0] ---- Always call `close()` at the end of a test that uses `provide` / `expect` @@ -159,14 +123,7 @@ etc.), name the specialization explicitly: [source,cpp] ---- -using socket_type = corosio::native_tcp_socket; -using acceptor_type = corosio::native_tcp_acceptor; -using mocket_type = corosio::test::basic_mocket; - -corosio::io_context ioc(backend); - -auto [m, peer] = - corosio::test::make_mocket_pair(ioc); +include::example$snippets/5a_mocket.cpp[tag=native,indent=0] ---- == Underlying Socket Access @@ -176,10 +133,7 @@ stack other streams (TLS, framing) on top of a mocket: [source,cpp] ---- -auto [m, peer] = corosio::test::make_mocket_pair(ioc); - -corosio::tcp_socket& under = m.socket(); -// Pass `under` into a TLS stream, a custom framing layer, etc. +include::example$snippets/5a_mocket.cpp[tag=socket_access,indent=0] ---- [IMPORTANT] diff --git a/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc b/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc index 9a3c8ae40..f0a5ecc67 100644 --- a/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc +++ b/doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc @@ -19,10 +19,7 @@ byte-level staging in xref:5.testing/5a.mocket.adoc[`mocket`] cannot reproduce. Code snippets assume: [source,cpp] ---- -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/5b_socket_pair.cpp[tag=assume] ---- ==== @@ -30,11 +27,7 @@ namespace capy = boost::capy; [source,cpp] ---- -template -std::pair -make_socket_pair(corosio::io_context& ctx); +include::example$snippets/5b_socket_pair.cpp[tag=signature] ---- The function: @@ -47,27 +40,15 @@ The function: Both sockets come back `is_open()` and ready to use. If bind, listen, accept, or connect fails, `make_socket_pair` throws -`std::runtime_error` with the underlying `error_code::message()` -appended. +`std::runtime_error` naming the failing step; the underlying +`error_code::message()` appears in the exception text for bind and +listen failures, and on `stderr` for accept and connect failures. == Round Trip [source,cpp] ---- -corosio::io_context ioc; - -auto [s1, s2] = corosio::test::make_socket_pair(ioc); - -auto task = [](corosio::tcp_socket& a, corosio::tcp_socket& b) - -> capy::task<> { - co_await a.write_some(capy::const_buffer("ping", 4)); - - char buf[8] = {}; - auto [ec, n] = co_await b.read_some(capy::make_buffer(buf)); - // buf[0..n] == "ping" -}; -capy::run_async(ioc.get_executor())(task(s1, s2)); -ioc.run(); +include::example$snippets/5b_socket_pair.cpp[tag=round_trip,indent=0] ---- == The `Linger` Template Parameter @@ -83,10 +64,7 @@ shutdown: [source,cpp] ---- -auto [s1, s2] = corosio::test::make_socket_pair< - corosio::tcp_socket, - corosio::tcp_acceptor, - /*Linger=*/false>(ioc); +include::example$snippets/5b_socket_pair.cpp[tag=linger_false,indent=0] ---- == When to Use vs. `mocket` diff --git a/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc b/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc index 30a1c401e..9cc987c44 100644 --- a/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc +++ b/doc/modules/ROOT/pages/5.testing/5c.patterns.adoc @@ -18,11 +18,7 @@ scenarios. Each recipe is a small standalone block; copy and adapt. Code snippets assume: [source,cpp] ---- -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/5c_patterns.cpp[tag=assume] ---- ==== @@ -33,18 +29,7 @@ with `expect()` and assert on `close()` at the end: [source,cpp] ---- -corosio::io_context ioc; -auto [m, peer] = corosio::test::make_mocket_pair(ioc); - -m.expect("GET /api/v1/users HTTP/1.1\r\n\r\n"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - co_await my_http_get(m_ref, "/api/v1/users"); -}; -capy::run_async(ioc.get_executor())(task(m)); -ioc.run(); - -auto ec = m.close(); // !ec means everything was written +include::example$snippets/5c_patterns.cpp[tag=request_format,indent=0] ---- == Staging Server Responses @@ -54,16 +39,7 @@ When a function under test must consume canned bytes, stage them with [source,cpp] ---- -m.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "Hello"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - auto response = co_await my_http_read(m_ref); - // assert on parsed response -}; +include::example$snippets/5c_patterns.cpp[tag=staged_response,indent=0] ---- == Simulating Chunked / Partial I/O @@ -73,19 +49,7 @@ much as the kernel had. Force short reads to flush out that bug: [source,cpp] ---- -auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, /*max_read_size=*/4); - -m.provide("ABCDEFGH"); - -auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { - std::string acc; - char buf[16]; - for (int i = 0; i < 2; ++i) - { - auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf)); - acc.append(buf, n); // n == 4 each time - } -}; +include::example$snippets/5c_patterns.cpp[tag=chunked_reads,indent=0] ---- The same applies to `max_write_size` for write-loop testing. @@ -103,11 +67,7 @@ higher-level stream, drive the mocket directly instead of layering on [source,cpp] ---- -auto [m, peer] = corosio::test::make_mocket_pair(ioc); - -// Pass m.socket() into a TLS stream or other layer in production code: -corosio::tcp_socket& under = m.socket(); -// e.g., openssl_stream tls(&under, tls_ctx); +include::example$snippets/5c_patterns.cpp[tag=layering,indent=0] ---- This is the right tool when the bytes you want to stage are below a @@ -121,20 +81,7 @@ When fidelity matters more than determinism — for example, exercising [source,cpp] ---- -corosio::io_context ioc; -auto [s1, s2] = corosio::test::make_socket_pair(ioc); - -auto task = [](corosio::tcp_socket& a, corosio::tcp_socket& b) - -> capy::task<> { - auto [wec, wn] = - co_await a.write_some(capy::const_buffer("payload", 7)); - - char buf[16] = {}; - auto [ec, n] = co_await b.read_some(capy::make_buffer(buf)); - // buf[0..n] == "payload" -}; -capy::run_async(ioc.get_executor())(task(s1, s2)); -ioc.run(); +include::example$snippets/5c_patterns.cpp[tag=end_to_end,indent=0] ---- == Deterministic Close-Verification @@ -144,10 +91,7 @@ Always call `close()` on the mocket at the end of a test that uses [source,cpp] ---- -auto ec = m.close(); -// ec == capy::error::test_failure means leftover provide() data was -// never read, or expect() data was never written. Either way, the test -// would have passed silently without this check. +include::example$snippets/5c_patterns.cpp[tag=close_verification,indent=0] ---- This is the line that catches "the test passed because the code under diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 7d88e27fb..3d038c726 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -100,56 +100,14 @@ and xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] for back NOTE: Code examples in this documentation assume these declarations are in effect: [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; +include::example$snippets/index_page.cpp[tag=convention] ---- == Quick Example [source,cpp] ---- -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; - -capy::task connect_example(corosio::io_context& ioc) -{ - corosio::tcp_socket s(ioc); - s.open(); - - // Connect using structured bindings - auto [ec] = co_await s.connect( - corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); - - if (ec) - { - std::cerr << "Connect failed: " << ec.message() << "\n"; - co_return; - } - - // Read some data - char buf[1024]; - auto [read_ec, n] = co_await s.read_some( - capy::mutable_buffer(buf, sizeof(buf))); - - if (!read_ec) - std::cout << "Received " << n << " bytes\n"; -} - -int main() -{ - corosio::io_context ioc; - capy::run_async(ioc.get_executor())(connect_example(ioc)); - ioc.run(); -} +include::example$programs/index_page_connect.cpp[tag=full] ---- == Next Steps diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index cbc3b3e70..691f6025c 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -1,221 +1,119 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// 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 -// - -= Quick Start - -This guide walks you through building your first network application with -Corosio: a simple echo server that accepts connections and echoes back -whatever clients send. - -[NOTE] -==== -Code snippets assume: -[source,cpp] ----- -#include -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; ----- -==== - -== Step 1: Create the I/O Context - -Every Corosio program starts with an `io_context`. This is the event loop that -processes all asynchronous operations: - -[source,cpp] ----- -int main() -{ - corosio::io_context ioc; - - // ... create and start server ... - - ioc.run(); // Process events until all work completes -} ----- - -The `run()` method blocks and processes events until there's no more work. - -== Step 2: Define the Server Class - -The `tcp_server` base class provides connection pooling and lifecycle management. -Derive from it and define a worker class: - -[source,cpp] ----- -class worker : public corosio::tcp_server::worker_base -{ - corosio::io_context& ctx_; - corosio::tcp_socket sock_; - std::string buf_; - -public: - worker(corosio::io_context& ctx) - : ctx_(ctx) - , sock_(ctx) - { - buf_.reserve(4096); - } - - corosio::tcp_socket& socket() override { return sock_; } - - void run(corosio::tcp_server::launcher launch) override - { - launch(ctx_.get_executor(), do_session()); - } - - capy::task<> do_session(); -}; - -// Build a pool of workers as a range of unique_ptr. -inline auto -make_workers(corosio::io_context& ctx, int n) -{ - std::vector> v; - v.reserve(n); - for (int i = 0; i < n; ++i) - v.push_back(std::make_unique(ctx)); - return v; -} - -class echo_server : public corosio::tcp_server -{ -public: - echo_server(corosio::io_context& ctx, int max_workers) - : tcp_server(ctx, ctx.get_executor()) - { - set_workers(make_workers(ctx, max_workers)); - } -}; ----- - -Key points: - -* Workers derive from `worker_base` and implement `socket()` and `run()` -* Each worker owns its socket and any per-connection state -* The `launcher` starts the session coroutine and returns the worker to the pool when done - -== Step 3: Write the Echo Session - -The session coroutine reads data and echoes it back: - -[source,cpp] ----- -capy::task<> worker::do_session() -{ - for (;;) - { - buf_.resize(4096); - - // Read some data - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf_.data(), buf_.size())); - - if (ec || n == 0) - break; // Connection closed or error - - buf_.resize(n); - - // Echo it back - auto [wec, wn] = co_await capy::write( - sock_, capy::const_buffer(buf_.data(), buf_.size())); - - if (wec) - break; // Write error - } - - sock_.close(); -} ----- - -Key points: - -* `read_some()` returns when _any_ data is available -* `capy::write()` (the free function) writes _all_ data or fails -* Structured bindings extract the error code and byte count -* When the coroutine ends, the worker automatically returns to the pool - -== Step 4: Put It Together - -[source,cpp] ----- -int main() -{ - corosio::io_context ioc; - - echo_server server(ioc, 100); - - auto ec = server.bind(corosio::endpoint(8080)); - if (ec) - { - std::cerr << "Bind failed: " << ec.message() << "\n"; - return 1; - } - - std::cout << "Echo server listening on port 8080\n"; - - server.start(); - ioc.run(); -} ----- - -== Testing the Server - -Start the server, then use netcat or telnet to test: - -[source,bash] ----- -$ telnet localhost 8080 -Trying 127.0.0.1... -Connected to localhost. -Hello, World! -Hello, World! ----- - -== Error Handling Patterns - -Corosio supports two error handling patterns: - -=== Structured Bindings (Recommended for Most Code) - -[source,cpp] ----- -auto [ec, n] = co_await sock.read_some(buf); -if (ec) -{ - // Handle error -} ----- - -=== Exceptions (Concise for Simple Cases) - -[source,cpp] ----- -auto [ec, n] = co_await sock.read_some(buf); -if (ec) throw std::system_error(ec); // Throws if read fails ----- - -Checking `ec` and throwing `std::system_error(ec)` is the explicit exception -style for handling a failed operation. - -== Next Steps - -Now that you have a working echo server: - -* xref:4.guide/4k.tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server -* xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals -* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands -* xref:3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — Make HTTP requests -* xref:4.guide/4c.io-context.adoc[I/O Context Guide] — Understand the event loop -* xref:4.guide/4d.sockets.adoc[Sockets Guide] — Deep dive into socket operations +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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 +// + += Quick Start + +This guide walks you through building your first network application with +Corosio: a simple echo server that accepts connections and echoes back +whatever clients send. + +[NOTE] +==== +Code snippets assume: +[source,cpp] +---- +include::example$programs/quick_start_echo.cpp[tag=assume] +---- +==== + +== Step 1: Create the I/O Context + +Every Corosio program starts with an `io_context`. This is the event loop that +processes all asynchronous operations: + +[source,cpp] +---- +include::example$programs/quick_start_context.cpp[tag=full] +---- + +The `run()` method blocks and processes events until there's no more work. + +== Step 2: Define the Server Class + +The `tcp_server` base class provides connection pooling and lifecycle management. +Derive from it and define a worker class: + +[source,cpp] +---- +include::example$programs/quick_start_echo.cpp[tag=server_class] +---- + +Key points: + +* Workers derive from `worker_base` and implement `socket()` and `run()` +* Each worker owns its socket and any per-connection state +* The `launcher` starts the session coroutine and returns the worker to the pool when done + +== Step 3: Write the Echo Session + +The session coroutine reads data and echoes it back: + +[source,cpp] +---- +include::example$programs/quick_start_echo.cpp[tag=session] +---- + +Key points: + +* `read_some()` returns when _any_ data is available +* `capy::write()` (the free function) writes _all_ data or fails +* Structured bindings extract the error code and byte count +* When the coroutine ends, the worker automatically returns to the pool + +== Step 4: Put It Together + +[source,cpp] +---- +include::example$programs/quick_start_echo.cpp[tag=main] +---- + +== Testing the Server + +Start the server, then use netcat or telnet to test: + +[source,bash] +---- +$ telnet localhost 8080 +Trying 127.0.0.1... +Connected to localhost. +Hello, World! +Hello, World! +---- + +== Error Handling Patterns + +Corosio supports two error handling patterns: + +=== Structured Bindings (Recommended for Most Code) + +[source,cpp] +---- +include::example$snippets/quick_start.cpp[tag=error_bindings,indent=0] +---- + +=== Exceptions (Concise for Simple Cases) + +[source,cpp] +---- +include::example$snippets/quick_start.cpp[tag=error_exceptions,indent=0] +---- + +Checking `ec` and throwing `std::system_error(ec)` is the explicit exception +style for handling a failed operation. + +== Next Steps + +Now that you have a working echo server: + +* xref:4.guide/4k.tcp-server.adoc[TCP Server Guide] — Deep dive into tcp_server +* xref:4.guide/4a.tcp-networking.adoc[TCP/IP Networking] — Networking fundamentals +* xref:4.guide/4b.concurrent-programming.adoc[Concurrent Programming] — Coroutines and strands +* xref:3.tutorials/3b.http-client.adoc[HTTP Client Tutorial] — Make HTTP requests +* xref:4.guide/4c.io-context.adoc[I/O Context Guide] — Understand the event loop +* xref:4.guide/4d.sockets.adoc[Sockets Guide] — Deep dive into socket operations diff --git a/doc/package-lock.json b/doc/package-lock.json index 38973813b..981979490 100644 --- a/doc/package-lock.json +++ b/doc/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@antora/collector-extension": "^1.0.3", "@antora/expand-path-helper": "^3.0.0", "@antora/lunr-extension": "^1.0.0-alpha.12", "@asciidoctor/tabs": "^1.0.0-beta.6", @@ -53,6 +54,22 @@ "node": ">=16.0.0" } }, + "node_modules/@antora/collector-extension": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@antora/collector-extension/-/collector-extension-1.0.3.tgz", + "integrity": "sha512-ji/6AAL2BL1CNogYhiCy4IErkGvAAmQtJZW4UwVlgwIv4LRC9xTeJJpstC4u5nMkvAHAKxSpja4bNEjMotklAw==", + "license": "MPL-2.0", + "dependencies": { + "@antora/expand-path-helper": "~3.0", + "@antora/run-command-helper": "~1.0", + "cache-directory": "~2.0", + "fast-glob": "~3.3", + "js-yaml": "~4.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@antora/content-aggregator": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.14.tgz", @@ -224,6 +241,15 @@ "node": ">=16.0.0" } }, + "node_modules/@antora/run-command-helper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@antora/run-command-helper/-/run-command-helper-1.0.3.tgz", + "integrity": "sha512-srrGuqScOL/4BJ51uRJl/aPEme4sQ1R2OmuXizhXQVJBJbh9rsYsi4ys6W9Q/J/UDEClH2c2L0Y0TfJ331/8Lg==", + "license": "MPL-2.0", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@antora/site-generator": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.14.tgz", @@ -1637,7 +1663,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2004,7 +2029,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4.0" } diff --git a/doc/package.json b/doc/package.json index cd9650963..add6848fd 100644 --- a/doc/package.json +++ b/doc/package.json @@ -5,6 +5,7 @@ "antora": "3.1.14" }, "dependencies": { + "@antora/collector-extension": "^1.0.3", "@antora/expand-path-helper": "^3.0.0", "@antora/lunr-extension": "^1.0.0-alpha.12", "@asciidoctor/tabs": "^1.0.0-beta.6", diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 995309ebe..0cb1a00f8 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -15,4 +15,9 @@ add_subdirectory(reconnect) if(WolfSSL_FOUND) add_subdirectory(https-client) -endif() \ No newline at end of file +endif() +# Doc-canonical TLS context examples; compiled so the excerpts in +# 3d.tls-context.adoc cannot rot. Exits nonzero without credential +# files, so it is built but not run. +add_executable(corosio_example_tls_context tls_context_examples.cpp) +target_link_libraries(corosio_example_tls_context PRIVATE Boost::corosio) diff --git a/example/client/http_client.cpp b/example/client/http_client.cpp index 6e47342ea..ed4134515 100644 --- a/example/client/http_client.cpp +++ b/example/client/http_client.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/corosio // +// tag::assume[] #include #include #include @@ -15,31 +16,43 @@ #include #include #include + +// end::assume[] #include #include #include #include +// tag::assume[] namespace corosio = boost::corosio; namespace capy = boost::capy; +// end::assume[] + +// tag::build_request[] +std::string build_request(std::string_view host) +{ + return "GET / HTTP/1.1\r\n" + "Host: " + std::string(host) + "\r\n" + "Connection: close\r\n" + "\r\n"; +} +// end::build_request[] +// tag::do_request[] // Coroutine that performs the HTTP GET request capy::task do_request( corosio::io_stream& stream, std::string_view host) { - // Build and send the HTTP request - std::string request = - "GET / HTTP/1.1\r\n" - "Host: " + std::string(host) + "\r\n" - "Connection: close\r\n" - "\r\n"; + // Build and send the request + std::string request = build_request(host); if (auto [ec, n] = co_await capy::write( stream, capy::const_buffer(request.data(), request.size())); ec) throw std::system_error(ec); // Read the entire response until EOF, one fixed chunk at a time + // tag::read_loop[] std::string response; for (;;) { @@ -55,10 +68,13 @@ do_request( break; } } + // end::read_loop[] std::cout << response << std::endl; } +// end::do_request[] +// tag::run_client[] // Parent coroutine that creates and connects the socket capy::task run_client( @@ -75,7 +91,9 @@ run_client( co_await do_request(s, addr.to_string()); } +// end::run_client[] +// tag::main[] int main(int argc, char* argv[]) { @@ -113,3 +131,4 @@ main(int argc, char* argv[]) return EXIT_SUCCESS; } +// end::main[] diff --git a/example/echo-server/echo_server.cpp b/example/echo-server/echo_server.cpp index be358d71e..872a45355 100644 --- a/example/echo-server/echo_server.cpp +++ b/example/echo-server/echo_server.cpp @@ -7,17 +7,22 @@ // Official repository: https://github.com/cppalliance/corosio // +// tag::assume[] #include #include #include #include +// end::assume[] #include #include +// tag::assume[] namespace corosio = boost::corosio; namespace capy = boost::capy; +// end::assume[] +// tag::worker_class[] class echo_worker : public corosio::tcp_server::worker_base { corosio::io_context& ctx_; @@ -41,24 +46,30 @@ class echo_worker : public corosio::tcp_server::worker_base launch(ctx_.get_executor(), do_session()); } - capy::task<> do_session() - { - for (;;) - { - auto [ec, n] = co_await sock_.read_some( - capy::mutable_buffer(buf_, sizeof buf_)); + capy::task<> do_session(); +}; +// end::worker_class[] - auto [wec, wn] = co_await capy::write( - sock_, capy::const_buffer(buf_, n)); +// tag::session[] +capy::task<> echo_worker::do_session() +{ + for (;;) + { + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf_, sizeof buf_)); - if (wec || ec) - break; - } + auto [wec, wn] = co_await capy::write( + sock_, capy::const_buffer(buf_, n)); - sock_.close(); + if (wec || ec) + break; } -}; + sock_.close(); +} +// end::session[] + +// tag::server[] inline auto make_echo_workers(corosio::io_context& ctx, int n) { @@ -78,7 +89,9 @@ class echo_server : public corosio::tcp_server set_workers(make_echo_workers(ctx, max_workers)); } }; +// end::server[] +// tag::main[] int main(int argc, char* argv[]) { if (argc != 3) @@ -132,3 +145,4 @@ int main(int argc, char* argv[]) return EXIT_SUCCESS; } +// end::main[] diff --git a/example/hash-server/hash_server.cpp b/example/hash-server/hash_server.cpp index 30917d9b6..786469229 100644 --- a/example/hash-server/hash_server.cpp +++ b/example/hash-server/hash_server.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/corosio // +// tag::assume[] #include #include #include @@ -17,15 +18,19 @@ #include #include +// end::assume[] #include #include #include #include +// tag::assume[] namespace corosio = boost::corosio; namespace capy = boost::capy; +// end::assume[] /// Compute FNV-1a hash on the thread pool. +// tag::hash_function[] capy::task compute_fnv1a( char const* data, std::size_t len ) { @@ -40,6 +45,7 @@ compute_fnv1a( char const* data, std::size_t len ) } co_return h; } +// end::hash_function[] /// Format a 64-bit value as 16 lowercase hex characters. std::string @@ -56,6 +62,7 @@ to_hex( std::uint64_t v ) } /// Handle a single client connection. +// tag::session[] capy::task<> do_session( corosio::tcp_socket sock, @@ -75,8 +82,10 @@ do_session( // Switch to thread pool for CPU-bound hash computation, // then automatically resume on io_context when done + // tag::run_switch[] auto hash = co_await capy::run( pool.get_executor() )( compute_fnv1a( buf, n ) ); + // end::run_switch[] // Send hex result back to client (on io_context) auto result = to_hex( hash ) + "\n"; @@ -88,8 +97,10 @@ do_session( sock.close(); } +// end::session[] /// Accept loop — spawns a session coroutine per connection. +// tag::accept[] capy::task<> do_accept( corosio::io_context& ioc, @@ -108,7 +119,9 @@ do_accept( do_session( std::move( peer ), pool ) ); } } +// end::accept[] +// tag::main[] int main( int argc, char* argv[] ) { @@ -145,3 +158,4 @@ main( int argc, char* argv[] ) return EXIT_SUCCESS; } +// end::main[] diff --git a/example/https-client/https_client.cpp b/example/https-client/https_client.cpp index b4050b804..46c86e888 100644 --- a/example/https-client/https_client.cpp +++ b/example/https-client/https_client.cpp @@ -8,7 +8,9 @@ // #include +// tag::tls_include[] #include +// end::tls_include[] #include #include #include @@ -25,18 +27,23 @@ namespace corosio = boost::corosio; namespace capy = boost::capy; +std::string build_request(std::string_view host) +{ + return "GET / HTTP/1.1\r\n" + "Host: " + std::string(host) + "\r\n" + "Connection: close\r\n" + "\r\n"; +} + +// tag::tls_client[] // Coroutine that performs the HTTPS GET request capy::task do_request( corosio::tls_stream& stream, std::string_view host) { - // Build and send the HTTP request - std::string request = - "GET / HTTP/1.1\r\n" - "Host: " + std::string(host) + "\r\n" - "Connection: close\r\n" - "\r\n"; + // Build and send the request + std::string request = build_request(host); if (auto [ec, n] = co_await capy::write( stream, capy::const_buffer(request.data(), request.size())); ec) throw std::system_error(ec); @@ -83,19 +90,22 @@ run_client( if (auto ec = ctx.set_verify_mode(corosio::tls_verify_mode::peer); ec) throw std::system_error(ec); - // Wrap socket in TLS stream + // tag::tls_wrap[] + // Wrap the connected socket without taking ownership (pointer form) corosio::wolfssl_stream secure(&s, ctx); secure.set_hostname(hostname); // Perform TLS handshake if (auto [ec] = co_await secure.handshake(corosio::tls_role::client); ec) throw std::system_error(ec); + // end::tls_wrap[] co_await do_request(secure, hostname); - if( auto [ec] = co_await secure.shutdown(); ec) + if (auto [ec] = co_await secure.shutdown(); ec) throw std::system_error(ec); } +// end::tls_client[] int main(int argc, char* argv[]) diff --git a/example/nslookup/nslookup.cpp b/example/nslookup/nslookup.cpp index 9508a3de0..764de5d68 100644 --- a/example/nslookup/nslookup.cpp +++ b/example/nslookup/nslookup.cpp @@ -7,18 +7,23 @@ // Official repository: https://github.com/cppalliance/corosio // +// tag::assume[] #include #include #include +// end::assume[] #include #include #include +// tag::assume[] namespace corosio = boost::corosio; namespace capy = boost::capy; +// end::assume[] // Coroutine that performs the DNS lookup +// tag::lookup[] capy::task do_lookup( corosio::io_context& ioc, @@ -56,7 +61,9 @@ do_lookup( std::cout << "\nTotal: " << results.size() << " addresses\n"; } +// end::lookup[] +// tag::main[] int main(int argc, char* argv[]) { @@ -81,3 +88,4 @@ main(int argc, char* argv[]) return EXIT_SUCCESS; } +// end::main[] diff --git a/example/reconnect/reconnect.cpp b/example/reconnect/reconnect.cpp index 24bb26f2a..9fea849fb 100644 --- a/example/reconnect/reconnect.cpp +++ b/example/reconnect/reconnect.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/corosio // +// tag::assume[] #include #include #include @@ -16,14 +17,17 @@ #include #include +// end::assume[] #include #include #include +// tag::assume[] #include #include namespace corosio = boost::corosio; namespace capy = boost::capy; +// end::assume[] /** Exponential backoff delay sequence. @@ -42,6 +46,7 @@ namespace capy = boost::capy; co_await delay(backoff.next()); // 500ms @endcode */ +// tag::backoff[] struct exponential_backoff { using duration = std::chrono::milliseconds; @@ -74,8 +79,10 @@ struct exponential_backoff delay_ = initial_; } }; +// end::backoff[] /// Read from the socket until the peer disconnects. +// tag::session[] capy::task<> do_session(corosio::tcp_socket& sock) { @@ -90,6 +97,7 @@ do_session(corosio::tcp_socket& sock) std::cout.flush(); } } +// end::session[] /** Connect to an endpoint with exponential backoff. @@ -111,6 +119,7 @@ do_session(corosio::tcp_socket& sock) @param max_attempts Maximum connection attempts before giving up. Zero means unlimited. */ +// tag::reconnect_loop[] capy::task<> connect_with_backoff( corosio::io_context& ioc, @@ -163,7 +172,9 @@ connect_with_backoff( } } } +// end::reconnect_loop[] +// tag::main[] int main(int argc, char* argv[]) { @@ -223,3 +234,4 @@ main(int argc, char* argv[]) return EXIT_SUCCESS; } +// end::main[] diff --git a/example/tls_context_examples.cpp b/example/tls_context_examples.cpp index 579c3e15a..c70ba3756 100644 --- a/example/tls_context_examples.cpp +++ b/example/tls_context_examples.cpp @@ -27,6 +27,7 @@ using namespace boost::corosio; +// tag::must[] // Throw std::system_error if an operation reported a failure. static void must(std::error_code ec) @@ -34,11 +35,13 @@ must(std::error_code ec) if (ec) throw std::system_error(ec); } +// end::must[] // // HTTPS Client Context // +// tag::https_client[] // Basic HTTPS client that trusts system CAs tls_context make_https_client() { @@ -50,8 +53,12 @@ tls_context make_https_client() // Verify the server certificate must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Modern TLS only + must(ctx.set_min_protocol_version( tls_version::tls_1_2 )); + return ctx; } +// end::https_client[] // HTTPS client with pinned CA (don't trust system store) tls_context make_pinned_ca_client( std::string_view ca_pem ) @@ -84,6 +91,7 @@ tls_context make_http2_client() // TLS Server Context // +// tag::basic_server[] // Basic TLS server (no client verification) tls_context make_basic_server() { @@ -98,6 +106,7 @@ tls_context make_basic_server() return ctx; } +// end::basic_server[] // mTLS server (requires client certificates) tls_context make_mtls_server() @@ -157,6 +166,7 @@ tls_context make_server_encrypted_key() // mTLS Client Context // +// tag::mtls_client[] // Client with client certificate for mTLS tls_context make_mtls_client() { @@ -172,6 +182,7 @@ tls_context make_mtls_client() return ctx; } +// end::mtls_client[] // // Protocol Version Configuration diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b5304b452..8fcd899a4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,3 +13,4 @@ if(NOT TARGET tests) endif() add_subdirectory(unit) +add_subdirectory(doc) diff --git a/test/Jamfile b/test/Jamfile index 899f58e9a..5313be213 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -8,3 +8,4 @@ # build-project unit ; +build-project doc ; diff --git a/test/doc/CMakeLists.txt b/test/doc/CMakeLists.txt new file mode 100644 index 000000000..c56aa7374 --- /dev/null +++ b/test/doc/CMakeLists.txt @@ -0,0 +1,76 @@ +# +# 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 +# + +# Compiled sources for documentation code blocks. The docs include these +# files by tag through the Antora collector, so a fragment that fails to +# build here is a fragment that would render broken on the site. + +file(GLOB SNIPPETS CONFIGURE_DEPENDS snippets/*.cpp) +set(PFILES ${SNIPPETS} CMakeLists.txt Jamfile) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) + +add_executable(boost_corosio_doc_tests ${PFILES}) +target_link_libraries( + boost_corosio_doc_tests PRIVATE + Boost::capy_test_suite_main + Boost::corosio) +target_include_directories(boost_corosio_doc_tests PRIVATE ../../) +if (WolfSSL_FOUND) + target_link_libraries(boost_corosio_doc_tests PRIVATE boost_corosio_wolfssl) + target_compile_definitions(boost_corosio_doc_tests + PRIVATE BOOST_COROSIO_HAS_WOLFSSL=1) +endif() +boost_capy_test_suite_discover_tests(boost_corosio_doc_tests) +add_dependencies(tests boost_corosio_doc_tests) + +# Pages that display a complete program get one executable each; the +# test passes when the program exits cleanly. +# +# Programs demonstrating bugs are built but never executed: running +# them would hang CI or trip sanitizers. +set(COMPILE_ONLY_PROGRAMS + 4k_tcp_server_echo + index_page_connect + quick_start_echo) + +# Programs register through a POST_BUILD-generated ctest include file +# (the DiscoverTests.cmake scheme) so the test carries the concrete +# binary path and plain `ctest` works in multi-config trees. +file(GLOB DOC_PROGRAMS CONFIGURE_DEPENDS programs/*.cpp) +foreach(src ${DOC_PROGRAMS}) + get_filename_component(name ${src} NAME_WE) + add_executable(boost_corosio_doc_${name} ${src}) + target_link_libraries(boost_corosio_doc_${name} PRIVATE Boost::corosio) + add_dependencies(tests boost_corosio_doc_${name}) + if(name IN_LIST COMPILE_ONLY_PROGRAMS) + continue() + endif() + + set(ctest_file ${CMAKE_CURRENT_BINARY_DIR}/${name}_tests.cmake) + add_custom_command( + TARGET boost_corosio_doc_${name} + POST_BUILD + BYPRODUCTS ${ctest_file} + COMMAND ${CMAKE_COMMAND} + -D "TEST_NAME=boost.corosio.doc.${name}" + -D "TEST_EXECUTABLE=$" + -D "CTEST_FILE=${ctest_file}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/WriteProgramTest.cmake" + VERBATIM) + + set(include_file ${CMAKE_CURRENT_BINARY_DIR}/${name}_include.cmake) + file(WRITE ${include_file} + "if(EXISTS \"${ctest_file}\")\n" + " include(\"${ctest_file}\")\n" + "else()\n" + " add_test(boost.corosio.doc.${name}_NOT_BUILT boost.corosio.doc.${name}_NOT_BUILT)\n" + "endif()\n") + set_property(DIRECTORY + APPEND PROPERTY TEST_INCLUDE_FILES ${include_file}) +endforeach() diff --git a/test/doc/Jamfile b/test/doc/Jamfile new file mode 100644 index 000000000..7e97a6a7b --- /dev/null +++ b/test/doc/Jamfile @@ -0,0 +1,43 @@ +# +# 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 +# + +import testing ; + +project boost/corosio/test/doc + : requirements + /boost/corosio//boost_corosio + ../.. + windows:_WIN32_WINNT=0x0602 + extra + on + # GCC mis-analyzes the structured-binding-from-co_await idiom in + # coroutine frames; see test/unit/Jamfile. + gcc:-Wno-maybe-uninitialized + ; + +run [ glob snippets/*.cpp ] + ../../../capy/extra/test_suite/test_main.cpp + ../../../capy/extra/test_suite/test_suite.cpp + : : : ../../../capy/extra/test_suite + : boost_corosio_doc_tests ; + +# Bug-demo programs are compiled but never run. +local compile-only = 4k_tcp_server_echo index_page_connect quick_start_echo ; + +for local f in [ glob programs/*.cpp ] +{ + if $(f:B) in $(compile-only) + { + compile $(f) : : doc_$(f:B) ; + } + else + { + run $(f) : target-name doc_$(f:B) ; + } +} diff --git a/test/doc/README.md b/test/doc/README.md new file mode 100644 index 000000000..906e04d22 --- /dev/null +++ b/test/doc/README.md @@ -0,0 +1,62 @@ +# Documentation example verification + +Two harnesses that verify the example code shipped in the documentation. + +## `run_examples.py` — runtime smoke tests + +Builds-and-runs the real programs under `example/` against a Python peer and +asserts on their behavior: + +| Example | Check | +|----------------|-------------------------------------------------------------| +| `nslookup` | resolves `localhost` to `127.0.0.1` / `::1` | +| `echo-server` | byte-for-byte round-trip of a payload | +| `hash-server` | returns a hex digest line for sent data | +| `http_client` | GETs a one-shot HTTP responder, prints the response, rc 0 | +| `reconnect` | connects, runs a session, re-enters the retry loop | +| `https-client` | build + usage only (a real session needs a trusted TLS peer)| + +Prerequisite: the examples must be built (e.g. `cmake --build build`). They are +found under `build/example/*/Release/`. + +``` +python3 test/doc/run_examples.py +``` + +Exit code is non-zero if any runnable example fails. + +## `snippet_check.py` — example-snippet compile check + +Extracts every `[source,cpp]` block from the AsciiDoc pages and every `@code` +block from the public headers, then compiles each (`clang -fsyntax-only`, +C++20) against the real corosio/capy headers. For each page it prepends that +page's own documented *"Code snippets assume:"* preamble and injects a +**type-correct, per-page context** (so `sock`, `ctx`, `buffer`, `acc` get the +types that page intends — tcp vs udp socket, `io_context` vs `tls_context`, tcp +vs local acceptor). Each block is wrapped as a file-scope unit, a coroutine +body, and a function body; it passes if any wrapping compiles cleanly. + +``` +python3 test/doc/snippet_check.py --scope all # docs + headers +python3 test/doc/snippet_check.py --scope docs -v # show fragment detail +``` + +### Result categories + +- **PASS** — compiled clean. Genuinely verified. +- **FRAGMENT** — fails only because it references identifiers defined in + surrounding prose or an earlier block (e.g. a user-defined `my_http_get`, or + a `worker` class shown earlier on the page). Reference-guide snippets are + fragments by design; this is expected, not a defect. +- **ELIDED** — contains `...` placeholder pseudocode. +- **NON_CODE** — a result table or prose block that happens to be tagged + `[source]` (e.g. the benchmark report). +- **API_FAIL** — fails with an error that names a real corosio/capy type or + namespace (`no member named X in 'boost::corosio…'`, wrong namespace, deleted + ctor, …). These survive missing context, so they are **genuine API bugs**. + The harness exits non-zero iff any API_FAIL is present. + +The harness deliberately does not try to force every fragment to compile in +isolation — doing so would require rewriting the guide into a doctest corpus. +Its contract is: maximize genuinely-compiled snippets, and drive `API_FAIL` to +zero so the documented API stays congruent with the headers. diff --git a/test/doc/WriteProgramTest.cmake b/test/doc/WriteProgramTest.cmake new file mode 100644 index 000000000..453cf8d39 --- /dev/null +++ b/test/doc/WriteProgramTest.cmake @@ -0,0 +1,15 @@ +# +# 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 +# + +# Runs at POST_BUILD with the target's resolved binary path, so the +# generated test carries a concrete path and plain `ctest` works in +# multi-config trees (same scheme as DiscoverTests.cmake). +file(WRITE "${CTEST_FILE}" + "add_test(\"${TEST_NAME}\" \"${TEST_EXECUTABLE}\")\n" +) diff --git a/test/doc/programs/4c_io_context_typical.cpp b/test/doc/programs/4c_io_context_typical.cpp new file mode 100644 index 000000000..9aa880a4e --- /dev/null +++ b/test/doc/programs/4c_io_context_typical.cpp @@ -0,0 +1,42 @@ +// +// 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 +// + +// The io_context page's typical usage pattern: create I/O objects, +// launch the initial coroutine, run until all work completes. + +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +// The page focuses on the launch pattern; the coroutine body is a +// placeholder so the program exits cleanly. +capy::task<> main_coroutine(corosio::tcp_socket&) +{ + co_return; +} + +// tag::full[] +int main() +{ + corosio::io_context ioc; + + // Create I/O objects + corosio::tcp_socket sock(ioc); + + // Launch initial coroutine + capy::run_async(ioc.get_executor())(main_coroutine(sock)); + + // Run until all work completes + ioc.run(); +} +// end::full[] diff --git a/test/doc/programs/4k_tcp_server_echo.cpp b/test/doc/programs/4k_tcp_server_echo.cpp new file mode 100644 index 000000000..6e86cb78e --- /dev/null +++ b/test/doc/programs/4k_tcp_server_echo.cpp @@ -0,0 +1,109 @@ +// +// 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 +// + +// Complete example shown in pages/4.guide/4k.tcp-server.adoc. Compiled +// and linked only: main() binds a port and runs forever, and the +// runtime behavior is covered by test/doc/run_examples.py against the +// real echo-server example. + +// tag::full[] +#include +#include +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +class echo_worker : public corosio::tcp_server::worker_base +{ + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + +public: + explicit echo_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf.reserve(4096); + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(corosio::tcp_server::launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } + + capy::task do_session() + { + for (;;) + { + buf.resize(4096); + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf.data(), buf.size())); + + if (ec || n == 0) + break; + + buf.resize(n); + auto [wec, wn] = co_await capy::write( + sock_, capy::const_buffer(buf.data(), buf.size())); + + if (wec) + break; + } + + sock_.close(); + } +}; + +auto make_echo_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} + +class echo_server : public corosio::tcp_server +{ +public: + echo_server(corosio::io_context& ctx, int max_workers) + : tcp_server(ctx, ctx.get_executor()) + { + set_workers(make_echo_workers(ctx, max_workers)); + } +}; + +int main() +{ + corosio::io_context ioc; + + echo_server server(ioc, 100); + + auto ec = server.bind(corosio::endpoint(8080)); + if (ec) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + return 1; + } + + std::cout << "Echo server listening on port 8080\n"; + + server.start(); + ioc.run(); +} +// end::full[] diff --git a/test/doc/programs/index_page_connect.cpp b/test/doc/programs/index_page_connect.cpp new file mode 100644 index 000000000..f3d0a7581 --- /dev/null +++ b/test/doc/programs/index_page_connect.cpp @@ -0,0 +1,53 @@ +// +// 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 +// + +// The landing page's quick example. Compiled and linked only: the +// outcome depends on whether anything listens on port 8080, and a +// listener that accepts but stays silent would block read_some forever. + +// tag::full[] +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +capy::task connect_example(corosio::io_context& ioc) +{ + corosio::tcp_socket s(ioc); + s.open(); + + // Connect using structured bindings + auto [ec] = co_await s.connect( + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); + + if (ec) + { + std::cerr << "Connect failed: " << ec.message() << "\n"; + co_return; + } + + // Read some data + char buf[1024]; + auto [read_ec, n] = co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + + if (!read_ec) + std::cout << "Received " << n << " bytes\n"; +} + +int main() +{ + corosio::io_context ioc; + capy::run_async(ioc.get_executor())(connect_example(ioc)); + ioc.run(); +} +// end::full[] diff --git a/test/doc/programs/quick_start_context.cpp b/test/doc/programs/quick_start_context.cpp new file mode 100644 index 000000000..2dd8b624f --- /dev/null +++ b/test/doc/programs/quick_start_context.cpp @@ -0,0 +1,26 @@ +// +// 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 +// + +// The quick-start page's first step: an io_context with no work runs +// to completion immediately. + +#include + +namespace corosio = boost::corosio; + +// tag::full[] +int main() +{ + corosio::io_context ioc; + + // ... create and start server ... + + ioc.run(); // Process events until all work completes +} +// end::full[] diff --git a/test/doc/programs/quick_start_echo.cpp b/test/doc/programs/quick_start_echo.cpp new file mode 100644 index 000000000..23f5a450f --- /dev/null +++ b/test/doc/programs/quick_start_echo.cpp @@ -0,0 +1,124 @@ +// +// 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 +// + +// Full program assembled across pages/quick-start.adoc. Compiled and +// linked only: main() binds a port and runs forever, and the runtime +// behavior is covered by test/doc/run_examples.py against the real +// echo-server example. + +// tag::assume[] +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include +#include + +// tag::server_class[] +class worker : public corosio::tcp_server::worker_base +{ + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf_; + +public: + worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf_.reserve(4096); + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(corosio::tcp_server::launcher launch) override + { + launch(ctx_.get_executor(), do_session()); + } + + capy::task<> do_session(); +}; + +// Build a pool of workers as a range of unique_ptr. +inline auto +make_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} + +class echo_server : public corosio::tcp_server +{ +public: + echo_server(corosio::io_context& ctx, int max_workers) + : tcp_server(ctx, ctx.get_executor()) + { + set_workers(make_workers(ctx, max_workers)); + } +}; +// end::server_class[] + +// tag::session[] +capy::task<> worker::do_session() +{ + for (;;) + { + buf_.resize(4096); + + // Read some data + auto [ec, n] = co_await sock_.read_some( + capy::mutable_buffer(buf_.data(), buf_.size())); + + if (ec || n == 0) + break; // Connection closed or error + + buf_.resize(n); + + // Echo it back + auto [wec, wn] = co_await capy::write( + sock_, capy::const_buffer(buf_.data(), buf_.size())); + + if (wec) + break; // Write error + } + + sock_.close(); +} +// end::session[] + +// tag::main[] +int main() +{ + corosio::io_context ioc; + + echo_server server(ioc, 100); + + auto ec = server.bind(corosio::endpoint(8080)); + if (ec) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + return 1; + } + + std::cout << "Echo server listening on port 8080\n"; + + server.start(); + ioc.run(); +} +// end::main[] diff --git a/test/doc/run_examples.py b/test/doc/run_examples.py new file mode 100644 index 000000000..4521e4445 --- /dev/null +++ b/test/doc/run_examples.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Build-and-run smoke tests for the Corosio example programs. + +Each example is a real, buildable program; this drives the runnable ones +end-to-end against a Python peer (or as a standalone), asserting on output. +https-client is exercised only for build + usage, since a successful TLS +session needs a peer whose certificate chains to a trusted root. + +Assumes the examples are already built under build/example/*/Release/. +Exit code is non-zero if any runnable example fails. +""" +import socket, subprocess, sys, os, time, threading, contextlib + +ROOT = '/home/sgerbino/workspace/boost/libs/corosio' +BUILD = ROOT + '/build/example' +BIN = { + 'echo': BUILD + '/echo-server/Release/corosio_example_echo_server', + 'hash': BUILD + '/hash-server/Release/corosio_example_hash_server', + 'http': BUILD + '/client/Release/corosio_example_client_http', + 'https': BUILD + '/https-client/Release/corosio_example_client_https', + 'nslookup': BUILD + '/nslookup/Release/corosio_example_nslookup', + 'reconnect': BUILD + '/reconnect/Release/corosio_example_reconnect', +} +results = [] +def record(name, ok, detail=''): + results.append((name, ok, detail)) + print(f"[{'PASS' if ok else 'FAIL'}] {name}" + (f" — {detail}" if detail else '')) + +def free_port(): + s = socket.socket(); s.bind(('127.0.0.1', 0)); p = s.getsockname()[1]; s.close(); return p + +def wait_port(port, timeout=6.0): + end = time.time() + timeout + while time.time() < end: + with contextlib.closing(socket.socket()) as s: + s.settimeout(0.3) + if s.connect_ex(('127.0.0.1', port)) == 0: return True + time.sleep(0.05) + return False + +def one_shot_server(port, handler, ready): + srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(('127.0.0.1', port)); srv.listen(1); srv.settimeout(8); ready.set() + try: + conn, _ = srv.accept() + with conn: handler(conn) + except Exception: + pass + finally: + srv.close() + +# ---- nslookup: standalone, resolve localhost -------------------------------- +def test_nslookup(): + try: + p = subprocess.run([BIN['nslookup'], 'localhost'], capture_output=True, + text=True, timeout=15) + except subprocess.TimeoutExpired: + return record('nslookup localhost', False, 'timeout') + out = p.stdout + p.stderr + ok = p.returncode == 0 and ('127.0.0.1' in out or '::1' in out) + record('nslookup localhost', ok, (out.strip().replace('\n',' ')[:60]) if ok else f'rc={p.returncode} out={out[:80]!r}') + +# ---- echo-server: round-trip ------------------------------------------------ +def test_echo(): + port = free_port() + srv = subprocess.Popen([BIN['echo'], str(port), '1'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + if not wait_port(port): return record('echo-server round-trip', False, 'server did not listen') + payload = b'hello corosio echo' + with contextlib.closing(socket.create_connection(('127.0.0.1', port), timeout=5)) as c: + c.sendall(payload) + got = b'' + c.settimeout(5) + while len(got) < len(payload): + chunk = c.recv(4096) + if not chunk: break + got += chunk + record('echo-server round-trip', got == payload, f'sent {len(payload)}B, echoed {len(got)}B') + finally: + srv.terminate(); srv.wait(timeout=5) + +# ---- hash-server: send data, expect a hex digest line ----------------------- +def test_hash(): + port = free_port() + srv = subprocess.Popen([BIN['hash'], str(port)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + try: + if not wait_port(port): return record('hash-server digest', False, 'server did not listen') + with contextlib.closing(socket.create_connection(('127.0.0.1', port), timeout=5)) as c: + c.sendall(b'the quick brown fox') + c.settimeout(5) + line = b'' + while not line.endswith(b'\n'): + chunk = c.recv(256) + if not chunk: break + line += chunk + txt = line.decode(errors='replace').strip() + ok = len(txt) >= 8 and all(ch in '0123456789abcdefABCDEF' for ch in txt) + record('hash-server digest', ok, f'digest={txt[:32]}') + finally: + srv.terminate(); srv.wait(timeout=5) + +# ---- http_client vs a one-shot HTTP responder ------------------------------- +def test_http(): + port = free_port(); ready = threading.Event() + def handler(conn): + conn.settimeout(5) + try: conn.recv(4096) # read the request + except Exception: pass + conn.sendall(b'HTTP/1.1 200 OK\r\nContent-Length: 13\r\n' + b'Connection: close\r\n\r\nhello, client') + t = threading.Thread(target=one_shot_server, args=(port, handler, ready)); t.start() + ready.wait(3) + try: + p = subprocess.run([BIN['http'], '127.0.0.1', str(port)], + capture_output=True, text=True, timeout=15) + ok = p.returncode == 0 and 'hello, client' in p.stdout + record('http_client GET', ok, f"rc={p.returncode}") + except subprocess.TimeoutExpired: + record('http_client GET', False, 'timeout') + finally: + t.join(timeout=5) + +# ---- reconnect: connects, runs a session, then re-enters the retry loop ------ +def test_reconnect(): + port = free_port(); ready = threading.Event() + def handler(conn): + time.sleep(0.2) # hold the session briefly, then close to trigger reconnect + t = threading.Thread(target=one_shot_server, args=(port, handler, ready)); t.start() + ready.wait(3) + proc = subprocess.Popen([BIN['reconnect'], '127.0.0.1', str(port)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + out = '' + end = time.time() + 4 + try: + while time.time() < end: + if proc.poll() is not None: break + time.sleep(0.1) + finally: + proc.terminate() + try: out = proc.communicate(timeout=5)[0] + except subprocess.TimeoutExpired: proc.kill(); out = proc.communicate()[0] + t.join(timeout=5) + record('reconnect connects', 'Connected on attempt 1' in (out or ''), + (out or '').strip().replace('\n', ' | ')[:70]) + +# ---- https-client: build + usage only --------------------------------------- +def test_https_usage(): + try: + p = subprocess.run([BIN['https']], capture_output=True, text=True, timeout=10) + ok = p.returncode != 0 and ('sage' in (p.stdout + p.stderr)) # prints usage + record('https_client (build+usage)', ok, 'runtime needs a trusted TLS peer; build+usage verified') + except Exception as e: + record('https_client (build+usage)', False, str(e)) + +def main(): + missing = [k for k, v in BIN.items() if not os.path.exists(v)] + if missing: + print('Missing built binaries (build the examples first):', missing); sys.exit(2) + test_nslookup(); test_echo(); test_hash(); test_http(); test_reconnect(); test_https_usage() + n_fail = sum(1 for _, ok, _ in results if not ok) + print(f"\n{len(results)-n_fail}/{len(results)} passed") + sys.exit(1 if n_fail else 0) + +if __name__ == '__main__': + main() diff --git a/test/doc/snippets/3a_echo_server.cpp b/test/doc/snippets/3a_echo_server.cpp new file mode 100644 index 000000000..31061dfe4 --- /dev/null +++ b/test/doc/snippets/3a_echo_server.cpp @@ -0,0 +1,230 @@ +// +// 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 +// + +// Compiled fragments shown in pages/3.tutorials/3a.echo-server.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +// Two same-name regions concatenate into the page's write_some/write +// comparison; separate scopes keep the duplicate bindings legal. +capy::task<> +composed_write_demo( + corosio::tcp_socket& sock, capy::const_buffer buf, bool& done) +{ + { + // tag::composed_write[] + // write_some: may write partial data + auto [ec, n] = co_await sock.write_some(buf); // n might be < buf.size() + + // end::composed_write[] + BOOST_TEST(!ec); + } + { + // tag::composed_write[] + // write: writes all data or fails + auto [ec, n] = co_await capy::write(sock, buf); // n == buf.size() or error + // end::composed_write[] + BOOST_TEST(!ec); + BOOST_TEST(n == buf.size()); + } + done = true; +} + +capy::task<> +advance_then_check_session( + corosio::tcp_socket& sock, capy::mutable_buffer buf, bool& done) +{ + for (;;) + { + // tag::advance_then_check[] + auto [ec, n] = co_await sock.read_some(buf); + auto [wec, wn] = co_await capy::write( + sock, capy::const_buffer(buf.data(), n)); + if (wec || ec) + break; // Normal termination path + // end::advance_then_check[] + } + done = true; +} + +capy::task<> +exception_style_session( + corosio::tcp_socket& sock, capy::mutable_buffer buf, bool& done) +{ + // tag::exceptions_eof[] + try { + auto [ec, n] = co_await sock.read_some(buf); + if (ec) throw std::system_error(ec); + } catch (...) { + // EOF is an exception here + } + // end::exceptions_eof[] + done = true; +} + +// Peer for the echo loop: send a message, read it back, then close so +// the session sees end-of-stream and terminates. +capy::task<> +echo_peer(corosio::tcp_socket& sock, std::string& reply, bool& done) +{ + co_await capy::write(sock, capy::const_buffer("echo", 4)); + char tmp[4] = {}; + auto [ec, n] = co_await capy::read( + sock, capy::mutable_buffer(tmp, sizeof tmp)); + reply.assign(tmp, n); + sock.close(); + done = true; +} + +capy::task<> +read_exactly( + corosio::tcp_socket& sock, capy::mutable_buffer buf, std::size_t& got) +{ + auto [ec, n] = co_await capy::read(sock, buf); + got = n; +} + +struct echo_server_test +{ + // Linger is disabled so close() performs a graceful shutdown and the + // fragments observe eof rather than a reset. + static std::pair + make_pair(corosio::io_context& ioc) + { + return corosio::test::make_socket_pair< + corosio::tcp_socket, corosio::tcp_acceptor, false>(ioc); + } + + void + testComposedWrite() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + auto [a, b] = make_pair(ioc); + + bool done = false; + std::size_t got = 0; + char rx[4] = {}; + capy::run_async(ex)(composed_write_demo( + a, capy::const_buffer("hi", 2), done)); + capy::run_async(ex)(read_exactly( + b, capy::mutable_buffer(rx, sizeof rx), got)); + ioc.run(); + + BOOST_TEST(done); + BOOST_TEST(got == 4); + BOOST_TEST(std::memcmp(rx, "hihi", 4) == 0); + a.close(); + b.close(); + ioc.restart(); + ioc.run(); + } + + void + testAdvanceThenCheck() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + auto [a, b] = make_pair(ioc); + + bool session_done = false; + bool peer_done = false; + std::string reply; + char storage[1024]; + capy::run_async(ex)(advance_then_check_session( + a, capy::mutable_buffer(storage, sizeof storage), session_done)); + capy::run_async(ex)(echo_peer(b, reply, peer_done)); + ioc.run(); + + BOOST_TEST(session_done); + BOOST_TEST(peer_done); + BOOST_TEST(reply == "echo"); + a.close(); + ioc.restart(); + ioc.run(); + } + + void + testExceptionStyle() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + auto [a, b] = make_pair(ioc); + + // Immediate close: the fragment's read fails with eof, which the + // exception style surfaces as a throw. + b.close(); + + bool done = false; + char storage[64]; + capy::run_async(ex)(exception_style_session( + a, capy::mutable_buffer(storage, sizeof storage), done)); + ioc.run(); + + BOOST_TEST(done); + a.close(); + ioc.restart(); + ioc.run(); + } + + void + run() + { + testComposedWrite(); + testAdvanceThenCheck(); + testExceptionStyle(); + } +}; + +} // namespace + +TEST_SUITE(echo_server_test, "boost.corosio.doc.3a_echo_server"); diff --git a/test/doc/snippets/3b_http_client.cpp b/test/doc/snippets/3b_http_client.cpp new file mode 100644 index 000000000..dc5adaca1 --- /dev/null +++ b/test/doc/snippets/3b_http_client.cpp @@ -0,0 +1,131 @@ +// +// 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 +// + +// Compiled fragments shown in pages/3.tutorials/3b.http-client.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +capy::task<> +bindings_pattern( + corosio::tcp_socket& s, corosio::endpoint ep, bool& done) +{ + // tag::error_bindings[] + auto [ec] = co_await s.connect(ep); + if (ec) + { + std::cerr << "Connect failed: " << ec.message() << "\n"; + co_return; + } + // end::error_bindings[] + done = true; +} + +capy::task<> +exceptions_pattern( + corosio::tcp_socket& s, corosio::endpoint ep, bool& done) +{ + // tag::error_exceptions[] + if (auto [ec] = co_await s.connect(ep); ec) // Throw on error + throw std::system_error(ec); + // end::error_exceptions[] + done = true; +} + +capy::task<> +accept_one(corosio::tcp_acceptor& acc, corosio::tcp_socket& peer) +{ + co_await acc.accept(peer); +} + +struct http_client_test +{ + // Both fragments connect for real against a loopback listener. + template + void + testConnectPattern(Fragment fragment) + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::tcp_acceptor acc(ioc); + acc.open(); + BOOST_TEST(!acc.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + auto ep = acc.local_endpoint(); + + corosio::tcp_socket s(ioc); + corosio::tcp_socket peer(ioc); + s.open(); + + bool done = false; + capy::run_async(ex)(accept_one(acc, peer)); + capy::run_async(ex)(fragment(s, ep, done)); + ioc.run(); + + BOOST_TEST(done); + s.close(); + peer.close(); + acc.close(); + ioc.restart(); + ioc.run(); + } + + void + run() + { + testConnectPattern(&bindings_pattern); + testConnectPattern(&exceptions_pattern); + } +}; + +} // namespace + +TEST_SUITE(http_client_test, "boost.corosio.doc.3b_http_client"); diff --git a/test/doc/snippets/3c_dns_lookup.cpp b/test/doc/snippets/3c_dns_lookup.cpp new file mode 100644 index 000000000..2ce604e83 --- /dev/null +++ b/test/doc/snippets/3c_dns_lookup.cpp @@ -0,0 +1,187 @@ +// +// 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 +// + +// Compiled fragments shown in pages/3.tutorials/3c.dns-lookup.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +// Resolving a public hostname needs the network; compiled, never run. +[[maybe_unused]] capy::task<> +resolver_overview(corosio::io_context& ioc) +{ + // tag::resolver_overview[] + corosio::resolver r(ioc); + auto [ec, results] = co_await r.resolve("www.example.com", "https"); + // end::resolver_overview[] +} + +std::uint16_t +inspect_endpoint(corosio::resolver_entry const& entry) +{ + // tag::entry_endpoint[] + auto ep = entry.get_endpoint(); + + if (ep.is_v4()) + { + // IPv4 address + corosio::ipv4_address addr = ep.v4_address(); + } + else + { + // IPv6 address + corosio::ipv6_address addr = ep.v6_address(); + } + + std::uint16_t port = ep.port(); + // end::entry_endpoint[] + return port; +} + +// Numeric flags skip DNS entirely, so this fragment runs for real. +capy::task<> +resolve_numeric( + corosio::io_context& ioc, + std::error_code& out_ec, + std::size_t& out_count) +{ + corosio::resolver r(ioc); + std::string_view host = "127.0.0.1"; + std::string_view service = "8080"; + // tag::resolve_with_flags[] + auto [ec, results] = co_await r.resolve( + host, service, + corosio::resolve_flags::numeric_host | + corosio::resolve_flags::numeric_service); + // end::resolve_with_flags[] + out_ec = ec; + out_count = results.size(); +} + +// Connecting to a resolved host needs the network; compiled, never run. +// tag::connect_to_host[] +capy::task connect_to_host( + corosio::io_context& ioc, + std::string_view host, + std::string_view service) +{ + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(host, service); + if (resolve_ec) + throw std::system_error(resolve_ec); + + corosio::tcp_socket sock(ioc); + sock.open(); + + // Try each address until one works + std::error_code last_ec; + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry.get_endpoint()); + if (!ec) + { + std::cout << "Connected to " << host << "\n"; + co_return; + } + last_ec = ec; + } + + throw std::system_error(last_ec, "all addresses failed"); +} +// end::connect_to_host[] + +[[maybe_unused]] capy::task (* const connect_demo)( + corosio::io_context&, std::string_view, std::string_view) = + &connect_to_host; + +struct dns_lookup_test +{ + void + testEntryEndpoint() + { + corosio::resolver_entry entry( + corosio::endpoint(corosio::ipv4_address::loopback(), 443), + "example.com", "https"); + BOOST_TEST(inspect_endpoint(entry) == 443); + } + + void + testResolveWithFlags() + { + corosio::io_context ioc; + std::error_code ec; + std::size_t count = 0; + capy::run_async(ioc.get_executor())( + resolve_numeric(ioc, ec, count)); + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST(count > 0); + } + + void + testCancel() + { + corosio::io_context ioc; + corosio::resolver r(ioc); + // tag::resolver_cancel[] + r.cancel(); // Cancel pending operation + // end::resolver_cancel[] + BOOST_TEST(true); + } + + void + run() + { + testEntryEndpoint(); + testResolveWithFlags(); + testCancel(); + } +}; + +} // namespace + +TEST_SUITE(dns_lookup_test, "boost.corosio.doc.3c_dns_lookup"); diff --git a/test/doc/snippets/3d_tls_context.cpp b/test/doc/snippets/3d_tls_context.cpp new file mode 100644 index 000000000..0c1435646 --- /dev/null +++ b/test/doc/snippets/3d_tls_context.cpp @@ -0,0 +1,488 @@ +// +// 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 +// + +// Compiled fragments shown in pages/3.tutorials/3d.tls-context.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +// The password-from-environment fragment uses std::getenv, which the +// Windows CRT deprecates under -Werror. +#if defined(_MSC_VER) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include + +namespace corosio = boost::corosio; +using namespace boost::corosio; +// end::assume[] + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Missing credential files make the load calls report an error code, +// which the fragments deliberately ignore; nothing throws. +void +construction() +{ + // tag::construction[] + // Create a default context + tls_context ctx; + + // Copy shares the same underlying state + tls_context ctx2 = ctx; // ctx and ctx2 share state + + // Move transfers ownership + tls_context ctx3 = std::move( ctx ); + // ctx is now empty + // end::construction[] +} + +void +typical_setup() +{ + // tag::typical_setup[] + tls_context ctx; + + // 1. Load credentials (for servers, or clients using client certs) + ctx.use_certificate_chain_file( "server.crt" ); + ctx.use_private_key_file( "server.key", tls_file_format::pem ); + + // 2. Configure trust anchors (for verifying peer certificates) + ctx.set_default_verify_paths(); // Use system CA store + + // 3. Set verification mode + ctx.set_verify_mode( tls_verify_mode::peer ); + + // 4. Configure protocol options (optional) + ctx.set_min_protocol_version( tls_version::tls_1_2 ); + // end::typical_setup[] +} + +void +load_separate(tls_context& ctx) +{ + // tag::load_separate[] + // Load certificate chain (leaf + intermediates) + ctx.use_certificate_chain_file( "fullchain.pem" ); + + // Load the matching private key + ctx.use_private_key_file( "privkey.key", tls_file_format::pem ); + // end::load_separate[] +} + +void +load_single(tls_context& ctx) +{ + // tag::load_single[] + ctx.use_certificate_file( "server.crt", tls_file_format::pem ); + ctx.use_private_key_file( "server.key", tls_file_format::pem ); + // end::load_single[] +} + +void +pkcs12_bundle(tls_context& ctx) +{ + // tag::pkcs12_file[] + ctx.use_pkcs12_file( "credentials.pfx", "bundle-password" ); + // end::pkcs12_file[] +} + +std::string +fetch_certificate_from_vault() +{ + return {}; +} + +std::string +fetch_key_from_vault() +{ + return {}; +} + +void +load_memory(tls_context& ctx) +{ + // tag::load_memory[] + std::string cert_pem = fetch_certificate_from_vault(); + std::string key_pem = fetch_key_from_vault(); + + ctx.use_certificate_chain( cert_pem ); + ctx.use_private_key( key_pem, tls_file_format::pem ); + // end::load_memory[] +} + +void +der_files(tls_context& ctx) +{ + // tag::der_files[] + ctx.use_certificate_file( "server.der", tls_file_format::der ); + ctx.use_private_key_file( "server.key.der", tls_file_format::der ); + // end::der_files[] +} + +void +system_trust(tls_context& ctx) +{ + // tag::system_trust[] + ctx.set_default_verify_paths(); + // end::system_trust[] +} + +void +ca_bundle(tls_context& ctx) +{ + // tag::ca_bundle[] + // Load CA bundle file (may contain multiple CAs) + ctx.load_verify_file( "/path/to/ca-bundle.crt" ); + // end::ca_bundle[] +} + +void +ca_directory(tls_context& ctx) +{ + // tag::ca_directory[] + ctx.add_verify_path( "/etc/ssl/certs" ); + // end::ca_directory[] +} + +std::string +load_ca_from_config() +{ + return {}; +} + +void +ca_individual( + tls_context& ctx, + std::string const& root_ca_pem, + std::string const& intermediate_ca_pem) +{ + // tag::ca_individual[] + // From memory + std::string internal_ca = load_ca_from_config(); + ctx.add_certificate_authority( internal_ca ); + + // Multiple CAs + ctx.add_certificate_authority( root_ca_pem ); + ctx.add_certificate_authority( intermediate_ca_pem ); + // end::ca_individual[] +} + +void +combine_trust(tls_context& ctx, std::string const& corporate_ca_pem) +{ + // tag::combine_trust[] + // Start with system trust store + ctx.set_default_verify_paths(); + + // Add an internal CA for corporate servers + ctx.add_certificate_authority( corporate_ca_pem ); + // end::combine_trust[] +} + +void +version_bounds(tls_context& ctx) +{ + // tag::version_bounds[] + // Require TLS 1.2 or newer (default) + ctx.set_min_protocol_version( tls_version::tls_1_2 ); + + // Require TLS 1.3 only + ctx.set_min_protocol_version( tls_version::tls_1_3 ); + ctx.set_max_protocol_version( tls_version::tls_1_3 ); + // end::version_bounds[] +} + +void +cipher_suites(tls_context& ctx) +{ + // tag::cipher_suites[] + // TLS 1.2 and below + ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ); + + // TLS 1.3 (distinct API and suite names) + ctx.set_ciphersuites_tls13( "TLS_AES_256_GCM_SHA384" ); + // end::cipher_suites[] +} + +void +alpn_offer(tls_context& ctx) +{ + // tag::alpn_offer[] + // HTTP/2 with HTTP/1.1 fallback + ctx.set_alpn( { "h2", "http/1.1" } ); + + // gRPC + ctx.set_alpn( { "h2" } ); + // end::alpn_offer[] +} + +// Reading the negotiated protocol needs a stream whose handshake +// completed against a real peer; compiled but never executed. +[[maybe_unused]] void +alpn_read(corosio::tls_stream& stream) +{ + // tag::alpn_read[] + std::string_view proto = stream.alpn_protocol(); // "h2", or empty if none + // end::alpn_read[] +} + +void +verify_modes(tls_context& ctx) +{ + // tag::verify_modes[] + // Don't verify peer (not recommended for production) + ctx.set_verify_mode( tls_verify_mode::none ); + + // Verify peer if certificate is presented + ctx.set_verify_mode( tls_verify_mode::peer ); + + // Require and verify peer certificate (mTLS server-side) + ctx.set_verify_mode( tls_verify_mode::require_peer ); + // end::verify_modes[] +} + +// Setting a hostname needs a TLS stream, which needs a backend +// library; compiled but never executed. +[[maybe_unused]] void +hostname_setup(corosio::tls_stream& secure) +{ + // tag::set_hostname[] + secure.set_hostname( "api.example.com" ); + // end::set_hostname[] +} + +void +verify_depth(tls_context& ctx) +{ + // tag::verify_depth[] + // Allow up to 3 intermediates (leaf -> 3 intermediates -> root) + ctx.set_verify_depth( 3 ); + // end::verify_depth[] +} + +std::vector const expected_pin; + +void +verify_callback(tls_context& ctx) +{ + // tag::verify_callback[] + ctx.set_verify_callback( + []( bool preverified, corosio::verify_context& verify_ctx ) -> bool + { + if( !preverified ) + return false; // chain did not verify + + auto der = verify_ctx.certificate(); // DER of the current cert + return der.size() == expected_pin.size() && + std::equal( der.begin(), der.end(), expected_pin.begin() ); + }); + // end::verify_callback[] +} + +void +revocation_policy(tls_context& ctx) +{ + // tag::revocation_policy[] + // Don't check revocation (default) + ctx.set_revocation_policy( tls_revocation_policy::disabled ); + + // Accept unknown status, reject a listed (revoked) certificate + ctx.set_revocation_policy( tls_revocation_policy::soft_fail ); + + // Also reject when status can't be determined (strict) + ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); + // end::revocation_policy[] +} + +std::string +fetch_crl_from_url(std::string_view) +{ + return {}; +} + +void +crl_load(tls_context& ctx, std::string_view crl_url) +{ + // tag::crl_load[] + // From file + ctx.add_crl_file( "/path/to/issuer.crl" ); + + // From memory (e.g., fetched via HTTP) + std::string crl_data = fetch_crl_from_url( crl_url ); + ctx.add_crl( crl_data ); + + ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); + // end::crl_load[] +} + +void +bootstrap_hardened() +{ + // tag::bootstrap_hardened[] + // Bootstrap context: for fetching revocation data + tls_context bootstrap_ctx; + bootstrap_ctx.set_default_verify_paths(); + bootstrap_ctx.set_verify_mode( tls_verify_mode::peer ); + bootstrap_ctx.set_revocation_policy( tls_revocation_policy::disabled ); + + // Hardened context: for sensitive connections + tls_context hardened_ctx; + hardened_ctx.set_default_verify_paths(); + hardened_ctx.set_verify_mode( tls_verify_mode::peer ); + hardened_ctx.add_crl_file( "cached.crl" ); + hardened_ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); + // end::bootstrap_hardened[] +} + +void +password_callback(tls_context& ctx) +{ + // tag::password_callback[] + // Set callback before loading encrypted key + ctx.set_password_callback( + []( std::size_t max_length, tls_password_purpose purpose ) + { + // purpose: for_reading (decrypt) or for_writing (encrypt) + return std::string( "my-secret-password" ); + }); + + // Now load encrypted private key + ctx.use_private_key_file( "encrypted.key", tls_file_format::pem ); + // end::password_callback[] +} + +std::string +prompt_user_for_password() +{ + return {}; +} + +void +password_env(tls_context& ctx) +{ + // tag::password_env[] + ctx.set_password_callback( + []( std::size_t max_length, tls_password_purpose purpose ) + { + // Read from environment + if( auto* pw = std::getenv( "TLS_KEY_PASSWORD" ) ) + return std::string( pw ); + + // Or prompt user + return prompt_user_for_password(); + }); + // end::password_env[] +} + +void +pkcs12_memory(tls_context& ctx, std::string_view pkcs12_data) +{ + // tag::pkcs12_memory[] + ctx.use_pkcs12( pkcs12_data, "bundle-password" ); + // end::pkcs12_memory[] +} + +// Throws std::system_error when the credential files are absent, as +// they are under the test runner; compiled but never executed. +[[maybe_unused]] void +error_handling(tls_context& ctx) +{ + // tag::error_handling[] + // Throw on error + if( auto ec = ctx.use_certificate_file( "cert.pem", tls_file_format::pem ) ) + throw std::system_error(ec); + + // Check error explicitly + if( auto ec = ctx.load_verify_file( "ca.crt" ) ) + { + std::cerr << "Failed to load CA: " << ec.message() << "\n"; + return; + } + // end::error_handling[] +} + +struct tls_context_3d_test +{ + void + run() + { + // Configuration calls record settings and report failures as + // error codes, so every context-only fragment executes safely + // without credential files or a TLS peer. + tls_context ctx; + construction(); + typical_setup(); + load_separate(ctx); + load_single(ctx); + pkcs12_bundle(ctx); + load_memory(ctx); + der_files(ctx); + system_trust(ctx); + ca_bundle(ctx); + ca_directory(ctx); + ca_individual(ctx, {}, {}); + combine_trust(ctx, {}); + version_bounds(ctx); + cipher_suites(ctx); + alpn_offer(ctx); + verify_modes(ctx); + verify_depth(ctx); + verify_callback(ctx); + revocation_policy(ctx); + crl_load(ctx, {}); + bootstrap_hardened(); + password_callback(ctx); + password_env(ctx); + pkcs12_memory(ctx, {}); + BOOST_TEST(true); + } +}; + +} // namespace + +TEST_SUITE(tls_context_3d_test, "boost.corosio.doc.3d_tls_context"); diff --git a/test/doc/snippets/3f_reconnect.cpp b/test/doc/snippets/3f_reconnect.cpp new file mode 100644 index 000000000..414ef3750 --- /dev/null +++ b/test/doc/snippets/3f_reconnect.cpp @@ -0,0 +1,109 @@ +// +// 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 +// + +// Compiled fragments shown in pages/3.tutorials/3f.reconnect.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +// Minimal stand-ins for the tutorial's backoff policy and retry loop; +// the page shows the full definitions, this TU only exercises the +// launch-and-cancel fragment. +struct exponential_backoff +{ +}; + +bool delay_canceled = false; + +// The delay gives the stop request something to cancel; a stopped +// token makes it complete with cond::canceled instead of waiting. +capy::task<> +connect_with_backoff( + corosio::io_context&, + corosio::endpoint, + exponential_backoff, + int) +{ + auto [ec] = co_await corosio::delay(std::chrono::seconds(2)); + delay_canceled = (ec == capy::cond::canceled); +} + +struct reconnect_test +{ + void + testStopTokenShutdown() + { + corosio::io_context ioc; + corosio::endpoint ep(corosio::ipv4_address::loopback(), 8080); + exponential_backoff backoff; + // tag::stop_token_shutdown[] + std::stop_source stop_src; + + capy::run_async(ioc.get_executor(), stop_src.get_token())( + connect_with_backoff(ioc, ep, backoff, 10)); + + // Later, from any thread: + stop_src.request_stop(); + // end::stop_token_shutdown[] + ioc.run(); + BOOST_TEST(delay_canceled); + } + + void + run() + { + testStopTokenShutdown(); + } +}; + +} // namespace + +TEST_SUITE(reconnect_test, "boost.corosio.doc.3f_reconnect"); diff --git a/test/doc/snippets/4a_tcp_networking.cpp b/test/doc/snippets/4a_tcp_networking.cpp new file mode 100644 index 000000000..d2e812485 --- /dev/null +++ b/test/doc/snippets/4a_tcp_networking.cpp @@ -0,0 +1,151 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4a.tcp-networking.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +// Resolving contacts real DNS servers; the coroutine compiles but +// never runs. +[[maybe_unused]] capy::task<> +resolve_hostname(corosio::io_context& ioc) +{ + // tag::resolver_lookup[] + corosio::resolver r(ioc); + auto [ec, results] = co_await r.resolve("www.example.com", "https"); + + for (auto const& entry : results) + { + auto ep = entry.get_endpoint(); + // Try connecting to ep... + } + // end::resolver_lookup[] +} + +void +set_no_delay(corosio::tcp_socket& sock) +{ + // tag::no_delay[] + sock.set_option(corosio::socket_option::no_delay(true)); + // end::no_delay[] +} + +capy::task<> +read_partial_wrong(corosio::tcp_socket& sock, capy::mutable_buffer buf) +{ + // tag::read_partial[] + // Wrong: might read less than buffer size + auto [ec, n] = co_await sock.read_some(buf); + // end::read_partial[] +} + +capy::task<> +read_partial_right( + corosio::tcp_socket& sock, capy::mutable_buffer buf, std::size_t& got) +{ + // tag::read_partial[] + + // Right: reads until buffer is full or EOF + auto [ec, n] = co_await capy::read(sock, buf); + // end::read_partial[] + got = n; +} + +struct tcp_networking_test +{ + void + testNoDelay() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + set_no_delay(s1); + BOOST_TEST( + s1.get_option().value()); + } + + void + testPartialReads() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + std::size_t got = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& r, corosio::tcp_socket& w, + std::size_t& out) -> capy::task<> + { + // Eight bytes ready up front: read_some may take any + // prefix through its four-byte buffer, leaving at + // least four for capy::read to fill completely. + char payload[8] = {}; + co_await capy::write( + w, capy::const_buffer(payload, sizeof(payload))); + char small[4]; + co_await read_partial_wrong( + r, capy::mutable_buffer(small, sizeof(small))); + co_await read_partial_right( + r, capy::mutable_buffer(small, sizeof(small)), out); + }(s1, s2, got)); + ioc.run(); + BOOST_TEST(got == 4u); + } + + void + run() + { + testNoDelay(); + testPartialReads(); + } +}; + +} // namespace + +TEST_SUITE(tcp_networking_test, "boost.corosio.doc.4a_tcp_networking"); diff --git a/test/doc/snippets/4b_concurrent_programming.cpp b/test/doc/snippets/4b_concurrent_programming.cpp new file mode 100644 index 000000000..52966ff3a --- /dev/null +++ b/test/doc/snippets/4b_concurrent_programming.cpp @@ -0,0 +1,436 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4b.concurrent-programming.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +using namespace std::chrono_literals; + +void use_resource() {} + +// tag::mutex_protection[] +std::mutex m; +int counter = 0; + +void increment() +{ + std::lock_guard lock(m); + ++counter; // Safe: only one thread at a time +} +// end::mutex_protection[] + +bool stopped = false; +int events_handled = 0; + +// Each simulated event arms the next until the source runs dry. +void wait_for_event() {} + +void handle_event() +{ + if (++events_handled == 3) + stopped = true; +} + +void +run_event_loop() +{ + // tag::event_loop[] + while (!stopped) + { + wait_for_event(); // Blocks until I/O completes + handle_event(); // Run the handler + } + // end::event_loop[] +} + +// tag::suspension_points[] +capy::task handle_client(corosio::tcp_socket sock) +{ + char buf[1024]; + + auto [ec, n] = co_await sock.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + // Suspends here until data arrives + + if (ec) + co_return; // Exit on error + + // Process data... +} +// end::suspension_points[] + +capy::task my_coroutine() { co_return; } + +char session_storage[64]; +capy::mutable_buffer buf(session_storage, sizeof(session_storage)); +capy::const_buffer response("OK", 2); + +// tag::strand_session[] +capy::task session(corosio::tcp_socket sock) +{ + // All code in this coroutine runs sequentially + auto [ec, n] = co_await sock.read_some(buf); + // No other code in this coroutine runs until above completes + + co_await sock.write_some(response); + // Still sequential +} +// end::strand_session[] + +// tag::accept_loop[] +capy::task accept_loop( + corosio::io_context& ioc, + corosio::tcp_acceptor& acc) +{ + for (;;) + { + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + if (ec) break; + + // Spawn independent coroutine for this client + capy::run_async(ioc.get_executor())( + handle_client(std::move(peer))); + } +} +// end::accept_loop[] + +// Accepting needs a live listening socket; the loop compiles but +// never runs. +[[maybe_unused]] capy::task (* const accept_loop_demo)( + corosio::io_context&, corosio::tcp_acceptor&) = &accept_loop; + +// tag::worker_pool[] +struct worker +{ + corosio::tcp_socket sock; + std::string buf; + bool in_use = false; + + explicit worker(corosio::io_context& ioc) : sock(ioc) {} +}; +// end::worker_pool[] + +std::size_t +make_worker_pool(corosio::io_context& ioc, int max_workers) +{ + // tag::worker_pool_use[] + // Preallocate workers + std::vector workers; + workers.reserve(max_workers); + for (int i = 0; i < max_workers; ++i) + workers.emplace_back(ioc); + + // Assign connections to free workers + // end::worker_pool_use[] + return workers.size(); +} + +capy::task read_message(corosio::tcp_socket&) +{ + co_return "request"; +} + +capy::task process(std::string const&) +{ + co_return "response"; +} + +capy::task write_response(corosio::tcp_socket&, std::string const&) +{ + co_return; +} + +// tag::pipeline[] +capy::task pipeline(corosio::tcp_socket sock) +{ + auto message = co_await read_message(sock); + auto result = co_await process(message); + co_await write_response(sock, result); +} +// end::pipeline[] + +// tag::blocking[] +// WRONG: blocks the entire io_context +capy::task bad() +{ + std::this_thread::sleep_for(1s); // Don't do this! + co_return; +} + +// RIGHT: suspend with an async delay +capy::task good() +{ + co_await corosio::delay(1s); +} +// end::blocking[] + +// Running either coroutine costs a full second of wall clock; the +// bodies are the demonstration, so nothing awaits them. +[[maybe_unused]] capy::task (* const bad_demo)() = &bad; +[[maybe_unused]] capy::task (* const good_demo)() = &good; + +capy::task<> use_socket(corosio::tcp_socket&) { co_return; } + +// The rvalue overload moves the socket into the frame so the RIGHT +// variant genuinely owns it; a by-value overload would make the +// lvalue call above ambiguous. +capy::task<> use_socket(corosio::tcp_socket&& s) +{ + auto owned = std::move(s); + co_return; +} + +// Both scopes spawn coroutines that would outlive the demo; the +// scoping bug is the lesson, so nothing calls this. +[[maybe_unused]] void +dangling_reference(corosio::io_context& ioc, capy::executor_ref ex) +{ + // tag::dangling_reference[] + // WRONG: socket destroyed while coroutine runs + { + corosio::tcp_socket sock(ioc); + capy::run_async(ex)(use_socket(sock)); // Takes reference! + } // sock destroyed here, coroutine still running + + // RIGHT: move socket into coroutine + { + corosio::tcp_socket sock(ioc); + capy::run_async(ex)(use_socket(std::move(sock))); + } // OK, coroutine owns the socket + // end::dangling_reference[] +} + +// Connecting from the wrong executor is the bug on display; the +// function compiles but never runs. +[[maybe_unused]] void +cross_executor( + corosio::io_context& ctx1, capy::executor_ref ex2, corosio::endpoint ep) +{ + // tag::cross_executor[] + // Dangerous: socket created on ctx1, used from ex2 + corosio::tcp_socket sock(ctx1); + capy::run_async(ex2)([&sock, ep]() -> capy::task { + co_await sock.connect(ep); // Wrong executor! + }()); + // end::cross_executor[] +} + +capy::task<> +feed(corosio::tcp_socket& s) +{ + static char const ping[] = {'p', 'i', 'n', 'g'}; + co_await capy::write(s, capy::const_buffer(ping, sizeof(ping))); +} + +struct concurrent_programming_test +{ + void + testRaceCondition() + { + // tag::race_condition[] + int counter = 0; + + // Task 1 // Task 2 + ++counter; ++counter; + // Both read 0, both write 1 + // Expected: 2, Actual: 1 (data race) + // end::race_condition[] + + // Sequential execution here; the comments describe the + // concurrent interleaving. + BOOST_TEST(counter == 2); + } + + void + testReadModifyWrite() + { + bool resource_available = true; + // tag::read_modify_write[] + if (resource_available) // Read + { + resource_available = false; // Write + use_resource(); + } + // end::read_modify_write[] + BOOST_TEST(!resource_available); + } + + void + testMutexProtection() + { + increment(); + BOOST_TEST(counter == 1); + } + + void + testEventLoop() + { + run_event_loop(); + BOOST_TEST(events_handled == 3); + } + + void + testHandleClient() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + bool done = false; + capy::run_async(ex)(feed(s2)); + capy::run_async(ex)( + [](corosio::tcp_socket s, bool& out) -> capy::task<> + { + co_await handle_client(std::move(s)); + out = true; + }(std::move(s1), done)); + ioc.run(); + BOOST_TEST(done); + } + + void + testExecutorAffinity() + { + corosio::io_context ioc; + // tag::executor_affinity[] + capy::run_async(ioc.get_executor())(my_coroutine()); + // my_coroutine resumes through ioc's executor + // end::executor_affinity[] + BOOST_TEST(ioc.run() > 0u); + } + + void + testSession() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + bool done = false; + capy::run_async(ex)(feed(s2)); + capy::run_async(ex)( + [](corosio::tcp_socket s, bool& out) -> capy::task<> + { + co_await session(std::move(s)); + out = true; + }(std::move(s1), done)); + ioc.run(); + BOOST_TEST(done); + } + + void + testMultiThreadedRun() + { + // tag::multi_threaded_run[] + corosio::io_context ioc(4); // Hint: 4 threads + + std::vector threads; + for (int i = 0; i < 4; ++i) + threads.emplace_back([&ioc] { ioc.run(); }); + + for (auto& t : threads) + t.join(); + // end::multi_threaded_run[] + BOOST_TEST(threads.size() == 4u); + } + + void + testWorkerPool() + { + corosio::io_context ioc; + BOOST_TEST(make_worker_pool(ioc, 4) == 4u); + } + + void + testPipeline() + { + corosio::io_context ioc; + corosio::tcp_socket sock(ioc); + bool done = false; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket s, bool& out) -> capy::task<> + { + co_await pipeline(std::move(s)); + out = true; + }(std::move(sock), done)); + ioc.run(); + BOOST_TEST(done); + } + + void + run() + { + testRaceCondition(); + testReadModifyWrite(); + testMutexProtection(); + testEventLoop(); + testHandleClient(); + testExecutorAffinity(); + testSession(); + testMultiThreadedRun(); + testWorkerPool(); + testPipeline(); + } +}; + +} // namespace + +TEST_SUITE( + concurrent_programming_test, + "boost.corosio.doc.4b_concurrent_programming"); diff --git a/test/doc/snippets/4c2_configuration.cpp b/test/doc/snippets/4c2_configuration.cpp new file mode 100644 index 000000000..3c731c10d --- /dev/null +++ b/test/doc/snippets/4c2_configuration.cpp @@ -0,0 +1,105 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4c2.configuration.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::options_basic_include[] +#include +// end::options_basic_include[] +// tag::options_native_include[] +#include +// end::options_native_include[] + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; + +namespace { + +struct configuration_test +{ + void + testOptionsBasic() + { + // tag::options_basic[] + corosio::io_context_options opts; + opts.max_events_per_poll = 256; + opts.inline_budget_max = 32; + + corosio::io_context ioc(opts); + // end::options_basic[] + ioc.run(); + BOOST_TEST(opts.max_events_per_poll == 256u); + } + + void + testOptionsNative() + { +#if BOOST_COROSIO_HAS_EPOLL + // tag::options_native[] + corosio::io_context_options opts; + opts.max_events_per_poll = 512; + + corosio::native_io_context ioc(opts); + // end::options_native[] + ioc.run(); + BOOST_TEST(opts.max_events_per_poll == 512u); +#endif + } + + void + testLockingUnsafe() + { + // tag::locking_unsafe[] + corosio::io_context_options opts; + opts.locking = corosio::locking_mode::unsafe; + + corosio::io_context ioc(opts); + ioc.run(); // only one thread may call this + // end::locking_unsafe[] + BOOST_TEST(opts.locking == corosio::locking_mode::unsafe); + } + + void + run() + { + testOptionsBasic(); + testOptionsNative(); + testLockingUnsafe(); + } +}; + +} // namespace + +TEST_SUITE(configuration_test, "boost.corosio.doc.4c2_configuration"); diff --git a/test/doc/snippets/4c_io_context.cpp b/test/doc/snippets/4c_io_context.cpp new file mode 100644 index 000000000..9a99bb4df --- /dev/null +++ b/test/doc/snippets/4c_io_context.cpp @@ -0,0 +1,332 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4c.io-context.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +bool my_coroutine_ran = false; + +capy::task<> my_coroutine() +{ + my_coroutine_ran = true; + co_return; +} + +// Dispatching a continuation with no coroutine behind it would crash +// the loop; the fragment is compiled but never executed. +[[maybe_unused]] void +executor_ops_fragment( + corosio::io_context& ioc, + capy::continuation& cont, + std::coroutine_handle<> handle) +{ + // tag::executor_ops[] + auto ex = ioc.get_executor(); + + // Dispatch a continuation: symmetric transfer if inside run(), + // otherwise post. Returns a handle to resume. + std::coroutine_handle<> next = ex.dispatch(cont); + + // Post a continuation: always queue for later execution + ex.post(cont); + + // Post a bare coroutine handle for later execution + ex.post(handle); + // end::executor_ops[] +} + +struct my_service : capy::execution_context::service +{ + explicit my_service(capy::execution_context&) {} + +protected: + void shutdown() override {} +}; + +struct io_context_test +{ + void + testOverview() + { + // tag::overview[] + corosio::io_context ioc; + + // ... create I/O objects and launch coroutines ... + + ioc.run(); // Process events until all work completes + // end::overview[] + } + + void + testConstructDefault() + { + // tag::construct_default[] + corosio::io_context ioc; + // end::construct_default[] + } + + void + testConstructHintOne() + { + // tag::construct_hint[] + corosio::io_context ioc(1); // one thread expected to call run() + // end::construct_hint[] + } + + void + testConstructHintFour() + { + // tag::construct_hint[] + corosio::io_context ioc(4); // up to 4 threads expected to call run() + // end::construct_hint[] + } + + void + testRun() + { + corosio::io_context ioc; + // tag::run[] + std::size_t n = ioc.run(); + std::cout << "Processed " << n << " handlers\n"; + // end::run[] + BOOST_TEST(n == 0); + } + + void + testRunOne() + { + corosio::io_context ioc; + // tag::run_one[] + std::size_t n = ioc.run_one(); // Returns 0 or 1 + // end::run_one[] + BOOST_TEST(n == 0); + } + + void + testRunFor() + { + corosio::io_context ioc; + // With no outstanding work both calls return immediately. + auto deadline = std::chrono::steady_clock::now(); + // tag::run_for[] + using namespace std::chrono_literals; + + auto n = ioc.run_for(100ms); // Run for 100 milliseconds + auto m = ioc.run_until(deadline); // Run until time point + // end::run_for[] + BOOST_TEST(n == 0); + BOOST_TEST(m == 0); + } + + void + testPoll() + { + corosio::io_context ioc; + // tag::poll[] + std::size_t n = ioc.poll(); // All ready handlers + std::size_t m = ioc.poll_one(); // At most one ready handler + // end::poll[] + BOOST_TEST(n == 0); + BOOST_TEST(m == 0); + } + + void + testStop() + { + corosio::io_context ioc; + // tag::stop[] + ioc.stop(); + // end::stop[] + BOOST_TEST(ioc.stopped()); + } + + void + testStopped() + { + corosio::io_context ioc; + ioc.stop(); + // tag::stopped[] + if (ioc.stopped()) + std::cout << "Event loop stopped\n"; + // end::stopped[] + BOOST_TEST(ioc.stopped()); + } + + void + testRestart() + { + corosio::io_context ioc; + // tag::restart[] + ioc.stop(); + // ... do something ... + ioc.restart(); + ioc.run(); // Can run again + // end::restart[] + // run() stops the context again once the work runs out; the + // restart() above is what allowed it to process at all. + BOOST_TEST(ioc.stopped()); + } + + void + testExecutor() + { + my_coroutine_ran = false; + corosio::io_context ioc; + // tag::executor[] + auto ex = ioc.get_executor(); + + // Launch a coroutine + capy::run_async(ex)(my_coroutine()); + + // Access the context + corosio::io_context& ctx = ex.context(); + + // Check if running inside the event loop + if (ex.running_in_this_thread()) + std::cout << "Inside run()\n"; + // end::executor[] + BOOST_TEST(&ctx == &ioc); + ioc.run(); + BOOST_TEST(my_coroutine_ran); + } + + void + testWorkTracking() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + // tag::work_tracking[] + ex.on_work_started(); // Increment work count + ex.on_work_finished(); // Decrement work count + // end::work_tracking[] + BOOST_TEST(ioc.run() == 0); + } + + void + testMultithreaded() + { + // tag::multithreaded[] + corosio::io_context ioc(4); + + std::vector threads; + for (int i = 0; i < 4; ++i) + threads.emplace_back([&ioc] { ioc.run(); }); + + for (auto& t : threads) + t.join(); + // end::multithreaded[] + BOOST_TEST(threads.size() == 4); + } + + void + testTeardown() + { + // tag::teardown[] + corosio::io_context ioc(4); + + std::vector threads; + for (int i = 0; i < 4; ++i) + threads.emplace_back([&ioc] { ioc.run(); }); + + // Join every run() thread before ioc leaves scope. + for (auto& t : threads) + t.join(); + // end::teardown[] + BOOST_TEST(threads.size() == 4); + } + + void + testServices() + { + corosio::io_context ioc; + // tag::services[] + // Create or get a service + my_service& svc = ioc.use_service(); + + // Check if service exists + if (ioc.has_service()) + { + // ... + } + // end::services[] + BOOST_TEST(ioc.has_service()); + BOOST_TEST(&ioc.use_service() == &svc); + } + + void + run() + { + testOverview(); + testConstructDefault(); + testConstructHintOne(); + testConstructHintFour(); + testRun(); + testRunOne(); + testRunFor(); + testPoll(); + testStop(); + testStopped(); + testRestart(); + testExecutor(); + testWorkTracking(); + testMultithreaded(); + testTeardown(); + testServices(); + } +}; + +} // namespace + +TEST_SUITE(io_context_test, "boost.corosio.doc.4c_io_context"); diff --git a/test/doc/snippets/4d_sockets.cpp b/test/doc/snippets/4d_sockets.cpp new file mode 100644 index 000000000..e20afdc3d --- /dev/null +++ b/test/doc/snippets/4d_sockets.cpp @@ -0,0 +1,570 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4d.sockets.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +// The header guard makes the copy shown inside the range-connect +// fragment expand to nothing; indented to match the second region so +// the page's indent=0 renders both flush. + // tag::connect_range[] + #include + // end::connect_range[] + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Fragments that connect to a remote peer are compiled but never +// executed; loopback fragments run over test::make_socket_pair. + +[[maybe_unused]] capy::task<> +overview_fragment(corosio::io_context& ioc) +{ + // tag::overview[] + corosio::tcp_socket s(ioc); + s.open(); + + auto [ec] = co_await s.connect( + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); + + char buf[1024]; + auto [read_ec, n] = co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + // end::overview[] +} + +void +construct_fragment(corosio::io_context& ioc) +{ + // tag::construct[] + // From io_context + corosio::tcp_socket s1(ioc); + + // From executor + auto ex = ioc.get_executor(); + corosio::tcp_socket s2(ex); + // end::construct[] +} + +void +open_fragment(corosio::tcp_socket& s) +{ + // tag::open[] + s.open(); // Creates IPv4 TCP socket, associates with the platform + // reactor (IOCP on Windows, epoll/kqueue/select on POSIX) + // end::open[] +} + +void +close_fragment(corosio::tcp_socket& s) +{ + // tag::close[] + s.close(); // Cancels pending ops, closes socket + // end::close[] +} + +void +is_open_fragment(corosio::tcp_socket& s) +{ + // tag::is_open[] + if (s.is_open()) + { + // Socket ready for I/O + } + // end::is_open[] +} + +[[maybe_unused]] capy::task<> +connect_fragment(corosio::tcp_socket& s, corosio::endpoint endpoint) +{ + // tag::connect[] + auto [ec] = co_await s.connect(endpoint); + // end::connect[] +} + +[[maybe_unused]] capy::task<> +connect_throw_fragment(corosio::tcp_socket& s, corosio::endpoint endpoint) +{ + // tag::connect_throw[] + if (auto [ec] = co_await s.connect(endpoint); ec) + throw std::system_error(ec); // Throws on error + // end::connect_throw[] +} + +[[maybe_unused]] capy::task<> +connect_range_fragment(corosio::io_context& ioc) +{ + // tag::connect_range[] + + corosio::resolver r(ioc); + auto [rec, results] = co_await r.resolve("www.boost.org", "80"); + if (rec) + co_return; + + corosio::tcp_socket s(ioc); + auto [cec, ep] = co_await corosio::connect(s, results); + if (cec) + co_return; + // `ep` is the endpoint that accepted the connection. + // end::connect_range[] +} + +// The page displays the library's signature; declaring it here keeps +// the shown declaration honest against the real one. +// tag::connect_signature[] +template + requires std::convertible_to< + std::ranges::range_reference_t, + typename Socket::endpoint_type> +capy::task> +connect(Socket& s, Range endpoints); +// end::connect_signature[] + +[[maybe_unused]] capy::task<> +connect_condition_fragment( + corosio::tcp_socket& s, corosio::resolver_results results) +{ + // tag::connect_condition[] + auto [ec, ep] = co_await corosio::connect( + s, + results, + [](std::error_code const&, corosio::endpoint const& e) { + return e.is_v4(); // IPv4 only. + }); + // end::connect_condition[] +} + +[[maybe_unused]] capy::task<> +connect_iterator_fragment( + corosio::tcp_socket& s, std::vector const& v) +{ + // tag::connect_iterator[] + auto [ec, it] = co_await corosio::connect(s, v.begin(), v.end()); + if (!ec) + std::cout << "connected to index " << (it - v.begin()) << "\n"; + // end::connect_iterator[] +} + +capy::task<> +read_some_fragment(corosio::tcp_socket& s, std::size_t& bytes_read) +{ + // tag::read_some[] + char buf[1024]; + auto [ec, n] = co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + // end::read_some[] + if (!ec) + bytes_read = n; +} + +capy::task<> +read_eof_fragment( + corosio::tcp_socket& s, capy::mutable_buffer buf, bool& got_eof) +{ + // tag::read_eof[] + auto [ec, n] = co_await s.read_some(buf); + + if (ec == capy::cond::eof) + { + // Connection closed normally + } + // end::read_eof[] + got_eof = (ec == capy::cond::eof); +} + +capy::task<> +read_all_fragment( + corosio::tcp_socket& s, capy::mutable_buffer buf, std::size_t& total) +{ + // tag::read_all[] + auto [ec, n] = co_await capy::read(s, buf); + // n == buffer_size(buf) or error occurred + // end::read_all[] + if (!ec) + total = n; +} + +capy::task<> +write_some_fragment(corosio::tcp_socket& s, std::size_t& bytes_written) +{ + // tag::write_some[] + std::string msg = "Hello"; + auto [ec, n] = co_await s.write_some( + capy::const_buffer(msg.data(), msg.size())); + // end::write_some[] + if (!ec) + bytes_written = n; +} + +capy::task<> +write_all_fragment( + corosio::tcp_socket& s, capy::const_buffer buf, std::size_t& total) +{ + // tag::write_all[] + auto [ec, n] = co_await capy::write(s, buf); + // n == buffer_size(buf) or error occurred + // end::write_all[] + if (!ec) + total = n; +} + +void +cancel_fragment(corosio::tcp_socket& s) +{ + // tag::cancel[] + s.cancel(); + // end::cancel[] +} + +// Cancellation delivery needs a stop source driving the launch; the +// fragment only shows that the awaiting code does not change. +[[maybe_unused]] capy::task<> +stop_token_fragment(corosio::tcp_socket& s, capy::mutable_buffer buf) +{ + // tag::stop_token[] + // Inside a coroutine launched with a stop token: + auto [ec, n] = co_await s.read_some(buf); + // Automatically cancelled if stop is requested + // end::stop_token[] +} + +void +move_assign_fragment(corosio::tcp_socket& s1, corosio::tcp_socket& s2) +{ + // tag::move_assign[] + s1 = std::move(s2); // Closes s1's socket if open, then moves s2 + // end::move_assign[] +} + +capy::const_buffer some_buffer("hi", 2); + +// Indented to match the second region inside the caller below, so the +// page's indent=0 renders both flush. + // tag::io_stream_poly[] + capy::task send_data(corosio::io_stream& stream) + { + co_await capy::write(stream, some_buffer); + } + // end::io_stream_poly[] + +// The shown caller writes on a socket that was never connected, so it +// is compiled but never executed; the test runs send_data over a +// connected loopback socket instead. +[[maybe_unused]] capy::task<> +io_stream_poly_fragment(corosio::io_context& ioc) +{ + // tag::io_stream_poly[] + + // Works with socket, wolfssl_stream, or any io_stream + corosio::tcp_socket sock(ioc); + co_await send_data(sock); + // end::io_stream_poly[] +} + +capy::task<> +buffer_sequences_fragment(corosio::tcp_socket& s) +{ + char data[8]; + std::size_t size = sizeof(data); + char header[4]; + std::size_t header_size = sizeof(header); + char body[4]; + std::size_t body_size = sizeof(body); + + // tag::buffer_sequences[] + // Single buffer + capy::mutable_buffer buf(data, size); + co_await s.read_some(buf); + + // Multiple buffers (scatter/gather I/O) + std::array bufs = { + capy::mutable_buffer(header, header_size), + capy::mutable_buffer(body, body_size) + }; + co_await s.read_some(bufs); + // end::buffer_sequences[] +} + +// tag::echo_client[] +capy::task echo_client(corosio::io_context& ioc) +{ + corosio::tcp_socket s(ioc); + s.open(); + + if (auto [ec] = co_await s.connect( + corosio::endpoint(corosio::ipv4_address::loopback(), 8080)); ec) + throw std::system_error(ec); + + std::string msg = "Hello, server!"; + if (auto [ec, n] = co_await capy::write( + s, capy::const_buffer(msg.data(), msg.size())); ec) + throw std::system_error(ec); + + char buf[1024]; + auto [ec, n] = co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + + if (!ec) + std::cout << "Server replied: " + << std::string_view(buf, n) << "\n"; +} +// end::echo_client[] + +capy::task<> +peer_write(corosio::tcp_socket& s, std::string_view text) +{ + co_await capy::write(s, capy::const_buffer(text.data(), text.size())); +} + +capy::task<> +peer_close(corosio::tcp_socket& s) +{ + s.close(); // graceful FIN + co_return; +} + +struct sockets_test +{ + void + testConstruct() + { + corosio::io_context ioc; + construct_fragment(ioc); + } + + void + testOpenClose() + { + corosio::io_context ioc; + corosio::tcp_socket s(ioc); + open_fragment(s); + BOOST_TEST(s.is_open()); + is_open_fragment(s); + close_fragment(s); + BOOST_TEST(!s.is_open()); + } + + void + testReadSome() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + std::size_t bytes_read = 0; + capy::run_async(ex)(peer_write(b, "hello")); + capy::run_async(ex)(read_some_fragment(a, bytes_read)); + ioc.run(); + + BOOST_TEST(bytes_read > 0); + a.close(); + b.close(); + } + + void + testReadEof() + { + corosio::io_context ioc; + // Linger=false => graceful FIN on close. + auto [a, b] = corosio::test::make_socket_pair< + corosio::tcp_socket, corosio::tcp_acceptor, false>(ioc); + auto ex = ioc.get_executor(); + + char storage[64]; + bool got_eof = false; + capy::run_async(ex)(peer_close(b)); + capy::run_async(ex)(read_eof_fragment( + a, capy::mutable_buffer(storage, sizeof(storage)), got_eof)); + ioc.run(); + + BOOST_TEST(got_eof); + a.close(); + } + + void + testReadAll() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + char storage[5]; + std::size_t total = 0; + capy::run_async(ex)(peer_write(b, "hello")); + capy::run_async(ex)(read_all_fragment( + a, capy::mutable_buffer(storage, sizeof(storage)), total)); + ioc.run(); + + BOOST_TEST(total == sizeof(storage)); + a.close(); + b.close(); + } + + void + testWriteSome() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + std::size_t bytes_written = 0; + capy::run_async(ex)(write_some_fragment(a, bytes_written)); + ioc.run(); + + BOOST_TEST(bytes_written == 5); + a.close(); + b.close(); + } + + void + testWriteAll() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + std::string_view payload = "payload"; + std::size_t total = 0; + capy::run_async(ex)(write_all_fragment( + a, capy::const_buffer(payload.data(), payload.size()), total)); + ioc.run(); + + BOOST_TEST(total == payload.size()); + a.close(); + b.close(); + } + + void + testCancel() + { + corosio::io_context ioc; + corosio::tcp_socket s(ioc); + s.open(); + cancel_fragment(s); + s.close(); + } + + void + testMoveAssign() + { + corosio::io_context ioc; + corosio::tcp_socket s1(ioc); + corosio::tcp_socket s2(ioc); + s2.open(); + move_assign_fragment(s1, s2); + BOOST_TEST(s1.is_open()); + s1.close(); + } + + void + testSendData() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + capy::run_async(ex)(send_data(a)); + ioc.run(); + + a.close(); + b.close(); + } + + void + testBufferSequences() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + auto ex = ioc.get_executor(); + + // 16 bytes cover both reads: the first takes at most 8, so + // data remains for the scatter read regardless of the split. + capy::run_async(ex)(peer_write(b, "0123456789abcdef")); + capy::run_async(ex)(buffer_sequences_fragment(a)); + ioc.run(); + + a.close(); + b.close(); + } + + void + run() + { + testConstruct(); + testOpenClose(); + testReadSome(); + testReadEof(); + testReadAll(); + testWriteSome(); + testWriteAll(); + testCancel(); + testMoveAssign(); + testSendData(); + testBufferSequences(); + } +}; + +} // namespace + +TEST_SUITE(sockets_test, "boost.corosio.doc.4d_sockets"); diff --git a/test/doc/snippets/4e_tcp_acceptor.cpp b/test/doc/snippets/4e_tcp_acceptor.cpp new file mode 100644 index 000000000..29e4e5458 --- /dev/null +++ b/test/doc/snippets/4e_tcp_acceptor.cpp @@ -0,0 +1,425 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4e.tcp-acceptor.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Fragments that bind the page's fixed port 8080 are compiled but +// never executed; the runnable tests bind ephemeral ports instead. +[[maybe_unused]] capy::task<> +overview(corosio::io_context& ioc) +{ + // tag::overview_accept[] + // Convenience constructor: open + SO_REUSEADDR + bind + listen on port 8080 + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); + + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + + if (!ec) + { + // peer is now a connected socket + } + // end::overview_accept[] +} + +void +construction(corosio::io_context& ioc) +{ + // tag::construction[] + // From io_context + corosio::tcp_acceptor acc1(ioc); + + // From executor + auto ex = ioc.get_executor(); + corosio::tcp_acceptor acc2(ex); + // end::construction[] +} + +[[maybe_unused]] void +convenience_ctor(corosio::io_context& ioc) +{ + // tag::convenience_ctor[] + // open + SO_REUSEADDR + bind + listen; address family deduced from the endpoint + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); + // end::convenience_ctor[] +} + +[[maybe_unused]] std::error_code +bind_listen(corosio::io_context& ioc) +{ + // tag::bind_listen[] + corosio::tcp_acceptor acc(ioc); + acc.open(); // create an IPv4 TCP socket + + if (auto ec = acc.bind(corosio::endpoint(8080))) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + return ec; + } + + if (auto ec = acc.listen()) + { + std::cerr << "Listen failed: " << ec.message() << "\n"; + return ec; + } + // end::bind_listen[] + return {}; +} + +// The page displays listen()'s declaration; compiling it inside a +// dummy struct keeps the shown signature honest. +struct listen_signature +{ + // tag::listen_signature[] + [[nodiscard]] std::error_code listen(int backlog = 128); + // end::listen_signature[] +}; + +[[maybe_unused]] void +bind_all_interfaces(corosio::io_context& ioc) +{ + // tag::bind_all_interfaces[] + // Port only - binds to 0.0.0.0 (all IPv4 interfaces) + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); + // end::bind_all_interfaces[] +} + +[[maybe_unused]] void +bind_loopback(corosio::io_context& ioc) +{ + // tag::bind_loopback[] + // Localhost only + corosio::tcp_acceptor acc(ioc, corosio::endpoint( + corosio::ipv4_address::loopback(), 8080)); + // end::bind_loopback[] +} + +capy::task<> +accept_one( + corosio::io_context& ioc, + corosio::tcp_acceptor& acc, + std::error_code& out) +{ + // tag::accept_into_peer[] + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + // end::accept_into_peer[] + out = ec; +} + +capy::task<> +accept_value(corosio::tcp_acceptor& acc, std::error_code& out) +{ + // tag::accept_returning[] + auto [ec, peer] = co_await acc.accept(); + // end::accept_returning[] + out = ec; +} + +void +cancel_pending(corosio::tcp_acceptor& acc) +{ + // tag::cancel[] + acc.cancel(); + // end::cancel[] +} + +capy::task<> +stop_token_accept( + corosio::tcp_acceptor& acc, + corosio::tcp_socket& peer, + bool& canceled) +{ + // tag::accept_stop_token[] + // Inside a cancellable task: + auto [ec] = co_await acc.accept(peer); + if (ec == std::errc::operation_canceled) + std::cout << "Accept cancelled\n"; + // end::accept_stop_token[] + canceled = ec == std::errc::operation_canceled; +} + +void +close_acceptor(corosio::tcp_acceptor& acc) +{ + // tag::close[] + acc.close(); + // end::close[] +} + +bool +ready_to_accept(corosio::tcp_acceptor& acc) +{ + // tag::is_open[] + if (acc.is_open()) + { + // Ready to accept + } + // end::is_open[] + return acc.is_open(); +} + +// The deleted-copy line renders from the excluded region below; the +// static_assert keeps its claim true against the real class. +static_assert(!std::is_copy_constructible_v); + +void +move_semantics(corosio::io_context& ioc) +{ + // tag::move_only[] + corosio::tcp_acceptor acc1(ioc); + corosio::tcp_acceptor acc2 = std::move(acc1); // OK + // end::move_only[] +#if 0 + // tag::move_only[] + + corosio::tcp_acceptor acc3 = acc2; // Error: deleted copy constructor + // end::move_only[] +#endif + + // tag::move_assign[] + acc1 = std::move(acc2); // Closes acc1's socket if open, then moves acc2 + // end::move_assign[] +} + +capy::task<> +handle_connection(corosio::tcp_socket peer) +{ + co_return; +} + +// tag::accept_loop[] +capy::task accept_loop( + corosio::io_context& ioc, + corosio::tcp_acceptor& acc) +{ + for (;;) + { + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + + if (ec) + { + if (ec == std::errc::operation_canceled) + break; // Shutdown requested + + std::cerr << "Accept error: " << ec.message() << "\n"; + continue; // Try again + } + + // Spawn a coroutine to handle this connection + capy::run_async(ioc.get_executor())( + handle_connection(std::move(peer))); + } +} +// end::accept_loop[] + +// run_server waits on real process signals; compiling it is the test. +// tag::graceful_shutdown[] +capy::task run_server(corosio::io_context& ioc) +{ + corosio::tcp_acceptor acc(ioc); + acc.open(); + if (auto ec = acc.bind(corosio::endpoint(8080))) + { + std::cerr << "Bind failed: " << ec.message() << "\n"; + co_return; + } + if (auto ec = acc.listen()) + { + std::cerr << "Listen failed: " << ec.message() << "\n"; + co_return; + } + + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + + // Spawn accept loop + capy::run_async(ioc.get_executor())(accept_loop(ioc, acc)); + + // Wait for shutdown signal + auto [ec, signum] = co_await signals.wait(); + if (!ec) + { + std::cout << "Received signal " << signum << ", shutting down\n"; + acc.cancel(); // Stop accepting + // Existing connections continue until complete + } +} +// end::graceful_shutdown[] + +capy::task<> +connect_client( + corosio::io_context& ioc, + corosio::endpoint ep, + std::error_code& out) +{ + corosio::tcp_socket s(ioc); + s.open(); + auto [ec] = co_await s.connect(ep); + out = ec; +} + +struct tcp_acceptor_test +{ + void + testConstruction() + { + corosio::io_context ioc; + construction(ioc); + move_semantics(ioc); + } + + void + testAccept() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::tcp_acceptor acc(ioc); + acc.open(); + BOOST_TEST(!acc.bind(corosio::endpoint( + corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + BOOST_TEST(ready_to_accept(acc)); + auto ep = acc.local_endpoint(); + + std::error_code accept_ec; + std::error_code connect_ec; + capy::run_async(ex)(accept_one(ioc, acc, accept_ec)); + capy::run_async(ex)(connect_client(ioc, ep, connect_ec)); + ioc.run(); + BOOST_TEST(!accept_ec); + BOOST_TEST(!connect_ec); + + ioc.restart(); + std::error_code accept2_ec; + std::error_code connect2_ec; + capy::run_async(ex)(accept_value(acc, accept2_ec)); + capy::run_async(ex)(connect_client(ioc, ep, connect2_ec)); + ioc.run(); + BOOST_TEST(!accept2_ec); + BOOST_TEST(!connect2_ec); + + close_acceptor(acc); + BOOST_TEST(!ready_to_accept(acc)); + } + + void + testCancel() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::tcp_acceptor acc(ioc); + acc.open(); + BOOST_TEST(!acc.bind(corosio::endpoint( + corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + + std::error_code accept_ec; + capy::run_async(ex)(accept_one(ioc, acc, accept_ec)); + // FIFO posting: the accept is already pending when cancel runs + capy::run_async(ex)( + [](corosio::tcp_acceptor& a) -> capy::task<> { + cancel_pending(a); + co_return; + }(acc)); + ioc.run(); + BOOST_TEST(accept_ec == std::errc::operation_canceled); + } + + void + testStopToken() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::tcp_acceptor acc(ioc); + acc.open(); + BOOST_TEST(!acc.bind(corosio::endpoint( + corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + + corosio::tcp_socket peer(ioc); + std::stop_source source; + bool canceled = false; + capy::run_async(ex, source.get_token())( + stop_token_accept(acc, peer, canceled)); + capy::run_async(ex)( + [](std::stop_source& s) -> capy::task<> { + s.request_stop(); + co_return; + }(source)); + ioc.run(); + BOOST_TEST(canceled); + } + + void + run() + { + testConstruction(); + testAccept(); + testCancel(); + testStopToken(); + } +}; + +} // namespace + +TEST_SUITE(tcp_acceptor_test, "boost.corosio.doc.4e_tcp_acceptor"); diff --git a/test/doc/snippets/4f_endpoints.cpp b/test/doc/snippets/4f_endpoints.cpp new file mode 100644 index 000000000..a720eccc6 --- /dev/null +++ b/test/doc/snippets/4f_endpoints.cpp @@ -0,0 +1,362 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4f.endpoints.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include + +namespace corosio = boost::corosio; +// end::assume[] + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +capy::task<> +connecting(corosio::io_context& ioc, bool& done) +{ + // tag::connecting[] + corosio::tcp_socket s(ioc); + s.open(); + + corosio::endpoint target( + corosio::ipv4_address::loopback(), 8080); + + auto [ec] = co_await s.connect(target); + // end::connecting[] + done = true; +} + +// Binds the page's fixed port 8080; compiled but never executed. +[[maybe_unused]] void +accepting(corosio::io_context& ioc) +{ + // tag::accepting[] + // Convenience constructor: open + SO_REUSEADDR + bind + listen + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); // bind to all interfaces + // end::accepting[] +} + +// Requires external DNS; compiled but never executed. +[[maybe_unused]] capy::task<> +resolver_results_fragment(corosio::io_context& ioc) +{ + // tag::resolver_results[] + corosio::resolver r(ioc); + auto [ec, results] = co_await r.resolve("www.example.com", "80"); + + for (auto const& entry : results) + { + corosio::endpoint ep = entry.get_endpoint(); + // Try connecting to ep... + } + // end::resolver_results[] +} + +// Connects to resolved public endpoints; compiled but never executed. +[[maybe_unused]] capy::task<> +implicit_conversion( + corosio::tcp_socket& s, + corosio::resolver_results const& results) +{ + // tag::implicit_conversion[] + for (corosio::endpoint ep : results) + { + auto [ec] = co_await s.connect(ep); + if (!ec) + break; + } + // end::implicit_conversion[] +} + +struct endpoints_test +{ + void + testOverview() + { + // tag::overview[] + // IPv4 endpoint + corosio::endpoint ep4(corosio::ipv4_address::loopback(), 8080); + + // IPv6 endpoint + corosio::endpoint ep6(corosio::ipv6_address::loopback(), 8080); + + // Port only (binds to all interfaces) + corosio::endpoint bind_ep(8080); + // end::overview[] + BOOST_TEST(ep4.is_v4()); + BOOST_TEST(ep6.is_v6()); + BOOST_TEST(bind_ep.port() == 8080); + BOOST_TEST(bind_ep.v4_address() == corosio::ipv4_address::any()); + } + + void + testFromV4() + { + // tag::from_v4[] + auto addr = corosio::ipv4_address::loopback(); // 127.0.0.1 + corosio::endpoint ep(addr, 8080); + // end::from_v4[] + BOOST_TEST(ep.is_v4()); + BOOST_TEST(ep.port() == 8080); + } + + void + testFromV6() + { + // tag::from_v6[] + auto addr = corosio::ipv6_address::loopback(); // ::1 + corosio::endpoint ep(addr, 8080); + // end::from_v6[] + BOOST_TEST(ep.is_v6()); + BOOST_TEST(ep.port() == 8080); + } + + void + testPortOnly() + { + // tag::port_only[] + corosio::endpoint ep(8080); // IPv4 any address (0.0.0.0) + // end::port_only[] + BOOST_TEST(ep.is_v4()); + BOOST_TEST(ep.v4_address() == corosio::ipv4_address::any()); + } + + void + testDefaultCtor() + { + // tag::default_ctor[] + corosio::endpoint ep; // IPv4 any address, port 0 + // end::default_ctor[] + BOOST_TEST(ep.is_v4()); + BOOST_TEST(ep.port() == 0); + } + + void + testQueryType() + { + corosio::endpoint ep(corosio::ipv4_address::loopback(), 8080); + // tag::query_type[] + if (ep.is_v4()) + std::cout << "IPv4 address\n"; + + if (ep.is_v6()) + std::cout << "IPv6 address\n"; + // end::query_type[] + BOOST_TEST(ep.is_v4()); + BOOST_TEST(!ep.is_v6()); + } + + void + testPort() + { + corosio::endpoint ep(corosio::ipv4_address::loopback(), 8080); + // tag::port[] + std::uint16_t port = ep.port(); // Host byte order + // end::port[] + BOOST_TEST(port == 8080); + } + + void + testV4Address() + { + corosio::endpoint ep(corosio::ipv4_address::loopback(), 8080); + // tag::v4_address[] + if (ep.is_v4()) + { + corosio::ipv4_address addr = ep.v4_address(); + std::cout << addr.to_string() << "\n"; + } + // end::v4_address[] + BOOST_TEST(ep.v4_address().to_string() == "127.0.0.1"); + } + + void + testV6Address() + { + corosio::endpoint ep(corosio::ipv6_address::loopback(), 8080); + // tag::v6_address[] + if (ep.is_v6()) + { + corosio::ipv6_address addr = ep.v6_address(); + std::cout << addr.to_string() << "\n"; + } + // end::v6_address[] + BOOST_TEST(ep.v6_address().to_string() == "::1"); + } + + void + testLoopback() + { + // tag::loopback[] + // IPv4 loopback: 127.0.0.1 + auto v4_loop = corosio::ipv4_address::loopback(); + + // IPv6 loopback: ::1 + auto v6_loop = corosio::ipv6_address::loopback(); + // end::loopback[] + BOOST_TEST(v4_loop.to_string() == "127.0.0.1"); + BOOST_TEST(v6_loop.to_string() == "::1"); + } + + void + testAny() + { + // tag::any[] + // IPv4 any: 0.0.0.0 (all interfaces) + auto v4_any = corosio::ipv4_address::any(); + + // IPv6 any: :: (all interfaces) + auto v6_any = corosio::ipv6_address::any(); + // end::any[] + BOOST_TEST(v4_any.to_string() == "0.0.0.0"); + BOOST_TEST(v6_any.to_string() == "::"); + } + + void + testBroadcast() + { + // tag::broadcast[] + // IPv4 broadcast: 255.255.255.255 + auto v4_bcast = corosio::ipv4_address::broadcast(); + // end::broadcast[] + BOOST_TEST(v4_bcast.to_string() == "255.255.255.255"); + } + + void + testParseAddresses() + { + // tag::parse_addresses[] + // IPv4 + corosio::ipv4_address addr; + if (auto ec = corosio::parse_ipv4_address("192.168.1.1", addr); !ec) + { + corosio::endpoint ep(addr, 8080); + } + + // IPv6 + corosio::ipv6_address addr6; + if (auto ec = corosio::parse_ipv6_address("2001:db8::1", addr6); !ec) + { + corosio::endpoint ep(addr6, 8080); + } + // end::parse_addresses[] + BOOST_TEST(addr.to_string() == "192.168.1.1"); + BOOST_TEST(addr6.to_string() == "2001:db8::1"); + } + + void + testParseEndpoint() + { + // tag::parse_endpoint[] + corosio::endpoint ep; + if (auto ec = corosio::parse_endpoint("192.168.1.1:8080", ep); !ec) + { + // Use ep... + } + // end::parse_endpoint[] + BOOST_TEST(ep.is_v4()); + BOOST_TEST(ep.v4_address().to_string() == "192.168.1.1"); + BOOST_TEST(ep.port() == 8080); + } + + void + testComparison() + { + // tag::comparison[] + corosio::endpoint ep1(corosio::ipv4_address::loopback(), 8080); + corosio::endpoint ep2(corosio::ipv4_address::loopback(), 8080); + corosio::endpoint ep3(corosio::ipv4_address::loopback(), 9090); + + assert(ep1 == ep2); // Same address and port + assert(ep1 != ep3); // Different port + // end::comparison[] + BOOST_TEST(ep1 == ep2); + BOOST_TEST(ep1 != ep3); + } + + void + testConnecting() + { + // Loopback connect completes either way: refused when the port + // is closed, connected when something happens to listen. + corosio::io_context ioc; + bool done = false; + capy::run_async(ioc.get_executor())(connecting(ioc, done)); + ioc.run(); + BOOST_TEST(done); + } + + void + run() + { + testOverview(); + testFromV4(); + testFromV6(); + testPortOnly(); + testDefaultCtor(); + testQueryType(); + testPort(); + testV4Address(); + testV6Address(); + testLoopback(); + testAny(); + testBroadcast(); + testParseAddresses(); + testParseEndpoint(); + testComparison(); + testConnecting(); + } +}; + +} // namespace + +TEST_SUITE(endpoints_test, "boost.corosio.doc.4f_endpoints"); diff --git a/test/doc/snippets/4g_composed_operations.cpp b/test/doc/snippets/4g_composed_operations.cpp new file mode 100644 index 000000000..cef5f9776 --- /dev/null +++ b/test/doc/snippets/4g_composed_operations.cpp @@ -0,0 +1,416 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4g.composed-operations.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include + +// tag::assume[] +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +// tag::slice_helper[] +#include +// end::slice_helper[] + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The pages show the algorithms with function-style synopses; the real +// entities have identical call signatures. The declarations only need +// to compile. +namespace synopsis { + +using namespace boost::capy; + +// tag::read_signature[] +auto +read( + ReadStream auto& stream, + MutableBufferSequence auto buffers) -> + capy::io_task; +// end::read_signature[] + +// tag::write_signature[] +auto +write( + WriteStream auto& stream, + ConstBufferSequence auto buffers) -> + capy::io_task; +// end::write_signature[] + +// tag::slice_interface[] +template + requires MutableBufferSequence + || ConstBufferSequence +slice_type +buffer_slice( + BufferSequence const& seq, + std::size_t offset = 0, + std::size_t length = (std::numeric_limits::max)()); +// end::slice_interface[] + +} // namespace synopsis + +capy::task<> primitives_frag(corosio::tcp_socket& s) +{ + // tag::primitives[] + char buf[1024]; + auto [ec, n] = co_await s.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + // n could be 1, 100, 500, or 1024 - no guarantee + // end::primitives[] + BOOST_TEST(!ec); + BOOST_TEST(n > 0); +} + +capy::task<> read_full_frag(corosio::tcp_socket& stream) +{ + // tag::read_full[] + char buf[1024]; + auto [ec, n] = co_await capy::read( + stream, capy::mutable_buffer(buf, sizeof(buf))); + + // Either: + // - n == 1024 and ec is default (success) + // - ec == capy::cond::eof and n < 1024 (reached end of stream) + // - ec is some other error + // end::read_full[] + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, sizeof(buf)); +} + +capy::task<> read_until_eof_frag( + corosio::tcp_socket& stream, std::string& out) +{ + // tag::read_until_eof[] + std::string content; + char chunk[2048]; + for (;;) + { + auto [ec, n] = co_await stream.read_some( + capy::mutable_buffer(chunk, sizeof(chunk))); + content.append(chunk, n); + if (ec == capy::cond::eof) + break; // success: the whole stream was consumed + if (ec) + throw std::system_error(ec); + } + // end::read_until_eof[] + out = content; +} + +capy::task<> write_full_frag(corosio::tcp_socket& stream) +{ + // tag::write_full[] + std::string msg = "Hello, World!"; + auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(msg.data(), msg.size())); + + // Either: + // - n == msg.size() and ec is default (all data written) + // - ec is an error + // end::write_full[] + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, msg.size()); +} + +capy::task<> error_bindings_frag( + corosio::tcp_socket& stream, capy::mutable_buffer buf, + std::error_code& out) +{ + // tag::error_bindings[] + auto [ec, n] = co_await capy::read(stream, buf); + if (ec) + { + if (ec == capy::cond::eof) + std::cout << "End of stream, read " << n << " bytes\n"; + else + std::cerr << "Error: " << ec.message() << "\n"; + } + // end::error_bindings[] + out = ec; +} + +capy::task<> error_exceptions_frag( + corosio::tcp_socket& stream, capy::mutable_buffer buf) +{ + // tag::error_exceptions[] + // For write (EOF doesn't apply) + auto [wec, n] = co_await capy::write(stream, buf); + if (wec) + capy::detail::throw_system_error(wec); + + // For read (need to handle EOF) + auto [rec, rn] = co_await capy::read(stream, buf); + if (rec && rec != capy::cond::eof) + capy::detail::throw_system_error(rec); + // end::error_exceptions[] + BOOST_TEST_EQ(rn, buf.size()); +} + +capy::task<> cancellation_frag( + corosio::tcp_socket& stream, std::error_code& out) +{ + char storage[4096]; + capy::mutable_buffer large_buffer(storage, sizeof(storage)); + // tag::cancellation[] + auto [ec, n] = co_await capy::read(stream, large_buffer); + if (ec == capy::cond::canceled) + std::cout << "Cancelled after reading " << n << " bytes\n"; + // end::cancellation[] + out = ec; +} + +// Contrasts call shapes only; issuing these reads needs a peer that +// sends this much data, so the coroutine is never launched. +[[maybe_unused]] capy::task<> multiple_buffers_frag( + corosio::tcp_socket& stream, char* header, char* body, + capy::mutable_buffer buf1, capy::mutable_buffer buf2) +{ + // tag::multiple_buffers[] + // Efficient: single system call per read_some() + std::array bufs = { + capy::mutable_buffer(header, 16), + capy::mutable_buffer(body, 1024)}; + co_await capy::read(stream, bufs); + + // Less efficient: may require more system calls + co_await capy::read(stream, buf1); + co_await capy::read(stream, buf2); + // end::multiple_buffers[] +} + +// tag::http_response[] +capy::task read_http_response(corosio::io_stream& stream) +{ + std::string response; + char chunk[2048]; + for (;;) + { + auto [ec, n] = co_await stream.read_some( + capy::mutable_buffer(chunk, sizeof(chunk))); + response.append(chunk, n); + if (ec == capy::cond::eof) + break; + if (ec) + throw std::system_error(ec); + } + co_return response; +} +// end::http_response[] + +capy::task<> send_and_close( + corosio::tcp_socket& s, std::string_view text) +{ + auto [ec, n] = co_await capy::write( + s, capy::const_buffer(text.data(), text.size())); + BOOST_TEST(!ec); + s.shutdown(corosio::shutdown_send); +} + +struct composed_operations_test +{ + void + testPrimitives() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> + { + std::string_view text = "hi"; + co_await capy::write( + b, capy::const_buffer(text.data(), text.size())); + co_await primitives_frag(a); + }(s1, s2)); + ioc.run(); + } + + void + testReadFull() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> + { + std::string big(1024, 'x'); + co_await capy::write( + b, capy::const_buffer(big.data(), big.size())); + co_await read_full_frag(a); + }(s1, s2)); + ioc.run(); + } + + void + testReadUntilEof() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + std::string content; + capy::run_async(ioc.get_executor())( + send_and_close(s2, "the whole stream")); + capy::run_async(ioc.get_executor())( + read_until_eof_frag(s1, content)); + ioc.run(); + BOOST_TEST_EQ(content, "the whole stream"); + } + + void + testWriteFull() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + capy::run_async(ioc.get_executor())(write_full_frag(s1)); + ioc.run(); + } + + void + testBufferSlice() + { + char header[16]; + char body[1024]; + // tag::slice_helper[] + + std::array bufs = { + capy::mutable_buffer(header, 16), + capy::mutable_buffer(body, 1024) + }; + + // After reading 20 bytes, slice off what was consumed: + auto rest = capy::buffer_slice(bufs, 20); + // rest covers: 4 bytes of header remaining + full body + // end::slice_helper[] + BOOST_TEST_EQ(capy::buffer_size(rest), 1020u); + } + + void + testErrorBindings() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + char storage[64]; + std::error_code ec; + capy::run_async(ioc.get_executor())(send_and_close(s2, "hello")); + capy::run_async(ioc.get_executor())(error_bindings_frag( + s1, capy::mutable_buffer(storage, sizeof(storage)), ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::eof); + } + + void + testErrorExceptions() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + char storage[16] = {}; + capy::run_async(ioc.get_executor())( + send_and_close(s2, std::string_view("0123456789abcdef", 16))); + capy::run_async(ioc.get_executor())(error_exceptions_frag( + s1, capy::mutable_buffer(storage, sizeof(storage)))); + ioc.run(); + } + + void + testCancellation() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + std::error_code ec; + std::stop_source source; + source.request_stop(); + capy::run_async(ioc.get_executor(), source.get_token())( + cancellation_frag(s1, ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::canceled); + } + + void + testHttpResponse() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + std::string response; + capy::run_async(ioc.get_executor())( + send_and_close(s2, "HTTP/1.1 200 OK\r\n\r\n")); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& s, std::string& out) -> capy::task<> + { + out = co_await read_http_response(s); + }(s1, response)); + ioc.run(); + BOOST_TEST_EQ(response, "HTTP/1.1 200 OK\r\n\r\n"); + } + + void + run() + { + testPrimitives(); + testReadFull(); + testReadUntilEof(); + testWriteFull(); + testBufferSlice(); + testErrorBindings(); + testErrorExceptions(); + testCancellation(); + testHttpResponse(); + } +}; + +} // namespace + +TEST_SUITE( + composed_operations_test, + "boost.corosio.doc.4g_composed_operations"); diff --git a/test/doc/snippets/4h_timers.cpp b/test/doc/snippets/4h_timers.cpp new file mode 100644 index 000000000..bea1d5be0 --- /dev/null +++ b/test/doc/snippets/4h_timers.cpp @@ -0,0 +1,222 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4h.timers.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +using namespace std::chrono_literals; +// end::assume[] + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +capy::task<> delay_duration_frag(std::error_code& out) +{ + // tag::delay_duration[] + auto [ec] = co_await corosio::delay(500ms); + if (!ec) + std::cout << "500ms elapsed\n"; + // end::delay_duration[] + out = ec; +} + +capy::task<> delay_timepoint_frag() +{ + // tag::delay_timepoint[] + auto next = std::chrono::steady_clock::now(); + for (int i = 0; i < 10; ++i) + { + next += 100ms; + auto [ec] = co_await corosio::delay(next); + if (ec) + break; + std::cout << "Tick " << i << "\n"; + } + // end::delay_timepoint[] +} + +capy::task<> delay_cancel_frag(std::error_code& out) +{ + // tag::delay_cancel[] + auto [ec] = co_await corosio::delay(10s); + if (ec == capy::cond::canceled) + std::cout << "Delay was cancelled\n"; + // end::delay_cancel[] + out = ec; +} + +capy::task<> timeout_read_frag( + corosio::tcp_socket& sock, capy::mutable_buffer buffer, + std::error_code& out) +{ + // tag::timeout_read[] + auto [ec, n] = co_await corosio::timeout( + sock.read_some(buffer), 200ms); + + if (ec == capy::cond::timeout) + std::cout << "No data within 200ms\n"; + else if (!ec) + std::cout << "Read " << n << " bytes\n"; + // end::timeout_read[] + out = ec; +} + +// The connect fragments dial real endpoints, so the coroutines are +// compiled but never launched. +capy::task<> timeout_deadline_frag( + corosio::tcp_socket& sock, corosio::endpoint ep) +{ + // tag::timeout_deadline[] + auto deadline = std::chrono::steady_clock::now() + 5s; + auto [ec] = co_await corosio::timeout(sock.connect(ep), deadline); + // end::timeout_deadline[] +} + +capy::task<> timeout_vs_cancel_frag( + corosio::tcp_socket& sock, corosio::endpoint ep) +{ + // tag::timeout_vs_cancel[] + auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); + if (ec == capy::cond::timeout) + std::cout << "Connect attempt timed out\n"; + else if (ec == capy::cond::canceled) + std::cout << "Connect attempt cancelled by caller\n"; + // end::timeout_vs_cancel[] +} + +// tag::connect_retry[] +capy::task<> +connect_with_deadline( + corosio::tcp_socket& sock, + corosio::endpoint ep, + int max_attempts) +{ + for (int attempt = 0; attempt < max_attempts; ++attempt) + { + auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); + if (!ec) + co_return; + + if (ec == capy::cond::canceled) + co_return; + + sock.close(); + auto [dec] = co_await corosio::delay(500ms); + if (dec == capy::cond::canceled) + co_return; + } +} +// end::connect_retry[] + +struct timers_test +{ + void + testDelayDuration() + { + corosio::io_context ioc; + std::error_code ec = make_error_code(std::errc::io_error); + capy::run_async(ioc.get_executor())(delay_duration_frag(ec)); + ioc.run(); + BOOST_TEST(!ec); + } + + void + testDelayTimePoint() + { + corosio::io_context ioc; + auto start = std::chrono::steady_clock::now(); + capy::run_async(ioc.get_executor())(delay_timepoint_frag()); + ioc.run(); + auto elapsed = std::chrono::steady_clock::now() - start; + BOOST_TEST(elapsed >= 1s); + } + + void + testDelayCancel() + { + corosio::io_context ioc; + std::error_code ec; + std::stop_source source; + source.request_stop(); + capy::run_async(ioc.get_executor(), source.get_token())( + delay_cancel_frag(ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::canceled); + } + + void + testTimeoutRead() + { + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + char data[128]; + std::error_code ec; + capy::run_async(ioc.get_executor())(timeout_read_frag( + s1, capy::mutable_buffer(data, sizeof(data)), ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::timeout); + } + + void + run() + { + testDelayDuration(); + testDelayTimePoint(); + testDelayCancel(); + testTimeoutRead(); + } +}; + +} // namespace + +TEST_SUITE(timers_test, "boost.corosio.doc.4h_timers"); diff --git a/test/doc/snippets/4i_signals.cpp b/test/doc/snippets/4i_signals.cpp new file mode 100644 index 000000000..e52ac4b6f --- /dev/null +++ b/test/doc/snippets/4i_signals.cpp @@ -0,0 +1,407 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4i.signals.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The page's move-semantics block is shown as pseudocode because its +// last line is intentionally ill-formed; these assertions keep the +// claims honest. +static_assert(std::is_move_constructible_v); +static_assert(!std::is_copy_constructible_v); + +capy::task<> overview_frag(corosio::io_context& ioc, int& out) +{ + // tag::overview[] + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + + auto [ec, signum] = co_await signals.wait(); + if (!ec) + std::cout << "Received signal " << signum << "\n"; + // end::overview[] + BOOST_TEST(!ec); + out = signum; +} + +capy::task<> raise_signal(int signum) +{ + std::raise(signum); + co_return; +} + +capy::task<> wait_switch_frag(corosio::signal_set& signals) +{ + // tag::wait_switch[] + auto [ec, signum] = co_await signals.wait(); + + if (!ec) + { + switch (signum) + { + case SIGINT: + std::cout << "Interrupt received\n"; + break; + case SIGTERM: + std::cout << "Termination requested\n"; + break; + } + } + // end::wait_switch[] + BOOST_TEST(!ec); + BOOST_TEST_EQ(signum, SIGINT); +} + +capy::task<> cancel_result_frag( + corosio::signal_set& signals, std::error_code& out) +{ + // tag::cancel_result[] + auto [ec, signum] = co_await signals.wait(); + if (ec == capy::cond::canceled) + std::cout << "Wait was cancelled\n"; + // end::cancel_result[] + out = ec; +} + +// The use-case coroutines wait for operator-sent signals in loops, so +// they are compiled but never launched. Fragments naming POSIX-only +// signals or sigaction flags follow the unit tests' platform guard. + +// tag::graceful_shutdown[] +capy::task shutdown_handler( + corosio::io_context& ioc, + std::atomic& running) +{ + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + + auto [ec, signum] = co_await signals.wait(); + if (!ec) + { + std::cout << "Shutdown signal received\n"; + running = false; + ioc.stop(); + } +} +// end::graceful_shutdown[] + +#if BOOST_COROSIO_POSIX + +// tag::signal_loop[] +capy::task signal_loop(corosio::io_context& ioc) +{ + corosio::signal_set signals(ioc, SIGUSR1); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + std::cout << "Received USR1, doing work...\n"; + // Handle signal + } +} +// end::signal_loop[] + +struct Config +{ + void reload() {} +}; + +// tag::config_reload[] +capy::task config_reloader( + corosio::io_context& ioc, + Config& config) +{ + corosio::signal_set signals(ioc, SIGHUP); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + std::cout << "Reloading configuration...\n"; + config.reload(); + } +} +// end::config_reload[] + +// tag::child_reaper[] +capy::task child_reaper(corosio::io_context& ioc) +{ + using flags = corosio::signal_set; + + corosio::signal_set signals(ioc); + + // Only notify on child termination, not stop/continue + // Prevent zombie processes automatically + signals.add(SIGCHLD, flags::no_child_stop | flags::no_child_wait); + + for (;;) + { + auto [ec, signum] = co_await signals.wait(); + if (ec) + break; + + // With no_child_wait, children are reaped automatically + std::cout << "Child process terminated\n"; + } +} +// end::child_reaper[] + +#endif // BOOST_COROSIO_POSIX + +// tag::server_shutdown[] +capy::task run_server(corosio::io_context& ioc) +{ + std::atomic running{true}; + + // Start signal handler + capy::run_async(ioc.get_executor())( + [](corosio::io_context& ioc, std::atomic& running) + -> capy::task + { + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + co_await signals.wait(); + running = false; + ioc.stop(); + }(ioc, running)); + + // Accept loop + corosio::tcp_acceptor acc(ioc, corosio::endpoint(8080)); + + while (running) + { + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + if (ec) + break; + + // Handle connection... + } +} +// end::server_shutdown[] + +struct signals_test +{ + void + testOverview() + { + corosio::io_context ioc; + int received = 0; + capy::run_async(ioc.get_executor())(overview_frag(ioc, received)); + capy::run_async(ioc.get_executor())(raise_signal(SIGTERM)); + ioc.run(); + BOOST_TEST_EQ(received, SIGTERM); + } + + void + testConstructEmpty() + { + corosio::io_context ioc; + // tag::construct_empty[] + corosio::signal_set signals(ioc); + signals.add(SIGINT); + signals.add(SIGTERM); + // end::construct_empty[] + } + +#if BOOST_COROSIO_POSIX + void + testConstructInitial() + { + corosio::io_context ioc; + // tag::construct_initial[] + // One signal + corosio::signal_set s1(ioc, SIGINT); + + // Two signals + corosio::signal_set s2(ioc, SIGINT, SIGTERM); + + // Three signals + corosio::signal_set s3(ioc, SIGINT, SIGTERM, SIGHUP); + // end::construct_initial[] + } + + void + testAdd() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc); + // tag::add_signal[] + signals.add(SIGUSR1); + // end::add_signal[] + } + + void + testAddFlags() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc); + // tag::add_flags[] + using flags = corosio::signal_set; + + // Restart interrupted system calls automatically + signals.add(SIGCHLD, flags::restart); + + // Multiple flags can be combined + signals.add(SIGCHLD, flags::restart | flags::no_child_stop); + // end::add_flags[] + } + + void + testFlagCompat() + { + corosio::io_context ioc; + using flags = corosio::signal_set; + // tag::flag_compat[] + corosio::signal_set s1(ioc); + corosio::signal_set s2(ioc); + + s1.add(SIGINT, flags::restart); // OK - first registration + s2.add(SIGINT, flags::restart); // OK - same flags + s2.add(SIGINT, flags::no_defer); // Error! - different flags + + // Use dont_care to accept existing flags + s2.add(SIGINT, flags::dont_care); // OK - accepts existing flags + // end::flag_compat[] + BOOST_TEST( + s2.add(SIGINT, flags::no_defer) == std::errc::invalid_argument); + } +#endif // BOOST_COROSIO_POSIX + + void + testRemove() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc, SIGINT); + // tag::remove_signal[] + signals.remove(SIGINT); + + // remove() returns an error_code instead of throwing + std::error_code ec = signals.remove(SIGINT); + // end::remove_signal[] + BOOST_TEST(!ec); + } + + void + testClear() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + // tag::clear_signals[] + signals.clear(); + + // clear() returns an error_code instead of throwing + std::error_code ec = signals.clear(); + // end::clear_signals[] + BOOST_TEST(!ec); + } + + void + testWaitSwitch() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc, SIGINT, SIGTERM); + capy::run_async(ioc.get_executor())(wait_switch_frag(signals)); + capy::run_async(ioc.get_executor())(raise_signal(SIGINT)); + ioc.run(); + } + + void + testCancel() + { + corosio::io_context ioc; + corosio::signal_set signals(ioc, SIGINT); + std::error_code ec; + capy::run_async(ioc.get_executor())( + cancel_result_frag(signals, ec)); + capy::run_async(ioc.get_executor())( + [](corosio::signal_set& signals) -> capy::task<> + { + // tag::cancel_call[] + signals.cancel(); + // end::cancel_call[] + co_return; + }(signals)); + ioc.run(); + BOOST_TEST(ec == capy::cond::canceled); + } + + void + run() + { + testOverview(); + testConstructEmpty(); +#if BOOST_COROSIO_POSIX + testConstructInitial(); + testAdd(); + testAddFlags(); + testFlagCompat(); +#endif + testRemove(); + testClear(); + testWaitSwitch(); + testCancel(); + } +}; + +} // namespace + +TEST_SUITE(signals_test, "boost.corosio.doc.4i_signals"); diff --git a/test/doc/snippets/4j_resolver.cpp b/test/doc/snippets/4j_resolver.cpp new file mode 100644 index 000000000..de2f5da0c --- /dev/null +++ b/test/doc/snippets/4j_resolver.cpp @@ -0,0 +1,400 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4j.resolver.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Fragments that resolve external names or connect to external hosts +// are compiled but never executed. +[[maybe_unused]] capy::task<> +overview(corosio::io_context& ioc) +{ + // tag::overview[] + corosio::resolver r(ioc); + auto [ec, results] = co_await r.resolve("www.example.com", "https"); + + for (auto const& entry : results) + { + auto ep = entry.get_endpoint(); + std::cout << ep.v4_address().to_string() << ":" << ep.port() << "\n"; + } + // end::overview[] +} + +void +construction() +{ + // tag::construction[] + corosio::io_context ioc; + corosio::resolver r(ioc); // From execution context + // end::construction[] +} + +[[maybe_unused]] capy::task<> +basic_resolution(corosio::resolver& r) +{ + // tag::basic_resolution[] + auto [ec, results] = co_await r.resolve("www.example.com", "80"); + // end::basic_resolution[] +} + +[[maybe_unused]] capy::task<> +with_flags(corosio::resolver& r) +{ + // tag::with_flags[] + auto [ec, results] = co_await r.resolve( + "www.example.com", + "https", + corosio::resolve_flags::address_configured); + // end::with_flags[] +} + +// Numeric host and service perform no DNS query, so this fragment runs. +capy::task<> +combined_flags(corosio::resolver& r) +{ + // tag::combined_flags[] + auto flags = + corosio::resolve_flags::numeric_host | + corosio::resolve_flags::numeric_service; + + auto [ec, results] = co_await r.resolve("127.0.0.1", "8080", flags); + // end::combined_flags[] + BOOST_TEST(!ec); + BOOST_TEST(!results.empty()); + if (!results.empty()) + { + auto ep = results.front().get_endpoint(); + BOOST_TEST(ep.is_v4()); + BOOST_TEST(ep.v4_address().to_string() == "127.0.0.1"); + BOOST_TEST(ep.port() == 8080); + } +} + +namespace results_synopsis { +using corosio::resolver_entry; +// tag::results_alias[] +using resolver_results = std::vector; +// end::results_alias[] +} // namespace results_synopsis + +void +iterate_results(corosio::resolver_results const& results) +{ + // tag::iterate_results[] + for (auto const& entry : results) + { + corosio::endpoint ep = entry.get_endpoint(); + + if (ep.is_v4()) + std::cout << "IPv4: " << ep.v4_address().to_string(); + else + std::cout << "IPv6: " << ep.v6_address().to_string(); + + std::cout << ":" << ep.port() << "\n"; + } + // end::iterate_results[] +} + +// Abridged interface listing; declarations compile, the real class +// lives in . +namespace entry_synopsis { +// tag::entry_synopsis[] +class resolver_entry +{ +public: + corosio::endpoint get_endpoint() const; + + // Implicit conversion to endpoint + operator corosio::endpoint() const; + + // Query strings used in the resolution + std::string const& host_name() const; + std::string const& service_name() const; +}; +// end::entry_synopsis[] +} // namespace entry_synopsis + +// tag::connect_to_service[] +capy::task connect_to_service( + corosio::io_context& ioc, + std::string_view host, + std::string_view service) +{ + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(host, service); + + if (resolve_ec) + throw std::system_error(resolve_ec); + + if (results.empty()) + throw std::runtime_error("No addresses found"); + + corosio::tcp_socket sock(ioc); + sock.open(); + + std::error_code last_error; + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry.get_endpoint()); + if (!ec) + co_return; // Connected successfully + + last_error = ec; + sock.close(); + sock.open(); + } + + throw std::system_error(last_error); +} +// end::connect_to_service[] + +void +cancel_pending(corosio::resolver& r) +{ + // tag::cancel[] + r.cancel(); + // end::cancel[] +} + +void +match_canceled(std::error_code ec) +{ + // tag::match_canceled[] + if (ec == capy::cond::canceled) + { + // resolution was cancelled + } + // end::match_canceled[] +} + +[[maybe_unused]] capy::task<> +sequential_resolves(corosio::resolver& resolver) +{ + // tag::single_inflight[] + // CORRECT: Sequential resolves on same resolver + auto [ec1, r1] = co_await resolver.resolve("host1", "80"); + auto [ec2, r2] = co_await resolver.resolve("host2", "80"); + + // end::single_inflight[] +} + +[[maybe_unused]] capy::task<> +parallel_resolves(corosio::io_context& ioc) +{ + // tag::single_inflight[] + // CORRECT: Parallel resolves with separate resolver instances + corosio::resolver r1(ioc), r2(ioc); + // In separate coroutines: + auto [ec1, res1] = co_await r1.resolve("host1", "80"); + auto [ec2, res2] = co_await r2.resolve("host2", "80"); + + // end::single_inflight[] +} + +[[maybe_unused]] void +concurrent_resolves(corosio::resolver& resolver) +{ + // tag::single_inflight[] + // WRONG: Concurrent resolves on same resolver - UNDEFINED BEHAVIOR + auto f1 = resolver.resolve("host1", "80"); + auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1 + // end::single_inflight[] +} + +// tag::http_get[] +capy::task http_get( + corosio::io_context& ioc, + std::string_view hostname) +{ + // Resolve hostname + corosio::resolver r(ioc); + auto [resolve_ec, results] = co_await r.resolve(hostname, "80"); + + if (resolve_ec) + { + std::cerr << "Resolution failed: " << resolve_ec.message() << "\n"; + co_return; + } + + // Connect to first address + corosio::tcp_socket sock(ioc); + sock.open(); + + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry); + if (!ec) + break; + } + + if (!sock.is_open()) + { + std::cerr << "Failed to connect\n"; + co_return; + } + + // Send HTTP request + std::string request = + "GET / HTTP/1.1\r\n" + "Host: " + std::string(hostname) + "\r\n" + "Connection: close\r\n" + "\r\n"; + + if (auto [ec, n] = co_await capy::write( + sock, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + // Read the response until the server closes the connection + std::string response; + for (;;) + { + char chunk[4096]; + auto [ec, n] = co_await capy::read( + sock, capy::mutable_buffer(chunk, sizeof(chunk))); + response.append(chunk, n); + if (ec) + break; + } + + std::cout << response << "\n"; +} +// end::http_get[] + +struct resolver_test +{ + void + testConstruction() + { + construction(); + BOOST_TEST(true); + } + + void + testCombinedFlags() + { + corosio::io_context ioc; + corosio::resolver r(ioc); + capy::run_async(ioc.get_executor())(combined_flags(r)); + ioc.run(); + } + + void + testIterateResults() + { + // Synthetic results keep the fragment's loop deterministic: + // no DNS query is needed to exercise it. + corosio::resolver_results results; + results.emplace_back( + corosio::endpoint(corosio::ipv4_address::loopback(), 80), + "localhost", "http"); + results.emplace_back( + corosio::endpoint(corosio::ipv6_address::loopback(), 443), + "localhost", "https"); + iterate_results(results); + BOOST_TEST(results.size() == 2); + } + + void + testCancel() + { + corosio::io_context ioc; + corosio::resolver r(ioc); + // Cancel with nothing in flight is a no-op. + cancel_pending(r); + BOOST_TEST(true); + } + + void + testMatchCanceled() + { + match_canceled(capy::error::canceled); + match_canceled({}); + BOOST_TEST(true); + } + + void + testMoveSemantics() + { + // The page's move-semantics block is pseudocode (it shows a + // deliberate compile error); verify the valid part here. + corosio::io_context ioc; + corosio::resolver r1(ioc); + corosio::resolver r2 = std::move(r1); + cancel_pending(r2); + BOOST_TEST(true); + } + + void + run() + { + testConstruction(); + testCombinedFlags(); + testIterateResults(); + testCancel(); + testMatchCanceled(); + testMoveSemantics(); + } +}; + +} // namespace + +TEST_SUITE(resolver_test, "boost.corosio.doc.4j_resolver"); diff --git a/test/doc/snippets/4k_tcp_server.cpp b/test/doc/snippets/4k_tcp_server.cpp new file mode 100644 index 000000000..aad0aa7bf --- /dev/null +++ b/test/doc/snippets/4k_tcp_server.cpp @@ -0,0 +1,316 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4k.tcp-server.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// tag::overview[] +class echo_worker : public corosio::tcp_server::worker_base +{ + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string buf; + +public: + explicit echo_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + { + buf.reserve(4096); + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(corosio::tcp_server::launcher launch) override + { + launch(ctx_.get_executor(), do_echo()); + } + + capy::task do_echo(); +}; + +// Build the worker pool as a range of pointer-like objects. +auto make_echo_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} + +class echo_server : public corosio::tcp_server +{ +public: + echo_server(corosio::io_context& ioc, int max_workers) + : tcp_server(ioc, ioc.get_executor()) + { + set_workers(make_echo_workers(ioc, max_workers)); + } +}; +// end::overview[] + +capy::task echo_worker::do_echo() +{ + co_return; +} + +// Abridged interface listing; the real class lives in +// , where `launcher` is a sibling +// member of the enclosing tcp_server and needs no qualification. +namespace worker_base_synopsis { +using launcher = corosio::tcp_server::launcher; +// tag::worker_base[] +class worker_base +{ + // Private list/bookkeeping members managed by tcp_server. +public: + worker_base(); + virtual ~worker_base(); + + virtual void run(launcher launch) = 0; + virtual corosio::tcp_socket& socket() = 0; +}; +// end::worker_base[] +} // namespace worker_base_synopsis + +// tag::my_worker[] +class my_worker : public corosio::tcp_server::worker_base +{ + corosio::io_context& ctx_; + corosio::tcp_socket sock_; + std::string request_buf; + std::string response_buf; + +public: + explicit my_worker(corosio::io_context& ctx) + : ctx_(ctx) + , sock_(ctx) + {} + + corosio::tcp_socket& socket() override { return sock_; } + + void run(corosio::tcp_server::launcher launch) override + { + // tag::launch_call[] + launch(ctx_.get_executor(), handle_connection()); + // end::launch_call[] + } + + capy::task handle_connection() + { + // Handle the connection using sock_ + // Worker is automatically returned to pool when coroutine ends + co_return; + } +}; +// end::my_worker[] + +// Abridged member signature; the real declaration lives in +// . +namespace set_workers_synopsis { +// tag::set_workers_signature[] +template +void set_workers(Range&& workers); +// end::set_workers_signature[] +} // namespace set_workers_synopsis + +// tag::make_workers[] +auto make_workers(corosio::io_context& ctx, int n) +{ + std::vector> v; + v.reserve(n); + for (int i = 0; i < n; ++i) + v.push_back(std::make_unique(ctx)); + return v; +} + +class my_server : public corosio::tcp_server +{ +public: + my_server(corosio::io_context& ioc, int max_workers) + : tcp_server(ioc, ioc.get_executor()) + { + set_workers(make_workers(ioc, max_workers)); + } +}; +// end::make_workers[] + +class launcher_demo : public corosio::tcp_server::worker_base +{ + corosio::tcp_socket sock_; + corosio::io_context::executor_type executor; + +public: + explicit launcher_demo(corosio::io_context& ctx) + : sock_(ctx) + , executor(ctx.get_executor()) + {} + + corosio::tcp_socket& socket() override { return sock_; } + + capy::task my_coroutine() { co_return; } + + // tag::launcher_run[] + void run(corosio::tcp_server::launcher launch) override + { + // Create and launch the session coroutine + launch(executor, my_coroutine()); + } + // end::launcher_run[] +}; + +// Abridged member signature of tcp_server::launcher's call operator. +namespace launcher_synopsis { +struct launcher +{ +// tag::launcher_signature[] +template +void operator()(Executor const& ex, capy::task task); +// end::launcher_signature[] +}; +} // namespace launcher_synopsis + +// Fragments that bind privileged or fixed ports are compiled but +// never executed. +[[maybe_unused]] void +bind_one(corosio::tcp_server& server) +{ + // tag::bind[] + auto ec = server.bind(corosio::endpoint(8080)); + if (ec) + std::cerr << "Bind failed: " << ec.message() << "\n"; + // end::bind[] +} + +[[maybe_unused]] void +bind_many(corosio::tcp_server& server) +{ + // tag::bind_many[] + server.bind(corosio::endpoint(80)); + server.bind(corosio::endpoint(443)); + // end::bind_many[] +} + +[[maybe_unused]] void +start_server(corosio::tcp_server& server) +{ + // tag::start[] + server.start(); + // end::start[] +} + +[[maybe_unused]] void +multiple_ports(corosio::tcp_server& server) +{ + // tag::multiple_ports[] + server.bind(corosio::endpoint(80)); // HTTP + server.bind(corosio::endpoint(443)); // HTTPS + server.start(); + // end::multiple_ports[] +} + +struct reuse_worker +{ + std::string request_; + std::string response_; + + // tag::worker_reuse[] + capy::task do_session() + { + // Reset state at session start + request_.clear(); + response_.clear(); + + // ... handle connection ... + // end::worker_reuse[] + co_return; + // tag::worker_reuse[] + + // Socket closed, worker returns to pool + } + // end::worker_reuse[] +}; + +struct tcp_server_test +{ + void + testEchoServer() + { + // Constructing the server installs the worker pool; no port + // is bound and the accept loop never starts. + corosio::io_context ioc; + echo_server server(ioc, 4); + BOOST_TEST(server.local_endpoint().port() == 0); + } + + void + testMyServer() + { + corosio::io_context ioc; + my_server server(ioc, 4); + BOOST_TEST(server.local_endpoint().port() == 0); + } + + void + run() + { + testEchoServer(); + testMyServer(); + } +}; + +} // namespace + +TEST_SUITE(tcp_server_test, "boost.corosio.doc.4k_tcp_server"); diff --git a/test/doc/snippets/4l_tls.cpp b/test/doc/snippets/4l_tls.cpp new file mode 100644 index 000000000..c0014b7ff --- /dev/null +++ b/test/doc/snippets/4l_tls.cpp @@ -0,0 +1,668 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4l.tls.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +using namespace boost::corosio; +// end::assume[] + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Stream-wrapping fragments construct wolfssl_stream, whose symbols +// live in the separate boost_corosio_wolfssl library; they compile +// only when the build provides it. +#if defined(BOOST_COROSIO_HAS_WOLFSSL) +[[maybe_unused]] void +verified_client(corosio::io_context& ioc) +{ + // tag::verified_client[] + tls_context ctx; + ctx.set_default_verify_paths(); // trust the system CAs + ctx.set_verify_mode(tls_verify_mode::peer); // require + verify the peer + + corosio::tcp_socket sock(ioc); + corosio::wolfssl_stream secure(&sock, ctx); + secure.set_hostname("example.com"); // SNI + hostname check + // end::verified_client[] +} + +// Needs a live TLS peer; compiled but never executed. +[[maybe_unused]] capy::task<> +typical_flow( + corosio::io_context& ioc, + corosio::endpoint endpoint, + capy::mutable_buffer buffer) +{ + // tag::typical_flow[] + // 1. Configure a context + tls_context ctx; + if (auto ec = ctx.set_default_verify_paths(); ec) + throw std::system_error(ec); + if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) + throw std::system_error(ec); + + // 2. Connect a socket + corosio::tcp_socket sock(ioc); + sock.open(); + if (auto [ec] = co_await sock.connect(endpoint); ec) + throw std::system_error(ec); + + // 3. Wrap the connected socket (pointer form; does not take ownership) + corosio::wolfssl_stream secure(&sock, ctx); + secure.set_hostname("api.example.com"); + if (auto [ec] = co_await secure.handshake(tls_role::client); ec) + throw std::system_error(ec); + + // 4. Use encrypted I/O + auto [ec, n] = co_await secure.read_some(buffer); + // end::typical_flow[] +} +#endif + +// Stand-in for certificate material obtained elsewhere (a config +// store, a secrets service, an embedded constant). +std::string load_cert_pem() { return {}; } + +// Real class shown abridged; a sketch namespace avoids colliding +// with corosio::tls_stream. +namespace interface_sketch { +// tag::tls_stream_interface[] +enum class tls_role { client, server }; + +class tls_stream +{ +public: + template + auto read_some(B const& buffers); // Decrypt and read + + template + auto write_some(B const& buffers); // Encrypt and write + + virtual capy::io_task<> handshake(tls_role role) = 0; + virtual capy::io_task<> shutdown() = 0; + + virtual capy::any_stream& next_layer() noexcept = 0; // Underlying stream +}; +// end::tls_stream_interface[] +} // namespace interface_sketch + +#if defined(BOOST_COROSIO_HAS_WOLFSSL) +[[maybe_unused]] void +wolfssl_construction(corosio::io_context& ioc) +{ + // tag::wolfssl_construct[] + #include + + corosio::tcp_socket sock(ioc); + // ... connect sock ... + + tls_context ctx; + // ... configure ctx ... + + // Reference form: sock must outlive secure + corosio::wolfssl_stream secure(&sock, ctx); + + // Or owning form: secure takes ownership of the socket + corosio::wolfssl_stream owned(std::move(sock), ctx); + // end::wolfssl_construct[] +} +#endif + +[[maybe_unused]] void +alpn_read_back(tls_context& ctx, corosio::tls_stream& stream) +{ + // tag::alpn_config[] + // Prefer HTTP/2, fall back to HTTP/1.1 + ctx.set_alpn({"h2", "http/1.1"}); + // ... after handshake: + std::string_view proto = stream.alpn_protocol(); // e.g. "h2", or empty + // end::alpn_config[] +} + +[[maybe_unused]] void +hostname_verification(corosio::tls_stream& secure) +{ + // tag::set_hostname[] + secure.set_hostname("api.example.com"); + // end::set_hostname[] +} + +// Handshake fragments need a live TLS peer; compiled but never executed. +[[maybe_unused]] capy::task<> +client_handshake(corosio::tls_stream& secure) +{ + // tag::client_handshake[] + auto [ec] = co_await secure.handshake(tls_role::client); + if (ec) + { + std::cerr << "Handshake failed: " << ec.message() << "\n"; + co_return; + } + // end::client_handshake[] +} + +[[maybe_unused]] capy::task<> +server_handshake(corosio::tls_stream& secure) +{ + // tag::server_handshake[] + auto [ec] = co_await secure.handshake(tls_role::server); + // end::server_handshake[] +} + +[[maybe_unused]] capy::task<> +encrypted_io(corosio::tls_stream& secure) +{ + // tag::encrypted_read_write[] + // Read encrypted data + char buf[1024]; + auto [ec, n] = co_await secure.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + + // Write encrypted data + std::string msg = "Hello, TLS!"; + auto [wec, wn] = co_await secure.write_some( + capy::const_buffer(msg.data(), msg.size())); + // end::encrypted_read_write[] +} + +[[maybe_unused]] capy::task<> +composed_io( + corosio::tls_stream& secure, + capy::mutable_buffer large_buffer, + capy::const_buffer data_buffer) +{ + // tag::composed_ops[] + // Read until buffer full + auto [ec, n] = co_await capy::read(secure, large_buffer); + + // Write all data + auto [wec, wn] = co_await capy::write(secure, data_buffer); + // end::composed_ops[] +} + +[[maybe_unused]] capy::task<> +tls_shutdown(corosio::tls_stream& secure, corosio::tcp_socket& sock) +{ + // tag::graceful_shutdown[] + auto [ec] = co_await secure.shutdown(); + // Then close the underlying socket + sock.close(); + // end::graceful_shutdown[] +} + +// tag::stream_overloads[] +capy::task send_request(corosio::io_stream& stream) +{ + std::string request = "GET / HTTP/1.1\r\n\r\n"; + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + char response[4096]; + co_await capy::read( + stream, capy::mutable_buffer(response, sizeof(response))); +} + +capy::task send_request(corosio::tls_stream& stream) +{ + std::string request = "GET / HTTP/1.1\r\n\r\n"; + if (auto [ec, n] = co_await capy::write( + stream, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + char response[4096]; + co_await capy::read( + stream, capy::mutable_buffer(response, sizeof(response))); +} +// end::stream_overloads[] + +#if defined(BOOST_COROSIO_HAS_WOLFSSL) +// The page shows this continuation flush-left, so the statements sit +// at column zero even though they live inside a scaffolding coroutine +// (Asciidoctor concatenates same-name tag regions). +[[maybe_unused]] capy::task<> +overload_selection(corosio::io_context& ioc, tls_context& ctx) +{ +// tag::stream_overloads[] + +// Plain socket uses the io_stream overload +corosio::tcp_socket sock(ioc); +co_await send_request(sock); + +// TLS stream uses the tls_stream overload +corosio::wolfssl_stream secure(&sock, ctx); +co_await send_request(secure); +// end::stream_overloads[] +} + +// Needs a resolvable name and a live TLS peer; compiled but never +// executed. +// tag::https_get[] +capy::task https_get( + corosio::io_context& ioc, + std::string_view hostname, + std::uint16_t port) +{ + // Resolve hostname + corosio::resolver resolver(ioc); + auto [resolve_ec, results] = co_await resolver.resolve( + hostname, std::to_string(port)); + if (resolve_ec) + throw std::system_error(resolve_ec); + + // Connect TCP socket + corosio::tcp_socket sock(ioc); + sock.open(); + + for (auto const& entry : results) + { + auto [ec] = co_await sock.connect(entry.get_endpoint()); + if (!ec) + break; + } + + // Configure TLS + tls_context ctx; + if (auto ec = ctx.set_default_verify_paths(); ec) + throw std::system_error(ec); + if (auto ec = ctx.set_verify_mode(tls_verify_mode::peer); ec) + throw std::system_error(ec); + + // Wrap the connected socket (pointer form) and handshake + corosio::wolfssl_stream secure(&sock, ctx); + secure.set_hostname(hostname); + if (auto [ec] = co_await secure.handshake(tls_role::client); ec) + throw std::system_error(ec); + + // Send HTTP request + std::string request = + "GET / HTTP/1.1\r\n" + "Host: " + std::string(hostname) + "\r\n" + "Connection: close\r\n" + "\r\n"; + + if (auto [ec, n] = co_await capy::write( + secure, capy::const_buffer(request.data(), request.size())); ec) + throw std::system_error(ec); + + // Read the response until EOF, one chunk at a time + std::string response; + for (;;) + { + char chunk[4096]; + auto [ec, n] = co_await capy::read( + secure, capy::mutable_buffer(chunk, sizeof(chunk))); + response.append(chunk, n); + if (ec) + { + // EOF expected when server closes + if (ec != capy::cond::eof) + throw std::system_error(ec); + break; + } + } + + std::cout << response << "\n"; + + // Graceful shutdown + co_await secure.shutdown(); +} +// end::https_get[] + +capy::task handle_tls_client( + corosio::tcp_socket sock, + tls_context ctx); + +// Binds a port and accepts forever; compiled but never executed. +// tag::tls_server[] +capy::task tls_server( + corosio::io_context& ioc, + std::uint16_t port) +{ + // Configure server TLS context + tls_context ctx; + ctx.use_certificate_chain_file("server-fullchain.pem"); + ctx.use_private_key_file("server.key", tls_file_format::pem); + + // Set up acceptor + corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); + + for (;;) + { + corosio::tcp_socket peer(ioc); + auto [ec] = co_await acc.accept(peer); + if (ec) break; + + // Spawn handler + capy::run_async(ioc.get_executor())( + handle_tls_client(std::move(peer), ctx)); + } +} + +capy::task handle_tls_client( + corosio::tcp_socket sock, + tls_context ctx) +{ + // Owning form: the handler owns the socket, so move it in + corosio::wolfssl_stream secure(std::move(sock), ctx); + + auto [ec] = co_await secure.handshake(tls_role::server); + if (ec) + co_return; + + // Handle encrypted connection... + char buf[1024]; + auto [read_ec, n] = co_await secure.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + + // Graceful shutdown + co_await secure.shutdown(); +} +// end::tls_server[] +#endif + +// The pin the verify-callback fragment compares against; a DER +// certificate starts with the ASN.1 SEQUENCE header. +constexpr std::array expected_pin{0x30, 0x82, 0x03}; + +struct tls_test +{ + void + testDefaultContext() + { + // tag::default_context[] + // Default context (TLS 1.2+ enabled) + tls_context ctx; + // end::default_context[] + BOOST_TEST(true); + } + + void + testLoadCertificates() + { + tls_context ctx; + // tag::load_certificates[] + // From file + ctx.use_certificate_file("server.crt", tls_file_format::pem); + + // From memory + std::string cert_data = load_cert_pem(); + ctx.use_certificate(cert_data, tls_file_format::pem); + + // Certificate chain (cert + intermediates) + ctx.use_certificate_chain_file("fullchain.pem"); + // end::load_certificates[] + BOOST_TEST(true); + } + + void + testLoadPrivateKeys() + { + tls_context ctx; + std::string key_data = load_cert_pem(); + // tag::load_private_keys[] + // From file + ctx.use_private_key_file("server.key", tls_file_format::pem); + + // From memory + ctx.use_private_key(key_data, tls_file_format::pem); + // end::load_private_keys[] + BOOST_TEST(true); + } + + void + testPasswordCallback() + { + tls_context ctx; + // tag::password_callback[] + ctx.set_password_callback( + [](std::size_t max_len, tls_password_purpose purpose) { + return std::string("my-key-password"); + }); + ctx.use_private_key_file("encrypted.key", tls_file_format::pem); + // end::password_callback[] + BOOST_TEST(true); + } + + void + testPkcs12() + { + tls_context ctx; + // tag::pkcs12[] + ctx.use_pkcs12_file("credentials.pfx", "bundle-password"); + // end::pkcs12[] + BOOST_TEST(true); + } + + void + testSystemTrust() + { + tls_context ctx; + // tag::system_trust[] + ctx.set_default_verify_paths(); + // end::system_trust[] + BOOST_TEST(true); + } + + void + testCustomCas() + { + tls_context ctx; + std::string ca_pem = load_cert_pem(); + // tag::custom_cas[] + // Single CA from memory + ctx.add_certificate_authority(ca_pem); + + // CA file (may contain multiple certs) + // tag::ca_bundle[] + ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); + // end::ca_bundle[] + + // Directory of hashed CA files + ctx.add_verify_path("/etc/ssl/certs"); + // end::custom_cas[] + BOOST_TEST(true); + } + + void + testProtocolVersions() + { + tls_context ctx; + // tag::protocol_versions[] + // Require TLS 1.3 minimum + ctx.set_min_protocol_version(tls_version::tls_1_3); + + // Cap at TLS 1.2 (unusual, but possible) + ctx.set_max_protocol_version(tls_version::tls_1_2); + // end::protocol_versions[] + BOOST_TEST(true); + } + + void + testCipherSuites() + { + tls_context ctx; + // tag::cipher_suites[] + // TLS 1.2-and-below cipher list (OpenSSL syntax) + ctx.set_ciphersuites("ECDHE+AESGCM:ECDHE+CHACHA20"); + // TLS 1.3 cipher suites (configured separately) + ctx.set_ciphersuites_tls13("TLS_AES_256_GCM_SHA384"); + // end::cipher_suites[] + BOOST_TEST(true); + } + + void + testVerifyModes() + { + tls_context ctx; + // tag::verify_modes[] + // Don't verify peer (not recommended for clients) + ctx.set_verify_mode(tls_verify_mode::none); + + // Verify if peer presents certificate + ctx.set_verify_mode(tls_verify_mode::peer); + + // Require peer certificate (fail if not presented) + ctx.set_verify_mode(tls_verify_mode::require_peer); + // end::verify_modes[] + BOOST_TEST(!ctx.set_verify_mode(tls_verify_mode::peer)); + } + + void + testVerifyDepth() + { + tls_context ctx; + // tag::verify_depth[] + ctx.set_verify_depth(10); // Max 10 intermediate certs + // end::verify_depth[] + BOOST_TEST(true); + } + + void + testVerifyCallback() + { + tls_context ctx; + // tag::verify_callback[] + ctx.set_verify_callback( + [](bool preverified, corosio::verify_context& ctx) { + if (!preverified) + return false; // reject if the chain didn't verify + + auto der = ctx.certificate(); // DER of the current certificate + return der.size() == expected_pin.size() && + std::equal(der.begin(), der.end(), expected_pin.begin()); + }); + // end::verify_callback[] + BOOST_TEST(true); + } + + void + testRevocation() + { + tls_context ctx; + std::string crl_data = load_cert_pem(); + // tag::revocation[] + // Load a CRL (PEM or DER), from file or memory + ctx.add_crl_file("issuer.crl"); + ctx.add_crl(crl_data); + + // Choose how strict to be + ctx.set_revocation_policy(tls_revocation_policy::hard_fail); + // end::revocation[] + BOOST_TEST(true); + } + + void + testMutualTlsServer() + { + // tag::mtls_server[] + tls_context server_ctx; + server_ctx.use_certificate_chain_file("server.pem"); + server_ctx.use_private_key_file("server.key", tls_file_format::pem); + + // Require client certificate + server_ctx.set_verify_mode(tls_verify_mode::require_peer); + server_ctx.load_verify_file("client-ca.pem"); + // end::mtls_server[] + BOOST_TEST(!server_ctx.set_verify_mode(tls_verify_mode::require_peer)); + } + + void + testMutualTlsClient() + { + // tag::mtls_client[] + tls_context client_ctx; + client_ctx.set_default_verify_paths(); + client_ctx.set_verify_mode(tls_verify_mode::peer); + + // Provide client certificate + client_ctx.use_certificate_file("client.crt", tls_file_format::pem); + client_ctx.use_private_key_file("client.key", tls_file_format::pem); + // end::mtls_client[] + BOOST_TEST(!client_ctx.set_verify_mode(tls_verify_mode::peer)); + } + + void + run() + { + testDefaultContext(); + testLoadCertificates(); + testLoadPrivateKeys(); + testPasswordCallback(); + testPkcs12(); + testSystemTrust(); + testCustomCas(); + testProtocolVersions(); + testCipherSuites(); + testVerifyModes(); + testVerifyDepth(); + testVerifyCallback(); + testRevocation(); + testMutualTlsServer(); + testMutualTlsClient(); + } +}; + +} // namespace + +TEST_SUITE(tls_test, "boost.corosio.doc.4l_tls"); diff --git a/test/doc/snippets/4m_error_handling.cpp b/test/doc/snippets/4m_error_handling.cpp new file mode 100644 index 000000000..68be4df33 --- /dev/null +++ b/test/doc/snippets/4m_error_handling.cpp @@ -0,0 +1,723 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4m.error-handling.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +using namespace std::chrono_literals; +// end::assume[] + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// A port that was just bound and released: connecting to it on +// loopback yields a real connection_refused. +corosio::endpoint +closed_endpoint(corosio::io_context& ioc) +{ + corosio::tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(corosio::socket_option::reuse_address(true)); + if (auto ec = acc.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0))) + throw std::runtime_error("bind failed: " + ec.message()); + auto ep = acc.local_endpoint(); + acc.close(); + return ep; +} + +// Produce a real OS error code for the category/comparison fragments. +std::error_code +refused_connect_ec(corosio::io_context& ioc) +{ + std::error_code out; + corosio::tcp_socket sock(ioc); + sock.open(); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& s, corosio::endpoint ep, + std::error_code& o) -> capy::task<> { + auto [ec] = co_await s.connect(ep); + o = ec; + }(sock, closed_endpoint(ioc), out)); + ioc.run(); + ioc.restart(); + return out; +} + +capy::task<> +bindings_void_result( + corosio::tcp_socket& sock, + corosio::endpoint endpoint, + std::error_code& out) +{ + // tag::structured_bindings[] + // Void result + auto [ec] = co_await sock.connect(endpoint); + if (ec) + std::cerr << "Connect failed: " << ec.message() << "\n"; + + // end::structured_bindings[] + out = ec; +} + +capy::task<> +bindings_value_result( + corosio::tcp_socket& sock, + capy::mutable_buffer buffer, + std::error_code& out_ec, + std::size_t& out_n) +{ + // tag::structured_bindings[] + // Value result + auto [ec, n] = co_await sock.read_some(buffer); + if (ec) + std::cerr << "Read failed: " << ec.message() << "\n"; + else + std::cout << "Read " << n << " bytes\n"; + // end::structured_bindings[] + out_ec = ec; + out_n = n; +} + +capy::task<> +direct_members( + corosio::tcp_socket& sock, + corosio::endpoint endpoint, + std::error_code& out) +{ + // tag::direct_members[] + auto result = co_await sock.connect(endpoint); + if (!result.ec) + std::cout << "Connected successfully\n"; + else + std::cerr << "Failed: " << result.ec.message() << "\n"; + // end::direct_members[] + out = result.ec; +} + +capy::task<> +throw_explicit( + corosio::tcp_socket& sock, + capy::mutable_buffer buffer, + std::size_t& out_n) +{ + // tag::throw_explicit[] + auto [ec, n] = co_await sock.read_some(buffer); + if (ec) + throw std::system_error(ec); + // 'n' bytes were read + // end::throw_explicit[] + out_n = n; +} + +capy::task<> +inspect_eof( + corosio::tcp_socket& sock, + capy::mutable_buffer buf, + std::error_code& out) +{ + // tag::inspect_eof[] + auto [ec, n] = co_await sock.read_some(buf); + if (ec == capy::cond::eof) + { + std::cout << "End of stream after " << n << " bytes\n"; + // Not an exceptional condition + } + // end::inspect_eof[] + out = ec; +} + +capy::task<> +throw_style( + corosio::tcp_socket& sock, + corosio::endpoint endpoint, + capy::const_buffer request, + capy::mutable_buffer buffer, + std::size_t& out_n) +{ + // tag::throw_style[] + auto throw_on_error = [](auto result) { + if (result.ec) + throw std::system_error(result.ec); + return result; + }; + + throw_on_error(co_await sock.connect(endpoint)); + throw_on_error(co_await capy::write(sock, request)); + auto [ec, n] = throw_on_error(co_await capy::read(sock, buffer)); + // end::throw_style[] + out_n = n; +} + +// The delay is awaited with an already-requested stop token, so the +// fragment sees a genuinely cancelled operation. +capy::task<> +canceled_condition(std::error_code& out) +{ + auto [ec] = co_await corosio::delay(1ms); + // tag::canceled_condition[] + if (ec == capy::cond::canceled) + std::cout << "Operation was cancelled\n"; + // end::canceled_condition[] + out = ec; +} + +capy::task<> +timeout_vs_cancel( + corosio::tcp_socket& sock, + corosio::endpoint ep, + std::error_code& out) +{ + // tag::timeout_vs_cancel[] + auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s); + if (ec == capy::cond::timeout) + std::cout << "Deadline elapsed before connecting\n"; + else if (ec == capy::cond::canceled) + std::cout << "Cancelled before the deadline\n"; + // end::timeout_vs_cancel[] + out = ec; +} + +capy::task<> +eof_expected( + corosio::tcp_socket& stream, + capy::mutable_buffer buffer, + std::error_code& out_ec, + std::size_t& out_n) +{ + // tag::eof_expected[] + auto [ec, n] = co_await capy::read(stream, buffer); + if (ec == capy::cond::eof) + { + std::cout << "Stream ended, read " << n << " bytes total\n"; + // This is often expected, not an error + } + else if (ec) + { + std::cerr << "Unexpected error: " << ec.message() << "\n"; + } + // end::eof_expected[] + out_ec = ec; + out_n = n; +} + +capy::task<> +eof_filtered( + corosio::tcp_socket& stream, + capy::mutable_buffer response, + std::size_t& out_n) +{ + // tag::eof_filtered[] + auto [ec, n] = co_await capy::read(stream, response); + if (ec && ec != capy::cond::eof) + throw std::system_error(ec); + // EOF is expected when server closes connection + // end::eof_filtered[] + out_n = n; +} + +capy::task<> +partial_success( + corosio::tcp_socket& stream, + capy::const_buffer large_buffer, + std::error_code& out) +{ + using capy::buffer_size; + // tag::partial_success[] + auto [ec, n] = co_await capy::write(stream, large_buffer); + if (ec) + { + std::cerr << "Error after writing " << n << " of " + << buffer_size(large_buffer) << " bytes\n"; + // Can potentially resume from here + } + // end::partial_success[] + out = ec; +} + +void +category_checks(std::error_code ec) +{ + // tag::error_categories[] + if (ec.category() == std::system_category()) + { + // Operating system error + } + + if (ec.category() == std::generic_category()) + { + // Portable POSIX-style error + } + // end::error_categories[] +} + +void +compare_errors(std::error_code ec) +{ + // tag::compare_errors[] + // Specific error (platform-dependent) + if (ec == std::errc::connection_refused) + { + // ... + } + + // Error condition (portable) + if (ec == capy::cond::canceled) + { + // Matches any cancellation error + } + + if (ec == capy::cond::eof) + { + // Matches end-of-stream + } + // end::compare_errors[] +} + +// The page shows `safe_operation` with `sock` and `endpoint` in scope; +// the fixture provides them without changing the shown code. +struct exception_safety_fixture +{ + corosio::tcp_socket sock; + corosio::endpoint endpoint; + + exception_safety_fixture(corosio::io_context& ioc, corosio::endpoint ep) + : sock(ioc) + , endpoint(ep) + { + sock.open(); + } + + // tag::exception_safety[] + capy::task safe_operation() + { + try + { + auto [ec] = co_await sock.connect(endpoint); + if (ec) + throw std::system_error(ec); + } + catch (std::system_error const& e) + { + std::cerr << "Connect failed: " << e.what() << "\n"; + // Exception handled here, doesn't propagate + } + } + // end::exception_safety[] +}; + +// tag::robust_connect[] +capy::task connect_with_retry( + corosio::io_context& ioc, + corosio::endpoint ep, + int max_retries) +{ + corosio::tcp_socket sock(ioc); + + for (int attempt = 0; attempt < max_retries; ++attempt) + { + sock.open(); + auto [ec] = co_await sock.connect(ep); + + if (!ec) + co_return; // Success + + std::cerr << "Attempt " << (attempt + 1) + << " failed: " << ec.message() << "\n"; + + sock.close(); + + // Wait before retry (exponential backoff) + auto [dec] = co_await corosio::delay(std::chrono::seconds(1 << attempt)); + if (dec == capy::cond::canceled) + co_return; // Cancellation aborts the retry loop + } + + throw std::runtime_error("Failed to connect after retries"); +} +// end::robust_connect[] + +struct error_handling_test +{ + void + testResultShapes() + { + using capy::io_result; + using corosio::resolver_results; + // tag::result_shapes[] + // Void result (connect, handshake) + io_result<> r1; // Contains: ec + + // Single value (read_some, write_some) + io_result r2; // Contains: ec, n (bytes transferred) + + // Typed result (resolve) + io_result r3; // Contains: ec, results + // end::result_shapes[] + BOOST_TEST(!r1.ec); + BOOST_TEST(!r2.ec); + BOOST_TEST(!r3.ec); + } + + void + testStructuredBindings() + { + corosio::io_context ioc; + corosio::tcp_socket sock(ioc); + sock.open(); + std::error_code connect_ec; + capy::run_async(ioc.get_executor())( + bindings_void_result(sock, closed_endpoint(ioc), connect_ec)); + ioc.run(); + ioc.restart(); + BOOST_TEST(connect_ec == std::errc::connection_refused); + + auto [a, b] = corosio::test::make_socket_pair(ioc); + char data[64]; + std::error_code read_ec; + std::size_t read_n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some(capy::const_buffer("hello", 5)); + }(b)); + capy::run_async(ioc.get_executor())( + bindings_value_result( + a, capy::mutable_buffer(data, sizeof(data)), + read_ec, read_n)); + ioc.run(); + BOOST_TEST(!read_ec); + BOOST_TEST(read_n == 5); + } + + void + testDirectMembers() + { + corosio::io_context ioc; + corosio::tcp_socket sock(ioc); + sock.open(); + std::error_code out; + capy::run_async(ioc.get_executor())( + direct_members(sock, closed_endpoint(ioc), out)); + ioc.run(); + BOOST_TEST(out == std::errc::connection_refused); + } + + void + testThrowExplicit() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char data[64]; + std::size_t n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some(capy::const_buffer("hello", 5)); + }(b)); + capy::run_async(ioc.get_executor())( + throw_explicit( + a, capy::mutable_buffer(data, sizeof(data)), n)); + ioc.run(); + BOOST_TEST(n == 5); + } + + void + testInspectEof() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + // A FIN from the peer surfaces as the eof condition. + b.shutdown(corosio::shutdown_send); + char data[64]; + std::error_code out; + capy::run_async(ioc.get_executor())( + inspect_eof( + a, capy::mutable_buffer(data, sizeof(data)), out)); + ioc.run(); + BOOST_TEST(out == capy::cond::eof); + } + + void + testThrowStyle() + { + corosio::io_context ioc; + corosio::tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(corosio::socket_option::reuse_address(true)); + BOOST_TEST(!acc.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + auto ep = acc.local_endpoint(); + + corosio::tcp_socket psock(ioc); + corosio::tcp_socket sock(ioc); + sock.open(); + std::string request_text = "GET /\r\n"; + char received[16] = {}; + std::size_t got = 0; + bool peer_ok = false; + + capy::run_async(ioc.get_executor())( + [](corosio::tcp_acceptor& a, corosio::tcp_socket& p, + std::size_t reqlen, bool& ok) -> capy::task<> { + auto [aec] = co_await a.accept(p); + if (aec) + co_return; + std::vector req(reqlen); + auto [rec, rn] = co_await capy::read( + p, capy::mutable_buffer(req.data(), req.size())); + if (rec || rn != reqlen) + co_return; + char resp[16]; + for (auto& c : resp) + c = 'x'; + auto [wec, wn] = co_await capy::write( + p, capy::const_buffer(resp, sizeof(resp))); + ok = !wec && wn == sizeof(resp); + }(acc, psock, request_text.size(), peer_ok)); + capy::run_async(ioc.get_executor())( + throw_style( + sock, ep, + capy::const_buffer( + request_text.data(), request_text.size()), + capy::mutable_buffer(received, sizeof(received)), + got)); + ioc.run(); + acc.close(); + BOOST_TEST(peer_ok); + BOOST_TEST(got == sizeof(received)); + } + + void + testCanceledCondition() + { + corosio::io_context ioc; + std::stop_source source; + source.request_stop(); + std::error_code out; + capy::run_async(ioc.get_executor(), source.get_token())( + canceled_condition(out)); + ioc.run(); + BOOST_TEST(out == capy::cond::canceled); + } + + void + testTimeoutVsCancel() + { + // The coroutine's own stop token is already requested, so the + // race reports cancellation rather than a timeout. + corosio::io_context ioc; + corosio::tcp_socket sock(ioc); + sock.open(); + std::stop_source source; + source.request_stop(); + std::error_code out; + capy::run_async(ioc.get_executor(), source.get_token())( + timeout_vs_cancel(sock, closed_endpoint(ioc), out)); + ioc.run(); + BOOST_TEST(out == capy::cond::canceled); + } + + void + testEofExpected() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char data[64]; + std::error_code out_ec; + std::size_t out_n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some(capy::const_buffer("hello", 5)); + peer.shutdown(corosio::shutdown_send); + }(b)); + capy::run_async(ioc.get_executor())( + eof_expected( + a, capy::mutable_buffer(data, sizeof(data)), + out_ec, out_n)); + ioc.run(); + BOOST_TEST(out_ec == capy::cond::eof); + BOOST_TEST(out_n == 5); + } + + void + testEofFiltered() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char data[64]; + std::size_t out_n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some(capy::const_buffer("hello", 5)); + peer.shutdown(corosio::shutdown_send); + }(b)); + capy::run_async(ioc.get_executor())( + eof_filtered( + a, capy::mutable_buffer(data, sizeof(data)), out_n)); + ioc.run(); + BOOST_TEST(out_n == 5); + } + + void + testPartialSuccess() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + // The pair sets linger(0): closing the peer sends a RST, so a + // large write fails partway through with a real error. + b.close(); + std::vector big(4 << 20, 'x'); + std::error_code out; + capy::run_async(ioc.get_executor())( + partial_success( + a, capy::const_buffer(big.data(), big.size()), out)); + ioc.run(); + BOOST_TEST(out); + } + + void + testCategories() + { + corosio::io_context ioc; + auto ec = refused_connect_ec(ioc); + category_checks(ec); + BOOST_TEST(ec.category() == std::system_category() || + ec.category() == std::generic_category()); + } + + void + testCompareErrors() + { + corosio::io_context ioc; + auto ec = refused_connect_ec(ioc); + compare_errors(ec); + BOOST_TEST(ec == std::errc::connection_refused); + BOOST_TEST(!(ec == capy::cond::canceled)); + BOOST_TEST(!(ec == capy::cond::eof)); + } + + void + testExceptionSafety() + { + corosio::io_context ioc; + exception_safety_fixture fx(ioc, closed_endpoint(ioc)); + bool done = false; + capy::run_async(ioc.get_executor())( + [](exception_safety_fixture& f, bool& d) -> capy::task<> { + co_await f.safe_operation(); + d = true; + }(fx, done)); + ioc.run(); + BOOST_TEST(done); + } + + void + testRobustConnect() + { + corosio::io_context ioc; + corosio::tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(corosio::socket_option::reuse_address(true)); + BOOST_TEST(!acc.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + auto ep = acc.local_endpoint(); + + corosio::tcp_socket psock(ioc); + bool accepted = false; + bool done = false; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_acceptor& a, corosio::tcp_socket& p, + bool& ok) -> capy::task<> { + auto [ec] = co_await a.accept(p); + ok = !ec; + }(acc, psock, accepted)); + capy::run_async(ioc.get_executor())( + [](corosio::io_context& ctx, corosio::endpoint e, + bool& d) -> capy::task<> { + // First attempt succeeds, so no backoff delay runs. + co_await connect_with_retry(ctx, e, 3); + d = true; + }(ioc, ep, done)); + ioc.run(); + acc.close(); + BOOST_TEST(accepted); + BOOST_TEST(done); + } + + void + run() + { + testResultShapes(); + testStructuredBindings(); + testDirectMembers(); + testThrowExplicit(); + testInspectEof(); + testThrowStyle(); + testCanceledCondition(); + testTimeoutVsCancel(); + testEofExpected(); + testEofFiltered(); + testPartialSuccess(); + testCategories(); + testCompareErrors(); + testExceptionSafety(); + testRobustConnect(); + } +}; + +} // namespace + +TEST_SUITE(error_handling_test, "boost.corosio.doc.4m_error_handling"); diff --git a/test/doc/snippets/4n_buffers.cpp b/test/doc/snippets/4n_buffers.cpp new file mode 100644 index 000000000..37c2eb7a9 --- /dev/null +++ b/test/doc/snippets/4n_buffers.cpp @@ -0,0 +1,606 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4n.buffers.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +// tag::buffer_slice_include[] +#include + +// end::buffer_slice_include[] +// tag::consuming_buffers_include[] +#include + +// end::consuming_buffers_include[] +// tag::buffer_param_include[] +#include + +// end::buffer_param_include[] + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The page's sketch of the socket signatures; compiling the +// declarations is the test. +namespace concept_sketch { + +// tag::sequence_concepts[] +// Readable buffers (for writing to sockets) +template +auto write_some(ConstBufferSequence const& buffers); + +// Writable buffers (for reading from sockets) +template +auto read_some(MutableBufferSequence const& buffers); +// end::sequence_concepts[] + +} // namespace concept_sketch + +capy::task<> +single_buffer_read( + corosio::tcp_socket& sock, char* data, std::size_t size) +{ + // tag::single_buffer[] + capy::mutable_buffer buf(data, size); + co_await sock.read_some(buf); // Works directly + // end::single_buffer[] +} + +capy::task<> +multi_buffer_read( + corosio::tcp_socket& sock, + char* header, std::size_t header_size, + char* body, std::size_t body_size) +{ + // tag::multi_buffer[] + // Array of buffers + std::array bufs = { + capy::mutable_buffer(header, header_size), + capy::mutable_buffer(body, body_size) + }; + co_await sock.read_some(bufs); + + // end::multi_buffer[] +} + +capy::task<> +multi_buffer_write( + corosio::tcp_socket& sock, + std::string const& header, + std::string const& body) +{ + // tag::multi_buffer[] + // Vector of buffers + std::vector send_bufs; + send_bufs.push_back(capy::const_buffer(header.data(), header.size())); + send_bufs.push_back(capy::const_buffer(body.data(), body.size())); + co_await sock.write_some(send_bufs); + // end::multi_buffer[] +} + +capy::task<> +slice_writes( + corosio::tcp_socket& sock, + std::array const& bufs) +{ + // tag::buffer_slice[] + // Send only the first 16 bytes of the sequence + co_await capy::write(sock, capy::buffer_slice(bufs, 0, 16)); + + // Everything after the first 16 bytes, as a value + auto rest = capy::buffer_slice(bufs, 16); + co_await capy::write(sock, rest); + // end::buffer_slice[] + sock.shutdown(corosio::shutdown_send); +} + +capy::task<> +consume_reads( + corosio::tcp_socket& sock, + std::array const& bufs, + std::size_t& out_n) +{ + // tag::consuming_buffers[] + capy::consuming_buffers consuming(bufs); + + // After reading n bytes: + auto [ec, n] = co_await sock.read_some(consuming.data()); + consuming.consume(n); // Advance past the bytes read + + // consuming.data() now spans the remaining unread bytes + // end::consuming_buffers[] + out_n = n; +} + +// tag::buffer_param[] +void accept_any_buffer(corosio::buffer_param buffers) +{ + capy::mutable_buffer temp[8]; + std::size_t n = buffers.copy_to(temp, 8); + // Use temp[0..n-1] +} + +// end::buffer_param[] + +// tag::lifetime[] +// WRONG: buffer outlives string +capy::task bad_example(corosio::tcp_socket& sock) +{ + capy::const_buffer buf; + { + std::string temp = "Hello"; + buf = capy::const_buffer(temp.data(), temp.size()); + } // temp destroyed here! + + co_await sock.write_some(buf); // Undefined behavior +} + +// CORRECT: keep storage alive +capy::task good_example(corosio::tcp_socket& sock) +{ + std::string msg = "Hello"; + co_await sock.write_some( + capy::const_buffer(msg.data(), msg.size())); +} +// end::lifetime[] + +// The buggy coroutine is compiled but never run. +[[maybe_unused]] auto* const lifetime_demo = &bad_example; + +capy::task<> +scatter_read(corosio::tcp_socket& sock, std::size_t& out_n) +{ + // tag::scatter_read[] + struct message_header + { + std::uint32_t type; + std::uint32_t length; + }; + + message_header header; + char body[1024]; + + std::array read_bufs = { + capy::mutable_buffer(&header, sizeof(header)), + capy::mutable_buffer(body, sizeof(body)) + }; + + auto [ec, n] = co_await sock.read_some(read_bufs); + // Data fills header first, then body + // end::scatter_read[] + out_n = n; +} + +capy::task<> +gather_write(corosio::tcp_socket& sock, std::size_t& out_n) +{ + // tag::gather_write[] + std::string header = "HTTP/1.1 200 OK\r\n\r\n"; + std::string body = "Hello, World!"; + + std::array write_bufs = { + capy::const_buffer(header.data(), header.size()), + capy::const_buffer(body.data(), body.size()) + }; + + auto [ec, n] = co_await sock.write_some(write_bufs); + // Sends header followed by body in a single operation + // end::gather_write[] + out_n = n; + sock.shutdown(corosio::shutdown_send); +} + +// tag::read_header[] +struct packet_header +{ + std::uint32_t magic; + std::uint32_t length; +}; + +capy::task read_header(corosio::io_stream& stream) +{ + packet_header header; + auto [ec, n] = co_await capy::read( + stream, capy::mutable_buffer(&header, sizeof(header))); + + if (ec) + throw std::system_error(ec); + + co_return header; +} +// end::read_header[] + +struct buffers_test +{ + void + testMutableBuffer() + { + // tag::mutable_buffer[] + char data[1024]; + capy::mutable_buffer buf(data, sizeof(data)); + // end::mutable_buffer[] + BOOST_TEST(buf.data() == data); + BOOST_TEST(buf.size() == sizeof(data)); + } + + void + testConstBuffer() + { + // tag::const_buffer[] + std::string msg = "Hello"; + capy::const_buffer buf(msg.data(), msg.size()); + // end::const_buffer[] + BOOST_TEST(buf.data() == msg.data()); + BOOST_TEST(buf.size() == 5); + } + + void + testFromRawArrays() + { + // tag::from_raw_arrays[] + char data[1024]; + capy::mutable_buffer mbuf(data, sizeof(data)); + + const char* str = "Hello"; + capy::const_buffer cbuf(str, 5); + // end::from_raw_arrays[] + BOOST_TEST(mbuf.size() == 1024); + BOOST_TEST(cbuf.data() == str); + BOOST_TEST(cbuf.size() == 5); + } + + void + testFromString() + { + // tag::from_string[] + std::string s = "Hello, World!"; + + // Writable (be careful with string invalidation) + capy::mutable_buffer mbuf(s.data(), s.size()); + + // Read-only + capy::const_buffer cbuf(s.data(), s.size()); + // end::from_string[] + BOOST_TEST(mbuf.data() == s.data()); + BOOST_TEST(mbuf.size() == 13); + BOOST_TEST(cbuf.size() == 13); + } + + void + testFromVector() + { + // tag::from_vector[] + std::vector vec(1024); + capy::mutable_buffer buf(vec.data(), vec.size()); + // end::from_vector[] + BOOST_TEST(buf.data() == vec.data()); + BOOST_TEST(buf.size() == 1024); + } + + void + testSingleBuffer() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char data[64] = {}; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some(capy::const_buffer("ping", 4)); + }(b)); + capy::run_async(ioc.get_executor())( + single_buffer_read(a, data, sizeof(data))); + ioc.run(); + BOOST_TEST(std::memcmp(data, "ping", 4) == 0); + } + + void + testMultipleBuffers() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + + // Scatter read: 8 bytes split across a 2-byte header and the + // body. + char header[2] = {}; + char body[6] = {}; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some( + capy::const_buffer("ABCDEFGH", 8)); + }(b)); + capy::run_async(ioc.get_executor())( + multi_buffer_read( + a, header, sizeof(header), body, sizeof(body))); + ioc.run(); + ioc.restart(); + BOOST_TEST(std::memcmp(header, "AB", 2) == 0); + BOOST_TEST(std::memcmp(body, "CDEFGH", 6) == 0); + + // Gather write in the other direction. + std::string hdr = "HDR"; + std::string bdy = "BODY!"; + char got[8] = {}; + std::size_t got_n = 0; + capy::run_async(ioc.get_executor())( + multi_buffer_write(a, hdr, bdy)); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer, char* out, std::size_t len, + std::size_t& n_out) -> capy::task<> { + auto [ec, n] = co_await capy::read( + peer, capy::mutable_buffer(out, len)); + n_out = n; + }(b, got, sizeof(got), got_n)); + ioc.run(); + BOOST_TEST(got_n == 8); + BOOST_TEST(std::memcmp(got, "HDRBODY!", 8) == 0); + } + + void + testBufferSize() + { + // tag::buffer_size[] + char header[16]; + char body[1024]; + + std::array bufs = { + capy::mutable_buffer(header, sizeof(header)), + capy::mutable_buffer(body, sizeof(body)) + }; + std::size_t total = capy::buffer_size(bufs); // 1040 + // end::buffer_size[] + BOOST_TEST(total == 1040); + } + + void + testBufferSlice() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + std::string part1(16, 'x'); + std::string part2(16, 'y'); + std::array bufs = { + capy::const_buffer(part1.data(), part1.size()), + capy::const_buffer(part2.data(), part2.size()) + }; + + std::string got; + bool eof = false; + capy::run_async(ioc.get_executor())(slice_writes(a, bufs)); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer, std::string& out, + bool& eof_out) -> capy::task<> { + char chunk[64]; + for (;;) + { + auto [ec, n] = co_await peer.read_some( + capy::mutable_buffer(chunk, sizeof(chunk))); + out.append(chunk, n); + if (ec) + { + eof_out = ec == capy::cond::eof; + co_return; + } + } + }(b, got, eof)); + ioc.run(); + BOOST_TEST(eof); + BOOST_TEST(got == part1 + part2); + } + + void + testConsumingBuffers() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char head[4] = {}; + char tail[8] = {}; + std::array bufs = { + capy::mutable_buffer(head, sizeof(head)), + capy::mutable_buffer(tail, sizeof(tail)) + }; + std::size_t n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + co_await peer.write_some( + capy::const_buffer("0123456789", 10)); + }(b)); + capy::run_async(ioc.get_executor())(consume_reads(a, bufs, n)); + ioc.run(); + BOOST_TEST(n > 0); + BOOST_TEST(std::memcmp(head, "0123", 4) == 0); + } + + void + testBufferParam() + { + char header[16]; + char body[64]; + // tag::buffer_param_call[] + // Works with any buffer sequence (implicit conversion) + std::array bufs = { + capy::mutable_buffer(header, sizeof(header)), + capy::mutable_buffer(body, sizeof(body)) + }; + accept_any_buffer(bufs); + // end::buffer_param_call[] + BOOST_TEST(capy::buffer_size(bufs) == 80); + } + + void + testLifetime() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + char got[8] = {}; + std::size_t got_n = 0; + capy::run_async(ioc.get_executor())(good_example(a)); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer, char* out, std::size_t len, + std::size_t& n_out) -> capy::task<> { + auto [ec, n] = co_await peer.read_some( + capy::mutable_buffer(out, len)); + n_out = n; + }(b, got, sizeof(got), got_n)); + ioc.run(); + BOOST_TEST(got_n == 5); + BOOST_TEST(std::memcmp(got, "Hello", 5) == 0); + } + + void + testStringInvalidation() + { + // tag::string_invalidation[] + std::string s = "Hello"; + capy::mutable_buffer buf(s.data(), s.size()); + + s += " World"; // May reallocate, invalidating buf! + + // Use buf here: UNDEFINED BEHAVIOR + // end::string_invalidation[] + BOOST_TEST(s == "Hello World"); + } + + void + testScatterRead() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + std::size_t n = 0; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer) -> capy::task<> { + char msg[16] = {}; + co_await peer.write_some( + capy::const_buffer(msg, sizeof(msg))); + }(b)); + capy::run_async(ioc.get_executor())(scatter_read(a, n)); + ioc.run(); + BOOST_TEST(n > 0); + } + + void + testGatherWrite() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + std::size_t written = 0; + std::string got; + capy::run_async(ioc.get_executor())(gather_write(a, written)); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer, + std::string& out) -> capy::task<> { + char chunk[64]; + for (;;) + { + auto [ec, n] = co_await peer.read_some( + capy::mutable_buffer(chunk, sizeof(chunk))); + out.append(chunk, n); + if (ec) + co_return; + } + }(b, got)); + ioc.run(); + BOOST_TEST(written == 32); + BOOST_TEST(got == "HTTP/1.1 200 OK\r\n\r\nHello, World!"); + } + + void + testReadHeader() + { + corosio::io_context ioc; + auto [a, b] = corosio::test::make_socket_pair(ioc); + packet_header sent{0xC0FFEE42, 1234}; + packet_header received{}; + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& peer, + packet_header h) -> capy::task<> { + co_await peer.write_some( + capy::const_buffer(&h, sizeof(h))); + }(b, sent)); + capy::run_async(ioc.get_executor())( + [](corosio::tcp_socket& sock, + packet_header& out) -> capy::task<> { + out = co_await read_header(sock); + }(a, received)); + ioc.run(); + BOOST_TEST(received.magic == sent.magic); + BOOST_TEST(received.length == sent.length); + } + + void + run() + { + testMutableBuffer(); + testConstBuffer(); + testFromRawArrays(); + testFromString(); + testFromVector(); + testSingleBuffer(); + testMultipleBuffers(); + testBufferSize(); + testBufferSlice(); + testConsumingBuffers(); + testBufferParam(); + testLifetime(); + testStringInvalidation(); + testScatterRead(); + testGatherWrite(); + testReadHeader(); + } +}; + +} // namespace + +TEST_SUITE(buffers_test, "boost.corosio.doc.4n_buffers"); diff --git a/test/doc/snippets/4o_file_io.cpp b/test/doc/snippets/4o_file_io.cpp new file mode 100644 index 000000000..6b6f6d361 --- /dev/null +++ b/test/doc/snippets/4o_file_io.cpp @@ -0,0 +1,400 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4o.file-io.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#else +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +capy::task<> +stream_read( + corosio::io_context& ioc, bool& eof_seen, std::size_t& bytes_read) +{ + // tag::stream_read[] + corosio::stream_file f(ioc); + f.open("data.bin", corosio::file_base::read_only); + + char buf[4096]; + auto [ec, n] = co_await f.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + + if (ec == capy::cond::eof) + // reached end of file + // end::stream_read[] + eof_seen = true; + bytes_read = n; +} + +capy::task<> +stream_write( + corosio::io_context& ioc, std::error_code& ec_out, std::size_t& n_out) +{ + // tag::stream_write[] + corosio::stream_file f(ioc); + f.open("output.bin", + corosio::file_base::write_only + | corosio::file_base::create + | corosio::file_base::truncate); + + std::string data = "hello world"; + auto [ec, n] = co_await f.write_some( + capy::const_buffer(data.data(), data.size())); + // end::stream_write[] + ec_out = ec; + n_out = n; +} + +std::uint64_t +reposition(corosio::stream_file& f) +{ + // tag::seek[] + f.seek(0, corosio::file_base::seek_set); // beginning + f.seek(100, corosio::file_base::seek_cur); // forward 100 bytes + f.seek(-10, corosio::file_base::seek_end); // 10 bytes before end + // end::seek[] + return f.seek(0, corosio::file_base::seek_cur); +} + +capy::task<> +read_at( + corosio::io_context& ioc, std::error_code& ec_out, + std::size_t& n_out, char& first) +{ + // tag::read_at[] + corosio::random_access_file f(ioc); + f.open("data.bin", corosio::file_base::read_only); + + char buf[256]; + auto [ec, n] = co_await f.read_some_at( + 1024, // byte offset + capy::mutable_buffer(buf, sizeof(buf))); + // end::read_at[] + ec_out = ec; + n_out = n; + first = buf[0]; +} + +capy::task<> +write_at( + corosio::io_context& ioc, std::error_code& ec_out, std::size_t& n_out) +{ + // tag::write_at[] + corosio::random_access_file f(ioc); + f.open("data.bin", corosio::file_base::read_write); + + auto [ec, n] = co_await f.write_some_at( + 512, capy::const_buffer("patched", 7)); + // end::write_at[] + ec_out = ec; + n_out = n; +} + +void +open_log(corosio::stream_file& f) +{ + // tag::open_flags[] + f.open("log.txt", + corosio::file_base::write_only + | corosio::file_base::create + | corosio::file_base::append); + // end::open_flags[] +} + +void +inspect_metadata(corosio::stream_file& f) +{ + // tag::metadata[] + auto bytes = f.size(); // file size in bytes + f.resize(1024); // truncate or extend + f.sync_data(); // flush data to stable storage + f.sync_all(); // flush data and metadata + // end::metadata[] +} + +// Opens and closes handles through the platform API directly: assign +// requires a handle Corosio has never registered (see the page NOTE). +corosio::native_handle_type +open_platform_handle(char const* path) +{ +#if BOOST_COROSIO_POSIX + return ::open(path, O_RDONLY); +#else + return reinterpret_cast( + ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr)); +#endif +} + +void +close_platform_handle( + corosio::native_handle_type h) +{ +#if BOOST_COROSIO_POSIX + ::close(h); +#else + ::CloseHandle(reinterpret_cast(h)); +#endif +} + +void +release_handle(corosio::random_access_file& f) +{ + // tag::native_release[] + // Release ownership — caller must close the handle + auto handle = f.release(); + assert(!f.is_open()); + // end::native_release[] + close_platform_handle(handle); +} + +void +adopt_handle( + corosio::io_context& ioc, + corosio::native_handle_type native_handle, + bool& adopted) +{ + // tag::native_adopt[] + // Adopt a handle obtained from the platform's file API — + // the file object takes ownership + corosio::random_access_file f2(ioc); + f2.assign(native_handle); + // end::native_adopt[] + adopted = f2.is_open(); + f2.close(); +} + +capy::task<> +read_at_eof( + corosio::stream_file& f, capy::mutable_buffer buf, + std::error_code& ec_out) +{ + // tag::error_handling[] + auto [ec, n] = co_await f.read_some(buf); + if (ec == capy::cond::eof) + { + // no more data + } + else if (ec) + { + // I/O error + } + // end::error_handling[] + ec_out = ec; +} + +struct file_io_test +{ + void + testStreamRead() + { + corosio::io_context ioc; + bool eof_seen = false; + std::size_t n = 0; + capy::run_async(ioc.get_executor())(stream_read(ioc, eof_seen, n)); + ioc.run(); + BOOST_TEST(!eof_seen); + BOOST_TEST_EQ(n, 2048u); + } + + void + testStreamWrite() + { + corosio::io_context ioc; + std::error_code ec; + std::size_t n = 0; + capy::run_async(ioc.get_executor())(stream_write(ioc, ec, n)); + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 11u); + + std::ifstream in("output.bin", std::ios::binary); + std::string contents( + (std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + BOOST_TEST(contents == "hello world"); + } + + void + testSeek() + { + corosio::io_context ioc; + corosio::stream_file f(ioc); + f.open("data.bin", corosio::file_base::read_only); + // 2048-byte file: 10 bytes before end is position 2038 + BOOST_TEST_EQ(reposition(f), 2038u); + } + + void + testReadAt() + { + corosio::io_context ioc; + std::error_code ec; + std::size_t n = 0; + char first = 0; + capy::run_async(ioc.get_executor())(read_at(ioc, ec, n, first)); + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 256u); + BOOST_TEST_EQ(first, 'a'); + } + + void + testWriteAt() + { + corosio::io_context ioc; + std::error_code ec; + std::size_t n = 0; + capy::run_async(ioc.get_executor())(write_at(ioc, ec, n)); + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 7u); + + std::ifstream in("data.bin", std::ios::binary); + in.seekg(512); + char patched[8] = {}; + in.read(patched, 7); + BOOST_TEST(std::string_view(patched, 7) == "patched"); + } + + void + testOpenFlagsAndMetadata() + { + corosio::io_context ioc; + corosio::stream_file f(ioc); + open_log(f); + BOOST_TEST(f.is_open()); + inspect_metadata(f); + BOOST_TEST_EQ(f.size(), 1024u); + } + + void + testNativeHandle() + { + corosio::io_context ioc; + corosio::random_access_file f(ioc); + f.open("data.bin", corosio::file_base::read_only); + release_handle(f); + + bool adopted = false; + adopt_handle(ioc, open_platform_handle("data.bin"), adopted); + BOOST_TEST(adopted); + } + + void + testErrorHandling() + { + corosio::io_context ioc; + corosio::stream_file f(ioc); + f.open("data.bin", corosio::file_base::read_only); + f.seek(0, corosio::file_base::seek_end); + char buf[64]; + std::error_code ec; + capy::run_async(ioc.get_executor())(read_at_eof( + f, capy::mutable_buffer(buf, sizeof(buf)), ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::eof); + } + + void + run() + { + namespace fs = std::filesystem; + + // The page fragments open files by relative name; run them + // inside a private temp directory so parallel test processes + // cannot collide and cleanup is automatic. + struct cwd_guard + { + fs::path old = fs::current_path(); + ~cwd_guard() + { + std::error_code ec; + fs::current_path(old, ec); + } + }; + cwd_guard guard; + boost::corosio::test::temp_socket_dir dir; + fs::current_path(fs::path(dir.path()).parent_path()); + + { + std::ofstream out("data.bin", std::ios::binary); + std::string filler(2048, 'a'); + out.write(filler.data(), + static_cast(filler.size())); + } + + testStreamRead(); + testStreamWrite(); + testSeek(); + testReadAt(); + testWriteAt(); + testOpenFlagsAndMetadata(); + testNativeHandle(); + testErrorHandling(); + } +}; + +} // namespace + +TEST_SUITE(file_io_test, "boost.corosio.doc.4o_file_io"); diff --git a/test/doc/snippets/4p_unix_sockets.cpp b/test/doc/snippets/4p_unix_sockets.cpp new file mode 100644 index 000000000..a439f86dd --- /dev/null +++ b/test/doc/snippets/4p_unix_sockets.cpp @@ -0,0 +1,357 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4p.unix-sockets.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#endif + +#include "test_suite.hpp" + +namespace { + +// The stream client/server fragments rendezvous on the page's +// literal path; stale files from crashed runs are unlinked first. +void +remove_stale(char const* path) +{ + std::remove(path); +} + +// tag::server[] +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))); +} +// end::server[] + +// tag::client[] +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))); +} +// end::client[] + +#if BOOST_COROSIO_POSIX +void +unlink_then_bind(corosio::io_context& ioc, bool& bound) +{ + corosio::local_stream_acceptor acc(ioc); + acc.open(); + // tag::unlink_bind[] + ::unlink("/tmp/my_app.sock"); // remove stale socket + acc.bind(corosio::local_endpoint("/tmp/my_app.sock")); + // end::unlink_bind[] + bound = std::filesystem::exists("/tmp/my_app.sock"); + acc.close(); + ::unlink("/tmp/my_app.sock"); +} +#endif + +capy::task<> +stream_pair( + corosio::io_context& ioc, std::size_t& n_out, bool& matched) +{ + // tag::stream_pair[] + corosio::local_stream_socket s1(ioc), s2(ioc); + if (auto ec = corosio::connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + // 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" + // end::stream_pair[] + n_out = n; + matched = std::memcmp(buf, "ping", 4) == 0; +} + +#if BOOST_COROSIO_POSIX + +capy::task<> +datagram_connectionless( + corosio::io_context& ioc, std::error_code& ec_out, std::size_t& n_out) +{ + char buf[64]; + // tag::datagram_connectionless[] + 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); + // end::datagram_connectionless[] + ec_out = ec; + n_out = n; +} + +capy::task<> +datagram_pair( + corosio::io_context& ioc, std::size_t& n_out, bool& matched) +{ + char buf[64]; + // tag::datagram_pair[] + corosio::local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = corosio::connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + co_await s1.send(capy::const_buffer("msg", 3)); + + auto [ec, n] = co_await s2.recv( + capy::mutable_buffer(buf, sizeof(buf))); + // end::datagram_pair[] + n_out = n; + matched = !ec && std::memcmp(buf, "msg", 3) == 0; +} + +#endif // BOOST_COROSIO_POSIX + +struct unix_sockets_test +{ + void + testStreamClientServer() + { + remove_stale("/tmp/my_app.sock"); + + corosio::io_context ioc; + auto ex = ioc.get_executor(); + bool server_done = false; + bool client_done = false; + + auto track = [](capy::task<> t, bool& done) -> capy::task<> + { + co_await std::move(t); + done = true; + }; + + capy::run_async(ex)(track(server(ioc), server_done)); + capy::run_async(ex)(track(client(ioc), client_done)); + ioc.run(); + + BOOST_TEST(server_done); + BOOST_TEST(client_done); + // The server bound the page's literal path + BOOST_TEST(std::filesystem::exists("/tmp/my_app.sock")); + remove_stale("/tmp/my_app.sock"); + } + +#if BOOST_COROSIO_POSIX + void + testUnlinkBind() + { + corosio::io_context ioc; + bool bound = false; + unlink_then_bind(ioc, bound); + BOOST_TEST(bound); + } +#endif + + void + testStreamPair() + { + corosio::io_context ioc; + std::size_t n = 0; + bool matched = false; + capy::run_async(ioc.get_executor())(stream_pair(ioc, n, matched)); + ioc.run(); + BOOST_TEST_EQ(n, 4u); + BOOST_TEST(matched); + } + +#if BOOST_COROSIO_POSIX + void + testDatagramConnectionless() + { + remove_stale("/tmp/my_dgram.sock"); + remove_stale("/tmp/peer.sock"); + + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + // A live peer bound to the page's literal path receives the + // fragment's datagram and answers it. + corosio::local_datagram_socket peer(ioc); + peer.open(); + auto bec = peer.bind(corosio::local_endpoint("/tmp/peer.sock")); + BOOST_TEST(!bec); + + std::error_code ec; + std::size_t n = 0; + capy::run_async(ex)(datagram_connectionless(ioc, ec, n)); + + auto responder = [&peer]() -> capy::task<> + { + char buf[64]; + corosio::local_endpoint from; + auto [rec, rn] = co_await peer.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), from); + if (rec) + co_return; + co_await peer.send_to(capy::const_buffer(buf, rn), from); + }; + capy::run_async(ex)(responder()); + + ioc.run(); + + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + + peer.close(); + remove_stale("/tmp/my_dgram.sock"); + remove_stale("/tmp/peer.sock"); + } + + void + testDatagramPair() + { + corosio::io_context ioc; + std::size_t n = 0; + bool matched = false; + capy::run_async(ioc.get_executor())(datagram_pair(ioc, n, matched)); + ioc.run(); + BOOST_TEST_EQ(n, 3u); + BOOST_TEST(matched); + } +#endif + + void + testEndpoints() + { + // tag::endpoints[] + // 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(); + // end::endpoints[] + BOOST_TEST(path == "/tmp/my_app.sock"); + BOOST_TEST(bound); + } + + void + testAbstract() + { + // tag::abstract[] + // Abstract socket — no file created + corosio::local_endpoint ep(std::string_view("\0/my_app", 8)); + assert(ep.is_abstract()); + // end::abstract[] + BOOST_TEST(ep.is_abstract()); + } + + void + run() + { +#if BOOST_COROSIO_POSIX + testStreamClientServer(); +#endif +#if BOOST_COROSIO_POSIX + testUnlinkBind(); +#endif + testStreamPair(); +#if BOOST_COROSIO_POSIX + testDatagramConnectionless(); + testDatagramPair(); +#endif + testEndpoints(); + testAbstract(); + } +}; + +} // namespace + +TEST_SUITE(unix_sockets_test, "boost.corosio.doc.4p_unix_sockets"); diff --git a/test/doc/snippets/4q_udp.cpp b/test/doc/snippets/4q_udp.cpp new file mode 100644 index 000000000..d5dd29339 --- /dev/null +++ b/test/doc/snippets/4q_udp.cpp @@ -0,0 +1,408 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4q.udp.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +// the page's bind fragment shows `if (ec) /* handle */;` verbatim +#pragma GCC diagnostic ignored "-Wempty-body" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#pragma warning(disable: 4390) // empty controlled statement; shown in the page +#endif + +// tag::assume[] +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +void +open_by_family(corosio::io_context& ioc) +{ + // tag::protocol[] + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); // SOCK_DGRAM, AF_INET + // or + sock.open(corosio::udp::v6()); // SOCK_DGRAM, AF_INET6 + // end::protocol[] +} + +void +open_and_bind(corosio::io_context& ioc) +{ + // tag::open_bind[] + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + + auto ec = sock.bind( + corosio::endpoint(corosio::ipv4_address::any(), 9000)); + if (ec) /* handle bind failure */; + // end::open_bind[] +} + +capy::task<> +send_datagram( + corosio::udp_socket& sock, std::error_code& ec_out, std::size_t& n_out) +{ + // tag::send_to[] + char const msg[] = "hello"; + corosio::endpoint dest(corosio::ipv4_address::loopback(), 9000); + + auto [ec, n] = co_await sock.send_to( + capy::const_buffer(msg, sizeof(msg)), dest); + // end::send_to[] + ec_out = ec; + n_out = n; +} + +capy::task<> +receive_datagram( + corosio::udp_socket& sock, std::error_code& ec_out, + std::size_t& n_out, corosio::endpoint& sender_out) +{ + // tag::recv_from[] + char buf[1500]; + corosio::endpoint sender; + + auto [ec, n] = co_await sock.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender); + if (!ec) + { + // buf[0..n) holds the datagram; sender holds the source address. + } + // end::recv_from[] + ec_out = ec; + n_out = n; + sender_out = sender; +} + +// tag::echo[] +capy::task<> echo(corosio::io_context& ioc) +{ + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + auto ec = sock.bind( + corosio::endpoint(corosio::ipv4_address::any(), 9000)); + if (ec) co_return; + + char buf[1500]; + for (;;) + { + corosio::endpoint sender; + auto [rec, n] = co_await sock.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender); + if (rec) co_return; + + co_await sock.send_to( + capy::const_buffer(buf, n), sender); + } +} +// end::echo[] + +// The connected-mode fragment expects a live responder on the page's +// literal port; it is compiled but never executed. +[[maybe_unused]] capy::task<> +connected_mode(corosio::io_context& ioc) +{ + // tag::connected[] + corosio::udp_socket sock(ioc); + auto [cec] = co_await sock.connect( + corosio::endpoint(corosio::ipv4_address::loopback(), 9000)); + if (cec) co_return; + + co_await sock.send(capy::const_buffer("ping", 4)); + + char buf[64]; + auto [rec, n] = co_await sock.recv( + capy::mutable_buffer(buf, sizeof(buf))); + // end::connected[] +} + +capy::task<> +peek_datagram( + corosio::udp_socket& sock, std::size_t& peeked, std::size_t& drained) +{ + char buf[64]; + corosio::endpoint sender; + // tag::peek[] + auto [ec, n] = co_await sock.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender, + corosio::message_flags::peek); + // end::peek[] + if (ec) + co_return; + peeked = n; + + // The peeked datagram is still queued; a plain recv drains it. + auto [ec2, n2] = co_await sock.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender); + if (!ec2) + drained = n2; +} + +bool +tune_options(corosio::udp_socket& sock) +{ + // tag::options[] + sock.set_option(corosio::socket_option::reuse_address(true)); + sock.set_option(corosio::socket_option::broadcast(true)); + sock.set_option(corosio::socket_option::receive_buffer_size(1 << 20)); + + auto bcast = sock.get_option(); + // end::options[] + return bcast.value(); +} + +// Joining a multicast group depends on the host's multicast routing +// (it can fail in containers), so this fragment never runs. +[[maybe_unused]] capy::task<> +multicast_join(corosio::io_context& ioc) +{ + // tag::multicast[] + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + sock.set_option(corosio::socket_option::reuse_address(true)); + + auto ec = sock.bind( + corosio::endpoint(corosio::ipv4_address::any(), 30001)); + if (ec) co_return; + + sock.set_option(corosio::socket_option::join_group_v4( + corosio::ipv4_address("239.255.0.1"))); + // end::multicast[] +} + +void +cancel_all(corosio::udp_socket& sock) +{ + // tag::cancel[] + sock.cancel(); + // end::cancel[] +} + +struct udp_test +{ + void + testProtocolAndBind() + { + corosio::io_context ioc; + open_by_family(ioc); + open_and_bind(ioc); + BOOST_TEST_PASS(); + } + + void + testSendTo() + { + corosio::io_context ioc; + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + + std::error_code ec; + std::size_t n = 0; + capy::run_async(ioc.get_executor())(send_datagram(sock, ec, n)); + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + } + + void + testRecvFrom() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + auto bec = sock.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + corosio::udp_socket helper(ioc); + helper.open(corosio::udp::v4()); + + std::error_code ec; + std::size_t n = 0; + corosio::endpoint sender; + capy::run_async(ex)(receive_datagram(sock, ec, n, sender)); + + auto feeder = [&]() -> capy::task<> + { + co_await helper.send_to( + capy::const_buffer("hello", 5), sock.local_endpoint()); + }; + capy::run_async(ex)(feeder()); + + ioc.run(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + } + + void + testEchoStops() + { + // A pre-signaled stop token makes every path through echo() + // terminate: bind failure returns, and an in-flight recv_from + // completes with the canceled error. + corosio::io_context ioc; + std::stop_source ss; + ss.request_stop(); + bool done = false; + + auto track = [](capy::task<> t, bool& d) -> capy::task<> + { + co_await std::move(t); + d = true; + }; + capy::run_async(ioc.get_executor(), ss.get_token())( + track(echo(ioc), done)); + ioc.run(); + BOOST_TEST(done); + } + + void + testPeek() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + auto bec = sock.bind( + corosio::endpoint(corosio::ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + corosio::udp_socket helper(ioc); + helper.open(corosio::udp::v4()); + + std::size_t peeked = 0; + std::size_t drained = 0; + capy::run_async(ex)(peek_datagram(sock, peeked, drained)); + + auto feeder = [&]() -> capy::task<> + { + co_await helper.send_to( + capy::const_buffer("data", 4), sock.local_endpoint()); + }; + capy::run_async(ex)(feeder()); + + ioc.run(); + BOOST_TEST_EQ(peeked, 4u); + BOOST_TEST_EQ(drained, 4u); + } + + void + testOptions() + { + corosio::io_context ioc; + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + BOOST_TEST(tune_options(sock)); + } + + void + testCancel() + { + corosio::io_context ioc; + corosio::udp_socket sock(ioc); + sock.open(corosio::udp::v4()); + cancel_all(sock); + BOOST_TEST_PASS(); + } + + void + testStopTokenCancel() + { + corosio::io_context ioc; + std::error_code task_ec; + + auto my_task = [&]() -> capy::task<> + { + corosio::udp_socket s(ioc); + s.open(corosio::udp::v4()); + if (auto bec = s.bind(corosio::endpoint( + corosio::ipv4_address::loopback(), 0))) + co_return; + char buf[64]; + corosio::endpoint sender; + auto [ec, n] = co_await s.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), sender); + task_ec = ec; + }; + + // tag::stop_cancel[] + std::stop_source ss; + capy::run_async(ioc.get_executor(), ss.get_token())(my_task()); + // ... + ss.request_stop(); // unblocks any in-flight recv_from inside my_task + // end::stop_cancel[] + + ioc.run(); + BOOST_TEST(task_ec == capy::cond::canceled); + } + + void + run() + { + testProtocolAndBind(); + testSendTo(); + testRecvFrom(); + testEchoStops(); +#if BOOST_COROSIO_POSIX + // IOCP delivery order does not guarantee the self-sent + // datagram is queued before the peek executes. + testPeek(); +#endif + testOptions(); + testCancel(); + testStopTokenCancel(); + } +}; + +} // namespace + +TEST_SUITE(udp_test, "boost.corosio.doc.4q_udp"); diff --git a/test/doc/snippets/4r_wait.cpp b/test/doc/snippets/4r_wait.cpp new file mode 100644 index 000000000..53bd450f9 --- /dev/null +++ b/test/doc/snippets/4r_wait.cpp @@ -0,0 +1,231 @@ +// +// 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 +// + +// Compiled fragments shown in pages/4.guide/4r.wait.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include +#include +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +using namespace std::chrono_literals; +// end::assume[] + +#include +#include +#include +#include +#include + +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The page shows the enum's shape; the real one lives in +// . +namespace api_sketch { +// tag::wait_type_enum[] +enum class wait_type { read, write, error }; +// end::wait_type_enum[] +} // namespace api_sketch + +capy::task<> +wait_readable(corosio::tcp_socket& sock, std::error_code& ec_out) +{ + // tag::wait_read[] + auto [ec] = co_await sock.wait(corosio::wait_type::read); + if (!ec) { + // sock is readable: a subsequent read_some will return data + // without blocking. + } + // end::wait_read[] + ec_out = ec; +} + +capy::task<> +wait_then_accept( + corosio::io_context& ioc, corosio::tcp_acceptor& acceptor, + std::error_code& wec_out, std::error_code& aec_out) +{ + // tag::acceptor_wait[] + auto [wec] = co_await acceptor.wait(corosio::wait_type::read); + if (wec) co_return; + + corosio::tcp_socket peer(ioc); + auto [aec] = co_await acceptor.accept(peer); + // end::acceptor_wait[] + wec_out = wec; + aec_out = aec; +} + +capy::task<> +wait_with_deadline(corosio::tcp_socket& sock, std::error_code& ec_out) +{ + // tag::wait_timeout[] + auto [ec] = co_await corosio::timeout( + sock.wait(corosio::wait_type::read), 200ms); + if (ec == capy::cond::timeout) + std::cout << "No readiness within 200ms\n"; + // end::wait_timeout[] + ec_out = ec; +} + +capy::task<> +await_and_flag(capy::task<> t, bool& done) +{ + co_await std::move(t); + done = true; +} + +struct wait_test +{ + void + testWaitRead() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + auto [sock, peer] = boost::corosio::test::make_socket_pair(ioc); + + std::error_code ec = std::make_error_code(std::errc::io_error); + capy::run_async(ex)(wait_readable(sock, ec)); + + auto writer = [&]() -> capy::task<> + { + co_await peer.write_some(capy::const_buffer("x", 1)); + }; + capy::run_async(ex)(writer()); + + ioc.run(); + BOOST_TEST(!ec); + } + + void + testAcceptorWait() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + + corosio::tcp_acceptor acceptor(ioc); + acceptor.open(); + acceptor.set_option(corosio::socket_option::reuse_address(true)); + auto bec = acceptor.bind(corosio::endpoint( + corosio::ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + BOOST_TEST(!acceptor.listen()); + auto port = acceptor.local_endpoint().port(); + + // Sentinels distinguish "never reached" from success. + std::error_code wec = std::make_error_code(std::errc::io_error); + std::error_code aec = std::make_error_code(std::errc::io_error); + capy::run_async(ex)(wait_then_accept(ioc, acceptor, wec, aec)); + + corosio::tcp_socket client(ioc); + client.open(); + auto connecter = [&]() -> capy::task<> + { + co_await client.connect(corosio::endpoint( + corosio::ipv4_address::loopback(), port)); + }; + capy::run_async(ex)(connecter()); + + ioc.run(); + BOOST_TEST(!wec); + BOOST_TEST(!aec); + } + + void + testWaitCancel() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + auto [sock, peer] = boost::corosio::test::make_socket_pair(ioc); + + // tag::wait_cancel[] + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await sock.wait(corosio::wait_type::read); + // ec == capy::cond::canceled if sock.cancel() was invoked + }; + // end::wait_cancel[] + + bool done = false; + capy::run_async(ex)(await_and_flag(waiter(), done)); + + // Posted after the waiter, so the wait is in flight when the + // cancel lands. + auto canceller = [&]() -> capy::task<> + { + sock.cancel(); + co_return; + }; + capy::run_async(ex)(canceller()); + + ioc.run(); + BOOST_TEST(done); + } + + void + testWaitTimeout() + { + corosio::io_context ioc; + auto ex = ioc.get_executor(); + // The peer stays silent, so readiness never arrives. + auto [sock, peer] = boost::corosio::test::make_socket_pair(ioc); + + std::error_code ec; + capy::run_async(ex)(wait_with_deadline(sock, ec)); + ioc.run(); + BOOST_TEST(ec == capy::cond::timeout); + } + + void + run() + { + testWaitRead(); + testAcceptorWait(); + testWaitCancel(); + testWaitTimeout(); + } +}; + +} // namespace + +TEST_SUITE(wait_test, "boost.corosio.doc.4r_wait"); diff --git a/test/doc/snippets/5a_mocket.cpp b/test/doc/snippets/5a_mocket.cpp new file mode 100644 index 000000000..0cb1de771 --- /dev/null +++ b/test/doc/snippets/5a_mocket.cpp @@ -0,0 +1,249 @@ +// +// 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 +// + +// Compiled fragments shown in pages/5.testing/5a.mocket.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The page's `backend` placeholder: this platform's native backend tag. +#if BOOST_COROSIO_HAS_EPOLL +constexpr auto backend = corosio::epoll; +#elif BOOST_COROSIO_HAS_KQUEUE +constexpr auto backend = corosio::kqueue; +#elif BOOST_COROSIO_HAS_IOCP +constexpr auto backend = corosio::iocp; +#endif + +struct mocket_page_test +{ + void + testCreating() + { + // tag::creating[] + corosio::io_context ioc; + + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + // end::creating[] + // "Both are open and immediately usable." + BOOST_TEST(m.is_open()); + BOOST_TEST(peer.is_open()); + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testProvide() + { + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // tag::provide[] + m.provide("HTTP/1.1 200 OK\r\n\r\nHello"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + char buf[64] = {}; + auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf)); + // buf[0..n] == "HTTP/1.1 200 OK\r\n\r\nHello" + }; + // end::provide[] + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + ioc.restart(); + + // A clean close proves the staged bytes were fully consumed. + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testExpect() + { + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // tag::expect[] + m.expect("GET / HTTP/1.1\r\n\r\n"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + auto [ec, n] = co_await m_ref.write_some( + capy::const_buffer("GET / HTTP/1.1\r\n\r\n", 18)); + // ec is empty; n == 18 + }; + // end::expect[] + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + ioc.restart(); + + // A clean close proves the expected bytes were all written. + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testChunked() + { + corosio::io_context ioc; + // tag::chunked[] + // max_read_size = 4, max_write_size = 3 force short transfers. + auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, 4, 3); + + m.provide("0123456789"); + m.expect("abcdef"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + char buf[16] = {}; + auto [rec, rn] = co_await m_ref.read_some(capy::make_buffer(buf)); + // rn == 4 ("0123") + + auto [wec, wn] = co_await m_ref.write_some( + capy::const_buffer("abcdef", 6)); + // wn == 3 (matched "abc") + }; + // end::chunked[] + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + ioc.restart(); + + // Draining the leftovers proves the shown task moved exactly + // 4 bytes out and matched exactly 3 bytes in. + auto drain = [](corosio::test::mocket& m_ref) -> capy::task<> { + char buf[16] = {}; + auto [ec1, n1] = co_await m_ref.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(std::string_view(buf, n1), "4567"); + auto [ec2, n2] = co_await m_ref.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(std::string_view(buf, n2), "89"); + auto [ec3, n3] = co_await m_ref.write_some( + capy::const_buffer("def", 3)); + BOOST_TEST(!ec3); + BOOST_TEST_EQ(n3, 3u); + }; + capy::run_async(ioc.get_executor())(drain(m)); + ioc.run(); + ioc.restart(); + + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testCloseCheck() + { + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // Deliberately leave staged data unconsumed so close() reports it. + m.provide("unread"); + + // tag::close_check[] + auto ec = m.close(); + if (ec == capy::error::test_failure) + { + // Either provide() data was never read, + // or expect() data was never written. + } + // end::close_check[] + BOOST_TEST(ec == capy::error::test_failure); + peer.close(); + } + + void + testNative() + { + // tag::native[] + using socket_type = corosio::native_tcp_socket; + using acceptor_type = corosio::native_tcp_acceptor; + using mocket_type = corosio::test::basic_mocket; + + corosio::io_context ioc(backend); + + auto [m, peer] = + corosio::test::make_mocket_pair(ioc); + // end::native[] + static_assert(std::is_same_v); + BOOST_TEST(m.is_open()); + BOOST_TEST(peer.is_open()); + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testSocketAccess() + { + corosio::io_context ioc; + // tag::socket_access[] + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + corosio::tcp_socket& under = m.socket(); + // Pass `under` into a TLS stream, a custom framing layer, etc. + // end::socket_access[] + BOOST_TEST(under.is_open()); + BOOST_TEST(!m.close()); + // `under` is the mocket's own socket, so it closed with it. + BOOST_TEST(!under.is_open()); + peer.close(); + } + + void + run() + { + testCreating(); + testProvide(); + testExpect(); + testChunked(); + testCloseCheck(); + testNative(); + testSocketAccess(); + } +}; + +} // namespace + +TEST_SUITE(mocket_page_test, "boost.corosio.doc.5a_mocket"); diff --git a/test/doc/snippets/5b_socket_pair.cpp b/test/doc/snippets/5b_socket_pair.cpp new file mode 100644 index 000000000..60d9f47ba --- /dev/null +++ b/test/doc/snippets/5b_socket_pair.cpp @@ -0,0 +1,131 @@ +// +// 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 +// + +// Compiled fragments shown in pages/5.testing/5b.socket-pair.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The overview block restates the header's signature; compiling the +// redeclaration keeps the page's synopsis honest. +// tag::signature[] +template +std::pair +make_socket_pair(corosio::io_context& ctx); +// end::signature[] + +struct socket_pair_page_test +{ + void + testRoundTrip() + { + // tag::round_trip[] + corosio::io_context ioc; + + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + + auto task = [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> { + co_await a.write_some(capy::const_buffer("ping", 4)); + + char buf[8] = {}; + auto [ec, n] = co_await b.read_some(capy::make_buffer(buf)); + // buf[0..n] == "ping" + }; + capy::run_async(ioc.get_executor())(task(s1, s2)); + ioc.run(); + // end::round_trip[] + ioc.restart(); + + // A reverse round trip with assertions proves data really flows. + auto verify = [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> { + auto [wec, wn] = co_await b.write_some( + capy::const_buffer("pong", 4)); + BOOST_TEST(!wec); + BOOST_TEST_EQ(wn, 4u); + + char buf[8] = {}; + auto [ec, n] = co_await a.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(std::string_view(buf, n), "pong"); + }; + capy::run_async(ioc.get_executor())(verify(s1, s2)); + ioc.run(); + + s1.close(); + s2.close(); + } + + void + testLingerFalse() + { + corosio::io_context ioc; + // tag::linger_false[] + auto [s1, s2] = corosio::test::make_socket_pair< + corosio::tcp_socket, + corosio::tcp_acceptor, + /*Linger=*/false>(ioc); + // end::linger_false[] + BOOST_TEST(s1.is_open()); + BOOST_TEST(s2.is_open()); + s1.close(); + s2.close(); + } + + void + run() + { + testRoundTrip(); + testLingerFalse(); + } +}; + +} // namespace + +TEST_SUITE(socket_pair_page_test, "boost.corosio.doc.5b_socket_pair"); diff --git a/test/doc/snippets/5c_patterns.cpp b/test/doc/snippets/5c_patterns.cpp new file mode 100644 index 000000000..c3e0c6141 --- /dev/null +++ b/test/doc/snippets/5c_patterns.cpp @@ -0,0 +1,235 @@ +// +// 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 +// + +// Compiled fragments shown in pages/5.testing/5c.patterns.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +// tag::assume[] +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::assume[] + +#include +#include + +#include "test_suite.hpp" + +namespace { + +// Stand-ins for the "function under test" the recipes call. +capy::task<> +my_http_get(corosio::test::mocket& m, std::string_view target) +{ + std::string req = "GET " + std::string(target) + " HTTP/1.1\r\n\r\n"; + co_await m.write_some(capy::const_buffer(req.data(), req.size())); +} + +capy::task +my_http_read(corosio::test::mocket& m) +{ + char buf[128] = {}; + auto [ec, n] = co_await m.read_some(capy::make_buffer(buf)); + if (ec) + co_return {}; + co_return std::string(buf, n); +} + +struct patterns_page_test +{ + void + testRequestFormat() + { + // tag::request_format[] + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + m.expect("GET /api/v1/users HTTP/1.1\r\n\r\n"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + co_await my_http_get(m_ref, "/api/v1/users"); + }; + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + + auto ec = m.close(); // !ec means everything was written + // end::request_format[] + BOOST_TEST(!ec); + peer.close(); + } + + void + testStagedResponse() + { + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // tag::staged_response[] + m.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "Hello"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + auto response = co_await my_http_read(m_ref); + // assert on parsed response + }; + // end::staged_response[] + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + ioc.restart(); + + // A clean close proves the consumer read the whole response. + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testChunkedReads() + { + corosio::io_context ioc; + // tag::chunked_reads[] + auto [m, peer] = corosio::test::make_mocket_pair(ioc, {}, /*max_read_size=*/4); + + m.provide("ABCDEFGH"); + + auto task = [](corosio::test::mocket& m_ref) -> capy::task<> { + std::string acc; + char buf[16]; + for (int i = 0; i < 2; ++i) + { + auto [ec, n] = co_await m_ref.read_some(capy::make_buffer(buf)); + acc.append(buf, n); // n == 4 each time + } + }; + // end::chunked_reads[] + capy::run_async(ioc.get_executor())(task(m)); + ioc.run(); + ioc.restart(); + + // A clean close proves the loop consumed all 8 staged bytes. + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testLayering() + { + corosio::io_context ioc; + // tag::layering[] + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // Pass m.socket() into a TLS stream or other layer in production code: + corosio::tcp_socket& under = m.socket(); + // e.g., openssl_stream tls(&under, tls_ctx); + // end::layering[] + BOOST_TEST(under.is_open()); + BOOST_TEST(!m.close()); + peer.close(); + } + + void + testEndToEnd() + { + // tag::end_to_end[] + corosio::io_context ioc; + auto [s1, s2] = corosio::test::make_socket_pair(ioc); + + auto task = [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> { + auto [wec, wn] = + co_await a.write_some(capy::const_buffer("payload", 7)); + + char buf[16] = {}; + auto [ec, n] = co_await b.read_some(capy::make_buffer(buf)); + // buf[0..n] == "payload" + }; + capy::run_async(ioc.get_executor())(task(s1, s2)); + ioc.run(); + // end::end_to_end[] + ioc.restart(); + + // A reverse round trip with assertions proves data really flows. + auto verify = [](corosio::tcp_socket& a, corosio::tcp_socket& b) + -> capy::task<> { + co_await b.write_some(capy::const_buffer("reply", 5)); + + char buf[16] = {}; + auto [ec, n] = co_await a.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(std::string_view(buf, n), "reply"); + }; + capy::run_async(ioc.get_executor())(verify(s1, s2)); + ioc.run(); + + s1.close(); + s2.close(); + } + + void + testCloseVerification() + { + corosio::io_context ioc; + auto [m, peer] = corosio::test::make_mocket_pair(ioc); + + // Deliberately leave staged data unconsumed so close() reports it. + m.expect("never written"); + + // tag::close_verification[] + auto ec = m.close(); + // ec == capy::error::test_failure means leftover provide() data was + // never read, or expect() data was never written. Either way, the test + // would have passed silently without this check. + // end::close_verification[] + BOOST_TEST(ec == capy::error::test_failure); + peer.close(); + } + + void + run() + { + testRequestFormat(); + testStagedResponse(); + testChunkedReads(); + testLayering(); + testEndToEnd(); + testCloseVerification(); + } +}; + +} // namespace + +TEST_SUITE(patterns_page_test, "boost.corosio.doc.5c_patterns"); diff --git a/test/doc/snippets/index_page.cpp b/test/doc/snippets/index_page.cpp new file mode 100644 index 000000000..d9d9e2c2a --- /dev/null +++ b/test/doc/snippets/index_page.cpp @@ -0,0 +1,38 @@ +// +// 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 +// + +// Compiled fragments shown in pages/index.adoc. + +// tag::convention[] +#include +#include +#include + +namespace corosio = boost::corosio; +namespace capy = boost::capy; +// end::convention[] + +#include "test_suite.hpp" + +namespace { + +struct index_page_test +{ + void + run() + { + // The convention block is includes and aliases; compiling this + // TU is the test. + BOOST_TEST(true); + } +}; + +} // namespace + +TEST_SUITE(index_page_test, "boost.corosio.doc.index_page"); diff --git a/test/doc/snippets/quick_start.cpp b/test/doc/snippets/quick_start.cpp new file mode 100644 index 000000000..08e59df6d --- /dev/null +++ b/test/doc/snippets/quick_start.cpp @@ -0,0 +1,87 @@ +// +// 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 +// + +// Compiled fragments shown in pages/quick-start.adoc. + +// Fragments deliberately leave results and bindings unused; the pages +// explain the values in prose instead. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#pragma GCC diagnostic ignored "-Wunused-variable" +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wunused-value" +#pragma GCC diagnostic ignored "-Wunused-result" +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined(__clang__) +#pragma clang diagnostic ignored "-Wunused-lambda-capture" +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif +#if defined(_MSC_VER) +#pragma warning(disable: 4834) // discarding [[nodiscard]] return value +#pragma warning(disable: 4189) // local variable initialized but not referenced +#pragma warning(disable: 4100) // unreferenced formal parameter +#pragma warning(disable: 4101) // unreferenced local variable +#pragma warning(disable: 4456) // declaration hides previous local declaration +#pragma warning(disable: 4457) // declaration hides function parameter +#pragma warning(disable: 4458) // declaration hides class member +#pragma warning(disable: 4459) // declaration hides global declaration +#endif + +#include +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace corosio = boost::corosio; +namespace capy = boost::capy; + +namespace { + +// The error-handling fragments read from a socket the page assumes is +// connected; they are compiled but never executed. +[[maybe_unused]] capy::task<> +structured_bindings(corosio::tcp_socket& sock, capy::mutable_buffer buf) +{ + // tag::error_bindings[] + auto [ec, n] = co_await sock.read_some(buf); + if (ec) + { + // Handle error + } + // end::error_bindings[] +} + +[[maybe_unused]] capy::task<> +exception_style(corosio::tcp_socket& sock, capy::mutable_buffer buf) +{ + // tag::error_exceptions[] + auto [ec, n] = co_await sock.read_some(buf); + if (ec) throw std::system_error(ec); // Throws if read fails + // end::error_exceptions[] +} + +struct quick_start_test +{ + void + run() + { + // The fragments above are compile-only; instantiating the + // enclosing coroutines is the test. + BOOST_TEST(true); + } +}; + +} // namespace + +TEST_SUITE(quick_start_test, "boost.corosio.doc.quick_start");