From fc17a6725320373504f53253d59867148c37b434 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Wed, 22 Jul 2026 23:09:45 +0200 Subject: [PATCH] docs: compile every documentation code block Replace all ~480 hand-typed [source,cpp] blocks across the 45 doc pages with includes of compiled sources, 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 with assertions on the behavior the prose claims - test/doc/programs: complete teaching programs, run by exit code; bug demos (races, deadlocks) are built but never executed - example/: tagged regions replace duplicated excerpts on the 15 example pages - @antora/collector-extension, registered in the library's own playbook, scans example/ and test/doc into the Antora examples family so pages use include::example$...[tag=...] without symlinks or duplication - Blocks that intentionally do not compile are explicit: rejected or sketch-level designs carry role=pseudocode (21), other-library comparisons carry role=external (13) test/tsan.supp suppresses reports from Apple libc++ stream internals, which lack TSan annotations for the concurrent std::cout the standard requires to be race-free. --- .github/workflows/ci.yml | 6 +- doc/antora.yml | 8 + doc/local-playbook.yml | 1 + .../2.cpp20-coroutines/2a.foundations.adoc | 39 +- .../pages/2.cpp20-coroutines/2b.syntax.adoc | 99 +-- .../2.cpp20-coroutines/2c.machinery.adoc | 185 +----- .../pages/2.cpp20-coroutines/2d.advanced.adoc | 273 +------- .../pages/3.concurrency/3a.foundations.adoc | 106 +--- .../3.concurrency/3b.synchronization.adoc | 102 +-- .../ROOT/pages/3.concurrency/3c.advanced.adoc | 138 +--- .../ROOT/pages/3.concurrency/3d.patterns.adoc | 185 +----- .../ROOT/pages/4.coroutines/4a.tasks.adoc | 107 +--- .../ROOT/pages/4.coroutines/4b.launching.adoc | 76 +-- .../ROOT/pages/4.coroutines/4c.executors.adoc | 117 +--- .../pages/4.coroutines/4d.io-awaitable.adoc | 123 +--- .../pages/4.coroutines/4e.cancellation.adoc | 230 +------ .../pages/4.coroutines/4f.composition.adoc | 187 +----- .../pages/4.coroutines/4g.allocators.adoc | 106 +--- .../4.coroutines/4h.lambda-captures.adoc | 87 +-- .../ROOT/pages/5.buffers/5a.overview.adoc | 21 +- .../ROOT/pages/5.buffers/5b.types.adoc | 94 +-- .../ROOT/pages/5.buffers/5c.sequences.adoc | 99 +-- .../ROOT/pages/5.buffers/5d.system-io.adoc | 37 +- .../ROOT/pages/5.buffers/5e.algorithms.adoc | 123 +--- .../ROOT/pages/6.streams/6a.overview.adoc | 40 +- .../ROOT/pages/6.streams/6b.streams.adoc | 100 +-- .../ROOT/pages/6.streams/6f.isolation.adoc | 121 +--- .../ROOT/pages/7.testing/7a.drivers.adoc | 208 +----- .../ROOT/pages/7.testing/7b.mock-streams.adoc | 180 +----- .../pages/7.testing/7e.buffer-inspection.adoc | 140 +--- .../ROOT/pages/8.examples/8a.hello-task.adoc | 29 +- .../8.examples/8b.producer-consumer.adoc | 83 +-- .../8.examples/8c.buffer-composition.adoc | 130 +--- .../8.examples/8d.mock-stream-testing.adoc | 164 +---- .../pages/8.examples/8e.type-erased-echo.adoc | 93 +-- .../8.examples/8f.timeout-cancellation.adoc | 264 +------- .../pages/8.examples/8g.parallel-fetch.adoc | 169 +---- .../8.examples/8i.echo-server-corosio.adoc | 96 +-- .../8.examples/8k.strand-serialization.adoc | 69 +- .../ROOT/pages/8.examples/8l.async-mutex.adoc | 82 +-- .../pages/8.examples/8m.parallel-tasks.adoc | 86 +-- .../pages/8.examples/8n.custom-executor.adoc | 166 +---- .../pages/8.examples/8o.sender-bridge.adoc | 60 +- .../pages/8.examples/8p.asio-use-capy.adoc | 42 +- .../ROOT/pages/9.design/9a.CapyLayering.adoc | 27 +- .../ROOT/pages/9.design/9c.ReadStream.adoc | 56 +- .../ROOT/pages/9.design/9f.WriteStream.adoc | 58 +- .../pages/9.design/9i.TypeEraseAwaitable.adoc | 10 +- .../ROOT/pages/9.design/9k.Executor.adoc | 92 +-- .../ROOT/pages/9.design/9l.RunApi.adoc | 103 +-- .../ROOT/pages/9.design/9m.WhyNotCobalt.adoc | 67 +- .../9.design/9n.WhyNotCobaltConcepts.adoc | 122 +--- .../ROOT/pages/9.design/9o.WhyNotTMC.adoc | 24 +- doc/modules/ROOT/pages/index.adoc | 35 +- doc/modules/ROOT/pages/quick-start.adoc | 49 +- doc/package-lock.json | 28 +- doc/package.json | 1 + example/asio/any_stream.cpp | 2 + example/asio/api/capy_streams.hpp | 2 + example/asio/api/use_capy.hpp | 2 + example/asio/use_capy_example.cpp | 2 + example/async-mutex/async_mutex.cpp | 6 + .../buffer-composition/buffer_composition.cpp | 10 +- example/custom-executor/custom_executor.cpp | 21 +- example/echo-server-corosio/echo_server.cpp | 12 + example/hello-task/hello_task.cpp | 8 + .../mock_stream_testing.cpp | 9 +- example/parallel-fetch/parallel_fetch.cpp | 8 + .../producer-consumer/producer_consumer.cpp | 16 + example/sender-bridge/sender_awaitable.hpp | 3 + example/sender-bridge/sender_bridge.cpp | 2 + .../strand_serialization.cpp | 8 + .../timeout_cancellation.cpp | 18 + example/type-erased-echo/echo.cpp | 4 + example/type-erased-echo/echo.hpp | 4 + example/type-erased-echo/main.cpp | 2 + test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/doc/CMakeLists.txt | 69 ++ test/doc/Jamfile | 43 ++ test/doc/WriteProgramTest.cmake | 15 + test/doc/programs/2b_syntax_counter.cpp | 66 ++ test/doc/programs/2c_machinery_fibonacci.cpp | 112 ++++ test/doc/programs/2c_machinery_generator.cpp | 107 ++++ test/doc/programs/2c_machinery_trace.cpp | 88 +++ test/doc/programs/2d_advanced_exception.cpp | 75 +++ test/doc/programs/3a_foundations_hello.cpp | 28 + test/doc/programs/3a_foundations_lambda.cpp | 27 + test/doc/programs/3a_foundations_member.cpp | 33 + test/doc/programs/3a_foundations_parallel.cpp | 33 + test/doc/programs/3a_foundations_ref.cpp | 32 + .../doc/programs/3b_synchronization_mutex.cpp | 41 ++ test/doc/programs/3b_synchronization_race.cpp | 36 ++ test/doc/programs/3c_advanced_atomics.cpp | 36 ++ .../3c_advanced_condition_variable.cpp | 48 ++ test/doc/programs/3d_patterns_async.cpp | 31 + .../doc/programs/3d_patterns_parallel_for.cpp | 52 ++ .../3d_patterns_producer_consumer.cpp | 78 +++ .../programs/3d_patterns_promise_future.cpp | 37 ++ .../doc/programs/3d_patterns_thread_local.cpp | 41 ++ test/doc/programs/4b_launching_run_async.cpp | 29 + .../programs/4c_executors_executor_ref.cpp | 78 +++ .../doc/programs/4c_executors_thread_pool.cpp | 40 ++ .../programs/8m_parallel_tasks_variadic.cpp | 90 +++ test/doc/programs/index_page_echo.cpp | 44 ++ test/doc/programs/quick_start_hello.cpp | 44 ++ test/doc/snippets/2a_foundations.cpp | 222 +++++++ test/doc/snippets/2b_syntax.cpp | 218 +++++++ test/doc/snippets/2c_machinery.cpp | 73 +++ test/doc/snippets/2d_advanced.cpp | 411 ++++++++++++ test/doc/snippets/3a_foundations.cpp | 117 ++++ test/doc/snippets/3b_synchronization.cpp | 154 +++++ test/doc/snippets/3c_advanced.cpp | 212 +++++++ test/doc/snippets/3d_patterns.cpp | 139 ++++ test/doc/snippets/4a_tasks.cpp | 316 +++++++++ test/doc/snippets/4b_launching.cpp | 260 ++++++++ test/doc/snippets/4c_executors.cpp | 250 ++++++++ test/doc/snippets/4d_io_awaitable.cpp | 335 ++++++++++ test/doc/snippets/4e_cancellation.cpp | 536 ++++++++++++++++ test/doc/snippets/4f_composition.cpp | 600 ++++++++++++++++++ test/doc/snippets/4g_allocators.cpp | 407 ++++++++++++ test/doc/snippets/4h_lambda_captures.cpp | 283 +++++++++ test/doc/snippets/5a_overview.cpp | 115 ++++ test/doc/snippets/5b_types.cpp | 275 ++++++++ test/doc/snippets/5c_sequences.cpp | 272 ++++++++ test/doc/snippets/5d_system_io.cpp | 157 +++++ test/doc/snippets/5e_algorithms.cpp | 418 ++++++++++++ test/doc/snippets/6a_overview.cpp | 162 +++++ test/doc/snippets/6b_streams.cpp | 319 ++++++++++ test/doc/snippets/6f_isolation.cpp | 353 +++++++++++ test/doc/snippets/7a_drivers.cpp | 410 ++++++++++++ test/doc/snippets/7b_mock_streams.cpp | 322 ++++++++++ test/doc/snippets/7e_buffer_inspection.cpp | 237 +++++++ test/doc/snippets/8c_buffer_composition.cpp | 102 +++ test/doc/snippets/8d_mock_stream_testing.cpp | 105 +++ test/doc/snippets/8f_timeout_cancellation.cpp | 80 +++ test/doc/snippets/9a_capy_layering.cpp | 127 ++++ test/doc/snippets/9c_read_stream.cpp | 200 ++++++ test/doc/snippets/9f_write_stream.cpp | 194 ++++++ test/doc/snippets/9k_executor.cpp | 285 +++++++++ test/doc/snippets/9l_run_api.cpp | 278 ++++++++ test/doc/snippets/9m_why_not_cobalt.cpp | 55 ++ .../snippets/9n_why_not_cobalt_concepts.cpp | 273 ++++++++ test/doc/snippets/9o_why_not_tmc.cpp | 143 +++++ test/doc/snippets/http_client.hpp | 45 ++ test/doc/snippets/index_page.cpp | 43 ++ test/doc/snippets/protocol.hpp | 28 + test/doc/snippets/quick_start.cpp | 114 ++++ test/tsan.supp | 7 + 149 files changed, 11733 insertions(+), 5056 deletions(-) create mode 100644 test/doc/CMakeLists.txt create mode 100644 test/doc/Jamfile create mode 100644 test/doc/WriteProgramTest.cmake create mode 100644 test/doc/programs/2b_syntax_counter.cpp create mode 100644 test/doc/programs/2c_machinery_fibonacci.cpp create mode 100644 test/doc/programs/2c_machinery_generator.cpp create mode 100644 test/doc/programs/2c_machinery_trace.cpp create mode 100644 test/doc/programs/2d_advanced_exception.cpp create mode 100644 test/doc/programs/3a_foundations_hello.cpp create mode 100644 test/doc/programs/3a_foundations_lambda.cpp create mode 100644 test/doc/programs/3a_foundations_member.cpp create mode 100644 test/doc/programs/3a_foundations_parallel.cpp create mode 100644 test/doc/programs/3a_foundations_ref.cpp create mode 100644 test/doc/programs/3b_synchronization_mutex.cpp create mode 100644 test/doc/programs/3b_synchronization_race.cpp create mode 100644 test/doc/programs/3c_advanced_atomics.cpp create mode 100644 test/doc/programs/3c_advanced_condition_variable.cpp create mode 100644 test/doc/programs/3d_patterns_async.cpp create mode 100644 test/doc/programs/3d_patterns_parallel_for.cpp create mode 100644 test/doc/programs/3d_patterns_producer_consumer.cpp create mode 100644 test/doc/programs/3d_patterns_promise_future.cpp create mode 100644 test/doc/programs/3d_patterns_thread_local.cpp create mode 100644 test/doc/programs/4b_launching_run_async.cpp create mode 100644 test/doc/programs/4c_executors_executor_ref.cpp create mode 100644 test/doc/programs/4c_executors_thread_pool.cpp create mode 100644 test/doc/programs/8m_parallel_tasks_variadic.cpp create mode 100644 test/doc/programs/index_page_echo.cpp create mode 100644 test/doc/programs/quick_start_hello.cpp create mode 100644 test/doc/snippets/2a_foundations.cpp create mode 100644 test/doc/snippets/2b_syntax.cpp create mode 100644 test/doc/snippets/2c_machinery.cpp create mode 100644 test/doc/snippets/2d_advanced.cpp create mode 100644 test/doc/snippets/3a_foundations.cpp create mode 100644 test/doc/snippets/3b_synchronization.cpp create mode 100644 test/doc/snippets/3c_advanced.cpp create mode 100644 test/doc/snippets/3d_patterns.cpp create mode 100644 test/doc/snippets/4a_tasks.cpp create mode 100644 test/doc/snippets/4b_launching.cpp create mode 100644 test/doc/snippets/4c_executors.cpp create mode 100644 test/doc/snippets/4d_io_awaitable.cpp create mode 100644 test/doc/snippets/4e_cancellation.cpp create mode 100644 test/doc/snippets/4f_composition.cpp create mode 100644 test/doc/snippets/4g_allocators.cpp create mode 100644 test/doc/snippets/4h_lambda_captures.cpp create mode 100644 test/doc/snippets/5a_overview.cpp create mode 100644 test/doc/snippets/5b_types.cpp create mode 100644 test/doc/snippets/5c_sequences.cpp create mode 100644 test/doc/snippets/5d_system_io.cpp create mode 100644 test/doc/snippets/5e_algorithms.cpp create mode 100644 test/doc/snippets/6a_overview.cpp create mode 100644 test/doc/snippets/6b_streams.cpp create mode 100644 test/doc/snippets/6f_isolation.cpp create mode 100644 test/doc/snippets/7a_drivers.cpp create mode 100644 test/doc/snippets/7b_mock_streams.cpp create mode 100644 test/doc/snippets/7e_buffer_inspection.cpp create mode 100644 test/doc/snippets/8c_buffer_composition.cpp create mode 100644 test/doc/snippets/8d_mock_stream_testing.cpp create mode 100644 test/doc/snippets/8f_timeout_cancellation.cpp create mode 100644 test/doc/snippets/9a_capy_layering.cpp create mode 100644 test/doc/snippets/9c_read_stream.cpp create mode 100644 test/doc/snippets/9f_write_stream.cpp create mode 100644 test/doc/snippets/9k_executor.cpp create mode 100644 test/doc/snippets/9l_run_api.cpp create mode 100644 test/doc/snippets/9m_why_not_cobalt.cpp create mode 100644 test/doc/snippets/9n_why_not_cobalt_concepts.cpp create mode 100644 test/doc/snippets/9o_why_not_tmc.cpp create mode 100644 test/doc/snippets/http_client.hpp create mode 100644 test/doc/snippets/index_page.cpp create mode 100644 test/doc/snippets/protocol.hpp create mode 100644 test/doc/snippets/quick_start.cpp create mode 100644 test/tsan.supp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60eae1240..090fb467f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,7 +163,9 @@ jobs: if: ${{ !matrix.coverage && !matrix.time-trace && !matrix.superproject-cmake && !matrix.clang-tidy }} env: ASAN_OPTIONS: ${{ ((matrix.compiler == 'apple-clang' || matrix.compiler == 'clang' || matrix.compiler == 'mingw') && 'detect_invalid_pointer_pairs=0:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1') || 'detect_invalid_pointer_pairs=2:strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1' }} - TSAN_OPTIONS: ${{ matrix.tsan && 'halt_on_error=1:second_deadlock_stack=1' || '' }} + # Relative to the b2 working directory (boost-root); absolute + # workspace paths differ between container and host runners. + TSAN_OPTIONS: ${{ matrix.tsan && 'halt_on_error=1:second_deadlock_stack=1:suppressions=libs/capy/test/tsan.supp' || '' }} with: source-dir: boost-root modules: capy @@ -190,7 +192,7 @@ jobs: generator: ${{ matrix.generator }} generator-toolset: ${{ matrix.generator-toolset }} build-type: ${{ matrix.build-type }} - build-target: boost_capy_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 7d5a3adf5..ad9990c7a 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 16e9ef3db..76ff7ec6d 100644 --- a/doc/local-playbook.yml +++ b/doc/local-playbook.yml @@ -19,6 +19,7 @@ ui: antora: extensions: + - require: '@antora/collector-extension' - require: '@sntke/antora-mermaid-extension' mermaid_library_url: https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs script_stem: header-scripts diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc index b523a295a..91e39b901 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc @@ -24,11 +24,7 @@ Consider this function: [source,cpp] ---- -int compute(int x, int y) -{ - int result = x * y + 42; - return result; -} +include::example$snippets/2a_foundations.cpp[tag=compute,indent=0] ---- When `compute` is called: @@ -57,7 +53,7 @@ When a coroutine resumes: This capability is implemented through a *coroutine frame*—a heap-allocated block of memory that stores the coroutine's state. Unlike stack frames, coroutine frames persist across suspension points because they live on the heap rather than the stack. -[source,cpp] +[source,cpp,role=pseudocode] ---- // Conceptual illustration (not real syntax) task fetch_and_process() @@ -80,14 +76,7 @@ In traditional synchronous code: [source,cpp] ---- -void handle_request(connection& conn) -{ - std::string request = conn.read(); // blocks until data arrives - auto parsed = parse_request(request); - auto data = database.query(parsed.id); // blocks until database responds - auto response = compute_response(data); - conn.write(response); // blocks until write completes -} +include::example$snippets/2a_foundations.cpp[tag=sync_request,indent=0] ---- This code reads naturally from top to bottom. But while waiting for the network or database, this function blocks the entire thread. If you have thousands of concurrent connections, you would need thousands of threads, each consuming memory and requiring operating system scheduling. @@ -98,18 +87,7 @@ The traditional solution uses callbacks: [source,cpp] ---- -void handle_request(connection& conn) -{ - conn.async_read([&conn](std::string request) { - auto parsed = parse_request(request); - database.async_query(parsed.id, [&conn](auto data) { - auto response = compute_response(data); - conn.async_write(response, [&conn]() { - // request complete - }); - }); - }); -} +include::example$snippets/2a_foundations.cpp[tag=callback_request,indent=0] ---- This code does not block. Each operation starts, registers a callback, and returns immediately. When the operation completes, the callback runs. @@ -122,14 +100,7 @@ Coroutines restore linear code structure while maintaining asynchronous behavior [source,cpp] ---- -task handle_request(connection& conn) -{ - std::string request = co_await conn.async_read(); - auto parsed = parse_request(request); - auto data = co_await database.async_query(parsed.id); - auto response = compute_response(data); - co_await conn.async_write(response); -} +include::example$snippets/2a_foundations.cpp[tag=coroutine_request,indent=0] ---- This code reads like the original blocking version. Local variables like `request`, `parsed`, and `data` exist naturally in their scope. Yet the function suspends at each `co_await` point, allowing other work to proceed while waiting. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc index a7113899b..d74508623 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc @@ -17,11 +17,7 @@ The `co_await` keyword suspends the coroutine and waits for some operation to co [source,cpp] ---- -task fetch_page(std::string url) -{ - auto response = co_await http_get(url); // suspends until HTTP completes - return response.body; // continues after resumption -} +include::example$snippets/2b_syntax.cpp[tag=co_await_example,indent=0] ---- === co_yield @@ -30,13 +26,7 @@ The `co_yield` keyword produces a value and suspends the coroutine. This pattern [source,cpp] ---- -generator count_to(int n) -{ - for (int i = 1; i <= n; ++i) - { - co_yield i; // produce value, suspend, resume when next value requested - } -} +include::example$snippets/2b_syntax.cpp[tag=co_yield_example,indent=0] ---- === co_return @@ -45,11 +35,7 @@ The `co_return` keyword completes the coroutine and optionally provides a final [source,cpp] ---- -task compute() -{ - int result = 42; - co_return result; // completes the coroutine with value 42 -} +include::example$snippets/2b_syntax.cpp[tag=co_return_example,indent=0] ---- For coroutines that do not return a value, use `co_return;` without an argument. @@ -65,24 +51,7 @@ Here is the minimal structure needed to create a coroutine: [source,cpp] ---- -#include - -struct SimpleCoroutine -{ - struct promise_type - { - SimpleCoroutine get_return_object() { return {}; } - std::suspend_never initial_suspend() { return {}; } - std::suspend_never final_suspend() noexcept { return {}; } - void return_void() {} - void unhandled_exception() {} - }; -}; - -SimpleCoroutine my_first_coroutine() -{ - co_return; // This makes it a coroutine -} +include::example$snippets/2b_syntax.cpp[tag=simple_coroutine,indent=0] ---- The `promise_type` nested structure provides the minimum scaffolding the compiler needs. You will learn what each method does in xref:2.cpp20-coroutines/2c.machinery.adoc[Part III: Coroutine Machinery]. @@ -101,59 +70,7 @@ When you write `co_await expr`, the expression `expr` must be an *awaitable*—s [source,cpp] ---- -#include -#include - -struct ReturnObject -{ - struct promise_type - { - ReturnObject get_return_object() { return {}; } - std::suspend_never initial_suspend() { return {}; } - std::suspend_never final_suspend() noexcept { return {}; } - void return_void() {} - void unhandled_exception() {} - }; -}; - -struct Awaiter -{ - std::coroutine_handle<>* handle_out; - - bool await_ready() { return false; } // always suspend - - void await_suspend(std::coroutine_handle<> h) - { - *handle_out = h; // store handle for later resumption - } - - void await_resume() {} // nothing to return -}; - -ReturnObject counter(std::coroutine_handle<>* handle) -{ - Awaiter awaiter{handle}; - - for (unsigned i = 0; ; ++i) - { - std::cout << "counter: " << i << std::endl; - co_await awaiter; - } -} - -int main() -{ - std::coroutine_handle<> h; - counter(&h); - - for (int i = 0; i < 3; ++i) - { - std::cout << "main: resuming" << std::endl; - h(); - } - - h.destroy(); -} +include::example$programs/2b_syntax_counter.cpp[tag=full] ---- *Output:* @@ -192,11 +109,7 @@ These are useful building blocks for promise types and custom awaitables. [source,cpp] ---- -// suspend_always causes suspension at this point -co_await std::suspend_always{}; - -// suspend_never continues immediately without suspending -co_await std::suspend_never{}; +include::example$snippets/2b_syntax.cpp[tag=standard_awaiters,indent=0] ---- You have now learned the three coroutine keywords and how awaitables work. In the next section, you will learn about the promise type and coroutine handle—the machinery that makes coroutines function. diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc index ef021c90a..4f23927da 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc @@ -39,7 +39,7 @@ The compiler transforms your coroutine body into something resembling this pseud NOTE: The `co_await` keywords below are intentional. This mirrors the {cpp} standard's own description ({cpp}20 [dcl.fct.def.coroutine]/5), which uses `co_await` to express the logical suspension points. The compiler expands each `co_await` into the full awaiter protocol (`await_ready`, `await_suspend`, `await_resume`) as described in xref:2.cpp20-coroutines/2b.syntax.adoc#_awaitables_and_awaiters[Awaitables and Awaiters]. -[source,cpp] +[source,cpp,role=pseudocode] ---- { promise_type promise; @@ -70,65 +70,7 @@ Important observations: [source,cpp] ---- -#include -#include - -struct TracePromise -{ - struct promise_type - { - promise_type() - { - std::cout << "promise constructed" << std::endl; - } - - ~promise_type() - { - std::cout << "promise destroyed" << std::endl; - } - - TracePromise get_return_object() - { - std::cout << "get_return_object called" << std::endl; - return {}; - } - - std::suspend_never initial_suspend() - { - std::cout << "initial_suspend called" << std::endl; - return {}; - } - - std::suspend_always final_suspend() noexcept - { - std::cout << "final_suspend called" << std::endl; - return {}; - } - - void return_void() - { - std::cout << "return_void called" << std::endl; - } - - void unhandled_exception() - { - std::cout << "unhandled_exception called" << std::endl; - } - }; -}; - -TracePromise trace_coroutine() -{ - std::cout << "coroutine body begins" << std::endl; - co_return; -} - -int main() -{ - std::cout << "calling coroutine" << std::endl; - auto result = trace_coroutine(); - std::cout << "coroutine returned" << std::endl; -} +include::example$programs/2c_machinery_trace.cpp[tag=full] ---- *Output:* @@ -142,6 +84,8 @@ coroutine body begins return_void called final_suspend called coroutine returned +destroying coroutine handle +promise destroyed ---- Notice that the promise is constructed first, then `get_return_object()` creates the return value, then `initial_suspend()` runs. Since `initial_suspend()` returns `suspend_never`, the coroutine body executes immediately. After `co_return`, `return_void()` is called, followed by `final_suspend()`. Since `final_suspend()` returns `suspend_always`, the coroutine suspends one last time, and the promise is not destroyed until the coroutine handle is explicitly destroyed. @@ -176,7 +120,7 @@ Inside `get_return_object()`, you can obtain the coroutine handle using: [source,cpp] ---- -std::coroutine_handle::from_promise(*this) +include::example$snippets/2c_machinery.cpp[tag=from_promise,indent=0] ---- Since `get_return_object()` is called on the promise object (as `this`), this method returns a handle to the coroutine containing that promise. @@ -189,7 +133,7 @@ A *generator* is a function that produces a sequence of values on demand. Instea The expression `co_yield value` is transformed by the compiler into: -[source,cpp] +[source,cpp,role=pseudocode] ---- co_await promise.yield_value(value) ---- @@ -200,99 +144,7 @@ The `yield_value` method receives the yielded value, stores it somewhere accessi [source,cpp] ---- -#include -#include - -struct Generator -{ - struct promise_type - { - int current_value; - - Generator get_return_object() - { - return Generator{ - std::coroutine_handle::from_promise(*this) - }; - } - - std::suspend_always initial_suspend() { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - - std::suspend_always yield_value(int value) - { - current_value = value; - return {}; - } - - void return_void() {} - void unhandled_exception() { std::terminate(); } - }; - - std::coroutine_handle handle; - - Generator(std::coroutine_handle h) : handle(h) {} - - ~Generator() - { - if (handle) - handle.destroy(); - } - - // Disable copying - Generator(Generator const&) = delete; - Generator& operator=(Generator const&) = delete; - - // Enable moving - Generator(Generator&& other) noexcept - : handle(other.handle) - { - other.handle = nullptr; - } - - Generator& operator=(Generator&& other) noexcept - { - if (this != &other) - { - if (handle) - handle.destroy(); - handle = other.handle; - other.handle = nullptr; - } - return *this; - } - - bool next() - { - if (!handle || handle.done()) - return false; - handle.resume(); - return !handle.done(); - } - - int value() const - { - return handle.promise().current_value; - } -}; - -Generator count_to(int n) -{ - for (int i = 1; i <= n; ++i) - { - co_yield i; - } -} - -int main() -{ - auto gen = count_to(5); - - while (gen.next()) - { - std::cout << gen.value() << std::endl; - } -} +include::example$programs/2c_machinery_generator.cpp[tag=full] ---- *Output:* @@ -325,28 +177,7 @@ Here is a more interesting generator that produces the Fibonacci sequence: [source,cpp] ---- -Generator fibonacci() -{ - int a = 0, b = 1; - while (true) - { - co_yield a; - int next = a + b; - a = b; - b = next; - } -} - -int main() -{ - auto fib = fibonacci(); - - for (int i = 0; i < 10 && fib.next(); ++i) - { - std::cout << fib.value() << " "; - } - std::cout << std::endl; -} +include::example$programs/2c_machinery_fibonacci.cpp[tag=full] ---- *Output:* diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc index ccf07fb74..355ad9e1e 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2d.advanced.adoc @@ -19,9 +19,7 @@ Consider a chain of coroutines where each awaits the next: [source,cpp] ---- -task<> a() { co_await b(); } -task<> b() { co_await c(); } -task<> c() { co_return; } +include::example$snippets/2d_advanced.cpp[tag=task_chain,indent=0] ---- Without symmetric transfer, when `a` awaits `b`: @@ -40,19 +38,12 @@ Each suspension adds a stack frame. With thousands of nested coroutines, the sta [source,cpp] ---- -std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) -{ - // store continuation for later - continuation_ = h; - - // return handle to resume (instead of calling resume()) - return next_coroutine_; -} +include::example$snippets/2d_advanced.cpp[tag=symmetric_await_suspend,indent=0] ---- When `await_suspend` returns a handle, the compiler generates code equivalent to: -[source,cpp] +[source,cpp,role=pseudocode] ---- auto next = awaiter.await_suspend(current); if (next != std::noop_coroutine()) @@ -80,24 +71,7 @@ A production generator uses symmetric transfer at `final_suspend` to return to w [source,cpp] ---- -auto final_suspend() noexcept -{ - struct awaiter - { - promise_type* p_; - - bool await_ready() const noexcept { return false; } - - std::coroutine_handle<> await_suspend(std::coroutine_handle<>) noexcept - { - // Return to the consumer that called resume() - return p_->consumer_handle_; - } - - void await_resume() const noexcept {} - }; - return awaiter{this}; -} +include::example$snippets/2d_advanced.cpp[tag=generator_final_suspend,indent=0] ---- == Coroutine Allocation @@ -130,12 +104,7 @@ HALO is most effective when: [source,cpp] ---- -// HALO might apply here because the task is awaited immediately -co_await compute_something(); - -// HALO cannot apply here because the task escapes -auto task = compute_something(); -store_for_later(std::move(task)); +include::example$snippets/2d_advanced.cpp[tag=halo,indent=0] ---- === Custom Allocators @@ -144,21 +113,7 @@ Promise types can customize allocation by providing `operator new` and `operator [source,cpp] ---- -struct promise_type -{ - // Custom allocation - static void* operator new(std::size_t size) - { - return my_allocator.allocate(size); - } - - static void operator delete(void* ptr, std::size_t size) - { - my_allocator.deallocate(ptr, size); - } - - // ... rest of promise type -}; +include::example$snippets/2d_advanced.cpp[tag=custom_operator_new,indent=0] ---- The promise's `operator new` receives only the frame size. To access allocator arguments passed to the coroutine, use the leading allocator convention with `std::allocator_arg_t` as the first parameter. @@ -182,40 +137,28 @@ When an exception is thrown inside a coroutine and not caught: [source,cpp] ---- -void unhandled_exception() -{ - std::terminate(); -} +include::example$snippets/2d_advanced.cpp[tag=unhandled_terminate,indent=0] ---- *Store for later retrieval:* [source,cpp] ---- -void unhandled_exception() -{ - exception_ = std::current_exception(); -} +include::example$snippets/2d_advanced.cpp[tag=unhandled_store,indent=0] ---- *Rethrow immediately:* [source,cpp] ---- -void unhandled_exception() -{ - throw; // propagates to whoever resumed the coroutine -} +include::example$snippets/2d_advanced.cpp[tag=unhandled_rethrow,indent=0] ---- *Swallow the exception:* [source,cpp] ---- -void unhandled_exception() -{ - // silently ignored - almost always a mistake -} +include::example$snippets/2d_advanced.cpp[tag=unhandled_swallow,indent=0] ---- === The Store-and-Rethrow Pattern @@ -224,91 +167,14 @@ For tasks and generators where callers expect results, store the exception and r [source,cpp] ---- -struct promise_type -{ - std::exception_ptr exception_; - - void unhandled_exception() - { - exception_ = std::current_exception(); - } -}; - -// In the return object's result accessor: -T get_result() -{ - if (handle_.promise().exception_) - std::rethrow_exception(handle_.promise().exception_); - return std::move(handle_.promise().result_); -} +include::example$snippets/2d_advanced.cpp[tag=store_and_rethrow,indent=0] ---- === Exception Example [source,cpp] ---- -#include -#include -#include -#include - -struct Task -{ - struct promise_type - { - std::exception_ptr exception; - - Task get_return_object() - { - return Task{std::coroutine_handle::from_promise(*this)}; - } - - std::suspend_always initial_suspend() { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - void return_void() {} - - void unhandled_exception() - { - exception = std::current_exception(); - } - }; - - std::coroutine_handle handle; - - Task(std::coroutine_handle h) : handle(h) {} - ~Task() { if (handle) handle.destroy(); } - - void run() { handle.resume(); } - - void check_exception() - { - if (handle.promise().exception) - std::rethrow_exception(handle.promise().exception); - } -}; - -Task risky_operation() -{ - std::cout << "Starting risky operation" << std::endl; - throw std::runtime_error("Something went wrong"); - co_return; // never reached -} - -int main() -{ - Task task = risky_operation(); - - try - { - task.run(); - task.check_exception(); - std::cout << "Operation completed successfully" << std::endl; - } - catch (std::exception const& e) - { - std::cout << "Operation failed: " << e.what() << std::endl; - } -} +include::example$programs/2d_advanced_exception.cpp[tag=full] ---- *Output:* @@ -328,120 +194,7 @@ With all these concepts, here is a production-quality generic generator: [source,cpp] ---- -#include -#include -#include - -template -class Generator -{ -public: - struct promise_type - { - T value_; - std::exception_ptr exception_; - - Generator get_return_object() - { - return Generator{Handle::from_promise(*this)}; - } - - std::suspend_always initial_suspend() noexcept { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - - std::suspend_always yield_value(T v) - { - value_ = std::move(v); - return {}; - } - - void return_void() noexcept {} - - void unhandled_exception() - { - exception_ = std::current_exception(); - } - - // Prevent co_await inside generators - template - std::suspend_never await_transform(U&&) = delete; - }; - - using Handle = std::coroutine_handle; - - class iterator - { - Handle handle_; - - public: - using iterator_category = std::input_iterator_tag; - using value_type = T; - using difference_type = std::ptrdiff_t; - - iterator() : handle_(nullptr) {} - explicit iterator(Handle h) : handle_(h) {} - - iterator& operator++() - { - handle_.resume(); - if (handle_.done()) - { - auto& promise = handle_.promise(); - handle_ = nullptr; - if (promise.exception_) - std::rethrow_exception(promise.exception_); - } - return *this; - } - - T& operator*() const { return handle_.promise().value_; } - bool operator==(iterator const& other) const - { - return handle_ == other.handle_; - } - }; - - iterator begin() - { - if (handle_) - { - handle_.resume(); - if (handle_.done()) - { - auto& promise = handle_.promise(); - if (promise.exception_) - std::rethrow_exception(promise.exception_); - return iterator{}; - } - } - return iterator{handle_}; - } - - iterator end() { return iterator{}; } - - ~Generator() { if (handle_) handle_.destroy(); } - - Generator(Generator const&) = delete; - Generator& operator=(Generator const&) = delete; - - Generator(Generator&& other) noexcept - : handle_(std::exchange(other.handle_, nullptr)) {} - - Generator& operator=(Generator&& other) noexcept - { - if (this != &other) - { - if (handle_) handle_.destroy(); - handle_ = std::exchange(other.handle_, nullptr); - } - return *this; - } - -private: - Handle handle_; - - explicit Generator(Handle h) : handle_(h) {} -}; +include::example$snippets/2d_advanced.cpp[tag=production_generator] ---- This generator: diff --git a/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc b/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc index 4252789f3..0c80f3f49 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3a.foundations.adoc @@ -36,21 +36,7 @@ The `` header provides `std::thread`, the standard way to create threads [source,cpp] ---- -#include -#include - -void say_hello() -{ - std::cout << "Hello from a new thread!\n"; -} - -int main() -{ - std::thread t(say_hello); - t.join(); - std::cout << "Back in the main thread.\n"; - return 0; -} +include::example$programs/3a_foundations_hello.cpp[tag=full] ---- The `std::thread` constructor takes a function (or any *callable*) and immediately starts a new thread running that function. Two bookmarks now move through your code simultaneously. @@ -61,25 +47,7 @@ The `join()` call makes the main thread wait until thread `t` finishes. Without [source,cpp] ---- -#include -#include - -void count_up(char const* name) -{ - for (int i = 1; i <= 5; ++i) - std::cout << name << ": " << i << "\n"; -} - -int main() -{ - std::thread alice(count_up, "Alice"); - std::thread bob(count_up, "Bob"); - - alice.join(); - bob.join(); - - return 0; -} +include::example$programs/3a_foundations_parallel.cpp[tag=full] ---- Run this and you might see output like: @@ -114,20 +82,7 @@ Lambda expressions are often the clearest choice: [source,cpp] ---- -#include -#include - -int main() -{ - int x = 42; - - std::thread t([x]() { - std::cout << "The value is: " << x << "\n"; - }); - - t.join(); - return 0; -} +include::example$programs/3a_foundations_lambda.cpp[tag=full] ---- The lambda captures `x` by value—it copies `x` into the lambda. By default, `std::thread` copies all arguments passed to it. Even if your function declares a reference parameter, the thread receives a copy. @@ -136,24 +91,7 @@ To pass by reference, use `std::ref()`: [source,cpp] ---- -#include -#include - -void increment(int& value) -{ - ++value; -} - -int main() -{ - int counter = 0; - - std::thread t(increment, std::ref(counter)); - t.join(); - - std::cout << "Counter is now: " << counter << "\n"; - return 0; -} +include::example$programs/3a_foundations_ref.cpp[tag=full] ---- Without `std::ref()`, the thread would modify a copy, leaving `counter` unchanged. @@ -164,26 +102,7 @@ For member functions, pass a pointer to the function and an instance: [source,cpp] ---- -#include -#include -#include - -class Greeter -{ -public: - void greet(std::string const& name) - { - std::cout << "Hello, " << name << "!\n"; - } -}; - -int main() -{ - Greeter g; - std::thread t(&Greeter::greet, &g, "World"); - t.join(); - return 0; -} +include::example$programs/3a_foundations_member.cpp[tag=full] ---- The `&Greeter::greet` syntax names the member function; `&g` provides the instance to call it on. @@ -198,9 +117,7 @@ Every thread must be either *joined* or *detached* before its `std::thread` obje [source,cpp] ---- -std::thread t(do_work); -// ... do other things ... -t.join(); // wait for do_work to finish +include::example$snippets/3a_foundations.cpp[tag=join,indent=0] ---- === detach() @@ -209,9 +126,7 @@ Sometimes you want a thread to run independently, continuing even after the `std [source,cpp] ---- -std::thread t(background_task); -t.detach(); // thread runs independently -// t is now "empty"—no longer associated with a thread +include::example$snippets/3a_foundations.cpp[tag=detach,indent=0] ---- A detached thread becomes a *daemon thread*. It runs until it finishes or the program exits. You lose all ability to wait for it or check its status. Use detachment sparingly—usually for fire-and-forget background work. @@ -222,12 +137,7 @@ Before joining or detaching, you can check if a thread is *joinable*: [source,cpp] ---- -std::thread t(some_function); - -if (t.joinable()) -{ - t.join(); -} +include::example$snippets/3a_foundations.cpp[tag=joinable,indent=0] ---- A thread is joinable if it represents an actual thread of execution. After joining or detaching, or after default construction, a `std::thread` is not joinable. diff --git a/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc b/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc index c51f5d9a8..3ea64e651 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3b.synchronization.adoc @@ -15,28 +15,7 @@ Consider this code: [source,cpp] ---- -#include -#include - -int counter = 0; - -void increment_many_times() -{ - for (int i = 0; i < 100000; ++i) - ++counter; -} - -int main() -{ - std::thread t1(increment_many_times); - std::thread t2(increment_many_times); - - t1.join(); - t2.join(); - - std::cout << "Counter: " << counter << "\n"; - return 0; -} +include::example$programs/3b_synchronization_race.cpp[tag=full] ---- Two threads, each incrementing 100,000 times. You would expect 200,000. But run this repeatedly and you will see different results—180,000, 195,327, maybe occasionally 200,000. Something is wrong. @@ -59,34 +38,7 @@ A *mutex* (mutual exclusion object) is a lockable resource. Before accessing sha [source,cpp] ---- -#include -#include -#include - -int counter = 0; -std::mutex counter_mutex; - -void increment_many_times() -{ - for (int i = 0; i < 100000; ++i) - { - counter_mutex.lock(); - ++counter; - counter_mutex.unlock(); - } -} - -int main() -{ - std::thread t1(increment_many_times); - std::thread t2(increment_many_times); - - t1.join(); - t2.join(); - - std::cout << "Counter: " << counter << "\n"; - return 0; -} +include::example$programs/3b_synchronization_mutex.cpp[tag=full] ---- Now the output is always 200,000. The mutex ensures that between `lock()` and `unlock()`, only one thread executes. The increment is now effectively atomic. @@ -101,22 +53,7 @@ Lock guards apply RAII to mutexes: [source,cpp] ---- -#include -#include -#include - -int counter = 0; -std::mutex counter_mutex; - -void increment_many_times() -{ - for (int i = 0; i < 100000; ++i) - { - std::lock_guard lock(counter_mutex); - ++counter; - // lock is automatically released when it goes out of scope - } -} +include::example$snippets/3b_synchronization.cpp[tag=lock_guard] ---- The `std::lock_guard` locks the mutex on construction and unlocks it on destruction. Even if an exception is thrown, the destructor runs and the mutex is released. This is the correct way to use mutexes. @@ -127,7 +64,7 @@ Since {cpp}17, `std::scoped_lock` is preferred. It works like `lock_guard` but c [source,cpp] ---- -std::scoped_lock lock(counter_mutex); // C++17 +include::example$snippets/3b_synchronization.cpp[tag=scoped_lock,indent=0] ---- === std::unique_lock @@ -136,14 +73,7 @@ For more control, use `std::unique_lock`. It can be unlocked before destruction, [source,cpp] ---- -std::unique_lock lock(some_mutex, std::defer_lock); -// mutex not yet locked - -lock.lock(); // lock when ready -// ... do work ... -lock.unlock(); // unlock early if needed -// ... do other work ... -// destructor unlocks again if still locked +include::example$snippets/3b_synchronization.cpp[tag=unique_lock,indent=0] ---- `std::unique_lock` is more flexible but slightly more expensive than `std::lock_guard`. Use the simplest tool that does the job. @@ -156,21 +86,7 @@ Imagine two threads and two mutexes. Thread A locks mutex 1, then tries to lock [source,cpp] ---- -std::mutex mutex1, mutex2; - -void thread_a() -{ - std::lock_guard lock1(mutex1); - std::lock_guard lock2(mutex2); // blocks, waiting for B - // ... -} - -void thread_b() -{ - std::lock_guard lock2(mutex2); - std::lock_guard lock1(mutex1); // blocks, waiting for A - // ... -} +include::example$snippets/3b_synchronization.cpp[tag=deadlock] ---- If both threads run and each acquires its first mutex before the other acquires the second, deadlock occurs. @@ -183,11 +99,7 @@ When you need to lock multiple mutexes and cannot guarantee order, use `std::sco [source,cpp] ---- -void safe_function() -{ - std::scoped_lock lock(mutex1, mutex2); // locks both atomically - // ... -} +include::example$snippets/3b_synchronization.cpp[tag=scoped_lock_multi] ---- `std::scoped_lock` uses a deadlock-avoidance algorithm internally, acquiring both mutexes without risk of circular waiting. diff --git a/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc b/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc index 8e3ce6059..a7dca8ebf 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3c.advanced.adoc @@ -15,29 +15,7 @@ An atomic operation completes entirely before any other thread can observe its e [source,cpp] ---- -#include -#include -#include - -std::atomic counter{0}; - -void increment_many_times() -{ - for (int i = 0; i < 100000; ++i) - ++counter; // atomic increment -} - -int main() -{ - std::thread t1(increment_many_times); - std::thread t2(increment_many_times); - - t1.join(); - t2.join(); - - std::cout << "Counter: " << counter << "\n"; - return 0; -} +include::example$programs/3c_advanced_atomics.cpp[tag=full] ---- No mutex, no lock guard, yet the result is always 200,000. The `std::atomic` ensures that increments are indivisible. @@ -59,19 +37,7 @@ Any trivially copyable type can be made atomic. [source,cpp] ---- -std::atomic value{0}; - -value.store(42); // atomic write -int x = value.load(); // atomic read -int old = value.exchange(10); // atomic read-modify-write -value.fetch_add(5); // atomic addition, returns old value -value.fetch_sub(3); // atomic subtraction, returns old value - -// Compare-and-swap (CAS) -int expected = 10; -bool success = value.compare_exchange_strong(expected, 20); -// If value == expected, sets value = 20 and returns true -// Otherwise, sets expected = value and returns false +include::example$snippets/3c_advanced.cpp[tag=atomic_ops,indent=0] ---- == Condition Variables: Efficient Waiting @@ -80,11 +46,7 @@ Sometimes a thread must wait for a specific condition before proceeding. You cou [source,cpp] ---- -// Inefficient busy-wait -while (!ready) -{ - std::this_thread::sleep_for(std::chrono::milliseconds(100)); -} +include::example$snippets/3c_advanced.cpp[tag=busy_wait,indent=0] ---- This works but wastes CPU cycles and introduces latency. *Condition variables* provide efficient waiting. @@ -93,41 +55,7 @@ A condition variable allows one thread to signal others that something has chang [source,cpp] ---- -#include -#include -#include -#include - -std::mutex mtx; -std::condition_variable cv; -bool ready = false; - -void worker() -{ - std::unique_lock lock(mtx); - cv.wait(lock, []{ return ready; }); // wait until ready is true - std::cout << "Worker proceeding!\n"; -} - -void signal_ready() -{ - { - std::lock_guard lock(mtx); - ready = true; - } - cv.notify_one(); // wake one waiting thread -} - -int main() -{ - std::thread t(worker); - - std::this_thread::sleep_for(std::chrono::seconds(1)); - signal_ready(); - - t.join(); - return 0; -} +include::example$programs/3c_advanced_condition_variable.cpp[tag=full] ---- The worker thread calls `cv.wait()`, which atomically releases the mutex and suspends the thread. When `signal_ready()` calls `notify_one()`, the worker wakes up, reacquires the mutex, checks the condition, and proceeds. @@ -147,15 +75,7 @@ Use `notify_one()` when only one thread needs to proceed (e.g., producer-consume [source,cpp] ---- -// Wait indefinitely -cv.wait(lock, predicate); - -// Wait with timeout -auto status = cv.wait_for(lock, std::chrono::seconds(5), predicate); -// Returns true if predicate is true, false on timeout - -// Wait until specific time point -auto status = cv.wait_until(lock, deadline, predicate); +include::example$snippets/3c_advanced.cpp[tag=wait_variants,indent=0] ---- == Shared Locks: Readers and Writers @@ -166,26 +86,7 @@ Consider a data structure that is read frequently but written rarely. A regular [source,cpp] ---- -#include -#include -#include -#include - -std::shared_mutex rw_mutex; -std::vector data; - -void reader(int id) -{ - std::shared_lock lock(rw_mutex); // shared access - std::cout << "Reader " << id << " sees " << data.size() << " elements\n"; -} - -void writer(int value) -{ - std::unique_lock lock(rw_mutex); // exclusive access - data.push_back(value); - std::cout << "Writer added " << value << "\n"; -} +include::example$snippets/3c_advanced.cpp[tag=shared_mutex,indent=0] ---- === Lock Types @@ -208,32 +109,7 @@ This pattern maximizes concurrency for read-heavy workloads. Use `std::shared_mu [source,cpp] ---- -#include -#include -#include -#include - -class ThreadSafeCache -{ - std::unordered_map cache_; - mutable std::shared_mutex mutex_; - -public: - std::optional get(std::string const& key) const - { - std::shared_lock lock(mutex_); // readers can proceed in parallel - auto it = cache_.find(key); - if (it != cache_.end()) - return it->second; - return std::nullopt; - } - - void put(std::string const& key, std::string const& value) - { - std::unique_lock lock(mutex_); // exclusive access for writing - cache_[key] = value; - } -}; +include::example$snippets/3c_advanced.cpp[tag=thread_safe_cache,indent=0] ---- Multiple threads can call `get()` simultaneously without blocking each other. Only `put()` requires exclusive access. diff --git a/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc b/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc index 868a14b03..93f4d6003 100644 --- a/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc +++ b/doc/modules/ROOT/pages/3.concurrency/3d.patterns.adoc @@ -15,30 +15,7 @@ A `std::promise` is a write-once container: a thread can set its value. A `std:: [source,cpp] ---- -#include -#include -#include - -void compute(std::promise result_promise) -{ - int answer = 6 * 7; // expensive computation - result_promise.set_value(answer); -} - -int main() -{ - std::promise promise; - std::future future = promise.get_future(); - - std::thread t(compute, std::move(promise)); - - std::cout << "Waiting for result...\n"; - int result = future.get(); // blocks until value is set - std::cout << "The answer is: " << result << "\n"; - - t.join(); - return 0; -} +include::example$programs/3d_patterns_promise_future.cpp[tag=full] ---- The worker thread calls `set_value()`. The main thread calls `get()`, which blocks until the value is available. @@ -56,24 +33,7 @@ Creating threads manually, managing promises, joining at the end—it is mechani [source,cpp] ---- -#include -#include - -int compute() -{ - return 6 * 7; -} - -int main() -{ - std::future future = std::async(compute); - - std::cout << "Computing...\n"; - int result = future.get(); - std::cout << "Result: " << result << "\n"; - - return 0; -} +include::example$programs/3d_patterns_async.cpp[tag=full] ---- `std::async` launches the function (potentially in a new thread), returning a future. No explicit thread creation, no promise management, no join call. @@ -84,14 +44,7 @@ By default, the system decides whether to run the function in a new thread or de [source,cpp] ---- -// Force a new thread -auto future = std::async(std::launch::async, compute); - -// Defer execution until get() -auto future = std::async(std::launch::deferred, compute); - -// Let the system decide (default) -auto future = std::async(std::launch::async | std::launch::deferred, compute); +include::example$snippets/3d_patterns.cpp[tag=launch_policies,indent=0] ---- For quick parallel tasks, `std::async` is often the cleanest choice. @@ -104,34 +57,7 @@ Declare it `thread_local`: [source,cpp] ---- -#include -#include - -thread_local int counter = 0; - -void increment_and_print(char const* name) -{ - ++counter; - std::cout << name << " counter: " << counter << "\n"; -} - -int main() -{ - std::thread t1([]{ - increment_and_print("T1"); - increment_and_print("T1"); - }); - - std::thread t2([]{ - increment_and_print("T2"); - increment_and_print("T2"); - }); - - t1.join(); - t2.join(); - - return 0; -} +include::example$programs/3d_patterns_thread_local.cpp[tag=full] ---- Each thread sees its own `counter`. T1 prints 1, then 2. T2 independently prints 1, then 2. No synchronization needed because the data is not shared. @@ -146,74 +72,14 @@ One or more threads produce work items; one or more threads consume them. A queu [source,cpp] ---- -#include -#include -#include -#include -#include - -template -class ThreadSafeQueue -{ - std::queue queue_; - std::mutex mutex_; - std::condition_variable cv_; - -public: - void push(T value) - { - { - std::lock_guard lock(mutex_); - queue_.push(std::move(value)); - } - cv_.notify_one(); - } - - T pop() - { - std::unique_lock lock(mutex_); - cv_.wait(lock, [this]{ return !queue_.empty(); }); - T value = std::move(queue_.front()); - queue_.pop(); - return value; - } -}; +include::example$snippets/3d_patterns.cpp[tag=thread_safe_queue,indent=0] ---- The producer pushes items; the consumer waits for items and processes them. The condition variable ensures the consumer sleeps efficiently when the queue is empty. [source,cpp] ---- -ThreadSafeQueue work_queue; - -void producer() -{ - for (int i = 0; i < 10; ++i) - { - work_queue.push(i); - std::cout << "Produced: " << i << "\n"; - } -} - -void consumer() -{ - for (int i = 0; i < 10; ++i) - { - int item = work_queue.pop(); - std::cout << "Consumed: " << item << "\n"; - } -} - -int main() -{ - std::thread prod(producer); - std::thread cons(consumer); - - prod.join(); - cons.join(); - - return 0; -} +include::example$programs/3d_patterns_producer_consumer.cpp[tag=full] ---- === Parallel For @@ -222,44 +88,7 @@ Split a loop across multiple threads: [source,cpp] ---- -#include -#include -#include -#include - -void parallel_for(int start, int end, int num_threads, - std::function func) -{ - std::vector threads; - int chunk_size = (end - start) / num_threads; - - for (int t = 0; t < num_threads; ++t) - { - int chunk_start = start + t * chunk_size; - int chunk_end = (t == num_threads - 1) ? end : chunk_start + chunk_size; - - threads.emplace_back([=]{ - for (int i = chunk_start; i < chunk_end; ++i) - func(i); - }); - } - - for (auto& thread : threads) - thread.join(); -} - -int main() -{ - std::mutex print_mutex; - - parallel_for(0, 20, 4, [&](int i){ - std::lock_guard lock(print_mutex); - std::cout << "Processing " << i << " on thread " - << std::this_thread::get_id() << "\n"; - }); - - return 0; -} +include::example$programs/3d_patterns_parallel_for.cpp[tag=full] ---- The work is divided into chunks, each handled by its own thread. For CPU-bound work on large datasets, this can dramatically reduce execution time. diff --git a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc b/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc index 42d3d5eb9..b73cdc849 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4a.tasks.adoc @@ -25,23 +25,7 @@ Any function that returns `task` and contains coroutine keywords (`co_await`, [source,cpp] ---- -#include -using namespace boost::capy; - -task compute_value() -{ - co_return 42; -} - -task fetch_greeting() -{ - co_return "Hello, Capy!"; -} - -task<> do_nothing() // task -{ - co_return; -} +include::example$snippets/4a_tasks.cpp[tag=declaring] ---- The syntax `task<>` is equivalent to `task` and represents a coroutine that completes without producing a value. @@ -52,17 +36,7 @@ Use `co_return` to complete the coroutine and provide its result: [source,cpp] ---- -task add(int a, int b) -{ - int result = a + b; - co_return result; // Completes with value -} - -task<> log_message(std::string msg) -{ - std::cout << msg << "\n"; - co_return; // Completes without value -} +include::example$snippets/4a_tasks.cpp[tag=returning] ---- For `task`, you can either use `co_return;` explicitly or let execution fall off the end of the function body. @@ -73,22 +47,7 @@ Tasks can await other tasks using `co_await`. This is the primary mechanism for [source,cpp] ---- -task step_one() -{ - co_return 10; -} - -task step_two(int x) -{ - co_return x * 2; -} - -task full_operation() -{ - int a = co_await step_one(); // Suspends until step_one completes - int b = co_await step_two(a); // Suspends until step_two completes - co_return b + 5; // Final result: 25 -} +include::example$snippets/4a_tasks.cpp[tag=awaiting] ---- When you `co_await` a task: @@ -104,20 +63,7 @@ A critical property of `task` is *lazy execution*: creating a task does not s [source,cpp] ---- -task compute() -{ - std::cout << "Computing...\n"; // Not printed until awaited - co_return 42; -} - -task<> example() -{ - auto t = compute(); // Task created, but "Computing..." NOT printed yet - std::cout << "Task created\n"; - - int result = co_await std::move(t); // NOW "Computing..." is printed - std::cout << "Result: " << result << "\n"; -} +include::example$snippets/4a_tasks.cpp[tag=lazy] ---- *Output:* @@ -138,9 +84,7 @@ Consider: [source,cpp] ---- -task<> a() { co_await b(); } -task<> b() { co_await c(); } -task<> c() { co_return; } +include::example$snippets/4a_tasks.cpp[tag=chain] ---- Without symmetric transfer, each `co_await` would add a stack frame, potentially causing stack overflow with deep nesting. With symmetric transfer, `c` returning to `b` returning to `a` uses constant stack space regardless of depth. @@ -149,11 +93,7 @@ This is implemented through the `await_suspend` returning a coroutine handle rat [source,cpp] ---- -// Inside task's final_suspend awaiter -std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept -{ - return continuation_; // Transfer directly to continuation -} +include::example$snippets/4a_tasks.cpp[tag=final_suspend,indent=0] ---- == Move Semantics @@ -162,17 +102,7 @@ Tasks are move-only. Copying a task would create aliasing problems where multipl [source,cpp] ---- -task compute(); - -task<> example() -{ - auto t1 = compute(); - auto t2 = std::move(t1); // OK: ownership transferred, t1 is now empty - - // auto t3 = t2; // Error: task is not copyable - - int result = co_await std::move(t2); -} +include::example$snippets/4a_tasks.cpp[tag=move_only] ---- After moving, the source task becomes empty and must not be awaited. @@ -183,24 +113,7 @@ Exceptions thrown inside a task are captured and rethrown when the task is await [source,cpp] ---- -task might_fail(bool should_fail) -{ - if (should_fail) - throw std::runtime_error("Operation failed"); - co_return 42; -} - -task<> example() -{ - try - { - int result = co_await might_fail(true); - } - catch (std::runtime_error const& e) - { - std::cout << "Caught: " << e.what() << "\n"; - } -} +include::example$snippets/4a_tasks.cpp[tag=exceptions] ---- The exception is stored in the promise when it occurs and rethrown in `await_resume` when the calling coroutine resumes. @@ -211,14 +124,14 @@ The `task` type is defined in: [source,cpp] ---- -#include +include::example$snippets/4a_tasks.cpp[tag=include_task] ---- Or included via the umbrella header: [source,cpp] ---- -#include +include::example$snippets/4a_tasks.cpp[tag=include_umbrella] ---- You have now learned how to declare, return values from, and await `task` coroutines. In the next section, you will learn how to launch tasks for execution using `run_async` and `run`. diff --git a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc b/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc index c58e4f579..53bfcf161 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc @@ -22,22 +22,7 @@ When a task is awaited, the awaiting coroutine provides context: an executor for [source,cpp] ---- -#include -using namespace boost::capy; - -task compute() -{ - co_return 42; -} - -int main() -{ - thread_pool pool; - run_async(pool.get_executor())(compute()); - // Task is now running on the thread pool - - pool.join(); // wait for outstanding work to complete -} +include::example$programs/4b_launching_run_async.cpp[tag=full] ---- === Two-Call Syntax @@ -54,12 +39,13 @@ This ordering matters because the task's coroutine frame is allocated during ste [WARNING] ==== -Do not store the result of `run_async(executor)` and call it later: +Do not store the result of `run_async(executor)` and call it later. The +wrapper's call operator is rvalue-qualified, so this fails to compile: -[source,cpp] +[source,cpp,role=pseudocode] ---- auto wrapper = run_async(pool.get_executor()); // Don't do this -wrapper(compute()); // TLS state no longer valid +wrapper(compute()); // Error: operator() requires an rvalue ---- Always use the two-call pattern in a single expression. @@ -71,21 +57,7 @@ Always use the two-call pattern in a single expression. [source,cpp] ---- -// Result handler only (an unhandled exception calls std::terminate) -run_async(ex, [](int result) { - std::cout << "Got: " << result << "\n"; -})(compute()); - -// Separate handlers for result and exception -run_async(ex, - [](int result) { std::cout << "Result: " << result << "\n"; }, - [](std::exception_ptr ep) { - try { std::rethrow_exception(ep); } - catch (std::exception const& e) { - std::cout << "Error: " << e.what() << "\n"; - } - } -)(compute()); +include::example$snippets/4b_launching.cpp[tag=handlers,indent=0] ---- When no result handler is provided, the result is discarded. An exception @@ -101,16 +73,7 @@ Inside a coroutine, use `run` to execute a child task on a different executor: [source,cpp] ---- -task compute_on_pool(thread_pool& pool) -{ - // This task runs on whatever executor we're already on - - // But this child task runs on the pool's executor: - int result = co_await run(pool.get_executor())(expensive_computation()); - - // After co_await, we're back on our original executor - co_return result; -} +include::example$snippets/4b_launching.cpp[tag=run_hop] ---- === Executor Affinity @@ -135,11 +98,7 @@ Since `run_async` is called from non-coroutine code, there is no caller token to [source,cpp] ---- -std::stop_source source; -run_async(ex, source.get_token())(cancellable_task()); - -// Later, to request cancellation: -source.request_stop(); +include::example$snippets/4b_launching.cpp[tag=inject_token,indent=0] ---- === Inheritance with run @@ -148,23 +107,14 @@ source.request_stop(); [source,cpp] ---- -task parent() -{ - // Child automatically receives our stop token - co_await run(pool.get_executor())(child_task()); -} +include::example$snippets/4b_launching.cpp[tag=inherit_token,indent=0] ---- To override with a different token, pass it explicitly: [source,cpp] ---- -task parent() -{ - std::stop_source local; - // Child gets local's token, not our caller's - co_await run(pool.get_executor(), local.get_token())(child_task()); -} +include::example$snippets/4b_launching.cpp[tag=override_token,indent=0] ---- == Handler Threading @@ -173,11 +123,7 @@ Handlers passed to `run_async` are invoked on whatever thread the executor sched [source,cpp] ---- -// If pool has 4 threads, the handler runs on one of those threads -run_async(pool.get_executor(), [](int result) { - // This runs on a pool thread, NOT the main thread - update_shared_state(result); -})(compute()); +include::example$snippets/4b_launching.cpp[tag=handler_thread,indent=0] ---- If you need results on a specific thread, use appropriate synchronization or dispatch mechanisms. diff --git a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc b/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc index ccbce2879..18ad79256 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4c.executors.adoc @@ -18,13 +18,7 @@ This rule is what keeps shared state safe by default. Consider a connection hand [source,cpp] ---- -task handle_client(connection& conn) -{ - auto req = co_await conn.read(); - auto resp = process(req); - co_await conn.write(resp); - conn.stats.requests++; -} +include::example$snippets/4c_executors.cpp[tag=handle_client] ---- Launch this on a strand, and every resumption—after `conn.read()` and after `conn.write()`—happens on that strand. The update to `conn.stats.requests` is therefore free of data races without any mutex. @@ -45,22 +39,7 @@ An *executor* is an object that can schedule work for execution. An executor mus [source,cpp] ---- -concept Executor = requires(E const& ce, E const& ce2, continuation& c) { - // Equality comparable - { ce == ce2 } noexcept -> std::convertible_to; - - // Owning context, returned as an lvalue reference to a type - // derived from execution_context - { ce.context() } noexcept; - - // Work tracking - { ce.on_work_started() } noexcept; - { ce.on_work_finished() } noexcept; - - // Scheduling - { ce.dispatch(c) } -> std::same_as>; - { ce.post(c) }; -}; +include::example$snippets/4c_executors.cpp[tag=executor_concept] ---- === dispatch() vs post() @@ -83,22 +62,11 @@ Returns a reference to the execution context that owns this executor. The contex [source,cpp] ---- -void schedule_work(executor_ref ex, continuation& c) -{ - ex.post(c); // Works with any executor -} - -int main() -{ - thread_pool pool; - auto pool_ex = pool.get_executor(); - executor_ref ex = pool_ex; // Type erasure; pool_ex must outlive ex - - continuation c = /* ... */; - schedule_work(ex, c); -} +include::example$programs/4c_executors_executor_ref.cpp[tag=full] ---- +Here `make_suspended_work` launches a coroutine on the pool and returns the `continuation` it parked; posting that continuation through the type-erased `ex` resumes it. + `executor_ref` stores a reference to the underlying executor—the original executor must outlive the `executor_ref`. == thread_pool: Multi-Threaded Execution @@ -107,31 +75,14 @@ int main() [source,cpp] ---- -#include - -int main() -{ - // Create pool with 4 threads - thread_pool pool(4); - - // Get an executor for this pool - auto ex = pool.get_executor(); - - // Launch work on the pool - run_async(ex)(my_task()); - - pool.join(); // wait for outstanding work to complete -} +include::example$programs/4c_executors_thread_pool.cpp[tag=full] ---- === Constructor Parameters [source,cpp] ---- -thread_pool( - std::size_t num_threads = 0, - std::string_view thread_name_prefix = "capy-pool-" -); +include::example$snippets/4c_executors.cpp[tag=thread_pool_ctor,indent=0] ---- * `num_threads` — Number of worker threads. If 0, uses hardware concurrency. @@ -152,13 +103,7 @@ Custom execution contexts inherit from `execution_context`: [source,cpp] ---- -class my_context : public execution_context -{ -public: - // ... custom implementation - - my_executor get_executor(); -}; +include::example$snippets/4c_executors.cpp[tag=my_context] ---- == strand: Serialization Without Mutexes @@ -167,33 +112,7 @@ A `strand` ensures that handlers are executed in order, with no two handlers exe [source,cpp] ---- -#include - -class shared_resource -{ - strand strand_; - int counter_ = 0; - -public: - explicit shared_resource(thread_pool& pool) - : strand_(pool.get_executor()) - { - } - - task increment() - { - // All increments are serialized through the strand - co_return co_await run(strand_)(do_increment()); - } - -private: - task do_increment() - { - // No mutex needed—strand ensures exclusive access - ++counter_; - co_return counter_; - } -}; +include::example$snippets/4c_executors.cpp[tag=shared_resource] ---- === How Strands Work @@ -220,9 +139,7 @@ For single-threaded applications, use a context with one thread: [source,cpp] ---- -thread_pool single_thread(1); -auto ex = single_thread.get_executor(); -// All work runs on the single thread +include::example$snippets/4c_executors.cpp[tag=single_thread,indent=0] ---- === Multi-Threaded with Shared Data @@ -231,11 +148,7 @@ For multi-threaded applications with shared data, use strands: [source,cpp] ---- -thread_pool pool(4); -strand data_strand(pool.get_executor()); - -// Use data_strand for all access to shared data -// Use pool.get_executor() for independent work +include::example$snippets/4c_executors.cpp[tag=data_strand,indent=0] ---- === Multi-Threaded with Independent Work @@ -244,13 +157,7 @@ For embarrassingly parallel work with no shared state: [source,cpp] ---- -thread_pool pool(4); -auto ex = pool.get_executor(); - -// Launch independent tasks directly on the pool -std::vector> tasks; -for (int i = 0; i < 100; ++i) - run_async(ex)(independent_task(i)); +include::example$snippets/4c_executors.cpp[tag=independent_tasks,indent=0] ---- == Reference diff --git a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc index 041c94afc..7a159378c 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc @@ -13,11 +13,7 @@ Standard {cpp}20 coroutines define awaiters with this `await_suspend` signature: [source,cpp] ---- -void await_suspend(std::coroutine_handle<> h); -// or -bool await_suspend(std::coroutine_handle<> h); -// or -std::coroutine_handle<> await_suspend(std::coroutine_handle<> h); +include::example$snippets/4d_io_awaitable.cpp[tag=std_await_suspend,indent=0] ---- The awaiter receives only a handle to the suspended coroutine. But real I/O code needs more: @@ -42,7 +38,7 @@ The IoAwaitable protocol extends `await_suspend` to receive context: [source,cpp] ---- -std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env); +include::example$snippets/4d_io_awaitable.cpp[tag=two_arg_await_suspend,indent=0] ---- This signature receives: @@ -61,11 +57,7 @@ An awaitable satisfies `IoAwaitable` if `a.await_suspend(h, env)` is a valid exp [source,cpp] ---- -template -concept IoAwaitable = - requires(A a, std::coroutine_handle<> h, io_env const* env) { - a.await_suspend(h, env); - }; +include::example$snippets/4d_io_awaitable.cpp[tag=io_awaitable_concept] ---- The concept constrains only the two-argument `await_suspend` that receives the `io_env`. It does not require `await_ready` or `await_resume`, nor does it constrain the return type of `await_suspend`. A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them. @@ -97,12 +89,7 @@ When you write `co_await child_task()` inside a `task`: [source,cpp] ---- -template -auto await_suspend(std::coroutine_handle h) -{ - // Forward caller's context to child - return awaitable_.await_suspend(h, promise_.environment()); -} +include::example$snippets/4d_io_awaitable.cpp[tag=context_flow,indent=0] ---- The child receives the parent's executor and stop token automatically. @@ -129,46 +116,7 @@ To create a custom IoAwaitable: [source,cpp] ---- -struct my_awaitable -{ - io_env const* env_ = nullptr; - continuation cont_; - result_type result_; - - bool await_ready() const noexcept - { - return false; // Or true if result is immediately available - } - - std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env) - { - // Store pointer to environment, never copy - env_ = env; - // Wrap the caller's handle in a continuation we own, so it stays - // at a stable address until the executor resumes it. - cont_.h = h; - - // Start async operation... - start_operation(); - - // Return noop to suspend - return std::noop_coroutine(); - } - - result_type await_resume() - { - return result_; - } - -private: - void on_completion() - { - // Resume the caller on its executor. post() takes the - // continuation by reference and queues it; never resume inline - // from a completion callback (it may run on the wrong thread). - env_->executor.post(cont_); - } -}; +include::example$snippets/4d_io_awaitable.cpp[tag=my_awaitable] ---- The key points: @@ -190,38 +138,14 @@ Post the resume through the executor instead of resuming inline: [source,cpp] ---- -struct stoppable_awaitable -{ - mutable continuation cont_; - - bool await_ready() { return false; } - - std::coroutine_handle<> await_suspend( - std::coroutine_handle<> h, io_env const* env) - { - if (env->stop_token.stop_requested()) - return h; // Already cancelled, resume immediately - - // Post through executor when stop is requested - cont_.h = h; - auto ex = env->executor; - stop_cb_.emplace(env->stop_token, - [this, ex]() mutable noexcept { ex.post(cont_); }); - - start_async_operation(); - return std::noop_coroutine(); - } - - void await_resume() { /* ... */ } -}; +include::example$snippets/4d_io_awaitable.cpp[tag=stoppable_awaitable] ---- The incorrect pattern—which compiles and appears to work but causes memory corruption—looks like this: [source,cpp] ---- -// WRONG: resumes coroutine on the calling thread -stop_cb_.emplace(env->stop_token, h); // h is a raw coroutine_handle +include::example$snippets/4d_io_awaitable.cpp[tag=wrong_stop_callback,indent=0] ---- See xref:4.coroutines/4e.cancellation.adoc#stoppable-awaitables[Implementing Stoppable Awaitables] for a complete example. @@ -237,8 +161,7 @@ Capy rejects it at compile time: [source,cpp] ---- -// In task.hpp, when the awaited type is not an IoAwaitable: -static_assert(sizeof(A) == 0, "requires IoAwaitable"); +include::example$snippets/4d_io_awaitable.cpp[tag=reject_plain,indent=0] ---- This is intentional. A plain awaitable receives only the coroutine handle; it can resume the coroutine on any thread by calling `handle.resume()` directly. That silently breaks the xref:4.coroutines/4c.executors.adoc#the-same-executor-invariant[same-executor invariant]—the coroutine could wake on a foreign completion thread, leaving shared state you believed was strand-protected exposed to races. Rejecting such an awaitable at compile time prevents that. The constraint does not lock you in; it requires environment propagation to be explicit rather than silently dropped. @@ -247,35 +170,7 @@ The escape hatch is to wrap the foreign awaitable (or callback, or future) in a [source,cpp] ---- -// Bridge a foreign async operation into a Capy coroutine. -struct foreign_bridge -{ - io_env const* env_ = nullptr; - continuation cont_; - result_type result_; - - bool await_ready() const noexcept { return false; } - - std::coroutine_handle<> await_suspend( - std::coroutine_handle<> h, io_env const* env) - { - env_ = env; - cont_.h = h; // stable address; the executor links continuations intrusively - - // Start the foreign operation. Its completion callback may run on - // ANY thread, so it must not resume h directly. Instead it posts - // the continuation back through the caller's executor, restoring - // the same-executor invariant. - start_foreign_op([this]() noexcept { - store_result(); - env_->executor.post(cont_); - }); - - return std::noop_coroutine(); - } - - result_type await_resume() { return std::move(result_); } -}; +include::example$snippets/4d_io_awaitable.cpp[tag=foreign_bridge] ---- The single rule that makes any bridge correct: *on completion, post through `env->executor` instead of resuming the handle inline.* diff --git a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc index 0b1d589af..0799c8c07 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4e.cancellation.adoc @@ -22,15 +22,7 @@ The obvious solution seems to be a boolean flag: [source,cpp] ---- -std::atomic should_cancel{false}; - -void worker() -{ - while (!should_cancel) - { - do_work(); - } -} +include::example$snippets/4e_cancellation.cpp[tag=naive_flag,indent=0] ---- This approach has problems: @@ -67,25 +59,7 @@ The *Observer Registration*. An RAII object that registers a callback to run whe [source,cpp] ---- -#include -#include - -void example() -{ - std::stop_source source; - - // Create tokens (distribute notification capability) - std::stop_token token1 = source.get_token(); - std::stop_token token2 = source.get_token(); // Same underlying state - - // Register callbacks (observers) - std::stop_callback cb1(token1, []{ std::cout << "Observer 1 notified\n"; }); - std::stop_callback cb2(token2, []{ std::cout << "Observer 2 notified\n"; }); - - std::cout << "Before signal\n"; - source.request_stop(); // Triggers all callbacks - std::cout << "After signal\n"; -} +include::example$snippets/4e_cancellation.cpp[tag=observer_pattern,indent=0] ---- *Output:* @@ -103,13 +77,7 @@ If a callback is registered after `request_stop()` was already called, the callb [source,cpp] ---- -std::stop_source source; -source.request_stop(); // Already signaled - -// Callback runs in constructor, not later -std::stop_callback cb(source.get_token(), []{ - std::cout << "Runs immediately!\n"; -}); +include::example$snippets/4e_cancellation.cpp[tag=immediate_invocation,indent=0] ---- This ensures observers never miss the signal, regardless of registration timing. @@ -148,19 +116,7 @@ To "reset," create an entirely new `stop_source`: [source,cpp] ---- -std::stop_source source; -auto token = source.get_token(); - -// ... distribute token to workers ... - -source.request_stop(); // Triggered, now permanently signaled - -// To "reset": create new source -source = std::stop_source{}; // New shared state -// Old tokens are now orphaned (stop_possible() returns false) - -// Must redistribute new tokens to ALL holders of the old token -auto new_token = source.get_token(); +include::example$snippets/4e_cancellation.cpp[tag=reset_workaround,indent=0] ---- This is manual and error-prone. Any code still holding the old token will not receive new signals. @@ -192,17 +148,7 @@ Capy propagates stop tokens downward through `co_await`. When you await a task, [source,cpp] ---- -task<> parent() -{ - // Our stop token is automatically passed to child - co_await child(); -} - -task<> child() -{ - // Receives parent's stop token via IoAwaitable protocol - auto token = co_await this_coro::stop_token; // Access current token -} +include::example$snippets/4e_cancellation.cpp[tag=token_propagation,indent=0] ---- No manual threading—the protocol handles it. @@ -213,15 +159,7 @@ Inside a task, use `co_await this_coro::stop_token` to access the current stop t [source,cpp] ---- -task<> cancellable_work() -{ - auto token = co_await this_coro::stop_token; - - while (!token.stop_requested()) - { - co_await do_chunk_of_work(); - } -} +include::example$snippets/4e_cancellation.cpp[tag=access_stop_token,indent=0] ---- === Why Not `coroutine_handle::destroy()`? @@ -241,18 +179,7 @@ The rule: [source,cpp] ---- -task<> process_items(std::vector const& items) -{ - auto token = co_await this_coro::stop_token; - - for (auto const& item : items) - { - if (token.stop_requested()) - co_return; // Exit early - - co_await process(item); - } -} +include::example$snippets/4e_cancellation.cpp[tag=check_token,indent=0] ---- === Cleanup with RAII @@ -261,17 +188,7 @@ RAII ensures resources are released on early exit: [source,cpp] ---- -task<> with_resource() -{ - auto resource = acquire_resource(); // RAII wrapper - auto token = co_await this_coro::stop_token; - - while (!token.stop_requested()) - { - co_await use_resource(resource); - } - // resource destructor runs regardless of how we exit -} +include::example$snippets/4e_cancellation.cpp[tag=raii_cleanup,indent=0] ---- === The canceled Convention @@ -280,18 +197,7 @@ When cancellation causes an operation to fail, the conventional error code is `e [source,cpp] ---- -task fetch_with_cancel() -{ - auto token = co_await this_coro::stop_token; - - if (token.stop_requested()) - { - throw std::system_error( - make_error_code(error::canceled)); - } - - co_return co_await do_fetch(); -} +include::example$snippets/4e_cancellation.cpp[tag=canceled_convention,indent=0] ---- == Part 7: OS Integration @@ -311,15 +217,7 @@ A `std::stop_callback` fires synchronously on whatever thread calls `request_sto [source,cpp] ---- -// WRONG — causes use-after-free -std::optional>> stop_cb; - -std::coroutine_handle<> await_suspend( - std::coroutine_handle<> h, io_env const* env) -{ - stop_cb.emplace(env->stop_token, h); // Resumes inline! - return std::noop_coroutine(); -} +include::example$snippets/4e_cancellation.cpp[tag=inline_resume_wrong,indent=0] ---- When an external thread calls `request_stop()`, `h.resume()` executes the coroutine on that thread. The coroutine machinery sets the thread-local frame allocator to the executor's allocator—poisoning the calling thread's TLS. When the executor's pool destructs, the TLS pointer becomes dangling. The next coroutine allocation on that thread dereferences freed memory. @@ -332,30 +230,7 @@ Post the coroutine handle through the executor instead of resuming it inline. Th [source,cpp] ---- -struct my_stoppable_awaitable -{ - mutable continuation cont_; - // ... other members for the async operation ... - - bool await_ready() { return false; } - - std::coroutine_handle<> await_suspend( - std::coroutine_handle<> h, io_env const* env) - { - if (env->stop_token.stop_requested()) - return h; // Already cancelled - - cont_.h = h; - auto ex = env->executor; - stop_cb_.emplace(env->stop_token, - [this, ex]() mutable noexcept { ex.post(cont_); }); - - start_async_operation(h, env); - return std::noop_coroutine(); - } - - void await_resume() { /* check result or throw */ } -}; +include::example$snippets/4e_cancellation.cpp[tag=stoppable_awaitable,indent=0] ---- When `request_stop()` fires the callback, the coroutine handle is posted to the executor's queue instead of resumed inline. The executor's worker thread picks it up and resumes it in the correct execution context. @@ -372,34 +247,7 @@ of that race; a user thread supplies the clock: [source,cpp] ---- -struct fetch_channel -{ - capy::async_waker fetch_ready; - std::atomic cancelled{false}; - std::string result; -}; - -// One side of the race: completes when the fetch worker thread -// wakes fetch_ready. If the deadline wins first, when_any's stop -// request cancels the wait; flag the worker so it stops early. -capy::io_task await_fetch(fetch_channel& ch) -{ - auto [ec] = co_await ch.fetch_ready.wait(); - if (ec) - { - ch.cancelled.store(true); - co_return capy::io_result{ec, {}}; - } - co_return capy::io_result{{}, std::move(ch.result)}; -} - -// The other side of the race: completes once whatever plays the -// clock wakes the waker. -capy::io_task<> deadline(capy::async_waker& waker) -{ - auto [ec] = co_await waker.wait(); - co_return capy::io_result<>{ec}; -} +include::example$snippets/4e_cancellation.cpp[tag=racing_deadline,indent=0] ---- `when_any(await_fetch(ch), deadline(waker))` returns as soon as either side @@ -419,32 +267,7 @@ Connect UI cancellation to stop tokens. Pass the token through `run_async` so it [source,cpp] ---- -class download_manager -{ - executor_ref executor_; - std::stop_source stop_source_; - -public: - void start_download(std::string url) - { - // Token propagated via io_env, not as a function argument - run_async(executor_, stop_source_.get_token())(download(url)); - } - - void cancel() - { - stop_source_.request_stop(); - } -}; - -task download(std::string url) -{ - auto token = co_await this_coro::stop_token; // From run_async's io_env - while (!token.stop_requested()) - { - co_await fetch_next_chunk(url); - } -} +include::example$snippets/4e_cancellation.cpp[tag=user_cancellation,indent=0] ---- === Graceful Shutdown @@ -453,30 +276,7 @@ Cancel all pending work during shutdown: [source,cpp] ---- -class server -{ - std::stop_source shutdown_source_; - -public: - void shutdown() - { - shutdown_source_.request_stop(); - // All pending operations receive stop request - } - - task<> handle_connection(connection conn) - { - auto token = shutdown_source_.get_token(); - - while (!token.stop_requested()) - { - co_await process_request(conn); - } - - // Graceful cleanup - co_await send_goodbye(conn); - } -}; +include::example$snippets/4e_cancellation.cpp[tag=graceful_shutdown,indent=0] ---- === when_any Cancellation @@ -489,7 +289,7 @@ The stop token mechanism is part of the {cpp} standard library: [source,cpp] ---- -#include +include::example$snippets/4e_cancellation.cpp[tag=include_stop_token,indent=0] ---- Key types: diff --git a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc index f0fe6c842..b104b4078 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc @@ -13,23 +13,14 @@ Sequential execution—one task after another—is the default when using `co_aw [source,cpp] ---- -task<> sequential() -{ - co_await task_a(); // Wait for A - co_await task_b(); // Then wait for B - co_await task_c(); // Then wait for C -} +include::example$snippets/4f_composition.cpp[tag=sequential,indent=0] ---- For independent operations, concurrent execution is more efficient: [source,cpp] ---- -task<> concurrent() -{ - // Run A, B, C simultaneously - co_await when_all(task_a(), task_b(), task_c()); -} +include::example$snippets/4f_composition.cpp[tag=concurrent,indent=0] ---- == when_all: Wait for All Tasks @@ -38,21 +29,7 @@ task<> concurrent() [source,cpp] ---- -#include - -io_task fetch_a() { co_return io_result{{}, 1}; } -io_task fetch_b() { co_return io_result{{}, 2}; } -io_task fetch_c() { co_return io_result{{}, "hello"}; } - -task<> example() -{ - auto [ec, a, b, c] = co_await when_all(fetch_a(), fetch_b(), fetch_c()); - - // ec == std::error_code{} (success) - // a == 1 - // b == 2 - // c == "hello" -} +include::example$snippets/4f_composition.cpp[tag=when_all_basic,indent=0] ---- === Result Type @@ -65,28 +42,14 @@ task<> example() [source,cpp] ---- -io_task<> void_task() { co_return io_result<>{}; } -io_task int_task() { co_return io_result{{}, 42}; } - -task<> example() -{ - auto [ec, a, b, c] = co_await when_all(int_task(), void_task(), int_task()); - // a == 42 (int) - // b == tuple<> (from void io_task) - // c == 42 (int) -} +include::example$snippets/4f_composition.cpp[tag=when_all_void_mix,indent=0] ---- When all children are `io_task<>`, just check `r.ec`: [source,cpp] ---- -task<> example() -{ - auto r = co_await when_all(void_task_a(), void_task_b()); - if (r.ec) - // handle error -} +include::example$snippets/4f_composition.cpp[tag=when_all_all_void,indent=0] ---- === Error Handling @@ -99,36 +62,14 @@ I/O errors are reported through the `ec` field of the `io_result`. When any chil [source,cpp] ---- -task<> example() -{ - auto [ec, a, b] = co_await when_all(task_a(), task_b()); - if (ec) - std::cerr << "Error: " << ec.message() << "\n"; -} +include::example$snippets/4f_composition.cpp[tag=when_all_error,indent=0] ---- If a task throws an exception, it is captured and rethrown after all tasks complete. Exceptions take priority over `ec`. [source,cpp] ---- -io_task might_throw(bool fail) -{ - if (fail) - throw std::runtime_error("failed"); - co_return io_result{{}, 42}; -} - -task<> example() -{ - try - { - co_await when_all(might_throw(true), might_throw(false)); - } - catch (std::runtime_error const& e) - { - // Catches the exception from the failing task - } -} +include::example$snippets/4f_composition.cpp[tag=when_all_exception,indent=0] ---- === Stop Propagation @@ -137,19 +78,7 @@ When one task fails, `when_all` requests stop for its siblings. Well-behaved tas [source,cpp] ---- -io_task<> long_running() -{ - auto token = co_await this_coro::stop_token; - - for (int i = 0; i < 1000; ++i) - { - if (token.stop_requested()) - co_return io_result<>{}; // Exit early when sibling fails - - co_await do_iteration(); - } - co_return io_result<>{}; -} +include::example$snippets/4f_composition.cpp[tag=stop_propagation,indent=0] ---- == when_any: First-to-Succeed Wins @@ -158,19 +87,7 @@ io_task<> long_running() [source,cpp] ---- -#include - -task<> example() -{ - auto result = co_await when_any( - fetch_int(), // io_task - fetch_string() // io_task - ); - // result is std::variant - // index 0: all tasks failed (error_code) - // index 1: fetch_int won - // index 2: fetch_string won -} +include::example$snippets/4f_composition.cpp[tag=when_any_basic,indent=0] ---- The result is a `variant` with `error_code` at index 0 (failure/no winner) and one alternative per input task at indices 1..N. Only tasks returning `!ec` can win; errors and exceptions do not count as winning. When a winner is found, stop is requested for all siblings. All tasks complete before `when_any` returns. @@ -191,30 +108,14 @@ The first pattern translates a specific, benign error into success. Other errors [source,cpp] ---- -// canceled is benign here: translate it to success so when_any picks this child. -io_task<> wrapped() -{ - auto [ec] = co_await inner(); - if (ec == cond::canceled) - co_return io_result<>{}; // success: when_any sees a winner - co_return io_result<>{ec}; // propagate other errors unchanged -} +include::example$snippets/4f_composition.cpp[tag=wrap_translate_error,indent=0] ---- The second pattern lifts the inner `ec` into the payload. The wrapper always succeeds, so it wins on its first completion, carrying the original error code to the caller: [source,cpp] ---- -// Always succeeds; the winner's payload carries the original ec. -io_task wrapped() -{ - auto [ec] = co_await inner(); - co_return io_result{{}, ec}; -} - -// when_any(wrapped(), ...) -> variant -// index 0: every child failed -// index i: child i won; std::get(result) is its original ec +include::example$snippets/4f_composition.cpp[tag=wrap_lift_error,indent=0] ---- == Practical Patterns @@ -225,22 +126,7 @@ Fetch multiple resources simultaneously: [source,cpp] ---- -io_task fetch_page_data(std::string url) -{ - auto [ec, header, body, sidebar] = co_await when_all( - fetch_header(url), - fetch_body(url), - fetch_sidebar(url) - ); - if (ec) - co_return io_result{ec, {}}; - - co_return io_result{{}, { - std::move(header), - std::move(body), - std::move(sidebar) - }}; -} +include::example$snippets/4f_composition.cpp[tag=parallel_fetch,indent=0] ---- === Fan-Out/Fan-In @@ -249,23 +135,7 @@ Process items in parallel, then combine results using the range overload: [source,cpp] ---- -io_task process_item(item const& i); - -task process_all(std::vector const& items) -{ - std::vector> tasks; - for (auto const& item : items) - tasks.push_back(process_item(item)); - - auto [ec, results] = co_await when_all(std::move(tasks)); - if (ec) - co_return 0; - - int total = 0; - for (auto v : results) - total += v; - co_return total; -} +include::example$snippets/4f_composition.cpp[tag=fan_out,indent=0] ---- === Racing a Deadline @@ -274,32 +144,11 @@ task process_all(std::vector const& items) [source,cpp] ---- -capy::io_task await_fetch(fetch_channel& ch) -{ - auto [ec] = co_await ch.fetch_ready.wait(); - if (ec) - { - ch.cancelled.store(true); - co_return capy::io_result{ec, {}}; - } - co_return capy::io_result{{}, std::move(ch.result)}; -} - -capy::io_task<> deadline(capy::async_waker& waker) -{ - auto [ec] = co_await waker.wait(); - co_return capy::io_result<>{ec}; -} - -auto race = [&]() -> capy::task<> -{ - auto result = co_await capy::when_any( - await_fetch(fetch_ch), - deadline(deadline_waker)); - winner = result.index(); - if (result.index() == 1) - fetch_result = std::get<1>(std::move(result)); -}; +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race_await_fetch] + +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race_deadline] + +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race_lambda,indent=0] ---- `deadline_waker.wake()` can come from any thread, a `std::thread` that sleeps for the deadline, hardware, or a completion callback from another library. See xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] for the full example, including the `fetch_channel` type and both `std::thread` objects that play fetch worker and clock. diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc index 80bf814ac..95c041e9c 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc @@ -25,10 +25,7 @@ This is why `run_async` uses two-call syntax: [source,cpp] ---- -run_async(executor)(my_task()); -// ↑ ↑ -// 1. Sets 2. Task allocated -// TLS using TLS allocator +include::example$snippets/4g_allocators.cpp[tag=two_call,indent=0] ---- == The Window @@ -49,8 +46,7 @@ To prevent this, any code that calls `.resume()` on a coroutine handle must use [source,cpp] ---- -// In your event loop or dispatch path: -capy::safe_resume(h); // saves and restores TLS around h.resume() +include::example$snippets/4g_allocators.cpp[tag=safe_resume,indent=0] ---- `safe_resume` saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (`thread_pool`, strands, `blocking_context`) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:8.examples/8n.custom-executor.adoc[Custom Executor] for an example. @@ -59,7 +55,7 @@ capy::safe_resume(h); // saves and restores TLS around h.resume() Custom allocators must meet the usual {cpp} allocator requirements, or be a `std::pmr::memory_resource*`. The library does not expose a separate public concept for them; a value-type allocator works as a frame allocator when it provides, illustratively: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Illustrative requirements — not a named public concept: typename A::value_type; @@ -77,18 +73,14 @@ Pass an allocator to `run_async`: [source,cpp] ---- -std::pmr::monotonic_buffer_resource resource; -std::pmr::polymorphic_allocator alloc(&resource); - -run_async(executor, alloc)(my_task()); +include::example$snippets/4g_allocators.cpp[tag=run_async_pmr_alloc,indent=0] ---- Or pass a `memory_resource*` directly: [source,cpp] ---- -std::pmr::monotonic_buffer_resource resource; -run_async(executor, &resource)(my_task()); +include::example$snippets/4g_allocators.cpp[tag=run_async_memory_resource,indent=0] ---- === Default Allocator @@ -113,13 +105,7 @@ Most users never need to allocate coroutine frames manually -- `task` and the [source,cpp] ---- -struct my_coroutine -{ - struct promise_type : capy::frame_alloc_mixin - { - // get_return_object, initial_suspend, ... - }; -}; +include::example$snippets/4g_allocators.cpp[tag=frame_alloc_mixin] ---- `frame_alloc_mixin` (in ``) supplies `operator new` and `operator delete` that: @@ -142,12 +128,7 @@ Capy's `task` uses the `[[clang::coro_await_elidable]]` attribute (when avail [source,cpp] ---- -template -struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE - task -{ - // ... -}; +include::example$snippets/4g_allocators.cpp[tag=task_elidable] ---- === When HALO Applies @@ -156,12 +137,7 @@ HALO is most effective for immediately-awaited tasks: [source,cpp] ---- -// HALO can apply: task is awaited immediately -int result = co_await compute(); - -// HALO cannot apply: task escapes to storage -auto t = compute(); -tasks.push_back(std::move(t)); +include::example$snippets/4g_allocators.cpp[tag=halo_patterns,indent=0] ---- === Measuring HALO Effectiveness @@ -184,18 +160,7 @@ When launching many short-lived tasks together, a monotonic buffer resource can [source,cpp] ---- -void process_batch(std::vector const& items) -{ - std::array buffer; - std::pmr::monotonic_buffer_resource resource( - buffer.data(), buffer.size()); - - for (auto const& item : items) - { - run_async(executor, &resource)(process(item)); - } - // All frames deallocated when resource goes out of scope -} +include::example$snippets/4g_allocators.cpp[tag=batch_allocator] ---- === Scope Variables to Reduce Frame Size @@ -206,59 +171,18 @@ Wrapping buffer usage in explicit braces can dramatically reduce frame size: [source,cpp] ---- -// BAD: buf lives in frame across all subsequent co_awaits -task<> process(stream& s) -{ - char buf[4096]; - auto [ec, n] = co_await s.read_some(buf); - co_await do_work(buf, n); - co_await s.write_some(reply); // buf wastes 4K in frame -} - -// GOOD: braces end buf's lifetime before next suspend -task<> process(stream& s) -{ - std::size_t n; - { - char buf[4096]; - auto [ec, n_] = co_await s.read_some(buf); - n = n_; - co_await do_work(buf, n); - } - co_await s.write_some(reply); // 4K saved -} +include::example$snippets/4g_allocators.cpp[tag=frame_scope_bad] + +include::example$snippets/4g_allocators.cpp[tag=frame_scope_good] ---- This technique also enables the compiler to *overlap* variables in the frame. When two variables have completely non-overlapping lifetimes (in separate scoped blocks), the compiler can reuse the same frame memory for both — even on Clang: [source,cpp] ---- -// BAD: both arrays in frame simultaneously (8K) -task<> pipeline(stream& in, stream& out) -{ - char read_buf[4096]; - auto [ec1, n] = co_await in.read_some(read_buf); - - char write_buf[4096]; - prepare(write_buf, read_buf, n); - co_await out.write_some(write_buf); -} - -// GOOD: non-overlapping scopes allow frame reuse (4K) -task<> pipeline(stream& in, stream& out) -{ - std::size_t n; - { - char read_buf[4096]; - auto [ec, n_] = co_await in.read_some(read_buf); - n = n_; - } - { - char write_buf[4096]; - prepare(write_buf, n); - co_await out.write_some(write_buf); - } -} +include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_bad] + +include::example$snippets/4g_allocators.cpp[tag=pipeline_overlap_good] ---- In the second version, `read_buf` and `write_buf` never coexist, so the compiler can place them at the same frame offset — halving the frame's buffer footprint. This optimization applies to any variables with non-overlapping lifetimes, not just arrays. diff --git a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc b/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc index 754d62bcf..70847320b 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4h.lambda-captures.adoc @@ -13,18 +13,7 @@ Consider this innocent-looking code: [source,cpp] ---- -namespace capy = boost::capy; - -void process(socket& sock) -{ - auto task = [&sock]() -> capy::task<> - { - char buf[1024]; - auto [ec, n] = co_await sock.read_some(make_buffer(buf)); - }(); - - run_async(executor)(std::move(task)); -} +include::example$snippets/4h_lambda_captures.cpp[tag=dangling_capture] ---- **This code has undefined behavior.** It may crash, corrupt memory, or appear to work until it doesn't. @@ -51,18 +40,7 @@ The solution is to pass values as **function parameters** instead of **lambda ca [source,cpp] ---- -namespace capy = boost::capy; - -void process(socket& sock) -{ - auto task = [](socket* s) -> capy::task<> - { - char buf[1024]; - auto [ec, n] = co_await s->read_some(make_buffer(buf)); - }(&sock); - - run_async(executor)(std::move(task)); -} +include::example$snippets/4h_lambda_captures.cpp[tag=iife_parameter] ---- This is an **Immediately Invoked Function Expression (IIFE)**. The parameter `s` is copied to the coroutine frame before the first suspension, so it remains valid for the coroutine's lifetime. @@ -73,44 +51,14 @@ This is an **Immediately Invoked Function Expression (IIFE)**. The parameter `s` [source,cpp] ---- -class connection_handler -{ - socket sock_; - std::string name_; - -public: - capy::task<> run() - { - // BROKEN: 'this' captured in lambda, lambda destroyed after invoke - return [this]() -> capy::task<> - { - log("Connection from", name_); // UB: 'this' is dangling - co_await handle_request(); - }(); - } -}; +include::example$snippets/4h_lambda_captures.cpp[tag=capture_this_broken] ---- === Correct: Using Parameters [source,cpp] ---- -class connection_handler -{ - socket sock_; - std::string name_; - -public: - capy::task<> run() - { - // CORRECT: 'self' is a parameter, copied to coroutine frame - return [](connection_handler* self) -> capy::task<> - { - log("Connection from", self->name_); - co_await self->handle_request(); - }(this); - } -}; +include::example$snippets/4h_lambda_captures.cpp[tag=parameter_self_correct] ---- == When Are Captures Safe? @@ -119,15 +67,7 @@ Captures are only safe when the lambda object **outlives the coroutine**: [source,cpp] ---- -// SAFE: lambda stored in 'handler', outlives coroutine -auto handler = [&sock]() -> capy::task<> -{ - co_await sock.read_some(...); -}; - -// Lambda 'handler' still exists here -capy::test::run_blocking()(handler()); // Blocks until coroutine completes -// Lambda destroyed after coroutine finishes +include::example$snippets/4h_lambda_captures.cpp[tag=stored_lambda_safe,indent=0] ---- This pattern is rare. Most async code immediately invokes the lambda and discards it, making captures unsafe. @@ -146,22 +86,7 @@ If the IIFE syntax feels awkward, use a named function instead: [source,cpp] ---- -class connection_handler -{ - socket sock_; - - capy::task<> do_handle() - { - // 'this' is an implicit parameter, handled correctly - co_await sock_.read_some(...); - } - -public: - capy::task<> run() - { - return do_handle(); - } -}; +include::example$snippets/4h_lambda_captures.cpp[tag=named_member_coroutine] ---- Member function coroutines work correctly because `this` is an implicit parameter, not a capture. The compiler copies it to the coroutine frame. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc b/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc index 9ce2cf267..e55518528 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc @@ -19,8 +19,7 @@ The instinctive {cpp} answer to "how should I represent a buffer?" is `std::span [source,cpp] ---- -void write_data(std::span data); -void read_data(std::span buffer); +include::example$snippets/5a_overview.cpp[tag=span_signatures,indent=0] ---- This works for single contiguous buffers. But I/O often involves multiple buffers—a technique called *scatter/gather I/O*. @@ -44,7 +43,7 @@ Extending the span reflex: `std::span>`: [source,cpp] ---- -void write_data(std::span const> buffers); +include::example$snippets/5a_overview.cpp[tag=span_of_spans,indent=0] ---- This works, but introduces a composition problem. @@ -55,23 +54,14 @@ Suppose you have: [source,cpp] ---- -using HeaderBuffers = std::array, 2>; // 2 buffers -using BodyBuffers = std::array, 3>; // 3 buffers +include::example$snippets/5a_overview.cpp[tag=span_aliases,indent=0] ---- To send headers followed by body, you need 5 buffers total. With `span>`: [source,cpp] ---- -HeaderBuffers headers = /* ... */; -BodyBuffers body = /* ... */; - -// To combine, you MUST allocate a new array: -std::array, 5> combined; -std::copy(headers.begin(), headers.end(), combined.begin()); -std::copy(body.begin(), body.end(), combined.begin() + 2); - -write_data(combined); +include::example$snippets/5a_overview.cpp[tag=span_combine,indent=0] ---- Every composition allocates. This leads to: @@ -86,8 +76,7 @@ Instead of concrete types, use concepts. Define `ConstBufferSequence` as "any ty [source,cpp] ---- -template -void write_data(Buffers const& buffers); +include::example$snippets/5a_overview.cpp[tag=concept_signature,indent=0] ---- This single signature accepts: diff --git a/doc/modules/ROOT/pages/5.buffers/5b.types.adoc b/doc/modules/ROOT/pages/5.buffers/5b.types.adoc index 38799868b..ded6be77a 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.types.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.types.adoc @@ -37,41 +37,21 @@ Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer [source,cpp] ---- -class const_buffer -{ -public: - const_buffer() = default; - const_buffer(void const* data, std::size_t size) noexcept; - const_buffer(mutable_buffer const& b) noexcept; // Implicit conversion - - void const* data() const noexcept; - std::size_t size() const noexcept; - - const_buffer& operator+=(std::size_t n) noexcept; // Remove prefix -}; +include::example$snippets/5b_types.cpp[tag=const_buffer_interface,indent=0] ---- === Construction [source,cpp] ---- -// From pointer and size -char data[] = "hello"; -const_buffer buf(data, 5); - -// From mutable_buffer (implicit) -mutable_buffer mbuf(data, 5); -const_buffer cbuf = mbuf; // OK: mutable -> const +include::example$snippets/5b_types.cpp[tag=const_buffer_construct,indent=0] ---- === Accessors [source,cpp] ---- -const_buffer buf(data, 5); - -void const* ptr = buf.data(); // Pointer to first byte -std::size_t len = buf.size(); // Number of bytes +include::example$snippets/5b_types.cpp[tag=const_buffer_accessors,indent=0] ---- === Prefix Removal @@ -80,11 +60,7 @@ The `+=` operator removes bytes from the front of the buffer: [source,cpp] ---- -const_buffer buf(data, 10); - -buf += 3; // Remove first 3 bytes -// buf.data() now points 3 bytes later -// buf.size() is now 7 +include::example$snippets/5b_types.cpp[tag=const_buffer_prefix,indent=0] ---- This is useful when processing a buffer incrementally. @@ -95,17 +71,7 @@ This is useful when processing a buffer incrementally. [source,cpp] ---- -class mutable_buffer -{ -public: - mutable_buffer() = default; - mutable_buffer(void* data, std::size_t size) noexcept; - - void* data() const noexcept; - std::size_t size() const noexcept; - - mutable_buffer& operator+=(std::size_t n) noexcept; -}; +include::example$snippets/5b_types.cpp[tag=mutable_buffer_interface,indent=0] ---- The interface mirrors `const_buffer`, but `data()` returns non-const `void*`. @@ -116,10 +82,7 @@ The interface mirrors `const_buffer`, but `data()` returns non-const `void*`. [source,cpp] ---- -void process(const_buffer buf); - -mutable_buffer mbuf(data, size); -process(mbuf); // OK: implicit conversion +include::example$snippets/5b_types.cpp[tag=mutable_to_const,indent=0] ---- The reverse is not allowed—you cannot implicitly convert `const_buffer` to `mutable_buffer`. @@ -130,34 +93,9 @@ The `make_buffer` function creates buffers from various sources: [source,cpp] ---- -#include - -// From pointer and size -auto buf = make_buffer(ptr, size); - -// From C array -char arr[10]; -auto buf = make_buffer(arr); +include::example$snippets/5b_types.cpp[tag=make_buffer_include] -// From std::array -std::array arr; -auto buf = make_buffer(arr); - -// From std::vector -std::vector vec(100); -auto buf = make_buffer(vec); - -// From std::string -std::string str = "hello"; -auto buf = make_buffer(str); - -// From std::string_view -std::string_view sv = "hello"; -auto buf = make_buffer(sv); - -// From a span (std::span or boost::span) -std::span sp(arr); -auto buf = make_buffer(sp); +include::example$snippets/5b_types.cpp[tag=make_buffer_sources,indent=0] ---- `make_buffer` accepts any sized, contiguous range of trivially-copyable @@ -186,26 +124,16 @@ A single buffer is a degenerate sequence—a sequence with one element. The `Con [source,cpp] ---- -template -void write_data(Buffers const& buffers); +include::example$snippets/5b_types.cpp[tag=write_data_signature,indent=0] -// All of these work: -write_data(make_buffer("hello")); // Single buffer -write_data(std::array{buf1, buf2, buf3}); // Multiple buffers -write_data(my_composite); // Custom sequence +include::example$snippets/5b_types.cpp[tag=write_data_calls,indent=0] ---- The library provides `begin()` and `end()` functions that work uniformly: [source,cpp] ---- -const_buffer single; -auto it = begin(single); // Returns pointer to single -auto e = end(single); // Returns pointer past single - -std::array multi; -auto it = begin(multi); // Returns multi.begin() -auto e = end(multi); // Returns multi.end() +include::example$snippets/5b_types.cpp[tag=begin_end_uniform,indent=0] ---- == Reference diff --git a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc index d0fa847bf..369122465 100644 --- a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc @@ -23,11 +23,7 @@ Treating a single buffer as a one-element sequence is a deliberate convenience, [source,cpp] ---- -template -concept ConstBufferSequence = - std::is_convertible_v || ( - std::ranges::bidirectional_range && - std::is_convertible_v, const_buffer>); +include::example$snippets/5c_sequences.cpp[tag=const_buffer_sequence_concept,indent=0] ---- A type satisfies `ConstBufferSequence` if: @@ -39,11 +35,7 @@ A type satisfies `ConstBufferSequence` if: [source,cpp] ---- -template -concept MutableBufferSequence = - std::is_convertible_v || ( - std::ranges::bidirectional_range && - std::is_convertible_v, mutable_buffer>); +include::example$snippets/5c_sequences.cpp[tag=mutable_buffer_sequence_concept,indent=0] ---- Same pattern, but for `mutable_buffer`. @@ -54,33 +46,22 @@ Many common types satisfy these concepts: [source,cpp] ---- -// Single buffers -const_buffer cb; // ConstBufferSequence -mutable_buffer mb; // MutableBufferSequence (and ConstBufferSequence) - -// Standard containers of buffers -std::vector v; // ConstBufferSequence -std::array a; // MutableBufferSequence - -// String types (convert to single buffer) -std::string str; // ConstBufferSequence (via make_buffer) -std::string_view sv; // ConstBufferSequence +include::example$snippets/5c_sequences.cpp[tag=concept_models,indent=0] ---- +Note that `std::string` and `std::string_view` are ranges of characters, +not of buffers, so they do not satisfy the concepts themselves; wrap them +with `make_buffer` to obtain a single-buffer sequence. + == Heterogeneous Composition Because the concept accepts anything convertible to buffer, you can mix types: [source,cpp] ---- -template -void send(Buffers const& bufs); - -// All of these work: -send(make_buffer("Hello")); // string literal -send(std::string_view{"Hello"}); // string_view -send(std::array{buf1, buf2}); // array of buffers -send(my_custom_buffer_sequence); // custom type +include::example$snippets/5c_sequences.cpp[tag=send_signature,indent=0] + +include::example$snippets/5c_sequences.cpp[tag=send_calls,indent=0] ---- == Iterating Buffer Sequences @@ -89,60 +70,46 @@ Use `begin()` and `end()` from ``: [source,cpp] ---- -template -void process(Buffers const& bufs) -{ - for (auto it = begin(bufs); it != end(bufs); ++it) - { - const_buffer buf = *it; - // Process buf.data(), buf.size() - } -} +include::example$snippets/5c_sequences.cpp[tag=iterate,indent=0] ---- These functions handle both single buffers (returning pointer-to-self) and ranges (returning standard iterators). == buffer_slice -When transferring data incrementally, `buffer_slice` returns a slice that tracks progress: +`buffer_slice` returns a byte sub-range of a buffer sequence, as a value: [source,cpp] ---- -#include - -template -task read_all(Stream& stream, Buffers buffers) -{ - auto remaining = buffer_slice(buffers); - std::size_t const total_size = buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.read_some(remaining.data()); - remaining.remove_prefix(n); - total += n; - if (ec) - break; - } - - co_return total; -} +include::example$snippets/5c_sequences.cpp[tag=buffer_slice_include] + +include::example$snippets/5c_sequences.cpp[tag=buffer_slice,indent=0] +---- + +`buffer_slice(seq, offset, length)` returns a value that is itself a buffer sequence: pass it directly to any operation expecting one. The `offset` and `length` parameters (both optional) make `buffer_slice` a general byte sub-range primitive. Except for the single-buffer case, the result borrows `seq`, so the sequence must outlive the slice. + +== consuming_buffers + +When transferring data incrementally, `consuming_buffers` is a cursor that tracks progress: + +[source,cpp] ---- +include::example$snippets/5c_sequences.cpp[tag=consuming_buffers_include] -`buffer_slice(seq, offset, length)` returns an object of unspecified type that satisfies the `Slice` concept, providing: +include::example$snippets/5c_sequences.cpp[tag=read_all,indent=0] +---- -* `data()` — Buffer sequence view of the slice's current bytes (pass to `read_some`/`write_some`) -* `remove_prefix(n)` — Advance the start by `n` bytes +A `consuming_buffers` cursor borrows the underlying sequence and provides: -The `offset` and `length` parameters (both optional) make `buffer_slice` a general byte sub-range primitive, not just an iteration-state holder. +* `data()` — Buffer sequence view of the remaining bytes (pass to `read_some`/`write_some`) +* `consume(n)` — Advance past `n` transferred bytes, in place == Why Bidirectional? The concepts require bidirectional ranges (not just forward ranges) for two reasons: 1. Some algorithms traverse buffers backwards -2. The buffer sequence view returned by `Slice::data()` needs to adjust the first and last buffers' bounds +2. The slice views produced by `buffer_slice` and `consuming_buffers::data()` need to adjust the first and last buffers' bounds If your custom buffer sequence only provides forward iteration, wrap it in a type that provides bidirectional access. @@ -158,8 +125,8 @@ If your custom buffer sequence only provides forward iteration, wrap it in a typ | `` | Byte sub-range slicing algorithm -| `` -| `Slice` concept +| `` +| Incremental consumption cursor |=== You have now learned how buffer sequences enable zero-allocation composition. Continue to xref:5.buffers/5d.system-io.adoc[System I/O Integration] to see how buffer sequences interface with operating system I/O. diff --git a/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc b/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc index 5a07768ad..8fc2e8a4c 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.system-io.adoc @@ -53,8 +53,7 @@ When you call an I/O function with a buffer sequence: [source,cpp] ---- -template -io_result write_some(Buffers const& buffers); +include::example$snippets/5d_system_io.cpp[tag=write_some_signature] ---- Internally, Capy: @@ -73,7 +72,7 @@ entries) is filled from the sequence and passed to the OS call. If the sequence has more buffers than fit in the window, the window is refilled and the OS call is repeated for the remaining buffers: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Pseudocode of internal implementation template @@ -106,16 +105,14 @@ Without scatter/gather: [source,cpp] ---- -write(fd, header, header_len); // syscall 1 -write(fd, body, body_len); // syscall 2 +include::example$snippets/5d_system_io.cpp[tag=two_syscalls,indent=0] ---- With scatter/gather: [source,cpp] ---- -iovec iov[2] = {{header, header_len}, {body, body_len}}; -writev(fd, iov, 2); // single syscall +include::example$snippets/5d_system_io.cpp[tag=gather_syscall,indent=0] ---- === Zero-Copy Transmission @@ -136,11 +133,7 @@ Buffers can be pre-registered with the kernel, eliminating per-operation address [source,cpp] ---- -// Registration (done once) -io_uring_register_buffers(ring, buffers, count); - -// Use (fast path - no translation) -io_uring_prep_write_fixed(sqe, fd, buf, len, offset, buf_index); +include::example$snippets/5d_system_io.cpp[tag=io_uring_fixed,indent=0] ---- === IOCP (Windows) @@ -157,13 +150,7 @@ Fewer buffers means less translation overhead: [source,cpp] ---- -// Prefer: single buffer when possible -auto buf = assemble_message(); // Build in one buffer -write(stream, buf); - -// Avoid: many tiny buffers -std::array tiny_bufs; -write(stream, tiny_bufs); // 100-element translation +include::example$snippets/5d_system_io.cpp[tag=minimize_buffer_count,indent=0] ---- === Reuse Buffer Structures @@ -172,15 +159,7 @@ For repeated I/O with the same structure, consider caching the platform buffer a [source,cpp] ---- -// Build once, use many times -struct message_buffers -{ - std::array iovecs; - - void set_header(void const* p, std::size_t n); - void set_body(void const* p, std::size_t n); - void set_footer(void const* p, std::size_t n); -}; +include::example$snippets/5d_system_io.cpp[tag=cached_iovecs] ---- === Profile Before Optimizing @@ -199,7 +178,7 @@ The buffer sequence concepts and translation utilities are in: [source,cpp] ---- -#include +include::example$snippets/5d_system_io.cpp[tag=include_buffers] ---- OS-specific I/O is handled by Corosio, which builds on Capy's buffer model. diff --git a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc index dc52b70f6..a8077ff01 100644 --- a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc @@ -15,19 +15,14 @@ Returns the total number of bytes across all buffers in a sequence: [source,cpp] ---- -template -std::size_t buffer_size(CB const& buffers); +include::example$snippets/5e_algorithms.cpp[tag=buffer_size_signature] ---- Example: [source,cpp] ---- -auto buf1 = make_buffer("hello"); // 5 bytes -auto buf2 = make_buffer("world"); // 5 bytes -auto combined = std::array{buf1, buf2}; - -std::size_t total = buffer_size(combined); // 10 +include::example$snippets/5e_algorithms.cpp[tag=buffer_size_example,indent=0] ---- Note: `buffer_size` returns the sum of bytes, not the count of buffers. @@ -38,8 +33,7 @@ Checks if a buffer sequence contains no data: [source,cpp] ---- -template -bool buffer_empty(CB const& buffers); +include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_signature] ---- A buffer sequence is empty if: @@ -49,11 +43,7 @@ A buffer sequence is empty if: [source,cpp] ---- -const_buffer empty_buf; -buffer_empty(empty_buf); // true - -const_buffer non_empty("data", 4); -buffer_empty(non_empty); // false +include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_example,indent=0] ---- === buffer_length @@ -62,19 +52,14 @@ Returns the number of buffers in a sequence: [source,cpp] ---- -template -std::size_t buffer_length(CB const& buffers); +include::example$snippets/5e_algorithms.cpp[tag=buffer_length_signature] ---- Example: [source,cpp] ---- -auto single = make_buffer("hello"); -buffer_length(single); // 1 - -auto arr = std::array{buf1, buf2, buf3}; -buffer_length(arr); // 3 +include::example$snippets/5e_algorithms.cpp[tag=buffer_length_example,indent=0] ---- Note the distinction: @@ -93,9 +78,7 @@ Copies data from one buffer sequence to another: [source,cpp] ---- -template -std::size_t buffer_copy(Target const& target, Source const& source, - std::size_t at_most = std::size_t(-1)); +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_signature] ---- Returns the number of bytes copied. @@ -104,13 +87,7 @@ Example: [source,cpp] ---- -char source_data[] = "hello world"; -char dest_data[20]; - -const_buffer src(source_data, 11); -mutable_buffer dst(dest_data, 20); - -std::size_t copied = buffer_copy(dst, src); // 11 +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_example,indent=0] ---- === Partial Copy with at_most @@ -119,7 +96,7 @@ Limit the number of bytes copied: [source,cpp] ---- -std::size_t copied = buffer_copy(dst, src, 5); // Copy at most 5 bytes +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_at_most,indent=0] ---- This is useful for implementing protocols with size limits. @@ -130,66 +107,29 @@ This is useful for implementing protocols with size limits. [source,cpp] ---- -// Source: 3 buffers -std::array src = {buf1, buf2, buf3}; - -// Target: 2 buffers with different sizes -std::array dst = {large_buf, small_buf}; - -// Copies across buffer boundaries as needed -std::size_t copied = buffer_copy(dst, src); +include::example$snippets/5e_algorithms.cpp[tag=buffer_copy_cross,indent=0] ---- The algorithm fills target buffers sequentially, reading from source buffers as needed, handling cases where a single source buffer spans multiple target buffers or vice versa. == Real I/O Patterns +Partial-transfer loops use a `consuming_buffers` cursor: `data()` +presents the not-yet-transferred remainder, and `consume(n)` advances +past the bytes the stream reported. + === Read Loop [source,cpp] ---- -template -task read_full(Stream& stream, Buffers buffers) -{ - auto remaining = buffer_slice(buffers); - std::size_t const total_size = buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.read_some(remaining.data()); - remaining.remove_prefix(n); - total += n; - if (ec) - co_return total; - } - - co_return total; -} +include::example$snippets/5e_algorithms.cpp[tag=read_loop] ---- === Write Loop [source,cpp] ---- -template -task write_full(Stream& stream, Buffers buffers) -{ - auto remaining = buffer_slice(buffers); - std::size_t const total_size = buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.write_some(remaining.data()); - remaining.remove_prefix(n); - total += n; - if (ec) - co_return total; - } - - co_return total; -} +include::example$snippets/5e_algorithms.cpp[tag=write_loop] ---- == Practical Benefits of Concept-Based Design @@ -200,12 +140,7 @@ Data never moves unnecessarily. The buffer sequence points to existing data, and [source,cpp] ---- -std::string header = build_header(); -std::vector body = load_body(); - -// No copying—header and body are written directly -std::array buffers = {make_buffer(header), make_buffer(body)}; -co_await write(stream, buffers); +include::example$snippets/5e_algorithms.cpp[tag=zero_copy,indent=0] ---- === Scatter/Gather Operations @@ -214,8 +149,7 @@ Multiple buffers transfer in a single operation: [source,cpp] ---- -std::array buffers = {header_buf, separator_buf, body_buf, footer_buf}; -co_await write(stream, buffers); // Single system call +include::example$snippets/5e_algorithms.cpp[tag=scatter_gather,indent=0] ---- === Custom Allocators and Memory-Mapped Buffers @@ -224,10 +158,7 @@ Any memory region can be a buffer: [source,cpp] ---- -// Memory-mapped file -void* mapped = mmap(...); -const_buffer file_buf(mapped, file_size); -co_await write(socket, file_buf); // Zero-copy network transmission +include::example$snippets/5e_algorithms.cpp[tag=mmap_buffer,indent=0] ---- === User-Defined Buffer Types @@ -236,17 +167,12 @@ Create custom types that satisfy the concepts: [source,cpp] ---- -class chunked_buffer_sequence -{ - std::vector> chunks_; - -public: - auto begin() const { /* return iterator over chunks as buffers */ } - auto end() const { /* return end iterator */ } -}; -// Satisfies ConstBufferSequence—works with all algorithms +include::example$snippets/5e_algorithms.cpp[tag=custom_sequence] ---- +Here `chunk_iterator` is a small bidirectional iterator whose +`operator*` returns each chunk as a `const_buffer`. + == Reference [cols="1,3"] @@ -258,6 +184,9 @@ public: | `` | Copy algorithm + +| `` +| Consumption cursor for partial-transfer loops |=== You have now learned how to measure and copy buffer sequences. Continue to xref:6.streams/6.intro.adoc[Stream Concepts] to learn how coroutines transfer data through streams. diff --git a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc index ec03da646..3719a7d48 100644 --- a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc +++ b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc @@ -33,13 +33,9 @@ Stream operations transfer *some* data and return. They do not guarantee a speci [source,cpp] ---- -// ReadStream: may return fewer bytes than buffer can hold -auto [ec, n] = co_await stream.read_some(buffer); -// n might be 1, might be 1000, might be buffer_size(buffer) +include::example$snippets/6a_overview.cpp[tag=read_stream_partial,indent=0] -// WriteStream: may write fewer bytes than provided -auto [ec, n] = co_await stream.write_some(buffers); -// n might be less than buffer_size(buffers) +include::example$snippets/6a_overview.cpp[tag=write_stream_partial,indent=0] ---- This matches raw OS behavior—syscalls return when data is available, not when buffers are full. @@ -82,42 +78,16 @@ Type-erased wrappers let you write transport-agnostic code: [source,cpp] ---- -// This function works with any stream implementation -task<> echo(any_stream& stream) -{ - char buf[1024]; - for (;;) - { - auto [ec, n] = co_await stream.read_some(make_buffer(buf)); - - auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); - - if (ec) - co_return; - - if (wec) - co_return; - } -} +include::example$snippets/6a_overview.cpp[tag=any_stream_echo] ---- The caller decides the concrete implementation: [source,cpp] ---- -// Owns a moved-in TCP socket (the lvalue form takes ownership by value) -any_stream s1{tcp_socket}; -echo(s1); - -// Wraps a TLS stream by pointer (reference semantics, must outlive s2) -any_stream s2{&tls_stream}; -echo(s2); - -// Owns a temporary test mock -any_stream s3{test::stream{}}; -echo(s3); +include::example$snippets/6a_overview.cpp[tag=caller_decides,indent=0] ---- -Same code, different transports: compile once, link anywhere. +Same code, different transports: the in-memory test stream shown here, a TCP socket, or a TLS stream. Compile once, link anywhere. Continue to xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] to learn the `ReadStream` and `WriteStream` concepts in detail. diff --git a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc index de55fefff..9088c9e20 100644 --- a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc +++ b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc @@ -13,11 +13,7 @@ A type satisfies `ReadStream` if it provides partial read operations via `read_s [source,cpp] ---- -template -concept ReadStream = - requires(T& stream, mutable_buffer_archetype buffers) { - { stream.read_some(buffers) } -> IoAwaitable; - }; +include::example$snippets/6b_streams.cpp[tag=read_stream_concept] ---- The `requires` clause names a single representative buffer (`mutable_buffer_archetype`) because a {cpp} concept cannot say "works with every buffer sequence." The real contract is that `read_some` accepts *any* `MutableBufferSequence`—one buffer or a range; the archetype only samples that requirement. @@ -26,8 +22,7 @@ The `requires` clause names a single representative buffer (`mutable_buffer_arch [source,cpp] ---- -template -IoAwaitable auto read_some(Buffers buffers); +include::example$snippets/6b_streams.cpp[tag=read_some_signature,indent=0] ---- Attempts to read up to `buffer_size(buffers)` bytes from the stream into the buffer sequence. Await-returns `(error_code, std::size_t)`: @@ -51,10 +46,7 @@ I/O conditions from the underlying system are reported via `ec`. Failures in the [source,cpp] ---- -char buf[1024]; -auto [ec, n] = co_await stream.read_some(make_buffer(buf)); -// n might be 1, might be 500, might be 1024 -// if !ec, then n >= 1 +include::example$snippets/6b_streams.cpp[tag=read_partial,indent=0] ---- This matches underlying OS behavior: reads return when *some* data is available. @@ -63,21 +55,7 @@ This matches underlying OS behavior: reads return when *some* data is available. [source,cpp] ---- -template -task<> dump_stream(Stream& stream) -{ - char buf[256]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(make_buffer(buf)); - - std::cout.write(buf, n); - - if (ec) - break; - } -} +include::example$snippets/6b_streams.cpp[tag=dump_stream] ---- == WriteStream @@ -86,11 +64,7 @@ A type satisfies `WriteStream` if it provides partial write operations via `writ [source,cpp] ---- -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) { - { stream.write_some(buffers) } -> IoAwaitable; - }; +include::example$snippets/6b_streams.cpp[tag=write_stream_concept] ---- As with `ReadStream`, the `const_buffer_archetype` is only a representative: the real contract is that `write_some` accepts *any* `ConstBufferSequence`, which a {cpp} concept cannot fully express. @@ -99,8 +73,7 @@ As with `ReadStream`, the `const_buffer_archetype` is only a representative: the [source,cpp] ---- -template -IoAwaitable auto write_some(Buffers buffers); +include::example$snippets/6b_streams.cpp[tag=write_some_signature,indent=0] ---- Attempts to write up to `buffer_size(buffers)` bytes from the buffer sequence to the stream. Await-returns `(error_code, std::size_t)`: @@ -124,8 +97,7 @@ I/O conditions from the underlying system are reported via `ec`. Failures in the [source,cpp] ---- -auto [ec, n] = co_await stream.write_some(make_buffer(large_data)); -// n might be less than large_data.size() +include::example$snippets/6b_streams.cpp[tag=write_partial,indent=0] ---- To write all data, loop until complete (or use the `write()` composed operation). @@ -138,15 +110,9 @@ Wraps any `ReadStream` in a type-erased container: [source,cpp] ---- -#include - -// Owning: takes ownership of a moved-in stream -template -any_read_stream(S stream); +include::example$snippets/6b_streams.cpp[tag=any_read_stream_include] -// Reference: wraps by pointer without ownership -template -any_read_stream(S* stream); +include::example$snippets/6b_streams.cpp[tag=any_read_stream_ctors,indent=0] ---- Each wrapper has two construction modes. Passing an object by value takes ownership: the wrapper moves the stream into internally allocated storage. Passing a pointer wraps the pointed-to stream by reference, and that stream must outlive the wrapper. @@ -157,13 +123,9 @@ Wraps any `WriteStream`: [source,cpp] ---- -#include +include::example$snippets/6b_streams.cpp[tag=any_write_stream_include] -template -any_write_stream(S stream); // owning - -template -any_write_stream(S* stream); // reference +include::example$snippets/6b_streams.cpp[tag=any_write_stream_ctors,indent=0] ---- === any_stream @@ -172,15 +134,9 @@ Wraps bidirectional streams (both `ReadStream` and `WriteStream`): [source,cpp] ---- -#include - -template - requires ReadStream && WriteStream -any_stream(S stream); // owning +include::example$snippets/6b_streams.cpp[tag=any_stream_include] -template - requires ReadStream && WriteStream -any_stream(S* stream); // reference +include::example$snippets/6b_streams.cpp[tag=any_stream_ctors,indent=0] ---- === Wrapper Characteristics @@ -196,40 +152,14 @@ Example usage: [source,cpp] ---- -void process_stream(any_stream& stream); - -tcp::socket socket; -// ... connect socket ... - -any_stream wrapped{&socket}; // Type erasure, references the existing socket -process_stream(wrapped); // process_stream doesn't know about tcp::socket +include::example$snippets/6b_streams.cpp[tag=wrapper_usage,indent=0] ---- == Example: Echo Server with any_stream [source,cpp] ---- -// echo.hpp - Header only declares the signature -task<> handle_connection(any_stream& stream); - -// echo.cpp - Implementation in separate translation unit -task<> handle_connection(any_stream& stream) -{ - char buf[1024]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(make_buffer(buf)); - - auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); - - if (ec) - break; - - if (wec) - break; - } -} +include::example$snippets/6b_streams.cpp[tag=echo_server] ---- The implementation doesn't know the concrete stream type. It compiles once and works with any transport. diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc index 004a391c1..77d6ab9b7 100644 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc @@ -15,36 +15,12 @@ Type-erased wrappers break this dependency: [source,cpp] ---- -// protocol.hpp - No template dependencies -#pragma once -#include -#include - -// Declaration only - no implementation details -task<> handle_protocol(any_stream& stream); +include::example$snippets/protocol.hpp[tag=protocol_header,indent=0] ---- [source,cpp] ---- -// protocol.cpp - Implementation isolated here -#include "protocol.hpp" -#include -#include - -task<> handle_protocol(any_stream& stream) -{ - char buf[1024]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(make_buffer(buf)); - if (ec) - co_return; - - // Process and respond... - co_await write(stream, make_buffer(response)); - } -} +include::example$snippets/6f_isolation.cpp[tag=protocol_impl,indent=0] ---- Changes to `protocol.cpp` only recompile that file. The header is stable. @@ -55,23 +31,14 @@ Changes to `protocol.cpp` only recompile that file. The header is stable. [source,cpp] ---- -// Old approach: template propagates everywhere -template -task<> handle_protocol(Stream& stream); - -// Every caller instantiates for their stream type -// Changes force recompilation of all callers +include::example$snippets/6f_isolation.cpp[tag=templates_before,indent=0] ---- === After (Type Erasure at Boundary) [source,cpp] ---- -// New approach: concrete signature -task<> handle_protocol(any_stream& stream); - -// Implementation compiles once -// Callers only depend on the signature +include::example$snippets/6f_isolation.cpp[tag=type_erasure_after,indent=0] ---- === Measured Impact @@ -89,32 +56,14 @@ Type erasure decouples your code from specific transport implementations: [source,cpp] ---- -// Your library code -task<> send_message(any_write_stream& stream, message const& msg) -{ - co_await write(stream, make_buffer(msg.header)); - co_await write(stream, make_buffer(msg.body)); -} +include::example$snippets/6f_isolation.cpp[tag=send_message,indent=0] ---- Callers provide any conforming implementation: [source,cpp] ---- -// TCP socket -tcp::socket socket; -any_write_stream stream{&socket}; // references socket -send_message(stream, msg); - -// TLS stream -tls::stream tls; -any_write_stream stream{&tls}; // references tls -send_message(stream, msg); - -// Test mock -test::write_stream mock; -any_write_stream stream{&mock}; // references mock -send_message(stream, msg); +include::example$snippets/6f_isolation.cpp[tag=callers,indent=0] ---- Same `send_message` function, different transports: compile once, use everywhere. @@ -125,22 +74,14 @@ Same `send_message` function, different transports: compile once, use everywhere [source,cpp] ---- -// Good: accepts any stream -task<> process(any_stream& stream); - -// Avoid: forces specific type -task<> process(tcp::socket& socket); +include::example$snippets/6f_isolation.cpp[tag=accept_type_erased,indent=0] ---- === Wrap at Call Site [source,cpp] ---- -void caller(tcp::socket& socket) -{ - any_stream stream{&socket}; // Wrap by reference here - process(stream); // Call with erased type -} +include::example$snippets/6f_isolation.cpp[tag=wrap_call_site,indent=0] ---- The wrapper creation is explicit and localized. @@ -149,13 +90,9 @@ The wrapper creation is explicit and localized. [source,cpp] ---- -// OK: factory returns concrete type -tcp::socket create_socket(); +include::example$snippets/6f_isolation.cpp[tag=return_concrete_decl,indent=0] -// Then caller wraps if needed -auto socket = create_socket(); -any_stream stream{&socket}; // reference; socket must outlive stream -// or: any_stream stream{std::move(socket)}; // wrapper takes ownership +include::example$snippets/6f_isolation.cpp[tag=return_concrete_use,indent=0] ---- Returning type-erased values forces heap allocation. Return concrete types when the caller knows what they need. @@ -164,47 +101,14 @@ Returning type-erased values forces heap allocation. Return concrete types when [source,cpp] ---- -// http_client.hpp -#pragma once -#include - -struct http_request -{ - std::string method; - std::string url; - std::map headers; -}; - -struct http_response -{ - int status_code; - std::map headers; - any_read_stream body; // Body is read as a stream -}; - -// Send request, receive response -// Works with any transport that provides any_stream -task send_request(any_stream& conn, http_request const& req); +include::example$snippets/http_client.hpp[tag=http_client_header,indent=0] ---- Users don't need to know how HTTP is implemented: [source,cpp] ---- -// User code -tcp::socket socket; -// ... connect ... - -any_stream conn{&socket}; // references socket -auto response = co_await send_request(conn, { - .method = "GET", - .url = "/api/data" -}); - -// Read body through type-erased source -char storage[4096]; -mutable_buffer buf(storage, sizeof(storage)); -auto [ec, n] = co_await response.body.read_some(buf); +include::example$snippets/6f_isolation.cpp[tag=user_code,indent=0] ---- The HTTP library is isolated from transport details. It compiles once. Users bring their own transport. @@ -230,5 +134,6 @@ Type-erased wrappers are in ``: * `any_stream` * `any_read_stream`, `any_write_stream` +* `any_buffer_source`, `any_buffer_sink` You have now completed the Stream Concepts section. These abstractions—streams and their type-erased wrappers—form the foundation for Capy's I/O model. Continue to xref:../8.examples/8a.hello-task.adoc[Example Programs] to see complete working examples. diff --git a/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc index 98505e0ce..dd45287e3 100644 --- a/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc +++ b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc @@ -29,23 +29,7 @@ No real executor or thread pool is involved. [source,cpp] ---- -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -task compute(int x) -{ - co_return x * 2; -} - -void test_compute() -{ - int result = 0; - run_blocking([&](int v) { result = v; })(compute(21)); - BOOST_TEST(result == 42); -} +include::example$snippets/7a_drivers.cpp[tag=run_blocking_basic] ---- === Result Capture @@ -56,32 +40,7 @@ handle the success and error cases independently: [source,cpp] ---- -// Discard result; rethrow on exception -run_blocking()(my_task()); - -// Capture result; rethrow on exception -int out = 0; -run_blocking([&](int v) { out = v; })(compute(21)); - -// Capture result; handle exception separately -run_blocking( - [&](int v) { out = v; }, - [](std::exception_ptr ep) { std::rethrow_exception(ep); } -)(compute(21)); - -// With a stop token (discards result) -std::stop_source src; -run_blocking(src.get_token())(my_task()); - -// With a stop token and a result handler -run_blocking(src.get_token(), [&](int v) { out = v; })(compute(21)); - -// With a stop token and separate handlers -run_blocking( - src.get_token(), - [&](int v) { out = v; }, - [](std::exception_ptr ep) { std::rethrow_exception(ep); } -)(compute(21)); +include::example$snippets/7a_drivers.cpp[tag=run_blocking_overloads,indent=0] ---- === How It Works @@ -130,18 +89,7 @@ paths without real I/O. [source,cpp] ---- -std::stop_source src; -src.request_stop(); - -run_blocking(src.get_token())([&]() -> task<> -{ - read_stream rs; - rs.provide("ignored"); - - char buf[32]; - auto [ec, n] = co_await rs.read_some(make_buffer(buf)); - assert(ec == cond::canceled); // honored the stop token -}()); +include::example$snippets/7a_drivers.cpp[tag=run_blocking_cancellation,indent=0] ---- The mock sources, sinks, and buffer adapters complete synchronously, so they @@ -159,25 +107,7 @@ location of the failing call. [source,cpp] ---- -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_with_fuse() -{ - fuse f; - auto r = f.armed([](fuse& f) { - auto ec = f.maybe_fail(); - if(ec) - return; // injected error: exit gracefully - - ec = f.maybe_fail(); - if(ec) - return; - }); - BOOST_TEST(r.success); -} +include::example$snippets/7a_drivers.cpp[tag=fuse_basic] ---- === armed() vs. inert() @@ -196,32 +126,7 @@ confirms it handles every error site: [source,cpp] ---- -fuse f; - -// Smoke test: happy path -auto r1 = f.inert([&](fuse&) -> task { - read_stream rs(f); - rs.provide("hello"); - - char buf[8]; - auto [ec, n] = co_await rs.read_some(make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST(std::string_view(buf, n) == "hello"); -}); -BOOST_TEST(r1.success); - -// Fault coverage: every error site -auto r2 = f.armed([&](fuse&) -> task { - read_stream rs(f); - rs.provide("hello"); - - char buf[8]; - auto [ec, n] = co_await rs.read_some(make_buffer(buf)); - if(ec) - co_return; // fuse injected an error; exit gracefully - BOOST_TEST(std::string_view(buf, n) == "hello"); -}); -BOOST_TEST(r2.success); +include::example$snippets/7a_drivers.cpp[tag=inert_vs_armed,indent=0] ---- The only difference is the `if(ec) co_return;` guard. In `inert()`, @@ -233,15 +138,7 @@ The only way to signal a test failure under `inert()` is to call [source,cpp] ---- -fuse f; -auto r = f.inert([](fuse& f) { - auto ec = f.maybe_fail(); // always returns {} - assert(!ec); - - if(some_condition_failed) - f.fail(); // the only way to signal failure in inert mode -}); -BOOST_TEST(r.success); +include::example$snippets/7a_drivers.cpp[tag=inert_fail,indent=0] ---- === The Early-Return Pattern @@ -253,14 +150,7 @@ internally, so this pattern applies to all I/O calls inside an armed test. [source,cpp] ---- -// Correct: early return on injected error -auto [ec, n] = co_await rs.read_some(buf); -if(ec) - co_return; // fuse injected an error -- exit gracefully - -// Wrong: asserting success unconditionally -auto [ec, n] = co_await rs.read_some(buf); -BOOST_TEST(!ec); // fails when fuse injects an error +include::example$snippets/7a_drivers.cpp[tag=early_return,indent=0] ---- === Coroutine Support @@ -271,17 +161,7 @@ You do not need to call `run_blocking` yourself: [source,cpp] ---- -fuse f; -auto r = f.armed([&](fuse&) -> task { - auto ec = f.maybe_fail(); - if(ec) - co_return; - - auto [ec2, n] = co_await rs.read_some(buf); - if(ec2) - co_return; -}); -BOOST_TEST(r.success); +include::example$snippets/7a_drivers.cpp[tag=armed_coroutine,indent=0] ---- === Custom Fail Points @@ -294,28 +174,7 @@ fault injection alongside every other site. [source,cpp] ---- -class widget -{ - fuse& f_; -public: - explicit widget(fuse& f) : f_(f) {} - - std::error_code process() - { - auto ec = f_.maybe_fail(); - if(ec) - return ec; - // ... actual work ... - return {}; - } -}; - -fuse f; -widget w(f); -w.process(); // maybe_fail() returns {} - -auto r = f.armed([&](fuse&) { w.process(); }); // both branches exercised -BOOST_TEST(r.success); +include::example$snippets/7a_drivers.cpp[tag=custom_fail_points,indent=0] ---- === Custom Error Code @@ -325,16 +184,7 @@ The default injected code is `error::test_failure`. Pass any [source,cpp] ---- -fuse f(std::make_error_code(std::errc::operation_canceled)); -auto r = f.armed([](fuse& f) { - auto ec = f.maybe_fail(); - if(ec) - { - assert(ec == std::errc::operation_canceled); - return; - } -}); -BOOST_TEST(r.success); +include::example$snippets/7a_drivers.cpp[tag=custom_error_code,indent=0] ---- [cols="1,2"] @@ -409,20 +259,7 @@ Platform limits on the name length: [source,cpp] ---- -#include -#include -#include -#include - -using namespace boost::capy; - -thread_pool pool(4); -run_async(pool.get_executor())([]() -> task { - set_current_thread_name("test-worker-0"); - // ... test work runs here; name appears in gdb thread list - co_return; -}()); -pool.join(); +include::example$snippets/7a_drivers.cpp[tag=thread_name,indent=0] ---- Note that `set_current_thread_name` lives in namespace `boost::capy`, not @@ -446,26 +283,7 @@ internally, so the test body uses `co_await` directly: [source,cpp] ---- -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -task add(int a, int b) -{ - co_return a + b; -} - -void test_add() -{ - fuse f; - auto r = f.armed([&](fuse&) -> task { - auto sum = co_await add(3, 4); - BOOST_TEST(sum == 7); - }); - BOOST_TEST(r.success); -} +include::example$snippets/7a_drivers.cpp[tag=canonical_skeleton] ---- When the operation under test needs a specific execution context -- for @@ -476,7 +294,7 @@ each iteration's task on a context it owns and returns any exception the task raised as a `std::exception_ptr` (null on success); `armed` rethrows it. `fuse` never learns about the context -- the caller owns the drive loop: -[source,cpp] +[source,cpp,role=external] ---- fuse f; auto io_runner = [](task t) -> std::exception_ptr { diff --git a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc index 046fe9021..d5cc14472 100644 --- a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc +++ b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc @@ -27,31 +27,7 @@ fail-point machinery as `f`. [source,cpp] ---- -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_read_stream() -{ - fuse f; - read_stream rs(f); - rs.provide("Hello, "); - rs.provide("World!"); - - auto r = f.armed([&](fuse&) -> task { - char buf[32]; - auto [ec, n] = co_await rs.read_some( - mutable_buffer(buf, sizeof(buf))); - if(ec) - co_return; - BOOST_TEST(std::string_view(buf, n) == "Hello, World!"); - }); - BOOST_TEST(r.success); -} +include::example$snippets/7b_mock_streams.cpp[tag=read_stream_basic] ---- === Chunked Delivery @@ -63,20 +39,7 @@ correctly on partial reads. [source,cpp] ---- -// At most 4 bytes per read_some call -fuse f; -read_stream rs(f, 4); -rs.provide("Hello, World!"); - -auto r = f.armed([&](fuse&) -> task { - char buf[32]; - auto [ec, n] = co_await rs.read_some( - mutable_buffer(buf, sizeof(buf))); - if(ec) - co_return; - BOOST_TEST(n == 4); // "Hell" -}); -BOOST_TEST(r.success); +include::example$snippets/7b_mock_streams.cpp[tag=read_stream_chunked,indent=0] ---- === EOF Behavior @@ -87,25 +50,7 @@ suspend; the result is available immediately. [source,cpp] ---- -fuse f; -read_stream rs(f); -rs.provide("hi"); - -auto r = f.inert([&](fuse&) -> task { - char buf[8]; - // First read: consumes "hi" - auto [ec, n] = co_await rs.read_some( - mutable_buffer(buf, sizeof(buf))); - BOOST_TEST(!ec); - BOOST_TEST(std::string_view(buf, n) == "hi"); - - // Second read: EOF - auto [ec2, n2] = co_await rs.read_some( - mutable_buffer(buf, sizeof(buf))); - BOOST_TEST(ec2 == cond::eof); - BOOST_TEST(n2 == 0); -}); -BOOST_TEST(r.success); +include::example$snippets/7b_mock_streams.cpp[tag=read_stream_eof,indent=0] ---- [cols="1,2"] @@ -148,28 +93,7 @@ fail-point machinery as `f`. [source,cpp] ---- -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_write_stream() -{ - fuse f; - write_stream ws(f); - - auto r = f.armed([&](fuse&) -> task { - auto [ec, n] = co_await ws.write_some( - const_buffer("Hello", 5)); - if(ec) - co_return; - BOOST_TEST(ws.data() == "Hello"); - }); - BOOST_TEST(r.success); -} +include::example$snippets/7b_mock_streams.cpp[tag=write_stream_basic] ---- === Chunked Writes @@ -180,16 +104,7 @@ to verify that your code loops correctly until all data is transferred. [source,cpp] ---- -fuse f; -write_stream ws(f, 4); // accept at most 4 bytes per call - -auto r = f.inert([&](fuse&) -> task { - auto [ec, n] = co_await ws.write_some( - const_buffer("Hello", 5)); - BOOST_TEST(!ec); - BOOST_TEST(n == 4); // only "Hell" was accepted -}); -BOOST_TEST(r.success); +include::example$snippets/7b_mock_streams.cpp[tag=write_stream_chunked,indent=0] ---- === Expected Data Verification @@ -201,17 +116,7 @@ returns `error::test_failure`. [source,cpp] ---- -fuse f; -write_stream ws(f); -ws.expect("Hello World"); - -auto r = f.inert([&](fuse&) -> task { - // Writing matching data succeeds - auto [ec, n] = co_await ws.write_some( - const_buffer("Hello World", 11)); - BOOST_TEST(!ec); -}); -BOOST_TEST(r.success); +include::example$snippets/7b_mock_streams.cpp[tag=write_stream_expect,indent=0] ---- [cols="1,2"] @@ -253,34 +158,7 @@ Both `stream` ends satisfy `ReadStream` and `WriteStream`. [source,cpp] ---- -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_stream_pair() -{ - fuse f; - auto [a, b] = make_stream_pair(f); - - auto r = f.armed([&](fuse&) -> task { - auto [ec, n] = co_await a.write_some( - const_buffer("hello", 5)); - if(ec) - co_return; - - char buf[32]; - auto [ec2, n2] = co_await b.read_some( - mutable_buffer(buf, sizeof(buf))); - if(ec2) - co_return; - BOOST_TEST(std::string_view(buf, n2) == "hello"); - }); - BOOST_TEST(r.success); -} +include::example$snippets/7b_mock_streams.cpp[tag=stream_pair_basic] ---- === Connected Semantics @@ -364,49 +242,7 @@ a different error-handling branch inside `read_line`. [source,cpp] ---- -#include -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -// Function under test: read until '\n' or EOF -template -task> -read_line(S& stream) -{ - std::string line; - char ch; - for(;;) - { - auto [ec, n] = co_await stream.read_some( - mutable_buffer(&ch, 1)); - if(ec) - co_return {ec, std::move(line)}; - if(ch == '\n') - break; - line += ch; - } - co_return {std::error_code{}, std::move(line)}; -} - -void test_read_line() -{ - fuse f; - auto r = f.armed([&](fuse&) -> task { - read_stream rs(f); - rs.provide("hello\n"); - - auto [ec, line] = co_await read_line(rs); - if(ec) - co_return; // fuse injected an error; exit gracefully - BOOST_TEST(line == "hello"); - }); - BOOST_TEST(r.success); -} +include::example$snippets/7b_mock_streams.cpp[tag=read_line_test] ---- == Reference diff --git a/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc index c1b21ced4..7daefaa24 100644 --- a/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc +++ b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc @@ -29,31 +29,7 @@ visit every split position, and there are no async failure sites to inject. [source,cpp] ---- -#include -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_all_splits() -{ - std::string data = "hello"; - auto cb = make_buffer(data); - - fuse f; - auto r = f.inert([&](fuse&) -> task<> { - bufgrind bg(cb); - while(bg) - { - auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ(buffer_to_string(b1.data(), b2.data()), data); - } - }); - BOOST_TEST(r.success); -} +include::example$snippets/7e_buffer_inspection.cpp[tag=all_splits,indent=0] ---- === Iteration Pattern @@ -80,16 +56,7 @@ size) is always visited regardless of step alignment. [source,cpp] ---- -std::string data = "0123456789"; // 10 bytes -auto cb = make_buffer(data); - -bufgrind bg(cb, 3); -// Visits positions: 0, 3, 6, 9, 10 -while(bg) -{ - auto [b1, b2] = co_await bg.next(); - // exercise parser at each split point -} +include::example$snippets/7e_buffer_inspection.cpp[tag=step_size,indent=0] ---- A step of 0 is treated as 1. A step larger than the total size reduces to @@ -98,26 +65,15 @@ two positions: 0 and size. === Mutability Preservation `bufgrind` is templated on a `ConstBufferSequence` but the slices it -produces follow the mutability of the input. Each half is a buffer-slice -view (`slice_type`); its `data()` member exposes the underlying buffer -sequence. Passing a `mutable_buffer` yields halves whose `data()` models -`MutableBufferSequence`; passing a `const_buffer` yields halves whose -`data()` models `ConstBufferSequence`. This matters for tests that need -to write into the produced buffers rather than only read from them. +produces follow the mutability of the input. Each half is itself a +buffer sequence (`slice_type`). Passing a `mutable_buffer` yields halves +that model `MutableBufferSequence`; passing a `const_buffer` yields +halves that model `ConstBufferSequence`. This matters for tests that +need to write into the produced buffers rather than only read from them. [source,cpp] ---- -char data[] = "hello"; -mutable_buffer mb(data, 5); - -bufgrind bg(mb); -while(bg) -{ - auto [b1, b2] = co_await bg.next(); - // b1 and b2 are buffer-slice views; data() exposes the underlying - // buffer sequence, here a MutableBufferSequence callers may write into - static_assert(MutableBufferSequence); -} +include::example$snippets/7e_buffer_inspection.cpp[tag=mutability,indent=0] ---- [cols="1,2"] @@ -147,23 +103,7 @@ use is asserting the combined content of a `bufgrind` split. [source,cpp] ---- -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_buffer_to_string() -{ - // Single buffer sequence - const_buffer cb(make_buffer(std::string_view("hello"))); - BOOST_TEST_EQ(buffer_to_string(cb), "hello"); - - // Multiple buffer sequences concatenated in order - const_buffer b1(make_buffer(std::string_view("hello"))); - const_buffer b2(make_buffer(std::string_view(" world"))); - BOOST_TEST_EQ(buffer_to_string(b1, b2), "hello world"); -} +include::example$snippets/7e_buffer_inspection.cpp[tag=buffer_to_string,indent=0] ---- === Use With bufgrind @@ -174,19 +114,7 @@ input: [source,cpp] ---- -std::string original = "hello world"; -auto cb = make_buffer(original); - -fuse f; -auto r = f.inert([&](fuse&) -> task<> { - bufgrind bg(cb); - while(bg) - { - auto [b1, b2] = co_await bg.next(); - BOOST_TEST_EQ(buffer_to_string(b1.data(), b2.data()), original); - } -}); -BOOST_TEST(r.success); +include::example$snippets/7e_buffer_inspection.cpp[tag=reconstruct,indent=0] ---- [cols="1,2"] @@ -207,53 +135,7 @@ the output at each split: [source,cpp] ---- -#include -#include -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -// Hypothetical parser: reads all bytes from a ReadStream -task read_all(read_stream& rs) -{ - std::string out; - std::array buf; - for(;;) - { - auto [ec, n] = co_await rs.read_some(make_buffer(buf)); - if(ec) - co_return out; - out.append(buf.data(), n); - } -} - -void test_parser_all_splits() -{ - std::string input = "GET / HTTP/1.1\r\n"; - auto cb = make_buffer(input); - - fuse f; - auto r = f.inert([&](fuse&) -> task<> { - bufgrind bg(cb); - while(bg) - { - auto [b1, b2] = co_await bg.next(); - - // Feed the split as two discrete reads - read_stream rs(f); - rs.provide(buffer_to_string(b1.data())); - rs.provide(buffer_to_string(b2.data())); - - std::string got = co_await read_all(rs); - BOOST_TEST_EQ(got, input); - } - }); - BOOST_TEST(r.success); -} +include::example$snippets/7e_buffer_inspection.cpp[tag=parser_all_splits,indent=0] ---- == Reference diff --git a/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc index 24b547646..12c848c62 100644 --- a/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc +++ b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc @@ -17,24 +17,7 @@ The minimal Capy program: a task that prints a message. [source,cpp] ---- -#include -#include - -namespace capy = boost::capy; - -capy::task<> say_hello() -{ - std::cout << "Hello from Capy!\n"; - co_return; -} - -int main() -{ - capy::thread_pool pool; - capy::run_async(pool.get_executor())(say_hello()); - pool.join(); - return 0; -} +include::example$hello-task/hello_task.cpp[tag=full] ---- == Build @@ -51,11 +34,7 @@ target_link_libraries(hello_task PRIVATE capy) [source,cpp] ---- -capy::task<> say_hello() -{ - std::cout << "Hello from Capy!\n"; - co_return; -} +include::example$hello-task/hello_task.cpp[tag=say_hello] ---- `task<>` is equivalent to `task`—a coroutine that completes without returning a value. The `co_return` keyword marks this as a coroutine. @@ -66,7 +45,7 @@ Tasks are lazy: calling `say_hello()` creates a task object but does not execute [source,cpp] ---- -capy::thread_pool pool; +include::example$hello-task/hello_task.cpp[tag=pool,indent=0] ---- `thread_pool` provides an execution context with worker threads. By default, it creates one thread per CPU core. @@ -77,7 +56,7 @@ Call `pool.join()` before the pool is destroyed to wait for all outstanding work [source,cpp] ---- -capy::run_async(pool.get_executor())(say_hello()); +include::example$hello-task/hello_task.cpp[tag=launch,indent=0] ---- `run_async` bridges non-coroutine code (like `main`) to coroutine code. The two-call syntax: diff --git a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc index e006eb581..595a9823c 100644 --- a/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc +++ b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc @@ -18,53 +18,7 @@ Two tasks communicating via an async event, with strand serialization. [source,cpp] ---- -#include -#include -#include -#include - -namespace capy = boost::capy; - -int main() -{ - capy::thread_pool pool; - capy::strand s{pool.get_executor()}; - std::latch done(1); - - auto on_complete = [&done](auto&&...) { done.count_down(); }; - auto on_error = [&done](std::exception_ptr) { done.count_down(); }; - - capy::async_event data_ready; - int shared_value = 0; - - auto producer = [&]() -> capy::io_task<> { - std::cout << "Producer: preparing data...\n"; - shared_value = 42; - std::cout << "Producer: data ready, signaling\n"; - data_ready.set(); - co_return capy::io_result<>{}; - }; - - auto consumer = [&]() -> capy::io_task<> { - std::cout << "Consumer: waiting for data...\n"; - auto [ec] = co_await data_ready.wait(); - (void)ec; - std::cout << "Consumer: received value " << shared_value << "\n"; - co_return capy::io_result<>{}; - }; - - // Run both tasks concurrently using when_all, through a strand. - // The strand serializes execution, ensuring thread-safe access - // to the shared async_event and shared_value. - auto run_both = [&]() -> capy::task<> { - (void) co_await capy::when_all(producer(), consumer()); - }; - - capy::run_async(s, on_complete, on_error)(run_both()); - - done.wait(); // Block until tasks complete - return 0; -} +include::example$producer-consumer/producer_consumer.cpp[tag=full] ---- == Build @@ -81,7 +35,7 @@ target_link_libraries(producer_consumer PRIVATE capy) [source,cpp] ---- -capy::strand s{pool.get_executor()}; +include::example$producer-consumer/producer_consumer.cpp[tag=strand,indent=0] ---- A `strand` is an executor adaptor that serializes execution. All coroutines dispatched through a strand are guaranteed not to run concurrently, making it safe to access shared state without explicit locking. Note that `async_event` is not thread-safe, so using a strand ensures safe access. @@ -90,7 +44,7 @@ A `strand` is an executor adaptor that serializes execution. All coroutines disp [source,cpp] ---- -capy::async_event data_ready; +include::example$producer-consumer/producer_consumer.cpp[tag=event,indent=0] ---- `async_event` is a manual-reset signaling mechanism. One task can `set()` it; other tasks can `wait()` for it. When set, all current waiters resume, and the event stays set (later `wait()` calls return immediately) until `clear()` is called. @@ -99,13 +53,7 @@ capy::async_event data_ready; [source,cpp] ---- -auto producer = [&]() -> capy::io_task<> { - std::cout << "Producer: preparing data...\n"; - shared_value = 42; - std::cout << "Producer: data ready, signaling\n"; - data_ready.set(); - co_return capy::io_result<>{}; -}; +include::example$producer-consumer/producer_consumer.cpp[tag=producer,indent=0] ---- The producer prepares data and signals completion by calling `set()`. @@ -114,13 +62,7 @@ The producer prepares data and signals completion by calling `set()`. [source,cpp] ---- -auto consumer = [&]() -> capy::io_task<> { - std::cout << "Consumer: waiting for data...\n"; - auto [ec] = co_await data_ready.wait(); - (void)ec; - std::cout << "Consumer: received value " << shared_value << "\n"; - co_return capy::io_result<>{}; -}; +include::example$producer-consumer/producer_consumer.cpp[tag=consumer,indent=0] ---- The consumer waits until the event is set. The `co_await data_ready.wait()` suspends until `set()` is called. @@ -129,14 +71,7 @@ The consumer waits until the event is set. The `co_await data_ready.wait()` susp [source,cpp] ---- -// Run both tasks concurrently using when_all, through a strand. -// The strand serializes execution, ensuring thread-safe access -// to the shared async_event and shared_value. -auto run_both = [&]() -> capy::task<> { - (void) co_await capy::when_all(producer(), consumer()); -}; - -capy::run_async(s, on_complete, on_error)(run_both()); +include::example$producer-consumer/producer_consumer.cpp[tag=run_both,indent=0] ---- `when_all` runs both tasks concurrently within the same parent coroutine context, but the strand ensures they don't run at the same time on different threads. The producer signals `data_ready` when the value is set, and the consumer waits for the signal before reading. @@ -145,11 +80,9 @@ capy::run_async(s, on_complete, on_error)(run_both()); [source,cpp] ---- -std::latch done(1); // std::latch - wait for completion -auto on_complete = [&done](auto&&...) { done.count_down(); }; -auto on_error = [&done](std::exception_ptr) { done.count_down(); }; +include::example$producer-consumer/producer_consumer.cpp[tag=completion,indent=0] // ... -done.wait(); // Block until tasks complete +include::example$producer-consumer/producer_consumer.cpp[tag=wait,indent=0] ---- The `std::latch` ensures `main()` waits for the tasks to complete before returning. diff --git a/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc index 4109a978d..19093b3c9 100644 --- a/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc +++ b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc @@ -17,119 +17,7 @@ Composing buffer sequences without allocation for scatter/gather I/O. [source,cpp] ---- -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -void demonstrate_single_buffers() -{ - std::cout << "=== Single Buffer Examples ===\n\n"; - - // Create buffers from various sources - std::string str = "Hello, World!"; - char arr[] = "Array data"; - std::vector vec = {'V', 'e', 'c', 't', 'o', 'r'}; - - // make_buffer creates buffer views (no copies) - auto str_buf = capy::make_buffer(str); // mutable_buffer - auto arr_buf = capy::make_buffer(arr, sizeof(arr) - 1); // mutable_buffer - Exclude null terminator - auto vec_buf = capy::make_buffer(vec); // mutable_buffer - - std::cout << "String buffer: " << str_buf.size() << " bytes\n"; - std::cout << "Array buffer: " << arr_buf.size() << " bytes\n"; - std::cout << "Vector buffer: " << vec_buf.size() << " bytes\n"; -} - -void demonstrate_two_buffer_scatter() -{ - std::cout << "\n=== Two-Buffer Scatter/Gather ===\n\n"; - - // A 2-element buffer sequence is just a std::array - std::string header = "Content-Type: text/plain\r\n\r\n"; - std::string body = "Hello, World!"; - - std::array message = {{ - capy::make_buffer(header), - capy::make_buffer(body) - }}; - - // Calculate total size - std::size_t total = capy::buffer_size(message); - std::cout << "Total message size: " << total << " bytes\n"; - std::cout << "Buffer count: " << capy::buffer_length(message) << "\n"; - - // Iterate through buffers - std::cout << "\nBuffer contents:\n"; - for (auto const& buf : message) // const_buffer const& - { - std::cout << " [" << buf.size() << " bytes]: "; - std::cout.write(static_cast(buf.data()), buf.size()); - std::cout << "\n"; - } -} - -void demonstrate_buffer_array() -{ - std::cout << "\n=== Multi-Buffer Array ===\n\n"; - - // Use std::array for more than 2 buffers - std::string status = "HTTP/1.1 200 OK\r\n"; - std::string content_type = "Content-Type: application/json\r\n"; - std::string server = "Server: Capy/1.0\r\n"; - std::string empty_line = "\r\n"; - std::string body = R"({"status":"ok"})"; - - std::array http_response = {{ - capy::make_buffer(status), - capy::make_buffer(content_type), - capy::make_buffer(server), - capy::make_buffer(empty_line), - capy::make_buffer(body) - }}; - - std::size_t total = capy::buffer_size(http_response); - std::cout << "HTTP response size: " << total << " bytes\n"; - std::cout << "Buffer count: " << capy::buffer_length(http_response) << "\n"; - - // In real code with streams: - // co_await write(stream, http_response); - // This performs scatter/gather I/O - single syscall for all buffers -} - -void demonstrate_mutable_buffers() -{ - std::cout << "\n=== Mutable Buffer Example ===\n\n"; - - // Mutable buffers for receiving data - char buf1[64]; - char buf2[64]; - - std::array recv_buffers = {{ - capy::mutable_buffer(buf1, sizeof(buf1)), - capy::mutable_buffer(buf2, sizeof(buf2)) - }}; - - std::cout << "Prepared " << capy::buffer_length(recv_buffers) - << " buffers with " << capy::buffer_size(recv_buffers) - << " bytes total capacity\n"; - - // In real code: - // auto [ec, n] = co_await stream.read_some(recv_buffers); -} - -int main() -{ - demonstrate_single_buffers(); - demonstrate_two_buffer_scatter(); - demonstrate_buffer_array(); - demonstrate_mutable_buffers(); - - return 0; -} +include::example$buffer-composition/buffer_composition.cpp[tag=full] ---- == Build @@ -146,8 +34,7 @@ target_link_libraries(buffer_composition PRIVATE capy) [source,cpp] ---- -auto str_buf = capy::make_buffer(str); // mutable_buffer -auto arr_buf = capy::make_buffer(arr, sizeof(arr) - 1); // mutable_buffer +include::example$buffer-composition/buffer_composition.cpp[tag=make_buffer,indent=0] ---- `make_buffer` creates buffer views from various sources. No data is copied—the buffers reference the original storage. @@ -156,10 +43,7 @@ auto arr_buf = capy::make_buffer(arr, sizeof(arr) - 1); // mutable_buffer [source,cpp] ---- -std::array message = {{ - capy::make_buffer(header), - capy::make_buffer(body) -}}; +include::example$buffer-composition/buffer_composition.cpp[tag=two_buffer,indent=0] ---- Capy's buffer-sequence concepts accept any range of `const_buffer` or `mutable_buffer`, so `std::array` is a buffer sequence with no further wrapping required. Use `mutable_buffer` for receive paths. @@ -168,11 +52,7 @@ Capy's buffer-sequence concepts accept any range of `const_buffer` or `mutable_b [source,cpp] ---- -std::array http_response = {{ - capy::make_buffer(status), - capy::make_buffer(content_type), - // ... -}}; +include::example$buffer-composition/buffer_composition.cpp[tag=multi_buffer,indent=0] ---- For more than two buffers, the same pattern works with a larger `std::array`. Buffer sequences support `buffer_size()` and `buffer_length()` for querying total bytes and buffer count. @@ -181,7 +61,7 @@ For more than two buffers, the same pattern works with a larger `std::array`. Bu [source,cpp] ---- -co_await write(stream, http_response); +include::example$snippets/8c_buffer_composition.cpp[tag=write_gather,indent=0] ---- When you write a buffer sequence, the OS receives all buffers in a single system call. This is *scatter/gather I/O*: diff --git a/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc index 534de6757..69e710cd4 100644 --- a/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc +++ b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc @@ -17,153 +17,7 @@ Unit testing protocol code with mock streams and error injection. [source,cpp] ---- -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// A simple protocol: read until newline, echo back uppercase -// Takes any_stream& so the function is transport-independent -capy::task echo_line_uppercase(capy::any_stream& stream) -{ - std::string line; - char c; - - // Read character by character until newline - while (true) - { - // ec: std::error_code, n: std::size_t - auto [ec, n] = co_await stream.read_some(capy::mutable_buffer(&c, 1)); - - if (n > 0) - { - if (c == '\n') - break; - line += static_cast(std::toupper(static_cast(c))); - } - - if (ec) - { - if (ec == capy::cond::eof) - break; - co_return false; - } - } - - line += '\n'; - - // Echo uppercase - loop until all bytes written - std::size_t written = 0; // std::size_t - total bytes written - while (written < line.size()) - { - // wec: std::error_code, wn: std::size_t - auto [wec, wn] = co_await stream.write_some( - capy::const_buffer(line.data() + written, line.size() - written)); - - written += wn; - - if (wec) - co_return false; - } - - co_return true; -} - -void test_happy_path() -{ - std::cout << "Test: happy path\n"; - - auto [a, b] = capy::test::make_stream_pair(); - b.provide("hello\n"); - - capy::any_stream stream{&a}; // any_stream - - bool result = false; // bool - capy::test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream)); - - assert(result == true); - assert(b.data() == "HELLO\n"); - - std::cout << " PASSED\n"; -} - -void test_partial_reads() -{ - std::cout << "Test: partial reads (1 byte at a time)\n"; - - auto [a, b] = capy::test::make_stream_pair(); - a.set_max_read_size(1); - b.provide("hi\n"); - - capy::any_stream stream{&a}; // any_stream - - bool result = false; // bool - capy::test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream)); - - assert(result == true); - assert(b.data() == "HI\n"); - - std::cout << " PASSED\n"; -} - -void test_with_error_injection() -{ - std::cout << "Test: error injection\n"; - - int success_count = 0; - int error_count = 0; - - // fuse::armed runs the test repeatedly, failing at each - // operation point until all paths are covered - capy::test::fuse f; // test::fuse - auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { // fuse::result - auto [a, b] = capy::test::make_stream_pair(f); - b.provide("test\n"); - - capy::any_stream stream{&a}; // any_stream - - // Run the protocol - fuse will inject errors at each step - bool result = co_await echo_line_uppercase(stream); // bool - - // Either succeeds with correct output, or fails cleanly - if (result) - { - ++success_count; - assert(b.data() == "TEST\n"); - } - else - { - ++error_count; - } - }); - - // Verify that fuse testing exercised both paths - std::cout << " Runs: " << (success_count + error_count) - << " (success=" << success_count - << ", error=" << error_count << ")\n"; - - assert(r.success); - assert(success_count > 0); // At least one successful run - assert(error_count > 0); // At least one error-injected run - - std::cout << " PASSED (all error paths tested)\n"; -} - -int main() -{ - test_happy_path(); - test_partial_reads(); - test_with_error_injection(); - - std::cout << "\nAll tests passed!\n"; - return 0; -} +include::example$mock-stream-testing/mock_stream_testing.cpp[tag=full] ---- == Build @@ -180,9 +34,7 @@ target_link_libraries(mock_stream_testing PRIVATE capy) [source,cpp] ---- -capy::test::fuse f; // test::fuse -auto [a, b] = capy::test::make_stream_pair(f); // connected test::stream pair -b.provide("hello\n"); // supply read input on one end +include::example$snippets/8d_mock_stream_testing.cpp[tag=mock_streams,indent=0] ---- `test::stream` is a bidirectional mock that satisfies both `ReadStream` and `WriteStream`: @@ -196,8 +48,7 @@ b.provide("hello\n"); // supply read input on one end [source,cpp] ---- -// Wrap one end in any_stream using pointer construction for reference semantics -capy::any_stream stream{&a}; // any_stream +include::example$mock-stream-testing/mock_stream_testing.cpp[tag=any_stream,indent=0] ---- Use pointer construction (`&a`) so the `any_stream` wrapper references the stream end without taking ownership. This allows inspecting `b.data()` after operations. @@ -206,8 +57,7 @@ Use pointer construction (`&a`) so the `any_stream` wrapper references the strea [source,cpp] ---- -bool result = false; // bool -capy::test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream)); +include::example$mock-stream-testing/mock_stream_testing.cpp[tag=run_blocking,indent=0] ---- `run_blocking` executes a coroutine synchronously, blocking until complete. Pass a handler to capture the result. @@ -216,11 +66,7 @@ capy::test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream [source,cpp] ---- -capy::test::fuse f; // test::fuse -auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { - auto [a, b] = capy::test::make_stream_pair(f); // connected test::stream pair - // ... run test ... -}); +include::example$snippets/8d_mock_stream_testing.cpp[tag=fuse_armed,indent=0] ---- `fuse::armed` runs the test function repeatedly, injecting errors at each operation point: diff --git a/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc index 61f01f2a0..0da7277d7 100644 --- a/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc +++ b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc @@ -19,99 +19,21 @@ Echo server demonstrating the compilation firewall pattern. [source,cpp] ---- -#ifndef ECHO_HPP -#define ECHO_HPP - -#include -#include - -namespace myapp { - -// Type-erased interface: no template dependencies -boost::capy::task<> echo_session(boost::capy::any_stream& stream); - -} // namespace myapp - -#endif +include::example$type-erased-echo/echo.hpp[tag=full] ---- === echo.cpp [source,cpp] ---- -#include "echo.hpp" -#include -#include -#include -#include - -namespace myapp { - -namespace capy = boost::capy; - -capy::task<> echo_session(capy::any_stream& stream) -{ - char buffer[1024]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(capy::make_buffer(buffer)); - - auto [wec, wn] = co_await capy::write(stream, capy::const_buffer(buffer, n)); - - if (ec) - co_return; - - if (wec) - co_return; - } -} - -} // namespace myapp +include::example$type-erased-echo/echo.cpp[tag=full] ---- === main.cpp [source,cpp] ---- -#include "echo.hpp" -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -void test_with_mock() -{ - auto [a, b] = capy::test::make_stream_pair(); - b.provide("Hello, "); - b.provide("World!\n"); - b.close(); - - // Using pointer construction (&a) for reference semantics - the - // wrapper does not take ownership, so a must outlive stream. - capy::any_stream stream{&a}; // any_stream - capy::test::run_blocking()(myapp::echo_session(stream)); - - std::cout << "Echo output: " << b.data() << "\n"; -} - -// With real sockets (using Corosio), you would write: -// -// task<> handle_client(corosio::tcp::socket socket) -// { -// // Value construction moves socket into wrapper (transfers ownership) -// any_stream stream{std::move(socket)}; -// co_await myapp::echo_session(stream); -// } - -int main() -{ - test_with_mock(); - return 0; -} +include::example$type-erased-echo/main.cpp[tag=full] ---- == Build @@ -131,8 +53,7 @@ target_link_libraries(echo_demo PRIVATE echo_lib) [source,cpp] ---- -// echo.hpp -boost::capy::task<> echo_session(boost::capy::any_stream& stream); +include::example$type-erased-echo/echo.hpp[tag=session_decl] ---- The header declares only the signature. It includes `any_stream` and `task`, but no concrete transport types. @@ -147,11 +68,7 @@ Clients of this header: [source,cpp] ---- -// echo.cpp -capy::task<> echo_session(capy::any_stream& stream) -{ - // Full implementation here -} +include::example$type-erased-echo/echo.cpp[tag=session_impl] ---- The implementation: diff --git a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc index 54dc9f2d0..9bf9edc54 100644 --- a/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc +++ b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc @@ -20,240 +20,7 @@ with a stop token. [source,cpp] ---- -// -// Copyright (c) 2026 Mungo Gill -// 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/capy -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// A slow operation that respects cancellation -capy::task slow_fetch(int steps) -{ - auto token = co_await capy::this_coro::stop_token; // std::stop_token - std::string result; - - for (int i = 0; i < steps; ++i) - { - // Check cancellation before each step - if (token.stop_requested()) - { - std::cout << " Cancelled at step " << i << std::endl; - throw std::system_error( - make_error_code(std::errc::operation_canceled)); - } - - result += "step" + std::to_string(i) + " "; - - // Simulate slow work (in real code, this would be I/O) - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - std::cout << " Completed step " << i << std::endl; - - // Yield to allow stop request to be processed before next check - // Extra 5ms ensures print completes before main thread prints - std::this_thread::sleep_for(std::chrono::milliseconds(15)); - } - - co_return result; -} - -// Shared between the fetch worker thread and the coroutine side of -// the race in demo_timeout below. -struct fetch_channel -{ - capy::async_waker fetch_ready; - std::atomic cancelled{false}; - std::string result; -}; - -// One side of the race: completes when the fetch worker thread -// wakes fetch_ready. If the deadline wins first, when_any's stop -// request cancels the wait; flag the worker so it stops early. -capy::io_task await_fetch(fetch_channel& ch) -{ - auto [ec] = co_await ch.fetch_ready.wait(); - if (ec) - { - ch.cancelled.store(true); - co_return capy::io_result{ec, {}}; - } - co_return capy::io_result{{}, std::move(ch.result)}; -} - -// The other side of the race: completes once a user thread wakes -// the waker. This is the escape hatch: the user supplies the -// thread and the clock. -capy::io_task<> deadline(capy::async_waker& waker) -{ - auto [ec] = co_await waker.wait(); - co_return capy::io_result<>{ec}; -} - -// Wraps slow_fetch, translating a cancellation exception into -// std::nullopt for the manual stop_token demo below. -capy::task> fetch_or_cancelled() -{ - try - { - auto result = co_await slow_fetch(5); // std::string - co_return result; - } - catch (std::system_error const& e) - { - if (e.code() == std::errc::operation_canceled) - co_return std::nullopt; - throw; - } -} - -void demo_timeout() -{ - std::cout << "Demo: Fetch races a deadline\n"; - - // Both when_any children are plain waker waits, so the whole - // race runs on one thread, as async_waker requires. The - // blocking work happens on user threads that wake them. - capy::thread_pool pool(1); - std::latch done(1); - - fetch_channel fetch_ch; - capy::async_waker deadline_waker; - - // Worker thread does the slow fetch and wakes fetch_ready on - // completion, checking the cancel flag between steps so a lost race stops - // the work promptly. Mirrors slow_fetch's step loop, kept - // separate since one polls an atomic and the other a stop token. - std::thread fetch_thread([&fetch_ch] { - for (int i = 0; i < 5; ++i) - { - if (fetch_ch.cancelled.load()) - { - std::cout << " Cancelled at step " << i << std::endl; - return; - } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - fetch_ch.result += "step" + std::to_string(i) + " "; - std::cout << " Completed step " << i << std::endl; - std::this_thread::sleep_for(std::chrono::milliseconds(15)); - } - fetch_ch.fetch_ready.wake(); - }); - - // Deadline thread: a user thread plays the clock, waking the - // waker after the allotted time. - std::thread deadline_thread([&deadline_waker] { - std::this_thread::sleep_for(std::chrono::milliseconds(30)); - deadline_waker.wake(); - }); - - // Index 1 = fetch won, index 2 = deadline won, index 0 = error. - std::size_t winner = 0; - std::string fetch_result; - - auto race = [&]() -> capy::task<> - { - auto result = co_await capy::when_any( - await_fetch(fetch_ch), - deadline(deadline_waker)); - winner = result.index(); - if (result.index() == 1) - fetch_result = std::get<1>(std::move(result)); - }; - - capy::run_async(pool.get_executor(), - [&done]() { done.count_down(); }, - [&done](std::exception_ptr) { done.count_down(); } - )(race()); - - done.wait(); // Block until the race completes - fetch_thread.join(); - deadline_thread.join(); - - // Report after the joins so the worker's final line lands - // before the verdict. - if (winner == 1) - // This demo's timing always favors the deadline; this branch - // shows how a fetch win would be consumed if it ever happened. - std::cout << "Result: " << fetch_result << "\n"; - else if (winner == 2) - std::cout << "Timed out waiting for fetch\n"; - else - std::cout << "Error\n"; -} - -void demo_cancellation() -{ - std::cout << "\nDemo: Cancellation after 2 steps\n"; - - capy::thread_pool pool; - std::stop_source source; - std::latch done(1); // std::latch - wait for 1 task - - // Launch the task - capy::run_async(pool.get_executor(), source.get_token(), - [&done](std::optional result) { - if (result) - std::cout << "Result: " << *result << "\n"; - else - std::cout << "Cancelled (returned nullopt)\n"; - done.count_down(); - }, - [&done](std::exception_ptr) { done.count_down(); } - )(fetch_or_cancelled()); - - // Simulate timeout: cancel after 2 steps complete - // Timing: each step is 10ms work + 15ms yield = 25ms total - // Step 1 prints at 35ms, step 2 check at 50ms - // Stop at 42ms: after step 1 print, before step 2 check - std::this_thread::sleep_for(std::chrono::milliseconds(42)); - std::cout << " Requesting stop..." << std::endl; - source.request_stop(); - - done.wait(); // Block until task completes (after cancellation) -} - -// Example: Manual stop token checking -capy::task process_items(std::vector const& items) -{ - auto token = co_await capy::this_coro::stop_token; // std::stop_token - int sum = 0; - - for (auto item : items) // int - { - if (token.stop_requested()) - { - std::cout << "Processing cancelled, partial sum: " << sum << "\n"; - co_return sum; // Return partial result - } - - sum += item; - } - - co_return sum; -} - -int main() -{ - demo_timeout(); - demo_cancellation(); - - return 0; -} +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=full] ---- == Build @@ -287,12 +54,7 @@ you spun up yourself to play the role of a clock, wakes it with `wake()`: [source,cpp] ---- -struct fetch_channel -{ - capy::async_waker fetch_ready; - std::atomic cancelled{false}; - std::string result; -}; +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=fetch_channel] ---- `wait()` returns an `io_result<>` that is empty on wakeup and carries @@ -311,9 +73,7 @@ races them with `when_any`: [source,cpp] ---- -auto result = co_await capy::when_any( - await_fetch(fetch_ch), - deadline(deadline_waker)); +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=race,indent=0] ---- `await_fetch` completes when the fetch worker thread finishes and wakes @@ -332,7 +92,7 @@ objects the demo owns directly. [source,cpp] ---- -auto token = co_await capy::this_coro::stop_token; // std::stop_token +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=get_stop_token,indent=0] ---- Inside a task, `this_coro::stop_token` retrieves the stop token propagated from the caller. You can also access it through the full environment via `co_await this_coro::environment`. @@ -341,10 +101,7 @@ Inside a task, `this_coro::stop_token` retrieves the stop token propagated from [source,cpp] ---- -if (token.stop_requested()) -{ - throw std::system_error(make_error_code(std::errc::operation_canceled)); -} +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=check_stop,indent=0] ---- Check `stop_requested()` at appropriate points—typically before expensive operations or at loop iterations. @@ -353,11 +110,7 @@ Check `stop_requested()` at appropriate points—typically before expensive oper [source,cpp] ---- -std::stop_source source; -capy::run_async(ex, source.get_token())(my_task()); - -// Later: -source.request_stop(); +include::example$snippets/8f_timeout_cancellation.cpp[tag=trigger_stop,indent=0] ---- The stop source controls the stop token. Calling `request_stop()` signals all holders of tokens from this source. @@ -366,10 +119,7 @@ The stop source controls the stop token. Calling `request_stop()` signals all ho [source,cpp] ---- -if (token.stop_requested()) -{ - co_return partial_result; // Return what we have -} +include::example$timeout-cancellation/timeout_cancellation.cpp[tag=partial_result,indent=0] ---- Cancellation doesn't have to throw. You can return partial results or a sentinel value. diff --git a/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc index 98513889a..02dcda3e4 100644 --- a/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc +++ b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc @@ -17,150 +17,7 @@ Running multiple operations concurrently with `when_all`. [source,cpp] ---- -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// Simulated async operations -capy::task fetch_user_id(std::string username) -{ - std::cout << "Fetching user ID for: " << username << "\n"; - // In real code: co_await http_get("/users/" + username); - co_return static_cast(username.length()) * 100; // Fake ID -} - -capy::task fetch_user_name(int id) -{ - std::cout << "Fetching name for user ID: " << id << "\n"; - co_return "User" + std::to_string(id); -} - -capy::task fetch_order_count(int user_id) -{ - std::cout << "Fetching order count for user: " << user_id << "\n"; - co_return user_id / 10; // Fake count -} - -capy::task fetch_account_balance(int user_id) -{ - std::cout << "Fetching balance for user: " << user_id << "\n"; - co_return user_id * 1.5; // Fake balance -} - -// Fetch all user data in parallel using variadic when_all. -// Heterogeneous return types are flattened into the result. -capy::task<> fetch_user_dashboard(std::string username) -{ - std::cout << "\n=== Fetching dashboard for: " << username << " ===\n"; - - // First, get the user ID (needed for other queries) - int user_id = co_await fetch_user_id(username); - std::cout << "Got user ID: " << user_id << "\n\n"; - - // when_all requires io_task children. Wrap plain tasks: - std::cout << "Starting parallel fetches...\n"; - - auto wrap = [](auto inner) -> capy::io_task { - co_return capy::io_result{ - {}, co_await std::move(inner)}; - }; - - auto [ec, name, orders, balance] = co_await capy::when_all( - wrap(fetch_user_name(user_id)), - wrap(fetch_order_count(user_id)), - wrap(fetch_account_balance(user_id))); - - std::cout << "\nDashboard results:\n"; - std::cout << " Name: " << name << "\n"; - std::cout << " Orders: " << orders << "\n"; - std::cout << " Balance: $" << balance << "\n"; -} - -// Example with void tasks -capy::io_task<> log_access(std::string resource) -{ - std::cout << "Logging access to: " << resource << "\n"; - co_return capy::io_result<>{}; -} - -capy::io_task<> update_metrics(std::string metric) -{ - std::cout << "Updating metric: " << metric << "\n"; - co_return capy::io_result<>{}; -} - -capy::task fetch_with_side_effects() -{ - std::cout << "\n=== Fetch with side effects ===\n"; - - auto r = co_await capy::when_all( - log_access("api/data"), - update_metrics("api_calls")); - if (r.ec) - co_return "error"; - - auto data = co_await fetch_user_name(42); - - std::cout << "Data: " << data << "\n"; - co_return data; -} - -// Error handling example -capy::io_task might_fail(bool should_fail, std::string name) -{ - std::cout << "Task " << name << " starting\n"; - - if (should_fail) - { - throw std::runtime_error(name + " failed!"); - } - - std::cout << "Task " << name << " completed\n"; - co_return capy::io_result{{}, 42}; -} - -capy::task<> demonstrate_error_handling() -{ - std::cout << "\n=== Error handling ===\n"; - - try - { - auto [ec2, a, b, c] = co_await capy::when_all( - might_fail(false, "A"), - might_fail(true, "B"), // This one fails - might_fail(false, "C")); - std::cout << "All succeeded: " << a << ", " - << b << ", " << c << "\n"; - } - catch (std::runtime_error const& e) - { - std::cout << "Caught error: " << e.what() << "\n"; - // Note: when_all waits for all tasks to complete (or respond to stop) - // before propagating the first exception - } -} - -int main() -{ - capy::thread_pool pool; - std::latch done(3); // std::latch - wait for 3 tasks - - // Completion handlers signal the latch when each task finishes - // Use generic lambda to accept any result type (or no result for task) - auto on_complete = [&done](auto&&...) { done.count_down(); }; - auto on_error = [&done](std::exception_ptr) { done.count_down(); }; - - capy::run_async(pool.get_executor(), on_complete, on_error)(fetch_user_dashboard("alice")); - capy::run_async(pool.get_executor(), on_complete, on_error)(fetch_with_side_effects()); - capy::run_async(pool.get_executor(), on_complete, on_error)(demonstrate_error_handling()); - - done.wait(); // Block until all tasks complete - return 0; -} +include::example$parallel-fetch/parallel_fetch.cpp[tag=full] ---- == Build @@ -177,10 +34,7 @@ target_link_libraries(parallel_fetch PRIVATE capy) [source,cpp] ---- -auto [ec, name, orders, balance] = co_await capy::when_all( - wrap(fetch_user_name(user_id)), - wrap(fetch_order_count(user_id)), - wrap(fetch_account_balance(user_id))); +include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_dashboard,indent=0] ---- `when_all` requires children returning `io_result`, so plain tasks are wrapped. All three run concurrently. The result is `io_result`, a single `ec` plus the flattened payloads in input order. @@ -189,11 +43,7 @@ auto [ec, name, orders, balance] = co_await capy::when_all( [source,cpp] ---- -auto r = co_await capy::when_all( - log_access("api/data"), - update_metrics("api_calls")); -if (r.ec) - co_return "error"; +include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_void,indent=0] ---- `io_task<>` children return `io_result<>` (just an error code, no payload). Check `r.ec` to detect failure. @@ -202,18 +52,7 @@ if (r.ec) [source,cpp] ---- -try -{ - auto [ec2, a, b, c] = co_await capy::when_all( - might_fail(false, "A"), - might_fail(true, "B"), - might_fail(false, "C")); -} -catch (...) -{ - // First exception is rethrown - // All tasks complete before exception propagates -} +include::example$parallel-fetch/parallel_fetch.cpp[tag=when_all_errors,indent=0] ---- I/O errors are reported via `ec` in the `io_result`. Thrown exceptions are captured separately — Upon error cancellation is requested and the first exception is rethrown after all tasks complete. diff --git a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc index ac00d7b3f..146aaae22 100644 --- a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc +++ b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc @@ -17,82 +17,7 @@ A complete echo server using Corosio for real network I/O. [source,cpp] ---- -#include -#include -#include - -namespace corosio = boost::corosio; -namespace capy = boost::capy; - -capy::task<> echo_session(corosio::tcp_socket sock) -{ - char buf[1024]; - - 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 (ec) - break; - - if (wec) - break; - } - - sock.close(); -} - -capy::task<> accept_loop( - corosio::tcp_acceptor& acc, - corosio::io_context& ioc) -{ - auto ep = acc.local_endpoint(); - std::cout << "Listening on port " << ep.port() << "\n"; - - for (;;) - { - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - - if (ec) - { - std::cout << "Accept error: " << ec.message() << "\n"; - continue; - } - - auto remote = peer.remote_endpoint(); - std::cout << "Connection from "; - if (remote.is_v4()) - std::cout << remote.v4_address(); - else - std::cout << remote.v6_address(); - std::cout << ":" << remote.port() << "\n"; - - capy::run_async(ioc.get_executor())( - echo_session(std::move(peer))); - } -} - -int main(int argc, char* argv[]) -{ - unsigned short port = 8080; - if (argc > 1) - port = static_cast(std::atoi(argv[1])); - - corosio::io_context ioc; - corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); - - capy::run_async(ioc.get_executor())( - accept_loop(acc, ioc)); - - ioc.run(); - - return 0; -} +include::example$echo-server-corosio/echo_server.cpp[tag=full] ---- == Build @@ -109,8 +34,7 @@ target_link_libraries(echo_server PRIVATE Boost::capy Boost::corosio) [source,cpp] ---- -corosio::io_context ioc; -corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); +include::example$echo-server-corosio/echo_server.cpp[tag=acceptor,indent=0] ---- The `io_context` drives all asynchronous I/O. The `tcp_acceptor` listens on the specified port. Corosio uses a flat namespace -- types like `tcp_socket`, `tcp_acceptor`, and `endpoint` live directly in `boost::corosio`. @@ -119,12 +43,7 @@ The `io_context` drives all asynchronous I/O. The `tcp_acceptor` listens on the [source,cpp] ---- -for (;;) -{ - corosio::tcp_socket peer(ioc); - auto [ec] = co_await acc.accept(peer); - // ... handle connection ... -} +include::example$echo-server-corosio/echo_server.cpp[tag=accept,indent=0] ---- The accept loop runs forever, creating a new `tcp_socket` for each connection. `acc.accept(peer)` suspends the coroutine until a client connects. @@ -133,11 +52,7 @@ The accept loop runs forever, creating a new `tcp_socket` for each connection. ` [source,cpp] ---- -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)); +include::example$echo-server-corosio/echo_server.cpp[tag=session_io,indent=0] ---- Each session reads data with `read_some` and writes it back with `write`. When the client disconnects, `read_some` returns an error and the loop exits. @@ -146,8 +61,7 @@ Each session reads data with `read_some` and writes it back with `write`. When t [source,cpp] ---- -capy::run_async(ioc.get_executor())( - echo_session(std::move(peer))); +include::example$echo-server-corosio/echo_server.cpp[tag=spawn_session,indent=0] ---- Each accepted connection moves the socket into a new task via `run_async`. The coroutine owns the socket for the lifetime of the session. Multiple clients are handled concurrently on the same `io_context`. diff --git a/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc index d7c66fc85..8eac01203 100644 --- a/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc +++ b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc @@ -17,62 +17,7 @@ Protecting shared state with a strand instead of a mutex. [source,cpp] ---- -#include -#include -#include -#include - -namespace capy = boost::capy; - -int main() -{ - constexpr int num_coroutines = 10; - constexpr int increments_per_coro = 1000; - - capy::thread_pool pool(4); - capy::strand s{pool.get_executor()}; - std::latch done(1); - - auto on_complete = [&done](auto&&...) { done.count_down(); }; - auto on_error = [&done](std::exception_ptr ep) { - try { std::rethrow_exception(ep); } - catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << "\n"; - } - catch (...) { - std::cerr << "Error: unknown exception\n"; - } - done.count_down(); - }; - - int counter = 0; - - // Each coroutine increments the shared counter without locks. - // The strand ensures only one coroutine runs at a time. - auto increment = [&](int id) -> capy::io_task<> { - for (int i = 0; i < increments_per_coro; ++i) - ++counter; - std::cout << "Coroutine " << id - << " finished, counter = " << counter << "\n"; - co_return capy::io_result<>{}; - }; - - auto run_all = [&]() -> capy::task<> { - std::vector> tasks; - for (int i = 0; i < num_coroutines; ++i) - tasks.push_back(increment(i)); - (void) co_await capy::when_all(std::move(tasks)); - }; - - capy::run_async(s, on_complete, on_error)(run_all()); - done.wait(); - - int expected = num_coroutines * increments_per_coro; - std::cout << "\nFinal counter: " << counter - << " (expected " << expected << ")\n"; - - return 0; -} +include::example$strand-serialization/strand_serialization.cpp[tag=full] ---- == Build @@ -89,7 +34,7 @@ target_link_libraries(strand_serialization PRIVATE Boost::capy) [source,cpp] ---- -capy::strand s{pool.get_executor()}; +include::example$strand-serialization/strand_serialization.cpp[tag=strand,indent=0] ---- A `strand` wraps an executor and guarantees that handlers dispatched through it never run concurrently. This replaces the need for a mutex when protecting shared state accessed by coroutines. @@ -98,13 +43,7 @@ A `strand` wraps an executor and guarantees that handlers dispatched through it [source,cpp] ---- -int counter = 0; - -auto increment = [&](int id) -> capy::io_task<> { - for (int i = 0; i < increments_per_coro; ++i) - ++counter; - // ... -}; +include::example$strand-serialization/strand_serialization.cpp[tag=counter,indent=0] ---- Multiple coroutines increment the same `counter` without any locks. The strand serializes execution so only one coroutine runs at a time, preventing data races. @@ -113,7 +52,7 @@ Multiple coroutines increment the same `counter` without any locks. The strand s [source,cpp] ---- -capy::run_async(s, on_complete, on_error)(run_all()); +include::example$strand-serialization/strand_serialization.cpp[tag=run_on_strand,indent=0] ---- Passing the strand `s` to `run_async` ensures the entire coroutine tree executes through the strand. Even though the underlying `thread_pool` has 4 threads, the strand constrains execution to one coroutine at a time. diff --git a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc index dfcac7d90..660d6d383 100644 --- a/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc +++ b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc @@ -17,78 +17,7 @@ Fair FIFO coroutine locking with `async_mutex`. [source,cpp] ---- -#include -#include -#include -#include - -namespace capy = boost::capy; - -int main() -{ - capy::thread_pool pool; - capy::strand s{pool.get_executor()}; - std::latch done(1); - - auto on_complete = [&done](auto&&...) { done.count_down(); }; - auto on_error = [&done](std::exception_ptr ep) { - try { std::rethrow_exception(ep); } - catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << "\n"; - } - catch (...) { - std::cerr << "Error: unknown exception\n"; - } - done.count_down(); - }; - - capy::async_mutex mtx; - int acquisition_order = 0; - std::vector order_log; - - auto worker = [&](int id) -> capy::io_task<> { - std::cout << "Worker " << id << " waiting for lock\n"; - auto [ec, guard] = co_await mtx.scoped_lock(); - if (ec) - { - std::cout << "Worker " << id - << " canceled: " << ec.message() << "\n"; - co_return capy::io_result<>{ec}; - } - - int seq = acquisition_order++; - order_log.push_back(id); - std::cout << "Worker " << id - << " acquired lock (sequence " << seq << ")\n"; - - std::cout << "Worker " << id << " releasing lock\n"; - co_return capy::io_result<>{}; - }; - - auto run_all = [&]() -> capy::task<> { - auto r = co_await capy::when_all( - worker(0), worker(1), worker(2), - worker(3), worker(4), worker(5)); - if(r.ec) - std::cerr << "when_all error: " - << r.ec.message() << "\n"; - }; - - // Run on a strand so async_mutex operations are single-threaded - capy::run_async(s, on_complete, on_error)(run_all()); - done.wait(); - - std::cout << "\nAcquisition order: "; - for (std::size_t i = 0; i < order_log.size(); ++i) - { - if (i > 0) - std::cout << " -> "; - std::cout << "W" << order_log[i]; - } - std::cout << "\n"; - - return 0; -} +include::example$async-mutex/async_mutex.cpp[tag=full] ---- == Build @@ -105,7 +34,7 @@ target_link_libraries(async_mutex PRIVATE Boost::capy) [source,cpp] ---- -capy::async_mutex mtx; +include::example$async-mutex/async_mutex.cpp[tag=mutex,indent=0] ---- `async_mutex` is a coroutine-aware mutex. Unlike `std::mutex`, it suspends the calling coroutine instead of blocking the thread, allowing other coroutines to run while waiting for the lock. @@ -114,12 +43,7 @@ capy::async_mutex mtx; [source,cpp] ---- -auto [ec, guard] = co_await mtx.scoped_lock(); -if (ec) -{ - // Lock was canceled - co_return capy::io_result<>{ec}; -} +include::example$async-mutex/async_mutex.cpp[tag=scoped_lock,indent=0] ---- `scoped_lock()` returns an `io_result` with an error code and an RAII guard. The guard automatically releases the lock when it goes out of scope. If the operation is canceled (e.g., via a stop token), `ec` will be set. diff --git a/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc index facdd194e..a496bf320 100644 --- a/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc @@ -16,76 +16,7 @@ Distributing CPU-bound work across a thread pool and collecting results. [source,cpp] ---- -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// Sum integers in [lo, hi) -capy::io_task partial_sum(int lo, int hi) -{ - std::ostringstream oss; - oss << " range [" << lo << ", " << hi - << ") on thread " << std::this_thread::get_id() << "\n"; - std::cout << oss.str(); - - long long sum = 0; - for (int i = lo; i < hi; ++i) - sum += i; - co_return capy::io_result{{}, sum}; -} - -int main() -{ - constexpr int total = 10000; - constexpr int num_tasks = 4; - constexpr int chunk = total / num_tasks; - - capy::thread_pool pool(num_tasks); - std::latch done(1); - - auto on_complete = [&done](auto&&...) { done.count_down(); }; - auto on_error = [&done](std::exception_ptr ep) { - try { std::rethrow_exception(ep); } - catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << "\n"; - } - catch (...) { - std::cerr << "Error: unknown exception\n"; - } - done.count_down(); - }; - - auto compute = [&]() -> capy::task<> { - std::cout << "Dispatching " << num_tasks - << " parallel tasks...\n"; - - auto [ec, s0, s1, s2, s3] = co_await capy::when_all( - partial_sum(0 * chunk, 1 * chunk), - partial_sum(1 * chunk, 2 * chunk), - partial_sum(2 * chunk, 3 * chunk), - partial_sum(3 * chunk, 4 * chunk)); - - long long total_sum = s0 + s1 + s2 + s3; - - // Arithmetic series: sum [0, N) = N*(N-1)/2 - long long expected = - static_cast(total) * (total - 1) / 2; - - std::cout << "\nPartial sums: " << s0 << " + " << s1 - << " + " << s2 << " + " << s3 << "\n"; - std::cout << "Total: " << total_sum - << " (expected " << expected << ")\n"; - }; - - capy::run_async(pool.get_executor(), on_complete, on_error)(compute()); - done.wait(); - - return 0; -} +include::example$programs/8m_parallel_tasks_variadic.cpp[tag=full] ---- == Build @@ -102,9 +33,7 @@ target_link_libraries(parallel_tasks PRIVATE Boost::capy) [source,cpp] ---- -constexpr int total = 10000; -constexpr int num_tasks = 4; -constexpr int chunk = total / num_tasks; +include::example$programs/8m_parallel_tasks_variadic.cpp[tag=partition,indent=0] ---- The range `[0, 10000)` is divided into 4 equal chunks, one per task. Each task computes a partial sum independently. @@ -113,11 +42,7 @@ The range `[0, 10000)` is divided into 4 equal chunks, one per task. Each task c [source,cpp] ---- -auto [ec, s0, s1, s2, s3] = co_await capy::when_all( - partial_sum(0 * chunk, 1 * chunk), - partial_sum(1 * chunk, 2 * chunk), - partial_sum(2 * chunk, 3 * chunk), - partial_sum(3 * chunk, 4 * chunk)); +include::example$programs/8m_parallel_tasks_variadic.cpp[tag=when_all_variadic,indent=0] ---- `when_all` launches all four tasks concurrently on the thread pool. Each task may run on a different thread. The result is `io_result`, a single `ec` plus the four partial sums in input order. @@ -126,10 +51,7 @@ auto [ec, s0, s1, s2, s3] = co_await capy::when_all( [source,cpp] ---- -std::ostringstream oss; -oss << " range [" << lo << ", " << hi - << ") on thread " << std::this_thread::get_id() << "\n"; -std::cout << oss.str(); +include::example$programs/8m_parallel_tasks_variadic.cpp[tag=thread_id,indent=0] ---- Each task prints its thread ID. On a multi-core system you will see different thread IDs, confirming true parallel execution. The `ostringstream` ensures each line is printed atomically. diff --git a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc index 2c9183118..3f69d232a 100644 --- a/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc +++ b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc @@ -16,148 +16,7 @@ Implementing the Executor concept with a single-threaded run loop. [source,cpp] ---- -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -// A minimal single-threaded execution context. -// Demonstrates how to satisfy the Executor concept -// for any custom scheduling system. -class run_loop : public capy::execution_context -{ - std::queue> queue_; - std::thread::id owner_; - -public: - class executor_type; - - run_loop() - : execution_context(this) - { - } - - ~run_loop() - { - shutdown(); - destroy(); - } - - run_loop(run_loop const&) = delete; - run_loop& operator=(run_loop const&) = delete; - - // Drain the queue until empty - void run() - { - owner_ = std::this_thread::get_id(); - while (!queue_.empty()) - { - auto h = queue_.front(); - queue_.pop(); - capy::safe_resume(h); - } - } - - void enqueue(std::coroutine_handle<> h) - { - queue_.push(h); - } - - bool is_running_on_this_thread() const noexcept - { - return std::this_thread::get_id() == owner_; - } - - executor_type get_executor() noexcept; -}; - -class run_loop::executor_type -{ - friend class run_loop; - run_loop* loop_ = nullptr; - - explicit executor_type(run_loop& loop) noexcept - : loop_(&loop) - { - } - -public: - executor_type() = default; - - capy::execution_context& context() const noexcept - { - return *loop_; - } - - void on_work_started() const noexcept {} - void on_work_finished() const noexcept {} - - std::coroutine_handle<> dispatch( - capy::continuation& c) const - { - if (loop_->is_running_on_this_thread()) - return c.h; - loop_->enqueue(c.h); - return std::noop_coroutine(); - } - - void post(capy::continuation& c) const - { - loop_->enqueue(c.h); - } - - bool operator==(executor_type const& other) const noexcept - { - return loop_ == other.loop_; - } -}; - -inline -run_loop::executor_type -run_loop::get_executor() noexcept -{ - return executor_type{*this}; -} - -// Verify the concept is satisfied -static_assert(capy::Executor); - -capy::io_task compute(int x) -{ - std::cout << " computing " << x << " * " << x << "\n"; - co_return capy::io_result{{}, x * x}; -} - -capy::task<> run_tasks() -{ - std::cout << "Launching 3 tasks with when_all...\n"; - - auto [ec, r1, r2, r3] = co_await capy::when_all( - compute(3), compute(7), compute(11)); - - std::cout << "\nResults: " << r1 << ", " << r2 - << ", " << r3 << "\n"; - std::cout << "Sum of squares: " - << r1 + r2 + r3 << "\n"; -} - -int main() -{ - run_loop loop; - - // Launch using run_async, just like with thread_pool - capy::run_async(loop.get_executor())(run_tasks()); - - // Drive the loop — all coroutines execute here - std::cout << "Running event loop on main thread...\n"; - loop.run(); - - std::cout << "Event loop finished.\n"; - return 0; -} +include::example$custom-executor/custom_executor.cpp[tag=full] ---- == Build @@ -174,14 +33,7 @@ target_link_libraries(custom_executor PRIVATE Boost::capy) [source,cpp] ---- -class run_loop : public capy::execution_context -{ - // ... - run_loop() - : execution_context(this) - { - } -}; +include::example$custom-executor/custom_executor.cpp[tag=inherit] ---- Custom execution contexts inherit from `execution_context` and pass `this` to the base constructor. The destructor must call `shutdown()` then `destroy()` to clean up coroutine state. @@ -198,7 +50,7 @@ The nested `executor_type` must provide: [source,cpp] ---- -static_assert(capy::Executor); +include::example$custom-executor/custom_executor.cpp[tag=concept_check] ---- The `static_assert` verifies at compile time that all concept requirements are met. @@ -207,14 +59,7 @@ The `static_assert` verifies at compile time that all concept requirements are m [source,cpp] ---- -std::coroutine_handle<> dispatch( - capy::continuation& c) const -{ - if (loop_->is_running_on_this_thread()) - return c.h; // resume inline - loop_->enqueue(c.h); - return std::noop_coroutine(); // defer -} +include::example$custom-executor/custom_executor.cpp[tag=dispatch,indent=0] ---- `dispatch` takes a `continuation&` and checks whether the caller is already running on the loop's thread. If so, it returns `c.h` directly for inline resumption via symmetric transfer. Otherwise it enqueues `c.h` and returns `noop_coroutine` so the caller continues without blocking. @@ -225,8 +70,7 @@ std::coroutine_handle<> dispatch( [source,cpp] ---- -capy::run_async(loop.get_executor())(run_tasks()); -loop.run(); +include::example$custom-executor/custom_executor.cpp[tag=drive,indent=0] ---- `run_async` enqueues the initial coroutine. `loop.run()` drains the queue, resuming coroutines one by one until all work completes. This is analogous to a GUI event loop or game tick loop. diff --git a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc index 3f42129a8..71441e105 100644 --- a/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc +++ b/doc/modules/ROOT/pages/8.examples/8o.sender-bridge.adoc @@ -20,17 +20,11 @@ This example is built only when `BOOST_CAPY_BUILD_P2300_EXAMPLES=ON` (it require == The Bridge -A P2300 sender is a foreign awaitable: it has no two-argument `await_suspend`, so a Capy `task` cannot `co_await` it directly. `await_sender` wraps it in an `IoAwaitable`. The key move is in the bridge's _receiver_: when the sender completes—on whatever thread its scheduler chose—it does not resume the coroutine inline. It stores the result and posts the continuation back through the caller's executor. +A P2300 sender is a foreign awaitable: it has no two-argument `await_suspend`, so a Capy `task` cannot `co_await` it directly. `await_sender` wraps it in an `IoAwaitable`. The key move is in the bridge's _receiver_: when the sender completes—on whatever thread its scheduler chose—it does not resume the coroutine inline. It stores the result and posts the continuation back through the caller's executor. From `example/sender-bridge/sender_awaitable.hpp`: [source,cpp] ---- -// From example/sender-bridge/sender_awaitable.hpp (abridged): -template -void set_value(Args&&... args) && noexcept -{ - result_->template emplace<1>(std::forward(args)...); - env_->executor.post(cont_); // resume on the caller's executor -} +include::example$sender-bridge/sender_awaitable.hpp[tag=set_value,indent=0] ---- `await_sender` inspects the sender's completion signatures. If the sender can complete with `set_error(std::error_code)`, the bridge yields `io_result` so the error stays a value rather than an exception; otherwise it yields the value directly. @@ -39,55 +33,7 @@ void set_value(Args&&... args) && noexcept [source,cpp] ---- -#include "sender_awaitable.hpp" - -#include -#include - -#include -#include -#include - -namespace capy = boost::capy; -namespace ex = beman::execution; - -capy::task compute(auto sched) -{ - int result = co_await capy::await_sender( - ex::schedule(sched) - | ex::then([] { - std::cout << " sender running on thread " - << std::this_thread::get_id() << "\n"; - return 42 * 42; - })); - - std::cout << " coroutine resumed on thread " - << std::this_thread::get_id() << "\n"; - - co_return result; -} - -int main() -{ - std::cout << "main thread: " << std::this_thread::get_id() << "\n"; - - capy::thread_pool pool; // Capy execution context - - ex::run_loop loop; // Beman context on its own thread - std::jthread loop_thread([&loop] { loop.run(); }); - auto sched = loop.get_scheduler(); - - std::latch done(1); - int answer = 0; - auto on_complete = [&](int v) { answer = v; done.count_down(); }; - auto on_error = [&](std::exception_ptr) { done.count_down(); }; - - capy::run_async(pool.get_executor(), on_complete, on_error)(compute(sched)); - - done.wait(); - loop.finish(); - std::cout << "result: " << answer << "\n"; -} +include::example$sender-bridge/sender_bridge.cpp[tag=full] ---- == Output diff --git a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc index fda96ecd4..a612213a0 100644 --- a/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc +++ b/doc/modules/ROOT/pages/8.examples/8p.asio-use-capy.adoc @@ -20,28 +20,11 @@ This example is built when a `Boost::asio` target is available. The full source == The Completion Token -Asio async operations accept a _completion token_ that decides what the call returns. `use_capy` is a token whose `async_result` returns an `IoAwaitable`. When co-awaited, its `await_suspend` starts the Asio operation and arranges the completion handler to post the resumption through the caller's executor—so the coroutine resumes on the executor it was launched with, never on whatever thread Asio completed on: +Asio async operations accept a _completion token_ that decides what the call returns. `use_capy` is a token whose `async_result` returns an `IoAwaitable`. When co-awaited, its `await_suspend` starts the Asio operation and arranges the completion handler to post the resumption through the caller's executor—so the coroutine resumes on the executor it was launched with, never on whatever thread Asio completed on. From `example/asio/api/use_capy.hpp`: [source,cpp] ---- -// From example/asio/api/use_capy.hpp (abridged): -std::coroutine_handle<> await_suspend( - std::coroutine_handle<> h, capy::io_env const* env) -{ - cancel_ = std::make_shared(env->stop_token); - - cont_.h = h; // stable address; posted through the executor on completion - auto handler = [this, ex = env->executor](Args... args) mutable - { - store_result(std::move(args)...); - ex.post(cont_); - }; - - std::move(op_)(net::bind_cancellation_slot( - cancel_->signal.slot(), std::move(handler))); - - return std::noop_coroutine(); -} +include::example$asio/api/use_capy.hpp[tag=await_suspend,indent=0] ---- == Using the Token @@ -50,26 +33,7 @@ Pass `use_capy` where an Asio operation expects a completion token. The `co_awai [source,cpp] ---- -capy::io_task<> -writer(net::ip::tcp::socket& socket, std::size_t total) -{ - char buf[128]; - std::memset(buf, 'X', sizeof(buf)); - - std::size_t written = 0; - while (written < total) - { - std::size_t chunk = (std::min)(sizeof(buf), total - written); - - auto [ec, n] = co_await socket.async_write_some( - net::buffer(buf, chunk), use_capy); - - if (ec) - co_return capy::io_result<>{ec}; - written += n; - } - co_return capy::io_result<>{}; -} +include::example$asio/use_capy_example.cpp[tag=writer] ---- The reader is symmetric, using `async_read_some`. `run_example` drives both concurrently over a connected socket pair with `when_all`, then reports completion. diff --git a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc index ab6137fb1..56023bdab 100644 --- a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc +++ b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc @@ -24,9 +24,7 @@ The first layer is concepts. These are the template-based interfaces: `ReadStrea [source,cpp] ---- -template -io_task -write(S& stream, CB buffers); +include::example$snippets/9a_capy_layering.cpp[tag=write_signature,indent=0] ---- The cost is that templates propagate. Every caller sees the full implementation. Compile times grow. You cannot hide this behind a `.cpp` file. @@ -35,18 +33,7 @@ The second layer is type-erased wrappers. `any_stream`, `any_read_stream`, `any_ [source,cpp] ---- -task<> echo(any_stream& stream) -{ - char buf[1024]; - for(;;) - { - auto [ec, n] = co_await stream.read_some( - make_buffer(buf)); - if(ec) - co_return; - co_await write(stream, const_buffer(buf, n)); - } -} +include::example$snippets/9a_capy_layering.cpp[tag=any_stream_echo,indent=0] ---- The cost is a virtual call per I/O operation. For operations dominated by syscalls and network latency, this cost is invisible. For tight loops over in-memory buffers, it matters. @@ -55,13 +42,7 @@ The third layer is coroutine type erasure via `task<>`. This is the most powerfu [source,cpp] ---- -class connection_base { -public: - task<> run(); -protected: - virtual task<> do_handshake() = 0; - virtual task<> do_shutdown() = 0; -}; +include::example$snippets/9a_capy_layering.cpp[tag=connection_base,indent=0] ---- The SSL derived class performs a TLS handshake inside `do_handshake()`. The TCP derived class just connects. The base class implements all the shared business logic against an `any_stream&` and never knows what is underneath. @@ -97,7 +78,7 @@ The derived class pattern is the practical application of everything described a Each derived class lives in its own translation unit. The linker only pulls in what is used. Users who need only TCP link only TCP code. Users who need SSL link the SSL translation unit. No variant that pulls in all transport code. No enum and switch that ties everything together. The type system enforces the separation: -[source,cpp] +[source,cpp,role=pseudocode] ---- // User who needs only plain TCP tcp_connection conn(ctx); diff --git a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc index 102b3a6f1..5f31a7d67 100644 --- a/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc @@ -8,15 +8,7 @@ This document describes the design of the `ReadStream` concept: the fundamental [source,cpp] ---- -template -concept ReadStream = - requires(T& stream, mutable_buffer_archetype buffers) - { - { stream.read_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.read_some(buffers)), - std::error_code, std::size_t>; - }; +include::example$snippets/9c_read_stream.cpp[tag=concept_definition,indent=0] ---- The `requires` clause checks `read_some` against a single representative buffer, `mutable_buffer_archetype`, because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a `ReadStream` must accept *any* `MutableBufferSequence`—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. @@ -58,8 +50,7 @@ The caller must ensure that the memory referenced by `buffers` remains valid unt [source,cpp] ---- -template -IoAwaitable auto read_some(Buffers buffers); +include::example$snippets/9c_read_stream.cpp[tag=read_some_signature,indent=0] ---- Buffer sequences should be accepted by value when the member function is a coroutine, to ensure the sequence lives in the coroutine frame across suspension points. @@ -94,27 +85,14 @@ A composed algorithm builds on `read_some`: [source,cpp] ---- -auto read(ReadStream auto& stream, - MutableBufferSequence auto const& buffers) - -> io_task; +include::example$snippets/9c_read_stream.cpp[tag=read_signature,indent=0] ---- Loops `read_some` until the entire buffer sequence is filled or an error (including EOF) occurs. On success, `n == buffer_size(buffers)`. [source,cpp] ---- -template -task<> read_header(Stream& stream) -{ - char header[16]; - auto [ec, n] = co_await read( - stream, make_buffer(header)); - if(ec == cond::eof) - co_return; // clean shutdown - if(ec) - co_return; - // header contains exactly 16 bytes -} +include::example$snippets/9c_read_stream.cpp[tag=read_header,indent=0] ---- == Use Cases @@ -125,25 +103,7 @@ When processing data as it arrives without waiting for a full buffer, `read_some [source,cpp] ---- -template -task<> echo(Stream& stream, WriteStream auto& dest) -{ - char buf[4096]; - for(;;) - { - auto [ec, n] = co_await stream.read_some( - make_buffer(buf)); - - auto [wec, nw] = co_await dest.write_some( - const_buffer(buf, n)); - - if(ec) - co_return; - - if(wec) - co_return; - } -} +include::example$snippets/9c_read_stream.cpp[tag=echo,indent=0] ---- == Relationship to the Write Side @@ -202,11 +162,7 @@ Every composed read algorithm that accumulates progress follows the same pattern [source,cpp] ---- -auto [ec, n] = co_await s.read_some( - mutable_buffer(buf + total, size - total)); -total += n; -if(ec) - co_return; +include::example$snippets/9c_read_stream.cpp[tag=canonical_loop,indent=0] ---- The advance-then-check ordering is the only correct pattern. It is required for any operation that can report partial progress alongside an error -- `read` returning `(eof, 47)` being the canonical example. If the check precedes the advance, the 47 bytes are silently dropped. diff --git a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc index 6759c2654..1a8050dd3 100644 --- a/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc +++ b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc @@ -8,15 +8,7 @@ This document describes the design of the `WriteStream` concept: the fundamental [source,cpp] ---- -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; +include::example$snippets/9f_write_stream.cpp[tag=write_stream_concept] ---- The `requires` clause checks `write_some` against a single representative buffer, `const_buffer_archetype`, because a {cpp} concept cannot quantify over "every buffer sequence." The contract is stronger than what the compiler verifies: a `WriteStream` must accept *any* `ConstBufferSequence`—a single buffer or a range of them—and the archetype stands in for that universally-quantified requirement. Read this as a textual requirement that the concept can only sample, not a claim that conformance is limited to the archetype type. @@ -54,8 +46,7 @@ The caller must ensure that the memory referenced by `buffers` remains valid unt [source,cpp] ---- -template -IoAwaitable auto write_some(Buffers buffers); +include::example$snippets/9f_write_stream.cpp[tag=write_some_signature,indent=0] ---- Buffer sequences should be accepted by value when the member function is a coroutine, to ensure the sequence lives in the coroutine frame across suspension points. @@ -81,9 +72,7 @@ Two composed algorithms build complete-write behavior on top of `write_some`: [source,cpp] ---- -auto write(WriteStream auto& stream, - ConstBufferSequence auto buffers) - -> io_task; +include::example$snippets/9f_write_stream.cpp[tag=write_signature] ---- Loops `write_some` until the entire buffer sequence is consumed. Always suspends (returns `task`). No frame caching. @@ -92,14 +81,7 @@ Loops `write_some` until the entire buffer sequence is consumed. Always suspends [source,cpp] ---- -template -class write_now -{ -public: - explicit write_now(Stream& s) noexcept; - - IoAwaitable auto operator()(ConstBufferSequence auto buffers); -}; +include::example$snippets/9f_write_stream.cpp[tag=write_now_sketch] ---- Loops `write_some` until the entire buffer sequence is consumed, with two advantages over the free function: @@ -115,7 +97,7 @@ A composed algorithm like `write_now` cannot do this. It receives a fixed buffer === Diagram: Relaying 100KB from a ReadSource through a TCP Socket -Consider relaying 100KB from a `ReadSource` to a TCP socket. The kernel's send buffer accepts at most 40KB per call. Compare two approaches: +Consider relaying 100KB from a `ReadStream` to a TCP socket. The kernel's send buffer accepts at most 40KB per call. Compare two approaches: ==== Approach A: `write_some` with Top-Up (3 syscalls) @@ -146,35 +128,7 @@ This example reads from a `ReadSource` and writes to a `WriteStream` using `writ [source,cpp] ---- -template -task<> relay_with_write_now(Source& src, Stream& dest) -{ - char buf[65536]; - write_now wn(dest); - - for(;;) - { - // Read a chunk from the source - auto [rec, nr] = co_await src.read( - mutable_buffer(buf, sizeof(buf))); - if(rec == cond::eof && nr == 0) - co_return; - - // write_now drains the chunk to completion. - // If the kernel accepts 40KB of 64KB, write_now - // internally calls write_some(24KB) for the - // remainder -- a small write that wastes a - // syscall. The caller cannot top up between - // write_now's internal iterations. - auto [wec, nw] = co_await wn( - const_buffer(buf, nr)); - if(wec) - co_return; - - if(rec == cond::eof) - co_return; - } -} +include::example$snippets/9f_write_stream.cpp[tag=relay_with_write_now] ---- After the kernel accepts 40KB of a 64KB chunk, `write_now` must send the remaining 24KB in a second `write_some`. The caller cannot intervene to refill the buffer because `write_now` owns the loop. That 24KB write wastes an opportunity to send a full 64KB payload. diff --git a/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc b/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc index 82fa9fae9..2d1a25252 100644 --- a/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc +++ b/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc @@ -10,7 +10,7 @@ The vtable layout depends on how many async operations the wrapper exposes and w When a wrapper exposes exactly one async operation (e.g. `any_read_stream` with `read_some`, or `any_write_stream` with `write_some`), all function pointers live in a single flat vtable: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Flat vtable -- 64 bytes, one cache line struct vtable @@ -32,7 +32,7 @@ The inner awaitable can be constructed in either `await_ready` or `await_suspend When there is no outer short-circuit, constructing in `await_ready` lets immediate completions skip `await_suspend` entirely: -[source,cpp] +[source,cpp,role=pseudocode] ---- bool await_ready() { vt_->construct_awaitable(stream_, cached_awaitable_, buffers); @@ -56,7 +56,7 @@ io_result await_resume() { When the outer awaitable has a short-circuit (empty buffers), construction is deferred to `await_suspend` so the inner awaitable is never created on the fast path: -[source,cpp] +[source,cpp,role=pseudocode] ---- bool await_ready() const noexcept { return buffers_.empty(); // short-circuit, no construct @@ -91,7 +91,7 @@ Two sub-cases arise, distinguished by whether the operations share an await-retu * *Same await-return type* (`any_read_source`: `read_some` and `read` both await-return `io_result`). One ops struct (`awaitable_ops`) serves both operations; both `construct_*_awaitable` pointers return the same layout. * *Different await-return types* (`any_buffer_source`: `pull` await-returns `io_result>` while its synthesized `read_some`/`read` await-return `io_result`; `any_buffer_sink` and `any_write_sink` similarly mix `io_result<>`, `io_result`). These need more than one ops struct (e.g. `awaitable_ops` plus `read_awaitable_ops`/`write_awaitable_ops`/`eof_awaitable_ops`), and each `construct` returns the ops matching its result type. -[source,cpp] +[source,cpp,role=pseudocode] ---- // Per-awaitable dispatch -- one struct per await-return type struct awaitable_ops @@ -115,7 +115,7 @@ struct vtable The inner awaitable is constructed in `await_suspend`. Outer `await_ready` handles short-circuits (e.g. empty buffers) before the inner type is ever created: -[source,cpp] +[source,cpp,role=pseudocode] ---- bool await_ready() const noexcept { return buffers_.empty(); // short-circuit diff --git a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc index d6762adeb..f96a83670 100644 --- a/doc/modules/ROOT/pages/9.design/9k.Executor.adoc +++ b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc @@ -10,27 +10,7 @@ The `Executor` concept exists to answer one question: when a coroutine is ready [source,cpp] ---- -template -concept Executor = - std::is_nothrow_copy_constructible_v && - std::is_nothrow_move_constructible_v && - requires(E& e, E const& ce, E const& ce2, - continuation c) - { - { ce == ce2 } noexcept -> std::convertible_to; - { ce.context() } noexcept; - requires std::is_lvalue_reference_v< - decltype(ce.context())> && - std::derived_from< - std::remove_reference_t< - decltype(ce.context())>, - execution_context>; - { ce.on_work_started() } noexcept; - { ce.on_work_finished() } noexcept; - - { ce.dispatch(c) } -> std::same_as>; - { ce.post(c) }; - }; +include::example$snippets/9k_executor.cpp[tag=executor_concept] ---- An `Executor` provides exactly two scheduling operations: @@ -93,13 +73,7 @@ A conforming implementation: [source,cpp] ---- -std::coroutine_handle<> dispatch(continuation& c) const -{ - if(ctx_.running_in_this_thread()) - return c.h; // symmetric transfer - post(c); - return std::noop_coroutine(); -} +include::example$snippets/9k_executor.cpp[tag=dispatch_impl,indent=0] ---- This design enables the common fast path -- same-executor dispatch at `final_suspend` -- to avoid queuing entirely, transferring control directly via symmetric transfer. @@ -163,11 +137,7 @@ The executor accepts `continuation&` rather than `std::coroutine_handle<>`. A `c [source,cpp] ---- -struct continuation -{ - std::coroutine_handle<> h; - void* reserved = nullptr; -}; +include::example$snippets/9k_executor.cpp[tag=continuation_struct] ---- This design has three consequences: @@ -200,12 +170,7 @@ In Corosio, `on_work_finished` triggers `stop()` when the atomic work count reac [source,cpp] ---- -void on_work_finished() noexcept -{ - if(outstanding_work_.fetch_sub( - 1, std::memory_order_acq_rel) == 1) - stop(); -} +include::example$snippets/9k_executor.cpp[tag=on_work_finished,indent=0] ---- Every `run_async` call increments the count. When the launched task completes, the count decrements. When no tasks remain, `run()` returns. Without work tracking in the executor, the event loop would need a separate signaling mechanism or would spin indefinitely. @@ -237,12 +202,7 @@ Corosio demonstrates this pattern throughout its public API. I/O objects accept [source,cpp] ---- -template - requires capy::Executor -explicit tcp_socket(Ex const& ex) - : tcp_socket(ex.context()) -{ -} +include::example$snippets/9k_executor.cpp[tag=socket_ctor,indent=0] ---- The socket constructor receives `execution_context&` and looks up the socket service. The concrete context type -- `epoll_context`, `iocp_context`, `select_context` -- is irrelevant to the socket. @@ -257,11 +217,7 @@ The entire object is two pointers: [source,cpp] ---- -class executor_ref -{ - void const* ex_; // pointer to the executor - detail::executor_vtable const* vt_; // pointer to the vtable -}; +include::example$snippets/9k_executor.cpp[tag=executor_ref_layout] ---- Two pointers fit in two registers. `executor_ref` can be passed by value as cheaply as passing a pointer. No heap allocation, no small-buffer optimization, no reference counting. @@ -298,16 +254,7 @@ When a coroutine `co_await`s an I/O awaitable, the awaitable's `await_suspend` r [source,cpp] ---- -std::coroutine_handle<> -await_suspend( - std::coroutine_handle<> h, - io_env const* env) noexcept -{ - cont_.h = h; - ex_ = env->executor; - // ... initiate I/O operation ... - return std::noop_coroutine(); -} +include::example$snippets/9k_executor.cpp[tag=capture_at_initiation,indent=0] ---- === Dispatch at Completion @@ -316,8 +263,7 @@ When the I/O completes (from the reactor thread for epoll, the completion port f [source,cpp] ---- -// Timer fires or I/O completes: -ex_.post(cont_); +include::example$snippets/9k_executor.cpp[tag=dispatch_at_completion,indent=0] ---- `post` links the continuation into the executor's work queue via `cont_.reserved`. No heap allocation occurs -- the continuation is embedded in the awaitable, which is alive for the duration of the suspension. A worker thread dequeues the continuation and calls `cont_.h.resume()`. @@ -338,13 +284,7 @@ The fix is to save and restore TLS around every `.resume()` call: [source,cpp] ---- -inline void -safe_resume(std::coroutine_handle<> h) noexcept -{ - auto* saved = get_current_frame_allocator(); - h.resume(); - set_current_frame_allocator(saved); -} +include::example$snippets/9k_executor.cpp[tag=safe_resume] ---- This makes TLS behave like a stack. Each nested resume pushes its own allocator; when the coroutine suspends and `.resume()` returns, the previous value is restored. The cost is two TLS accesses (one read, one write) per `.resume()` call -- negligible compared to the cost of resuming a coroutine. @@ -382,19 +322,7 @@ A minimal executor implementation: [source,cpp] ---- -class my_executor -{ -public: - execution_context& context() const noexcept; - - void on_work_started() const noexcept; - void on_work_finished() const noexcept; - - std::coroutine_handle<> dispatch(continuation& c) const; - void post(continuation& c) const; - - bool operator==(my_executor const&) const noexcept; -}; +include::example$snippets/9k_executor.cpp[tag=minimal_executor] ---- == Summary diff --git a/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc b/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc index e18a805f5..ee7790cdc 100644 --- a/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc +++ b/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc @@ -12,29 +12,7 @@ This document explains the naming conventions and call syntax of the two launche [source,cpp] ---- -// Executor only (uses default recycling allocator) -run_async(ex)(my_task()); - -// With a stop token for cooperative cancellation -std::stop_source source; -run_async(ex, source.get_token())(cancellable_task()); - -// With a custom memory resource -run_async(ex, my_pool)(my_task()); - -// With a result handler -run_async(ex, [](int result) { - std::cout << "Got: " << result << "\n"; -})(compute_value()); - -// With separate success and error handlers -run_async(ex, - [](int result) { std::cout << "Got: " << result << "\n"; }, - [](std::exception_ptr ep) { /* handle error */ } -)(compute_value()); - -// Full: executor, stop_token, allocator, success handler, error handler -run_async(ex, st, alloc, h1, h2)(my_task()); +include::example$snippets/9l_run_api.cpp[tag=run_async_usage,indent=0] ---- === `run` -- Awaitable Launch Within a Coroutine @@ -43,31 +21,7 @@ run_async(ex, st, alloc, h1, h2)(my_task()); [source,cpp] ---- -// Switch to a different executor for CPU-bound work -task parent() -{ - int result = co_await run(worker_ex)(compute_on_worker()); - // Completion returns to parent's executor -} - -// Customize stop token, inherit caller's executor -task with_timeout() -{ - std::stop_source source; - co_await run(source.get_token())(subtask()); -} - -// Customize allocator, inherit caller's executor -task with_custom_alloc() -{ - co_await run(my_alloc)(subtask()); -} - -// Switch executor AND customize allocator -task full_control() -{ - co_await run(worker_ex, my_alloc)(cpu_bound_task()); -} +include::example$snippets/9l_run_api.cpp[tag=run_usage] ---- === `run_async` on a Strand @@ -76,11 +30,7 @@ A common pattern for launching per-connection coroutines on a strand, ensuring s [source,cpp] ---- -void on_accept(tcp::socket sock) -{ - strand my_strand(ioc.get_executor()); - run_async(my_strand)(handle_connection(std::move(sock))); -} +include::example$snippets/9l_run_api.cpp[tag=run_async_strand] ---- == Alternatives Considered @@ -111,7 +61,7 @@ Several alternative naming and syntax proposals were evaluated and discarded. Th What this looks like in practice: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Rejected: builder pattern capy::on(ex).spawn(my_task()); @@ -128,7 +78,7 @@ The builder pattern reads well as English, but it creates problems in {cpp} prac === Single-Call with Named Method -[source,cpp] +[source,cpp,role=pseudocode] ---- // Rejected: single-call run_async(ex, my_task()); @@ -138,7 +88,7 @@ This fails the allocator timing constraint entirely. The task argument `my_task( === Named Method on Wrapper -[source,cpp] +[source,cpp,role=pseudocode] ---- // Rejected: named method instead of operator() run_async(ex).spawn(my_task()); @@ -168,7 +118,7 @@ The `run` prefix was chosen for several reasons: An alternative proposal suggested replacing the two-call syntax with a builder-style API: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Rejected builder pattern capy::on(ex).spawn(my_task()); @@ -182,7 +132,7 @@ While the English readability of `on(ex).spawn(t)` is genuinely appealing, the a - **Namespace pollution.** `on` and `with` are among the most common English words in programming. In a Boost library used alongside dozens of other namespaces, these names invite collisions. Consider what happens with `using namespace capy;`: + -[source,cpp] +[source,cpp,role=pseudocode] ---- int on = 42; // local variable on(ex).spawn(my_task()); // ambiguous: variable or function? @@ -195,7 +145,7 @@ void handle(auto with) { // parameter name - **Semantic ambiguity.** `with(st)` versus `with(alloc)` -- with _what_, exactly? The current API uses `run(st)` and `run(alloc)` where overload resolution disambiguates naturally because the verb `run` provides context. A bare preposition provides none. + -[source,cpp] +[source,cpp,role=pseudocode] ---- // What does "with" mean here? Stop token or allocator? co_await capy::with(x).call(subtask()); @@ -206,7 +156,7 @@ co_await run(x)(subtask()); - **Builder illusion.** Dot-chaining suggests composability that does not exist. Users will naturally try: + -[source,cpp] +[source,cpp,role=pseudocode] ---- // These look reasonable but don't work capy::on(ex).with(alloc).call(my_task()); @@ -262,11 +212,7 @@ In the expression `run_async(ex)(my_task())`: [source,cpp] ---- -// Step 1: wrapper constructor sets TLS allocator -// v~~~~~~~~~~~~~~v - run_async(ex, alloc) (my_task()); -// ^~~~~~~~~^ -// Step 2: task frame allocated using TLS allocator +include::example$snippets/9l_run_api.cpp[tag=postfix_order,indent=0] ---- This sequencing is not an implementation detail -- it is the _only correct way_ to inject an allocator into a coroutine's frame allocation when the allocator is not known at compile time. @@ -277,22 +223,7 @@ The `run_async_wrapper` constructor sets the thread-local allocator: [source,cpp] ---- -run_async_wrapper(Ex ex, std::stop_token st, Handlers h, Alloc a) noexcept - : tr_(detail::make_trampoline( - std::move(ex), std::move(h), std::move(a))) - , st_(std::move(st)) - , saved_tls_(get_current_frame_allocator()) // remember prior TLS -{ - // Set TLS before task argument is evaluated - set_current_frame_allocator(tr_.h_.promise().get_resource()); -} - -~run_async_wrapper() -{ - // Restore the prior TLS so a stale pointer does not outlive - // the execution context that owns the trampoline's resource. - set_current_frame_allocator(saved_tls_); -} +include::example$snippets/9l_run_api.cpp[tag=wrapper_tls,indent=0] ---- The constructor saves the current thread-local allocator and the destructor restores it. This save/restore is the load-bearing correctness guarantee: once the wrapper (and the trampoline it owns) is gone, the TLS no longer points at a resource that may have been destroyed. @@ -301,18 +232,12 @@ The task's `operator new` reads it: [source,cpp] ---- -static void* operator new(std::size_t size) -{ - auto* mr = get_current_frame_allocator(); - if(!mr) - mr = std::pmr::get_default_resource(); - return mr->allocate(size, alignof(std::max_align_t)); -} +include::example$snippets/9l_run_api.cpp[tag=operator_new,indent=0] ---- The wrapper is `[[nodiscard]]` and its `operator()` is rvalue-ref-qualified, preventing misuse: -[source,cpp] +[source,cpp,role=pseudocode] ---- // Correct: wrapper is a temporary, used immediately run_async(ex)(my_task()); diff --git a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc index 2c8e6d194..1e0e406e7 100644 --- a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc +++ b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc @@ -18,32 +18,7 @@ Capy achieves the Dimovian Ideal. The proof is in `example/asio/`. [source,cpp] ---- -#include -#include -#include - -#include - -namespace boost { namespace asio { class io_context; } } - -class asio_context : public capy::execution_context -{ - struct impl; - impl* impl_; - -public: - using executor_type = capy::executor_ref; - - asio_context(); - ~asio_context(); - - net::io_context& context() noexcept; - executor_type get_executor() noexcept; - void run(); -}; - -std::pair -make_stream_pair(asio_context& ctx); +include::example$asio/api/capy_streams.hpp[tag=header] ---- Asio appears only as a forward declaration. The context uses pimpl. The factory returns `capy::any_stream` — a type-erased stream that hides the concrete socket type entirely. @@ -58,39 +33,7 @@ Asio appears only as a forward declaration. The context uses pimpl. The factory [source,cpp] ---- -capy::task<> -writer(capy::any_stream& stream, std::size_t total) -{ - char buf[128]; - std::memset(buf, 'X', sizeof(buf)); - - std::size_t written = 0; - while(written < total) - { - std::size_t chunk = (std::min)(sizeof(buf), total - written); - auto [ec, n] = co_await stream.write_some( - capy::make_buffer(buf, chunk)); - if(ec) - co_return; - written += n; - } -} - -capy::task<> -reader(capy::any_stream& stream, std::size_t total) -{ - char buf[128]; - - std::size_t read_total = 0; - while(read_total < total) - { - auto [ec, n] = co_await stream.read_some( - capy::make_buffer(buf)); - if(ec) - co_return; - read_total += n; - } -} +include::example$asio/any_stream.cpp[tag=writer_reader] ---- `writer()` and `reader()` operate on `capy::any_stream&`. They don't know what I/O backend produced the stream. They never need to know. @@ -346,7 +289,7 @@ Capy introduces the https://wg21.link/P4003[IoAwaitable protocol] and uses it fo [source,cpp] ---- -auto await_suspend(std::coroutine_handle<> h, io_env const* env); +include::example$snippets/9m_why_not_cobalt.cpp[tag=await_suspend_env,indent=0] ---- No thread-local state. No ambient context. The executor and stop token flow forward through the call chain via the `io_env` parameter. @@ -436,7 +379,7 @@ Cobalt does not provide buffer sequence types. Users who need these features use Cobalt sets up a thread-local PMR resource via `main` or `thread`. All coroutines on that thread share it. Every awaitable embeds a fixed SBO buffer: -[source,cpp] +[source,cpp,role=external] ---- // cobalt/op.hpp struct awaitable : awaitable_base @@ -539,7 +482,7 @@ Capy's coroutine-driven serializer runs at 1.69x the baseline. Cobalt's `promise The Capy implementation: -[source,cpp] +[source,cpp,role=external] ---- namespace { diff --git a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc index 0c9f37ac7..e0dae2b69 100644 --- a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc +++ b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc @@ -4,9 +4,11 @@ Both Capy and Cobalt allow you to write a non-template coroutine algorithm that [source,cpp] ---- -// Capy -capy::task<> my_algo(capy::any_write_stream& stream); +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=capy_signature] +---- +[source,cpp,role=external] +---- // Cobalt cobalt::task<> my_algo(cobalt::io::write_stream& stream); ---- @@ -30,34 +32,14 @@ IoRunnable [source,cpp] ---- -template -concept IoAwaitable = - requires(A a, std::coroutine_handle<> h, io_env const* env) - { - a.await_suspend(h, env); - }; +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_awaitable_concept] ---- `IoRunnable` refines `IoAwaitable` with operations needed to start a task from non-coroutine contexts: `handle()`, `release()`, `exception()`, `result()`, and the promise-level `set_continuation()` and `set_environment()`. [source,cpp] ---- -template -concept IoRunnable = - IoAwaitable && - requires { typename T::promise_type; } && - requires(T& t, T const& ct, typename T::promise_type const& cp, - typename T::promise_type& p) - { - { ct.handle() } noexcept - -> std::same_as>; - { cp.exception() } noexcept -> std::same_as; - { t.release() } noexcept; - { p.set_continuation(std::coroutine_handle<>{}) } noexcept; - { p.set_environment(static_cast(nullptr)) } noexcept; - } && - (std::is_void_v().await_resume())> || - requires(typename T::promise_type& p) { p.result(); }); +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_runnable_concept] ---- Context injection (`set_environment`, `set_continuation`) consists of `noexcept` requirements that `IoRunnable` places on `T::promise_type`; launch functions invoke them through the typed handle returned by `handle()` before resuming the frame. @@ -82,7 +64,7 @@ That way custom coroutines can support cancellation, executors and allocators. In a custom awaitable you can obtain them like this: ____ -[source,cpp] +[source,cpp,role=external] ---- struct my_awaitable { @@ -137,14 +119,14 @@ Capy's `IoAwaitable` protocol passes the execution environment as an explicit pa [source,cpp] ---- -auto await_suspend(std::coroutine_handle<> h, io_env const* env); +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=await_suspend_env,indent=0] ---- The executor and stop token flow forward structurally through the call chain via `io_env`. If a task's machinery does not provide them, the code does not compile. There is no fallback. Cobalt's approach probes the calling promise. The relevant code from `cobalt/detail/task.hpp`: -[source,cpp] +[source,cpp,role=external] ---- template std::coroutine_handle await_suspend(std::coroutine_handle h) @@ -197,7 +179,7 @@ In Capy, the equivalent of "no cancellation" is passing `std::stop_token{}` (a n Cobalt's `write_stream` accepts buffers through a concrete type: -[source,cpp] +[source,cpp,role=external] ---- struct write_stream { @@ -208,7 +190,7 @@ struct write_stream `const_buffer_sequence` is a fixed type that accepts either a single `asio::const_buffer` or a `std::span`: -[source,cpp] +[source,cpp,role=external] ---- struct const_buffer_sequence { @@ -231,22 +213,14 @@ Capy's `WriteStream` concept requires `write_some` to accept any `ConstBufferSeq [source,cpp] ---- -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - // ... - }; +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept_short] ---- The type-erased wrapper `any_write_stream` also models the `WriteStream` concept. Its `write_some` is a template that accepts any `ConstBufferSequence`: [source,cpp] ---- -template -auto -any_write_stream::write_some(CB buffers); +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=any_write_some_template] ---- Both the concept and the wrapper accept full buffer sequences. Type erasure does not narrow the interface. @@ -282,7 +256,7 @@ ____ Followed by the struct definition: -[source,cpp] +[source,cpp,role=external] ---- struct write_stream { @@ -297,49 +271,14 @@ Capy's `WriteStream` concept includes semantic requirements in the concept's doc [source,cpp] ---- -// From capy/concept/write_stream.hpp - -// Semantic Requirements: -// -// Attempts to write up to buffer_size( buffers ) bytes from -// the buffer sequence to the stream. -// -// If buffer_size( buffers ) > 0: -// -// If !ec, then n >= 1 && n <= buffer_size( buffers ). -// n bytes were written from the buffer sequence. -// If ec, then n >= 0 && n < buffer_size( buffers ). -// n is the number of bytes written before the I/O -// condition arose. -// -// Equivalently, n == buffer_size( buffers ) implies !ec: a -// completion that writes the entire buffer sequence is a success, -// even when the underlying operation also signals a condition. That -// condition is reported on a subsequent write. This lets generic -// composition algorithms such as when_all and when_any distinguish -// a completed transfer from a failure. -// -// If buffer_empty( buffers ) is true, n is 0. The empty -// buffer is not itself a cause for error, but ec may reflect -// the state of the stream. -// -// Buffers in the sequence are consumed in order. -// -// Buffer Lifetime: -// -// The caller must ensure that the memory referenced by buffers -// remains valid until the co_await expression returns. +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=semantics_quote] ---- The concept also includes a coroutine-specific warning about buffer lifetime: [source,cpp] ---- -// Warning: When implementing coroutine member functions, prefer -// accepting buffer sequences by value rather than by reference. -// Buffer sequences passed by reference may become dangling if -// the caller's stack frame is destroyed before the coroutine -// completes. +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=lifetime_warning] ---- [cols="1,1,1"] @@ -375,7 +314,7 @@ The concept also includes a coroutine-specific warning about buffer lifetime: Cobalt's `write_stream::write_some` returns `write_op`, a concrete operation type. The `write_op` struct from `cobalt/io/ops.hpp`: -[source,cpp] +[source,cpp,role=external] ---- struct write_op final : op { @@ -442,7 +381,7 @@ Every `co_await` on a `write_op` in Cobalt allocates a fixed-size buffer in the 3. `op<>::operator co_await()` returns `op<>::awaitable` 4. `op<>::awaitable` contains a 4096-byte SBO buffer: -[source,cpp] +[source,cpp,role=external] ---- // cobalt/op.hpp struct awaitable : awaitable_base @@ -461,20 +400,7 @@ Capy's `any_write_stream` takes a different approach. The constructor preallocat [source,cpp] ---- -// capy/io/any_write_stream.hpp -template - requires (!std::same_as, any_write_stream>) -any_write_stream::any_write_stream(S s) - : vt_(&vtable_for_impl::value) -{ - // ... - storage_ = ::operator new(sizeof(S)); - stream_ = ::new(storage_) S(std::move(s)); - - // Preallocate the awaitable storage - cached_awaitable_ = ::operator new(vt_->awaitable_size); - // ... -} +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=ctor_prealloc] ---- After construction, each `co_await stream.write_some(buf)` reuses this preallocated storage. There is no per-operation allocation and no fixed-size buffer in the coroutine frame. @@ -512,15 +438,7 @@ Capy separates the abstraction from the wrapper. `WriteStream` is a {cpp}20 conc [source,cpp] ---- -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; +include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=write_stream_concept] ---- `any_write_stream` is a type-erased wrapper that satisfies this concept. It is one possible reification, not the only one. Users can: diff --git a/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc b/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc index 8bb472d2f..b84474b33 100644 --- a/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc +++ b/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc @@ -48,7 +48,7 @@ Both libraries propagate executor context. They differ in mechanism and mobility TMC allows a coroutine to switch executors mid-body: -[source,cpp] +[source,cpp,role=external] ---- tmc::task example() { // Running on executor A @@ -144,9 +144,7 @@ HALO (Heap Allocation Lowering Optimization) lets compilers eliminate coroutine [source,cpp] ---- -std::pmr::monotonic_buffer_resource arena; -run_async(ex, &arena)(handle_connection(socket)); -// On disconnect: entire arena reclaimed instantly +include::example$snippets/9o_why_not_tmc.cpp[tag=arena,indent=0] ---- *TMC provides:* @@ -165,11 +163,7 @@ When you write `co_await something`, what happens? [source,cpp] ---- -void await_suspend(std::coroutine_handle<> h); -// or -bool await_suspend(std::coroutine_handle<> h); -// or -std::coroutine_handle<> await_suspend(std::coroutine_handle<> h); +include::example$snippets/9o_why_not_tmc.cpp[tags=std_suspend_void;std_suspend_bool;std_suspend_handle,indent=0] ---- The awaitable receives a handle to resume. That's all. No information about where to resume, no cancellation mechanism. @@ -178,7 +172,7 @@ The awaitable receives a handle to resume. That's all. No information about wher [source,cpp] ---- -auto await_suspend(std::coroutine_handle<> h, io_env const* env); +include::example$snippets/9o_why_not_tmc.cpp[tag=await_suspend_env,indent=0] ---- The awaitable receives: @@ -193,7 +187,7 @@ The awaitable receives: Standard signature, plus traits-based context injection: -[source,cpp] +[source,cpp,role=external] ---- // TMC propagates context via awaitable_traits awaitable_traits::set_continuation(awaitable, continuation); @@ -212,16 +206,12 @@ What happens when you `co_await` an awaitable that doesn't implement the extende [source,cpp] ---- -// From task.hpp transform_awaitable() -else -{ - static_assert(sizeof(A) == 0, "requires IoAwaitable"); -} +include::example$snippets/9o_why_not_tmc.cpp[tag=strict_static_assert,indent=0] ---- *TMC:* Wrap in a trampoline that captures current context. -[source,cpp] +[source,cpp,role=external] ---- // From task.hpp await_transform() return tmc::detail::safe_wrap(std::forward(awaitable)); diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 2aaad8fd3..3de9726b8 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -99,8 +99,7 @@ Unless otherwise specified, all code examples in this documentation assume the f [source,cpp] ---- -#include -using namespace boost::capy; +include::example$snippets/index_page.cpp[tag=convention] ---- ==== @@ -110,37 +109,7 @@ This example demonstrates a minimal coroutine that reads from a stream and echoe [source,cpp] ---- -#include - -using namespace boost::capy; - -task<> echo(any_stream& stream) -{ - char buf[1024]; - for(;;) - { - auto [ec, n] = co_await stream.read_some(make_buffer(buf)); - - auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); - - if(ec) - co_return; - - if(wec) - co_return; - } -} - -int main() -{ - // In a real application, you would obtain a stream from Corosio, - // then launch the coroutine on its io_context and run it: - // - // corosio::io_context ioc; - // corosio::tcp_socket stream = /* from an acceptor or connect */; - // run_async(ioc.get_executor())(echo(stream)); - // ioc.run(); -} +include::example$programs/index_page_echo.cpp[tag=full] ---- The `echo` function accepts an `any_stream&`—a type-erased wrapper that works with any concrete stream implementation. The function reads data into a buffer, then writes it back. Both operations use `co_await` to suspend until the I/O completes. diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index 370f46698..456974de7 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -19,37 +19,7 @@ Create a file `hello_coro.cpp`: [source,cpp] ---- -#include -#include -#include -#include - -namespace capy = boost::capy; - -// A coroutine that returns a value -capy::task answer() -{ - co_return 42; -} - -// A coroutine that awaits another coroutine -capy::task greet() -{ - int n = co_await answer(); - std::cout << "The answer is " << n << "\n"; -} - -int main() -{ - capy::thread_pool pool(1); - - // Launch the coroutine on the pool's executor - capy::run_async(pool.get_executor())(greet()); - - // join() waits for outstanding work to complete; the pool - // destructor only stops the pool and discards pending work - pool.join(); -} +include::example$programs/quick_start_hello.cpp[tag=full] ---- == Build and Run @@ -88,9 +58,7 @@ To receive a task's result outside a coroutine, provide a completion handler: [source,cpp] ---- -capy::run_async(executor)(answer(), [](int result) { - std::cout << "Got: " << result << "\n"; -}); +include::example$snippets/quick_start.cpp[tag=handler,indent=0] ---- == Handling Errors @@ -99,18 +67,7 @@ Exceptions propagate through coroutine chains. To handle them at the top level: [source,cpp] ---- -capy::run_async(executor)(might_fail(), - [](int result) { - std::cout << "Success: " << result << "\n"; - }, - [](std::exception_ptr ep) { - try { - if (ep) std::rethrow_exception(ep); - } catch (std::exception const& e) { - std::cerr << "Error: " << e.what() << "\n"; - } - } -); +include::example$snippets/quick_start.cpp[tag=errors,indent=0] ---- == Next Steps diff --git a/doc/package-lock.json b/doc/package-lock.json index cce35dfc6..8aae7fdb6 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", @@ -54,6 +55,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", @@ -225,6 +242,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", @@ -1644,7 +1670,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" @@ -2011,7 +2036,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 9fcd57662..0fea5e859 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/asio/any_stream.cpp b/example/asio/any_stream.cpp index 3d28bb94c..9955e6a56 100644 --- a/example/asio/any_stream.cpp +++ b/example/asio/any_stream.cpp @@ -23,6 +23,7 @@ constexpr std::size_t total_bytes = 1024; +// tag::writer_reader[] capy::io_task<> writer( capy::any_stream& stream, @@ -70,6 +71,7 @@ reader( std::printf("reader: done, read %zu bytes\n", read_total); co_return capy::io_result<>{}; } +// end::writer_reader[] capy::task<> run_example( diff --git a/example/asio/api/capy_streams.hpp b/example/asio/api/capy_streams.hpp index 81fedf193..38e6f6399 100644 --- a/example/asio/api/capy_streams.hpp +++ b/example/asio/api/capy_streams.hpp @@ -10,6 +10,7 @@ #ifndef CAPY_EXAMPLE_CAPY_STREAMS_HPP #define CAPY_EXAMPLE_CAPY_STREAMS_HPP +// tag::header[] #include #include #include @@ -45,5 +46,6 @@ class asio_context : public capy::execution_context // Create a connected pair of any_stream std::pair make_stream_pair(asio_context& ctx); +// end::header[] #endif diff --git a/example/asio/api/use_capy.hpp b/example/asio/api/use_capy.hpp index a2b3f19f0..2c13e1e8a 100644 --- a/example/asio/api/use_capy.hpp +++ b/example/asio/api/use_capy.hpp @@ -124,6 +124,7 @@ class capy_awaitable return false; } + // tag::await_suspend[] std::coroutine_handle<> await_suspend( std::coroutine_handle<> h, capy::io_env const* env) @@ -150,6 +151,7 @@ class capy_awaitable return std::noop_coroutine(); } + // end::await_suspend[] result_type await_resume() { diff --git a/example/asio/use_capy_example.cpp b/example/asio/use_capy_example.cpp index 93af499ac..83772cd01 100644 --- a/example/asio/use_capy_example.cpp +++ b/example/asio/use_capy_example.cpp @@ -27,6 +27,7 @@ constexpr std::size_t total_bytes = 1024; // Writer coroutine using use_capy with raw Asio socket. // Returns io_task<> so it can be composed with when_all. +// tag::writer[] capy::io_task<> writer( net::ip::tcp::socket& socket, @@ -55,6 +56,7 @@ writer( std::printf("writer: done, wrote %zu bytes\n", written); co_return capy::io_result<>{}; } +// end::writer[] // Reader coroutine using use_capy with raw Asio socket. // Returns io_task<> so it can be composed with when_all. diff --git a/example/async-mutex/async_mutex.cpp b/example/async-mutex/async_mutex.cpp index c65c593c5..71f9b34e9 100644 --- a/example/async-mutex/async_mutex.cpp +++ b/example/async-mutex/async_mutex.cpp @@ -15,6 +15,7 @@ // the mutex ensures mutual exclusion and FIFO acquisition order. // +// tag::full[] #include #include #include @@ -40,12 +41,15 @@ int main() done.count_down(); }; + // tag::mutex[] capy::async_mutex mtx; + // end::mutex[] int acquisition_order = 0; std::vector order_log; auto worker = [&](int id) -> capy::io_task<> { std::cout << "Worker " << id << " waiting for lock\n"; + // tag::scoped_lock[] auto [ec, guard] = co_await mtx.scoped_lock(); if (ec) { @@ -53,6 +57,7 @@ int main() << " canceled: " << ec.message() << "\n"; co_return capy::io_result<>{ec}; } + // end::scoped_lock[] int seq = acquisition_order++; order_log.push_back(id); @@ -87,3 +92,4 @@ int main() return 0; } +// end::full[] diff --git a/example/buffer-composition/buffer_composition.cpp b/example/buffer-composition/buffer_composition.cpp index 554a8ae03..90356b806 100644 --- a/example/buffer-composition/buffer_composition.cpp +++ b/example/buffer-composition/buffer_composition.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include #include #include @@ -25,8 +26,10 @@ void demonstrate_single_buffers() std::vector vec = {'V', 'e', 'c', 't', 'o', 'r'}; // make_buffer creates buffer views (no copies) + // tag::make_buffer[] auto str_buf = capy::make_buffer(str); // mutable_buffer auto arr_buf = capy::make_buffer(arr, sizeof(arr) - 1); // mutable_buffer - Exclude null terminator + // end::make_buffer[] auto vec_buf = capy::make_buffer(vec); // mutable_buffer std::cout << "String buffer: " << str_buf.size() << " bytes\n"; @@ -42,10 +45,12 @@ void demonstrate_two_buffer_scatter() std::string header = "Content-Type: text/plain\r\n\r\n"; std::string body = "Hello, World!"; + // tag::two_buffer[] std::array message = {{ capy::make_buffer(header), capy::make_buffer(body) }}; + // end::two_buffer[] // Calculate total size std::size_t total = capy::buffer_size(message); @@ -73,6 +78,7 @@ void demonstrate_buffer_array() std::string empty_line = "\r\n"; std::string body = R"({"status":"ok"})"; + // tag::multi_buffer[] std::array http_response = {{ capy::make_buffer(status), capy::make_buffer(content_type), @@ -80,6 +86,7 @@ void demonstrate_buffer_array() capy::make_buffer(empty_line), capy::make_buffer(body) }}; + // end::multi_buffer[] std::size_t total = capy::buffer_size(http_response); std::cout << "HTTP response size: " << total << " bytes\n"; @@ -117,6 +124,7 @@ int main() demonstrate_two_buffer_scatter(); demonstrate_buffer_array(); demonstrate_mutable_buffers(); - + return 0; } +// end::full[] diff --git a/example/custom-executor/custom_executor.cpp b/example/custom-executor/custom_executor.cpp index 8dec6bf0c..07d55e57a 100644 --- a/example/custom-executor/custom_executor.cpp +++ b/example/custom-executor/custom_executor.cpp @@ -15,30 +15,34 @@ // tied to thread_pool and can integrate with any scheduling system. // +// tag::full[] #include #include #include #include #include -#include namespace capy = boost::capy; // A minimal single-threaded execution context. // Demonstrates how to satisfy the Executor concept // for any custom scheduling system. +// tag::inherit[] class run_loop : public capy::execution_context { +// end::inherit[] std::queue> queue_; std::thread::id owner_; public: class executor_type; + // tag::inherit[] run_loop() : execution_context(this) { } + // end::inherit[] ~run_loop() { @@ -72,7 +76,9 @@ class run_loop : public capy::execution_context } executor_type get_executor() noexcept; +// tag::inherit[] }; +// end::inherit[] class run_loop::executor_type { @@ -95,14 +101,16 @@ class run_loop::executor_type void on_work_started() const noexcept {} void on_work_finished() const noexcept {} + // tag::dispatch[] std::coroutine_handle<> dispatch( capy::continuation& c) const { if (loop_->is_running_on_this_thread()) - return c.h; + return c.h; // resume inline loop_->enqueue(c.h); - return std::noop_coroutine(); + return std::noop_coroutine(); // defer } + // end::dispatch[] void post(capy::continuation& c) const { @@ -123,7 +131,9 @@ run_loop::get_executor() noexcept } // Verify the concept is satisfied +// tag::concept_check[] static_assert(capy::Executor); +// end::concept_check[] capy::io_task compute(int x) { @@ -149,12 +159,17 @@ int main() run_loop loop; // Launch using run_async, just like with thread_pool + // tag::drive[] capy::run_async(loop.get_executor())(run_tasks()); + // end::drive[] // Drive the loop — all coroutines execute here std::cout << "Running event loop on main thread...\n"; + // tag::drive[] loop.run(); + // end::drive[] std::cout << "Event loop finished.\n"; return 0; } +// end::full[] diff --git a/example/echo-server-corosio/echo_server.cpp b/example/echo-server-corosio/echo_server.cpp index 2220397c6..efb084308 100644 --- a/example/echo-server-corosio/echo_server.cpp +++ b/example/echo-server-corosio/echo_server.cpp @@ -14,6 +14,7 @@ // Demonstrates Capy coroutines driving actual TCP connections. // +// tag::full[] #include #include #include @@ -27,11 +28,13 @@ capy::task<> echo_session(corosio::tcp_socket sock) for (;;) { + // tag::session_io[] 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)); + // end::session_io[] if (ec) break; @@ -50,10 +53,12 @@ capy::task<> accept_loop( auto ep = acc.local_endpoint(); std::cout << "Listening on port " << ep.port() << "\n"; + // tag::accept[] for (;;) { corosio::tcp_socket peer(ioc); auto [ec] = co_await acc.accept(peer); + // end::accept[] if (ec) { @@ -69,9 +74,13 @@ capy::task<> accept_loop( std::cout << remote.v6_address(); std::cout << ":" << remote.port() << "\n"; + // tag::spawn_session[] capy::run_async(ioc.get_executor())( echo_session(std::move(peer))); + // end::spawn_session[] + // tag::accept[] } + // end::accept[] } int main(int argc, char* argv[]) @@ -80,8 +89,10 @@ int main(int argc, char* argv[]) if (argc > 1) port = static_cast(std::atoi(argv[1])); + // tag::acceptor[] corosio::io_context ioc; corosio::tcp_acceptor acc(ioc, corosio::endpoint(port)); + // end::acceptor[] capy::run_async(ioc.get_executor())( accept_loop(acc, ioc)); @@ -90,3 +101,4 @@ int main(int argc, char* argv[]) return 0; } +// end::full[] diff --git a/example/hello-task/hello_task.cpp b/example/hello-task/hello_task.cpp index 2c497f913..86243fba5 100644 --- a/example/hello-task/hello_task.cpp +++ b/example/hello-task/hello_task.cpp @@ -7,21 +7,29 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include #include namespace capy = boost::capy; +// tag::say_hello[] capy::task<> say_hello() { std::cout << "Hello from Capy!\n"; co_return; } +// end::say_hello[] int main() { + // tag::pool[] capy::thread_pool pool; + // end::pool[] + // tag::launch[] capy::run_async(pool.get_executor())(say_hello()); + // end::launch[] pool.join(); return 0; } +// end::full[] diff --git a/example/mock-stream-testing/mock_stream_testing.cpp b/example/mock-stream-testing/mock_stream_testing.cpp index 8f32d969d..98cbc34b7 100644 --- a/example/mock-stream-testing/mock_stream_testing.cpp +++ b/example/mock-stream-testing/mock_stream_testing.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include #include #include @@ -71,11 +72,16 @@ void test_happy_path() auto [a, b] = capy::test::make_stream_pair(); b.provide("hello\n"); + // tag::any_stream[] + // Wrap one end in any_stream using pointer construction for reference semantics capy::any_stream stream{&a}; // any_stream + // end::any_stream[] + // tag::run_blocking[] bool result = false; // bool capy::test::run_blocking([&](bool r) { result = r; })(echo_line_uppercase(stream)); - + // end::run_blocking[] + assert(result == true); assert(b.data() == "HELLO\n"); @@ -153,3 +159,4 @@ int main() std::cout << "\nAll tests passed!\n"; return 0; } +// end::full[] diff --git a/example/parallel-fetch/parallel_fetch.cpp b/example/parallel-fetch/parallel_fetch.cpp index 34885d0cf..2be734fbe 100644 --- a/example/parallel-fetch/parallel_fetch.cpp +++ b/example/parallel-fetch/parallel_fetch.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include #include #include @@ -59,10 +60,12 @@ capy::task<> fetch_user_dashboard(std::string username) {}, co_await std::move(inner)}; }; + // tag::when_all_dashboard[] auto [ec, name, orders, balance] = co_await capy::when_all( wrap(fetch_user_name(user_id)), wrap(fetch_order_count(user_id)), wrap(fetch_account_balance(user_id))); + // end::when_all_dashboard[] std::cout << "\nDashboard results:\n"; std::cout << " Name: " << name << "\n"; @@ -87,11 +90,13 @@ capy::task fetch_with_side_effects() { std::cout << "\n=== Fetch with side effects ===\n"; + // tag::when_all_void[] auto r = co_await capy::when_all( log_access("api/data"), update_metrics("api_calls")); if (r.ec) co_return "error"; + // end::when_all_void[] auto data = co_await fetch_user_name(42); @@ -117,6 +122,7 @@ capy::task<> demonstrate_error_handling() { std::cout << "\n=== Error handling ===\n"; + // tag::when_all_errors[] try { auto [ec2, a, b, c] = co_await capy::when_all( @@ -132,6 +138,7 @@ capy::task<> demonstrate_error_handling() // Note: when_all waits for all tasks to complete (or respond to stop) // before propagating the first exception } + // end::when_all_errors[] } int main() @@ -151,3 +158,4 @@ int main() done.wait(); // Block until all tasks complete return 0; } +// end::full[] diff --git a/example/producer-consumer/producer_consumer.cpp b/example/producer-consumer/producer_consumer.cpp index eb67e9134..32f56e186 100644 --- a/example/producer-consumer/producer_consumer.cpp +++ b/example/producer-consumer/producer_consumer.cpp @@ -14,6 +14,7 @@ // for signaling when data is ready, with strand for serialization. // +// tag::full[] #include #include #include @@ -24,15 +25,22 @@ namespace capy = boost::capy; int main() { capy::thread_pool pool; + // tag::strand[] capy::strand s{pool.get_executor()}; + // end::strand[] + // tag::completion[] std::latch done(1); auto on_complete = [&done](auto&&...) { done.count_down(); }; auto on_error = [&done](std::exception_ptr) { done.count_down(); }; + // end::completion[] + // tag::event[] capy::async_event data_ready; + // end::event[] int shared_value = 0; + // tag::producer[] auto producer = [&]() -> capy::io_task<> { std::cout << "Producer: preparing data...\n"; shared_value = 42; @@ -40,7 +48,9 @@ int main() data_ready.set(); co_return capy::io_result<>{}; }; + // end::producer[] + // tag::consumer[] auto consumer = [&]() -> capy::io_task<> { std::cout << "Consumer: waiting for data...\n"; auto [ec] = co_await data_ready.wait(); @@ -48,7 +58,9 @@ int main() std::cout << "Consumer: received value " << shared_value << "\n"; co_return capy::io_result<>{}; }; + // end::consumer[] + // tag::run_both[] // Run both tasks concurrently using when_all, through a strand. // The strand serializes execution, ensuring thread-safe access // to the shared async_event and shared_value. @@ -57,7 +69,11 @@ int main() }; capy::run_async(s, on_complete, on_error)(run_both()); + // end::run_both[] + // tag::wait[] done.wait(); // Block until tasks complete + // end::wait[] return 0; } +// end::full[] diff --git a/example/sender-bridge/sender_awaitable.hpp b/example/sender-bridge/sender_awaitable.hpp index 11ae8f62c..a18ac2deb 100644 --- a/example/sender-bridge/sender_awaitable.hpp +++ b/example/sender-bridge/sender_awaitable.hpp @@ -122,13 +122,16 @@ struct bridge_receiver return {env_->stop_token}; } + // tag::set_value[] template void set_value(Args&&... args) && noexcept { result_->template emplace<1>( std::forward(args)...); + // resume on the caller's executor env_->executor.post(cont_); } + // end::set_value[] template void set_error(E&& e) && noexcept diff --git a/example/sender-bridge/sender_bridge.cpp b/example/sender-bridge/sender_bridge.cpp index cf65267fe..a1f853245 100644 --- a/example/sender-bridge/sender_bridge.cpp +++ b/example/sender-bridge/sender_bridge.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include "sender_awaitable.hpp" #include @@ -80,3 +81,4 @@ int main() std::cout << "result: " << answer << "\n"; } +// end::full[] diff --git a/example/strand-serialization/strand_serialization.cpp b/example/strand-serialization/strand_serialization.cpp index 930a32b8d..94f639994 100644 --- a/example/strand-serialization/strand_serialization.cpp +++ b/example/strand-serialization/strand_serialization.cpp @@ -15,6 +15,7 @@ // multi-threaded thread_pool; the strand guarantees serialized access. // +// tag::full[] #include #include #include @@ -28,7 +29,9 @@ int main() constexpr int increments_per_coro = 1000; capy::thread_pool pool(4); + // tag::strand[] capy::strand s{pool.get_executor()}; + // end::strand[] std::latch done(1); auto on_complete = [&done](auto&&...) { done.count_down(); }; @@ -43,6 +46,7 @@ int main() done.count_down(); }; + // tag::counter[] int counter = 0; // Each coroutine increments the shared counter without locks. @@ -54,6 +58,7 @@ int main() << " finished, counter = " << counter << "\n"; co_return capy::io_result<>{}; }; + // end::counter[] auto run_all = [&]() -> capy::task<> { std::vector> tasks; @@ -62,7 +67,9 @@ int main() (void) co_await capy::when_all(std::move(tasks)); }; + // tag::run_on_strand[] capy::run_async(s, on_complete, on_error)(run_all()); + // end::run_on_strand[] done.wait(); int expected = num_coroutines * increments_per_coro; @@ -71,3 +78,4 @@ int main() return 0; } +// end::full[] diff --git a/example/timeout-cancellation/timeout_cancellation.cpp b/example/timeout-cancellation/timeout_cancellation.cpp index be5abb445..99cd96f49 100644 --- a/example/timeout-cancellation/timeout_cancellation.cpp +++ b/example/timeout-cancellation/timeout_cancellation.cpp @@ -8,6 +8,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include #include #include @@ -23,18 +24,22 @@ namespace capy = boost::capy; // A slow operation that respects cancellation capy::task slow_fetch(int steps) { + // tag::get_stop_token[] auto token = co_await capy::this_coro::stop_token; // std::stop_token + // end::get_stop_token[] std::string result; for (int i = 0; i < steps; ++i) { // Check cancellation before each step + // tag::check_stop[] if (token.stop_requested()) { std::cout << " Cancelled at step " << i << std::endl; throw std::system_error( make_error_code(std::errc::operation_canceled)); } + // end::check_stop[] result += "step" + std::to_string(i) + " "; @@ -52,16 +57,19 @@ capy::task slow_fetch(int steps) // Shared between the fetch worker thread and the coroutine side of // the race in demo_timeout below. +// tag::fetch_channel[] struct fetch_channel { capy::async_waker fetch_ready; std::atomic cancelled{false}; std::string result; }; +// end::fetch_channel[] // One side of the race: completes when the fetch worker thread // wakes fetch_ready. If the deadline wins first, when_any's stop // request cancels the wait; flag the worker so it stops early. +// tag::race_await_fetch[] capy::io_task await_fetch(fetch_channel& ch) { auto [ec] = co_await ch.fetch_ready.wait(); @@ -72,15 +80,18 @@ capy::io_task await_fetch(fetch_channel& ch) } co_return capy::io_result{{}, std::move(ch.result)}; } +// end::race_await_fetch[] // The other side of the race: completes once a user thread wakes // the waker. This is the escape hatch: the user supplies the // thread and the clock. +// tag::race_deadline[] capy::io_task<> deadline(capy::async_waker& waker) { auto [ec] = co_await waker.wait(); co_return capy::io_result<>{ec}; } +// end::race_deadline[] // Wraps slow_fetch, translating a cancellation exception into // std::nullopt for the manual stop_token demo below. @@ -143,15 +154,19 @@ void demo_timeout() std::size_t winner = 0; std::string fetch_result; + // tag::race_lambda[] auto race = [&]() -> capy::task<> { + // tag::race[] auto result = co_await capy::when_any( await_fetch(fetch_ch), deadline(deadline_waker)); + // end::race[] winner = result.index(); if (result.index() == 1) fetch_result = std::get<1>(std::move(result)); }; + // end::race_lambda[] capy::run_async(pool.get_executor(), [&done]() { done.count_down(); }, @@ -213,11 +228,13 @@ capy::task process_items(std::vector const& items) for (auto item : items) // int { + // tag::partial_result[] if (token.stop_requested()) { std::cout << "Processing cancelled, partial sum: " << sum << "\n"; co_return sum; // Return partial result } + // end::partial_result[] sum += item; } @@ -232,3 +249,4 @@ int main() return 0; } +// end::full[] diff --git a/example/type-erased-echo/echo.cpp b/example/type-erased-echo/echo.cpp index c11b7310f..9ff6d3520 100644 --- a/example/type-erased-echo/echo.cpp +++ b/example/type-erased-echo/echo.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include "echo.hpp" #include #include @@ -17,6 +18,7 @@ namespace myapp { namespace capy = boost::capy; +// tag::session_impl[] capy::task<> echo_session(capy::any_stream& stream) { char buffer[1024]; @@ -36,5 +38,7 @@ capy::task<> echo_session(capy::any_stream& stream) co_return; } } +// end::session_impl[] } // namespace myapp +// end::full[] diff --git a/example/type-erased-echo/echo.hpp b/example/type-erased-echo/echo.hpp index 1dcf9fe66..124eb0c33 100644 --- a/example/type-erased-echo/echo.hpp +++ b/example/type-erased-echo/echo.hpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #ifndef ECHO_HPP #define ECHO_HPP @@ -16,8 +17,11 @@ namespace myapp { // Type-erased interface: no template dependencies +// tag::session_decl[] boost::capy::task<> echo_session(boost::capy::any_stream& stream); +// end::session_decl[] } // namespace myapp #endif +// end::full[] diff --git a/example/type-erased-echo/main.cpp b/example/type-erased-echo/main.cpp index 9b9cb8ce4..ab03e1c3d 100644 --- a/example/type-erased-echo/main.cpp +++ b/example/type-erased-echo/main.cpp @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +// tag::full[] #include "echo.hpp" #include #include @@ -45,3 +46,4 @@ int main() test_with_mock(); return 0; } +// end::full[] diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 15235ca73..858554619 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 bdf433253..722a51ddc 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..7f7788795 --- /dev/null +++ b/test/doc/CMakeLists.txt @@ -0,0 +1,69 @@ +# +# 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/capy +# + +# 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_capy_doc_tests ${PFILES}) +target_link_libraries( + boost_capy_doc_tests PRIVATE + Boost::capy_test_suite_main + Boost::capy) +target_include_directories(boost_capy_doc_tests PRIVATE ../../) +boost_capy_test_suite_discover_tests(boost_capy_doc_tests) +add_dependencies(tests boost_capy_doc_tests) + +# Pages that display a complete program get one executable each; the +# test passes when the program exits cleanly. +# +# Programs demonstrating bugs (data races, deadlocks) are built but +# never executed: running them would hang CI or trip sanitizers. +set(COMPILE_ONLY_PROGRAMS + 3b_synchronization_race) + +# 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_capy_doc_${name} ${src}) + target_link_libraries(boost_capy_doc_${name} PRIVATE Boost::capy) + add_dependencies(tests boost_capy_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_capy_doc_${name} + POST_BUILD + BYPRODUCTS ${ctest_file} + COMMAND ${CMAKE_COMMAND} + -D "TEST_NAME=boost.capy.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.capy.doc.${name}_NOT_BUILT boost.capy.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..76174cc47 --- /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/capy +# + +import testing ; + +project + : requirements + $(c20-requires) + /boost/capy//boost_capy + ../.. + extra + on + ; + +run [ glob snippets/*.cpp ] + ../../extra/test_suite/test_main.cpp + ../../extra/test_suite/test_suite.cpp + : : : ../../extra/test_suite + : boost_capy_doc_tests ; + +# Bug-demo programs (races, deadlocks) are compiled but never run. +local compile-only = 3b_synchronization_race ; + +# Concurrent std::cout in teaching programs is race-free per the +# standard but unannotated in Apple's libc++; test/tsan.supp carries +# the corresponding TSan suppressions. +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/WriteProgramTest.cmake b/test/doc/WriteProgramTest.cmake new file mode 100644 index 000000000..8064842be --- /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/capy +# + +# 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/2b_syntax_counter.cpp b/test/doc/programs/2b_syntax_counter.cpp new file mode 100644 index 000000000..eceae0c01 --- /dev/null +++ b/test/doc/programs/2b_syntax_counter.cpp @@ -0,0 +1,66 @@ +// +// 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/capy +// + +// Full program shown in pages/2.cpp20-coroutines/2b.syntax.adoc. + +// tag::full[] +#include +#include + +struct ReturnObject +{ + struct promise_type + { + ReturnObject get_return_object() { return {}; } + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; +}; + +struct Awaiter +{ + std::coroutine_handle<>* handle_out; + + bool await_ready() { return false; } // always suspend + + void await_suspend(std::coroutine_handle<> h) + { + *handle_out = h; // store handle for later resumption + } + + void await_resume() {} // nothing to return +}; + +ReturnObject counter(std::coroutine_handle<>* handle) +{ + Awaiter awaiter{handle}; + + for (unsigned i = 0; ; ++i) + { + std::cout << "counter: " << i << std::endl; + co_await awaiter; + } +} + +int main() +{ + std::coroutine_handle<> h; + counter(&h); + + for (int i = 0; i < 3; ++i) + { + std::cout << "main: resuming" << std::endl; + h(); + } + + h.destroy(); +} +// end::full[] diff --git a/test/doc/programs/2c_machinery_fibonacci.cpp b/test/doc/programs/2c_machinery_fibonacci.cpp new file mode 100644 index 000000000..4e87cd4a9 --- /dev/null +++ b/test/doc/programs/2c_machinery_fibonacci.cpp @@ -0,0 +1,112 @@ +// +// 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/capy +// + +// Fibonacci generator shown in pages/2.cpp20-coroutines/2c.machinery.adoc. +// The Generator type below is the one built earlier on the same page; +// only the tagged region is displayed. + +#include +#include +#include + +struct Generator +{ + struct promise_type + { + int current_value; + + Generator get_return_object() + { + return Generator{ + std::coroutine_handle::from_promise(*this) + }; + } + + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + + std::suspend_always yield_value(int value) + { + current_value = value; + return {}; + } + + void return_void() {} + void unhandled_exception() { std::terminate(); } + }; + + std::coroutine_handle handle; + + Generator(std::coroutine_handle h) : handle(h) {} + + ~Generator() + { + if (handle) + handle.destroy(); + } + + Generator(Generator const&) = delete; + Generator& operator=(Generator const&) = delete; + + Generator(Generator&& other) noexcept + : handle(other.handle) + { + other.handle = nullptr; + } + + Generator& operator=(Generator&& other) noexcept + { + if (this != &other) + { + if (handle) + handle.destroy(); + handle = other.handle; + other.handle = nullptr; + } + return *this; + } + + bool next() + { + if (!handle || handle.done()) + return false; + handle.resume(); + return !handle.done(); + } + + int value() const + { + return handle.promise().current_value; + } +}; + +// tag::full[] +Generator fibonacci() +{ + int a = 0, b = 1; + while (true) + { + co_yield a; + int next = a + b; + a = b; + b = next; + } +} + +int main() +{ + auto fib = fibonacci(); + + for (int i = 0; i < 10 && fib.next(); ++i) + { + std::cout << fib.value() << " "; + } + std::cout << std::endl; +} +// end::full[] diff --git a/test/doc/programs/2c_machinery_generator.cpp b/test/doc/programs/2c_machinery_generator.cpp new file mode 100644 index 000000000..5c3f52c15 --- /dev/null +++ b/test/doc/programs/2c_machinery_generator.cpp @@ -0,0 +1,107 @@ +// +// 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/capy +// + +// Full program shown in pages/2.cpp20-coroutines/2c.machinery.adoc. + +// tag::full[] +#include +#include +#include + +struct Generator +{ + struct promise_type + { + int current_value; + + Generator get_return_object() + { + return Generator{ + std::coroutine_handle::from_promise(*this) + }; + } + + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + + std::suspend_always yield_value(int value) + { + current_value = value; + return {}; + } + + void return_void() {} + void unhandled_exception() { std::terminate(); } + }; + + std::coroutine_handle handle; + + Generator(std::coroutine_handle h) : handle(h) {} + + ~Generator() + { + if (handle) + handle.destroy(); + } + + // Disable copying + Generator(Generator const&) = delete; + Generator& operator=(Generator const&) = delete; + + // Enable moving + Generator(Generator&& other) noexcept + : handle(other.handle) + { + other.handle = nullptr; + } + + Generator& operator=(Generator&& other) noexcept + { + if (this != &other) + { + if (handle) + handle.destroy(); + handle = other.handle; + other.handle = nullptr; + } + return *this; + } + + bool next() + { + if (!handle || handle.done()) + return false; + handle.resume(); + return !handle.done(); + } + + int value() const + { + return handle.promise().current_value; + } +}; + +Generator count_to(int n) +{ + for (int i = 1; i <= n; ++i) + { + co_yield i; + } +} + +int main() +{ + auto gen = count_to(5); + + while (gen.next()) + { + std::cout << gen.value() << std::endl; + } +} +// end::full[] diff --git a/test/doc/programs/2c_machinery_trace.cpp b/test/doc/programs/2c_machinery_trace.cpp new file mode 100644 index 000000000..451335390 --- /dev/null +++ b/test/doc/programs/2c_machinery_trace.cpp @@ -0,0 +1,88 @@ +// +// 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/capy +// + +// Full program shown in pages/2.cpp20-coroutines/2c.machinery.adoc. + +// tag::full[] +#include +#include + +struct TracePromise +{ + struct promise_type + { + promise_type() + { + std::cout << "promise constructed" << std::endl; + } + + ~promise_type() + { + std::cout << "promise destroyed" << std::endl; + } + + TracePromise get_return_object() + { + std::cout << "get_return_object called" << std::endl; + return TracePromise{ + std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_never initial_suspend() + { + std::cout << "initial_suspend called" << std::endl; + return {}; + } + + std::suspend_always final_suspend() noexcept + { + std::cout << "final_suspend called" << std::endl; + return {}; + } + + void return_void() + { + std::cout << "return_void called" << std::endl; + } + + void unhandled_exception() + { + std::cout << "unhandled_exception called" << std::endl; + } + }; + + std::coroutine_handle handle; + + explicit TracePromise(std::coroutine_handle h) + : handle(h) + { + } + + // final_suspend() returns suspend_always, so the frame stays alive + // until the handle is destroyed explicitly. + ~TracePromise() + { + std::cout << "destroying coroutine handle" << std::endl; + handle.destroy(); + } +}; + +TracePromise trace_coroutine() +{ + std::cout << "coroutine body begins" << std::endl; + co_return; +} + +int main() +{ + std::cout << "calling coroutine" << std::endl; + [[maybe_unused]] auto result = trace_coroutine(); + std::cout << "coroutine returned" << std::endl; +} +// end::full[] diff --git a/test/doc/programs/2d_advanced_exception.cpp b/test/doc/programs/2d_advanced_exception.cpp new file mode 100644 index 000000000..99c948d22 --- /dev/null +++ b/test/doc/programs/2d_advanced_exception.cpp @@ -0,0 +1,75 @@ +// +// 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/capy +// + +// Full program shown in pages/2.cpp20-coroutines/2d.advanced.adoc. + +// tag::full[] +#include +#include +#include +#include + +struct Task +{ + struct promise_type + { + std::exception_ptr exception; + + Task get_return_object() + { + return Task{std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_void() {} + + void unhandled_exception() + { + exception = std::current_exception(); + } + }; + + std::coroutine_handle handle; + + Task(std::coroutine_handle h) : handle(h) {} + ~Task() { if (handle) handle.destroy(); } + + void run() { handle.resume(); } + + void check_exception() + { + if (handle.promise().exception) + std::rethrow_exception(handle.promise().exception); + } +}; + +Task risky_operation() +{ + std::cout << "Starting risky operation" << std::endl; + throw std::runtime_error("Something went wrong"); + co_return; // never reached +} + +int main() +{ + Task task = risky_operation(); + + try + { + task.run(); + task.check_exception(); + std::cout << "Operation completed successfully" << std::endl; + } + catch (std::exception const& e) + { + std::cout << "Operation failed: " << e.what() << std::endl; + } +} +// end::full[] diff --git a/test/doc/programs/3a_foundations_hello.cpp b/test/doc/programs/3a_foundations_hello.cpp new file mode 100644 index 000000000..de61e87a0 --- /dev/null +++ b/test/doc/programs/3a_foundations_hello.cpp @@ -0,0 +1,28 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3a.foundations.adoc. + +// tag::full[] +#include +#include + +void say_hello() +{ + std::cout << "Hello from a new thread!\n"; +} + +int main() +{ + std::thread t(say_hello); + t.join(); + std::cout << "Back in the main thread.\n"; + return 0; +} +// end::full[] diff --git a/test/doc/programs/3a_foundations_lambda.cpp b/test/doc/programs/3a_foundations_lambda.cpp new file mode 100644 index 000000000..54aa152f3 --- /dev/null +++ b/test/doc/programs/3a_foundations_lambda.cpp @@ -0,0 +1,27 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3a.foundations.adoc. + +// tag::full[] +#include +#include + +int main() +{ + int x = 42; + + std::thread t([x]() { + std::cout << "The value is: " << x << "\n"; + }); + + t.join(); + return 0; +} +// end::full[] diff --git a/test/doc/programs/3a_foundations_member.cpp b/test/doc/programs/3a_foundations_member.cpp new file mode 100644 index 000000000..41935847f --- /dev/null +++ b/test/doc/programs/3a_foundations_member.cpp @@ -0,0 +1,33 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3a.foundations.adoc. + +// tag::full[] +#include +#include +#include + +class Greeter +{ +public: + void greet(std::string const& name) + { + std::cout << "Hello, " << name << "!\n"; + } +}; + +int main() +{ + Greeter g; + std::thread t(&Greeter::greet, &g, "World"); + t.join(); + return 0; +} +// end::full[] diff --git a/test/doc/programs/3a_foundations_parallel.cpp b/test/doc/programs/3a_foundations_parallel.cpp new file mode 100644 index 000000000..ba4b3b8cc --- /dev/null +++ b/test/doc/programs/3a_foundations_parallel.cpp @@ -0,0 +1,33 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3a.foundations.adoc. +// Output interleaving varies run to run; that is the point of the demo. + +// tag::full[] +#include +#include + +void count_up(char const* name) +{ + for (int i = 1; i <= 5; ++i) + std::cout << name << ": " << i << "\n"; +} + +int main() +{ + std::thread alice(count_up, "Alice"); + std::thread bob(count_up, "Bob"); + + alice.join(); + bob.join(); + + return 0; +} +// end::full[] diff --git a/test/doc/programs/3a_foundations_ref.cpp b/test/doc/programs/3a_foundations_ref.cpp new file mode 100644 index 000000000..dd9eb84d9 --- /dev/null +++ b/test/doc/programs/3a_foundations_ref.cpp @@ -0,0 +1,32 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3a.foundations.adoc. + +// tag::full[] +#include +#include +#include + +void increment(int& value) +{ + ++value; +} + +int main() +{ + int counter = 0; + + std::thread t(increment, std::ref(counter)); + t.join(); + + std::cout << "Counter is now: " << counter << "\n"; + return 0; +} +// end::full[] diff --git a/test/doc/programs/3b_synchronization_mutex.cpp b/test/doc/programs/3b_synchronization_mutex.cpp new file mode 100644 index 000000000..318f4c449 --- /dev/null +++ b/test/doc/programs/3b_synchronization_mutex.cpp @@ -0,0 +1,41 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3b.synchronization.adoc. + +// tag::full[] +#include +#include +#include + +int counter = 0; +std::mutex counter_mutex; + +void increment_many_times() +{ + for (int i = 0; i < 100000; ++i) + { + counter_mutex.lock(); + ++counter; + counter_mutex.unlock(); + } +} + +int main() +{ + std::thread t1(increment_many_times); + std::thread t2(increment_many_times); + + t1.join(); + t2.join(); + + std::cout << "Counter: " << counter << "\n"; + return 0; +} +// end::full[] diff --git a/test/doc/programs/3b_synchronization_race.cpp b/test/doc/programs/3b_synchronization_race.cpp new file mode 100644 index 000000000..ec074a964 --- /dev/null +++ b/test/doc/programs/3b_synchronization_race.cpp @@ -0,0 +1,36 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3b.synchronization.adoc. +// Deliberate data-race demo: build only, never run as a test. + +// tag::full[] +#include +#include + +int counter = 0; + +void increment_many_times() +{ + for (int i = 0; i < 100000; ++i) + ++counter; +} + +int main() +{ + std::thread t1(increment_many_times); + std::thread t2(increment_many_times); + + t1.join(); + t2.join(); + + std::cout << "Counter: " << counter << "\n"; + return 0; +} +// end::full[] diff --git a/test/doc/programs/3c_advanced_atomics.cpp b/test/doc/programs/3c_advanced_atomics.cpp new file mode 100644 index 000000000..a0f1c33b0 --- /dev/null +++ b/test/doc/programs/3c_advanced_atomics.cpp @@ -0,0 +1,36 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3c.advanced.adoc. + +// tag::full[] +#include +#include +#include + +std::atomic counter{0}; + +void increment_many_times() +{ + for (int i = 0; i < 100000; ++i) + ++counter; // atomic increment +} + +int main() +{ + std::thread t1(increment_many_times); + std::thread t2(increment_many_times); + + t1.join(); + t2.join(); + + std::cout << "Counter: " << counter << "\n"; + return 0; +} +// end::full[] diff --git a/test/doc/programs/3c_advanced_condition_variable.cpp b/test/doc/programs/3c_advanced_condition_variable.cpp new file mode 100644 index 000000000..d4246fcc3 --- /dev/null +++ b/test/doc/programs/3c_advanced_condition_variable.cpp @@ -0,0 +1,48 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3c.advanced.adoc. + +// tag::full[] +#include +#include +#include +#include + +std::mutex mtx; +std::condition_variable cv; +bool ready = false; + +void worker() +{ + std::unique_lock lock(mtx); + cv.wait(lock, []{ return ready; }); // wait until ready is true + std::cout << "Worker proceeding!\n"; +} + +void signal_ready() +{ + { + std::lock_guard lock(mtx); + ready = true; + } + cv.notify_one(); // wake one waiting thread +} + +int main() +{ + std::thread t(worker); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + signal_ready(); + + t.join(); + return 0; +} +// end::full[] diff --git a/test/doc/programs/3d_patterns_async.cpp b/test/doc/programs/3d_patterns_async.cpp new file mode 100644 index 000000000..ff6fb272d --- /dev/null +++ b/test/doc/programs/3d_patterns_async.cpp @@ -0,0 +1,31 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3d.patterns.adoc. + +// tag::full[] +#include +#include + +int compute() +{ + return 6 * 7; +} + +int main() +{ + std::future future = std::async(compute); + + std::cout << "Computing...\n"; + int result = future.get(); + std::cout << "Result: " << result << "\n"; + + return 0; +} +// end::full[] diff --git a/test/doc/programs/3d_patterns_parallel_for.cpp b/test/doc/programs/3d_patterns_parallel_for.cpp new file mode 100644 index 000000000..f30165fbc --- /dev/null +++ b/test/doc/programs/3d_patterns_parallel_for.cpp @@ -0,0 +1,52 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3d.patterns.adoc. + +// tag::full[] +#include +#include +#include +#include +#include + +void parallel_for(int start, int end, int num_threads, + std::function func) +{ + std::vector threads; + int chunk_size = (end - start) / num_threads; + + for (int t = 0; t < num_threads; ++t) + { + int chunk_start = start + t * chunk_size; + int chunk_end = (t == num_threads - 1) ? end : chunk_start + chunk_size; + + threads.emplace_back([=]{ + for (int i = chunk_start; i < chunk_end; ++i) + func(i); + }); + } + + for (auto& thread : threads) + thread.join(); +} + +int main() +{ + std::mutex print_mutex; + + parallel_for(0, 20, 4, [&](int i){ + std::lock_guard lock(print_mutex); + std::cout << "Processing " << i << " on thread " + << std::this_thread::get_id() << "\n"; + }); + + return 0; +} +// end::full[] diff --git a/test/doc/programs/3d_patterns_producer_consumer.cpp b/test/doc/programs/3d_patterns_producer_consumer.cpp new file mode 100644 index 000000000..01829ddb2 --- /dev/null +++ b/test/doc/programs/3d_patterns_producer_consumer.cpp @@ -0,0 +1,78 @@ +// +// 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/capy +// + +// Producer/consumer program shown in pages/3.concurrency/3d.patterns.adoc. +// The page shows ThreadSafeQueue in its own block (compiled in +// snippets/3d_patterns.cpp); here it is scaffolding outside the tag. + +#include +#include +#include +#include +#include + +template +class ThreadSafeQueue +{ + std::queue queue_; + std::mutex mutex_; + std::condition_variable cv_; + +public: + void push(T value) + { + { + std::lock_guard lock(mutex_); + queue_.push(std::move(value)); + } + cv_.notify_one(); + } + + T pop() + { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this]{ return !queue_.empty(); }); + T value = std::move(queue_.front()); + queue_.pop(); + return value; + } +}; + +// tag::full[] +ThreadSafeQueue work_queue; + +void producer() +{ + for (int i = 0; i < 10; ++i) + { + work_queue.push(i); + std::cout << "Produced: " << i << "\n"; + } +} + +void consumer() +{ + for (int i = 0; i < 10; ++i) + { + int item = work_queue.pop(); + std::cout << "Consumed: " << item << "\n"; + } +} + +int main() +{ + std::thread prod(producer); + std::thread cons(consumer); + + prod.join(); + cons.join(); + + return 0; +} +// end::full[] diff --git a/test/doc/programs/3d_patterns_promise_future.cpp b/test/doc/programs/3d_patterns_promise_future.cpp new file mode 100644 index 000000000..28601cb1f --- /dev/null +++ b/test/doc/programs/3d_patterns_promise_future.cpp @@ -0,0 +1,37 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3d.patterns.adoc. + +// tag::full[] +#include +#include +#include + +void compute(std::promise result_promise) +{ + int answer = 6 * 7; // expensive computation + result_promise.set_value(answer); +} + +int main() +{ + std::promise promise; + std::future future = promise.get_future(); + + std::thread t(compute, std::move(promise)); + + std::cout << "Waiting for result...\n"; + int result = future.get(); // blocks until value is set + std::cout << "The answer is: " << result << "\n"; + + t.join(); + return 0; +} +// end::full[] diff --git a/test/doc/programs/3d_patterns_thread_local.cpp b/test/doc/programs/3d_patterns_thread_local.cpp new file mode 100644 index 000000000..c9a2233ff --- /dev/null +++ b/test/doc/programs/3d_patterns_thread_local.cpp @@ -0,0 +1,41 @@ +// +// 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/capy +// + +// Full program shown in pages/3.concurrency/3d.patterns.adoc. + +// tag::full[] +#include +#include + +thread_local int counter = 0; + +void increment_and_print(char const* name) +{ + ++counter; + std::cout << name << " counter: " << counter << "\n"; +} + +int main() +{ + std::thread t1([]{ + increment_and_print("T1"); + increment_and_print("T1"); + }); + + std::thread t2([]{ + increment_and_print("T2"); + increment_and_print("T2"); + }); + + t1.join(); + t2.join(); + + return 0; +} +// end::full[] diff --git a/test/doc/programs/4b_launching_run_async.cpp b/test/doc/programs/4b_launching_run_async.cpp new file mode 100644 index 000000000..58f846824 --- /dev/null +++ b/test/doc/programs/4b_launching_run_async.cpp @@ -0,0 +1,29 @@ +// +// 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/capy +// + +// Full program shown in pages/4.coroutines/4b.launching.adoc. + +// tag::full[] +#include +using namespace boost::capy; + +task compute() +{ + co_return 42; +} + +int main() +{ + thread_pool pool; + run_async(pool.get_executor())(compute()); + // Task is now running on the thread pool + + pool.join(); // wait for outstanding work to complete +} +// end::full[] diff --git a/test/doc/programs/4c_executors_executor_ref.cpp b/test/doc/programs/4c_executors_executor_ref.cpp new file mode 100644 index 000000000..3f72c1996 --- /dev/null +++ b/test/doc/programs/4c_executors_executor_ref.cpp @@ -0,0 +1,78 @@ +// +// 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/capy +// + +// Full program shown in pages/4.coroutines/4c.executors.adoc. + +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace boost::capy; + +namespace { + +continuation parked; +std::binary_semaphore parked_ready{0}; + +// Suspends its coroutine and publishes the continuation so another +// thread can schedule the resumption. +struct park +{ + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, io_env const*) + { + parked.h = h; + parked_ready.release(); + return std::noop_coroutine(); + } + + void await_resume() {} +}; + +task parked_task() +{ + co_await park{}; +} + +// Launch a coroutine on the pool and block until it has parked, +// leaving its continuation ready to be scheduled. +continuation& make_suspended_work(thread_pool& pool) +{ + run_async(pool.get_executor())(parked_task()); + parked_ready.acquire(); + return parked; +} + +} // namespace + +// tag::full[] +void schedule_work(executor_ref ex, continuation& c) +{ + ex.post(c); // Works with any executor +} + +int main() +{ + thread_pool pool; + auto pool_ex = pool.get_executor(); + executor_ref ex = pool_ex; // Type erasure; pool_ex must outlive ex + + continuation& c = make_suspended_work(pool); // a coroutine parked on the pool + schedule_work(ex, c); + pool.join(); +} +// end::full[] diff --git a/test/doc/programs/4c_executors_thread_pool.cpp b/test/doc/programs/4c_executors_thread_pool.cpp new file mode 100644 index 000000000..d36263775 --- /dev/null +++ b/test/doc/programs/4c_executors_thread_pool.cpp @@ -0,0 +1,40 @@ +// +// 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/capy +// + +// Full program shown in pages/4.coroutines/4c.executors.adoc. + +// tag::full[] +#include + +// end::full[] +#include +#include + +using namespace boost::capy; + +task my_task() +{ + co_return; +} + +// tag::full[] +int main() +{ + // Create pool with 4 threads + thread_pool pool(4); + + // Get an executor for this pool + auto ex = pool.get_executor(); + + // Launch work on the pool + run_async(ex)(my_task()); + + pool.join(); // wait for outstanding work to complete +} +// end::full[] diff --git a/test/doc/programs/8m_parallel_tasks_variadic.cpp b/test/doc/programs/8m_parallel_tasks_variadic.cpp new file mode 100644 index 000000000..0d64b2da4 --- /dev/null +++ b/test/doc/programs/8m_parallel_tasks_variadic.cpp @@ -0,0 +1,90 @@ +// +// 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/capy +// + +// Full program shown in pages/8.examples/8m.parallel-tasks.adoc. Uses the +// variadic when_all overload; example/parallel-tasks uses the range overload. + +// tag::full[] +#include +#include +#include +#include +#include + +namespace capy = boost::capy; + +// Sum integers in [lo, hi) +capy::io_task partial_sum(int lo, int hi) +{ + // tag::thread_id[] + std::ostringstream oss; + oss << " range [" << lo << ", " << hi + << ") on thread " << std::this_thread::get_id() << "\n"; + std::cout << oss.str(); + // end::thread_id[] + + long long sum = 0; + for (int i = lo; i < hi; ++i) + sum += i; + co_return capy::io_result{{}, sum}; +} + +int main() +{ + // tag::partition[] + constexpr int total = 10000; + constexpr int num_tasks = 4; + constexpr int chunk = total / num_tasks; + // end::partition[] + + capy::thread_pool pool(num_tasks); + std::latch done(1); + + auto on_complete = [&done](auto&&...) { done.count_down(); }; + auto on_error = [&done](std::exception_ptr ep) { + try { std::rethrow_exception(ep); } + catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << "\n"; + } + catch (...) { + std::cerr << "Error: unknown exception\n"; + } + done.count_down(); + }; + + auto compute = [&]() -> capy::task<> { + std::cout << "Dispatching " << num_tasks + << " parallel tasks...\n"; + + // tag::when_all_variadic[] + auto [ec, s0, s1, s2, s3] = co_await capy::when_all( + partial_sum(0 * chunk, 1 * chunk), + partial_sum(1 * chunk, 2 * chunk), + partial_sum(2 * chunk, 3 * chunk), + partial_sum(3 * chunk, 4 * chunk)); + // end::when_all_variadic[] + + long long total_sum = s0 + s1 + s2 + s3; + + // Arithmetic series: sum [0, N) = N*(N-1)/2 + long long expected = + static_cast(total) * (total - 1) / 2; + + std::cout << "\nPartial sums: " << s0 << " + " << s1 + << " + " << s2 << " + " << s3 << "\n"; + std::cout << "Total: " << total_sum + << " (expected " << expected << ")\n"; + }; + + capy::run_async(pool.get_executor(), on_complete, on_error)(compute()); + done.wait(); + + return 0; +} +// end::full[] diff --git a/test/doc/programs/index_page_echo.cpp b/test/doc/programs/index_page_echo.cpp new file mode 100644 index 000000000..29f37a65f --- /dev/null +++ b/test/doc/programs/index_page_echo.cpp @@ -0,0 +1,44 @@ +// +// 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/capy +// + +// Full program shown in pages/index.adoc. + +// tag::full[] +#include + +using namespace boost::capy; + +task<> echo(any_stream& stream) +{ + char buf[1024]; + for(;;) + { + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + + auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); + + if(ec) + co_return; + + if(wec) + co_return; + } +} + +int main() +{ + // In a real application, you would obtain a stream from Corosio, + // then launch the coroutine on its io_context and run it: + // + // corosio::io_context ioc; + // corosio::tcp_socket stream = /* from an acceptor or connect */; + // run_async(ioc.get_executor())(echo(stream)); + // ioc.run(); +} +// end::full[] diff --git a/test/doc/programs/quick_start_hello.cpp b/test/doc/programs/quick_start_hello.cpp new file mode 100644 index 000000000..68c07457d --- /dev/null +++ b/test/doc/programs/quick_start_hello.cpp @@ -0,0 +1,44 @@ +// +// 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/capy +// + +// Full program shown in pages/quick-start.adoc. + +// tag::full[] +#include +#include +#include +#include + +namespace capy = boost::capy; + +// A coroutine that returns a value +capy::task answer() +{ + co_return 42; +} + +// A coroutine that awaits another coroutine +capy::task greet() +{ + int n = co_await answer(); + std::cout << "The answer is " << n << "\n"; +} + +int main() +{ + capy::thread_pool pool(1); + + // Launch the coroutine on the pool's executor + capy::run_async(pool.get_executor())(greet()); + + // join() waits for outstanding work to complete; the pool + // destructor only stops the pool and discards pending work + pool.join(); +} +// end::full[] diff --git a/test/doc/snippets/2a_foundations.cpp b/test/doc/snippets/2a_foundations.cpp new file mode 100644 index 000000000..aae2ca368 --- /dev/null +++ b/test/doc/snippets/2a_foundations.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/capy +// + +// Compiled fragments shown in pages/2.cpp20-coroutines/2a.foundations.adoc. +// Pages include the tagged regions; scaffolding stays outside the tags. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +// tag::compute[] +int compute(int x, int y) +{ + int result = x * y + 42; + return result; +} +// end::compute[] + +// Each handle_request variant lives in its own namespace so the three +// versions shown on the page can coexist in one translation unit. + +namespace sync_api { + +struct parsed_request +{ + int id; +}; + +struct connection +{ + std::string read() { return "request"; } + void write(std::string const&) {} +}; + +struct database_t +{ + std::string query(int) { return "row"; } +}; + +database_t database; + +parsed_request parse_request(std::string const&) { return {1}; } +std::string compute_response(std::string const&) { return "response"; } + +// tag::sync_request[] +void handle_request(connection& conn) +{ + std::string request = conn.read(); // blocks until data arrives + auto parsed = parse_request(request); + auto data = database.query(parsed.id); // blocks until database responds + auto response = compute_response(data); + conn.write(response); // blocks until write completes +} +// end::sync_request[] + +} // namespace sync_api + +namespace callback_api { + +struct parsed_request +{ + int id; +}; + +// Stubs invoke their callbacks immediately so the fragment can run +// to completion inside the test. +struct connection +{ + template + void async_read(Handler h) { h(std::string("request")); } + + template + void async_write(std::string const&, Handler h) { h(); } +}; + +struct database_t +{ + template + void async_query(int, Handler h) { h(std::string("row")); } +}; + +database_t database; + +parsed_request parse_request(std::string const&) { return {1}; } +std::string compute_response(std::string const&) { return "response"; } + +// tag::callback_request[] +void handle_request(connection& conn) +{ + conn.async_read([&conn](std::string request) { + auto parsed = parse_request(request); + database.async_query(parsed.id, [&conn](auto data) { + auto response = compute_response(data); + conn.async_write(response, []() { + // request complete + }); + }); + }); +} +// end::callback_request[] + +} // namespace callback_api + +namespace coro_api { + +struct parsed_request +{ + int id; +}; + +struct connection +{ + capy::task async_read() { co_return std::string("request"); } + capy::task async_write(std::string) { co_return; } +}; + +struct database_t +{ + capy::task async_query(int) { co_return std::string("row"); } +}; + +database_t database; + +parsed_request parse_request(std::string const&) { return {1}; } +std::string compute_response(std::string const&) { return "response"; } + +using capy::task; + +// tag::coroutine_request[] +task handle_request(connection& conn) +{ + std::string request = co_await conn.async_read(); + auto parsed = parse_request(request); + auto data = co_await database.async_query(parsed.id); + auto response = compute_response(data); + co_await conn.async_write(response); +} +// end::coroutine_request[] + +} // namespace coro_api + +struct foundations_test +{ + void testCompute() + { + BOOST_TEST(compute(2, 3) == 48); + } + + void testSyncRequest() + { + sync_api::connection conn; + sync_api::handle_request(conn); + } + + void testCallbackRequest() + { + callback_api::connection conn; + callback_api::handle_request(conn); + } + + void testCoroutineRequest() + { + capy::thread_pool pool(1); + coro_api::connection conn; + capy::run_async(pool.get_executor())( + coro_api::handle_request(conn)); + pool.join(); + } + + void run() + { + testCompute(); + testSyncRequest(); + testCallbackRequest(); + testCoroutineRequest(); + } +}; + +} // namespace + +TEST_SUITE(foundations_test, "boost.capy.doc.2a_foundations"); diff --git a/test/doc/snippets/2b_syntax.cpp b/test/doc/snippets/2b_syntax.cpp new file mode 100644 index 000000000..bbcae3385 --- /dev/null +++ b/test/doc/snippets/2b_syntax.cpp @@ -0,0 +1,218 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/2.cpp20-coroutines/2b.syntax.adoc. +// Pages include the tagged regions; scaffolding stays outside the tags. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +// The simple_coroutine fragment shows this include; the tag region is +// split so the directive stays at file scope. +// tag::simple_coroutine[] +#include +// end::simple_coroutine[] + +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using capy::task; + +struct http_response +{ + std::string body; +}; + +task http_get(std::string) +{ + co_return http_response{""}; +} + +// tag::co_await_example[] +task fetch_page(std::string url) +{ + auto response = co_await http_get(url); // suspends until HTTP completes + co_return response.body; // continues after resumption +} +// end::co_await_example[] + +// Minimal generator so the co_yield fragment compiles; the page teaches +// the keyword, not a production generator type. +template +struct generator +{ + struct promise_type + { + T value_; + + generator get_return_object() + { + return generator{ + std::coroutine_handle::from_promise(*this)}; + } + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + std::suspend_always yield_value(T v) + { + value_ = v; + return {}; + } + void return_void() {} + void unhandled_exception() {} + }; + + std::coroutine_handle h_; + + explicit generator(std::coroutine_handle h) : h_(h) {} + generator(generator&& other) noexcept : h_(std::exchange(other.h_, {})) {} + ~generator() { if (h_) h_.destroy(); } + + bool next() + { + h_.resume(); + return !h_.done(); + } + + T value() const { return h_.promise().value_; } +}; + +// tag::co_yield_example[] +generator count_to(int n) +{ + for (int i = 1; i <= n; ++i) + { + co_yield i; // produce value, suspend, resume when next value requested + } +} +// end::co_yield_example[] + +// tag::co_return_example[] +task compute() +{ + int result = 42; + co_return result; // completes the coroutine with value 42 +} +// end::co_return_example[] + +// tag::simple_coroutine[] + +struct SimpleCoroutine +{ + struct promise_type + { + SimpleCoroutine get_return_object() { return {}; } + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; +}; + +SimpleCoroutine my_first_coroutine() +{ + co_return; // This makes it a coroutine +} +// end::simple_coroutine[] + +// Never invoked: awaiting suspend_always here would leave the frame +// suspended forever. Compiling the fragment is the test. +[[maybe_unused]] +SimpleCoroutine standard_awaiters() +{ + // tag::standard_awaiters[] + // suspend_always causes suspension at this point + co_await std::suspend_always{}; + + // suspend_never continues immediately without suspending + co_await std::suspend_never{}; + // end::standard_awaiters[] +} + +struct syntax_test +{ + void testFetchPage() + { + capy::thread_pool pool(1); + capy::run_async(pool.get_executor(), [](std::string const& body) { + BOOST_TEST(body == ""); + })(fetch_page("https://example.com")); + pool.join(); + } + + void testCountTo() + { + auto gen = count_to(3); + int expected = 1; + while (gen.next()) + BOOST_TEST(gen.value() == expected++); + BOOST_TEST(expected == 4); + } + + void testCompute() + { + capy::thread_pool pool(1); + capy::run_async(pool.get_executor(), [](int result) { + BOOST_TEST(result == 42); + })(compute()); + pool.join(); + } + + void testFirstCoroutine() + { + // Runs to completion immediately: both suspend points are + // suspend_never, so the frame is freed before this returns. + my_first_coroutine(); + } + + void run() + { + testFetchPage(); + testCountTo(); + testCompute(); + testFirstCoroutine(); + } +}; + +} // namespace + +TEST_SUITE(syntax_test, "boost.capy.doc.2b_syntax"); diff --git a/test/doc/snippets/2c_machinery.cpp b/test/doc/snippets/2c_machinery.cpp new file mode 100644 index 000000000..05eb31468 --- /dev/null +++ b/test/doc/snippets/2c_machinery.cpp @@ -0,0 +1,73 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/2.cpp20-coroutines/2c.machinery.adoc. +// Declaration-only scaffolding; compiling is the test. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +namespace { + +// Minimal return type showing how get_return_object obtains a handle +struct handle_demo +{ + struct promise_type + { + handle_demo get_return_object() + { + auto h = + // tag::from_promise[] + std::coroutine_handle::from_promise(*this) + // end::from_promise[] + ; + return handle_demo{h}; + } + + std::suspend_never initial_suspend() { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; + + std::coroutine_handle handle; +}; + +[[maybe_unused]] handle_demo make_handle_demo() +{ + co_return; +} + +} // namespace diff --git a/test/doc/snippets/2d_advanced.cpp b/test/doc/snippets/2d_advanced.cpp new file mode 100644 index 000000000..52a3a6604 --- /dev/null +++ b/test/doc/snippets/2d_advanced.cpp @@ -0,0 +1,411 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/2.cpp20-coroutines/2d.advanced.adoc. +// Pages include the tagged regions; scaffolding stays outside the tags. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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" + +// The production generator's tagged region carries its own includes, so +// it lives at file scope; the page shows it as a standalone listing. + +// tag::production_generator[] +#include +#include +#include +#include + +template +class Generator +{ +public: + struct promise_type + { + T value_; + std::exception_ptr exception_; + + Generator get_return_object() + { + return Generator{Handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + + std::suspend_always yield_value(T v) + { + value_ = std::move(v); + return {}; + } + + void return_void() noexcept {} + + void unhandled_exception() + { + exception_ = std::current_exception(); + } + + // Prevent co_await inside generators + template + std::suspend_never await_transform(U&&) = delete; + }; + + using Handle = std::coroutine_handle; + + class iterator + { + Handle handle_; + + public: + using iterator_category = std::input_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + + iterator() : handle_(nullptr) {} + explicit iterator(Handle h) : handle_(h) {} + + iterator& operator++() + { + handle_.resume(); + if (handle_.done()) + { + auto& promise = handle_.promise(); + handle_ = nullptr; + if (promise.exception_) + std::rethrow_exception(promise.exception_); + } + return *this; + } + + T& operator*() const { return handle_.promise().value_; } + bool operator==(iterator const& other) const + { + return handle_ == other.handle_; + } + }; + + iterator begin() + { + if (handle_) + { + handle_.resume(); + if (handle_.done()) + { + auto& promise = handle_.promise(); + if (promise.exception_) + std::rethrow_exception(promise.exception_); + return iterator{}; + } + } + return iterator{handle_}; + } + + iterator end() { return iterator{}; } + + ~Generator() { if (handle_) handle_.destroy(); } + + Generator(Generator const&) = delete; + Generator& operator=(Generator const&) = delete; + + Generator(Generator&& other) noexcept + : handle_(std::exchange(other.handle_, nullptr)) {} + + Generator& operator=(Generator&& other) noexcept + { + if (this != &other) + { + if (handle_) handle_.destroy(); + handle_ = std::exchange(other.handle_, nullptr); + } + return *this; + } + +private: + Handle handle_; + + explicit Generator(Handle h) : handle_(h) {} +}; +// end::production_generator[] + +namespace capy = boost::capy; + +namespace { + +using capy::task; + +task<> b(); +task<> c(); + +// tag::task_chain[] +task<> a() { co_await b(); } +task<> b() { co_await c(); } +task<> c() { co_return; } +// end::task_chain[] + +// Awaiter scaffolding for the symmetric-transfer fragment +struct chained_awaiter +{ + std::coroutine_handle<> continuation_; + std::coroutine_handle<> next_coroutine_; + + bool await_ready() const noexcept { return false; } + + // tag::symmetric_await_suspend[] + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) + { + // store continuation for later + continuation_ = h; + + // return handle to resume (instead of calling resume()) + return next_coroutine_; + } + // end::symmetric_await_suspend[] + + void await_resume() const noexcept {} +}; + +// A promise sketch large enough for the final_suspend fragment +namespace symmetric_generator { + +struct promise_type +{ + std::coroutine_handle<> consumer_handle_; + + // tag::generator_final_suspend[] + auto final_suspend() noexcept + { + struct awaiter + { + promise_type* p_; + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> await_suspend(std::coroutine_handle<>) noexcept + { + // Return to the consumer that called resume() + return p_->consumer_handle_; + } + + void await_resume() const noexcept {} + }; + return awaiter{this}; + } + // end::generator_final_suspend[] +}; + +} // namespace symmetric_generator + +task compute_something() +{ + co_return; +} + +void store_for_later(task) {} + +[[maybe_unused]] task halo_demo() +{ + // tag::halo[] + // HALO might apply here because the task is awaited immediately + co_await compute_something(); + + // HALO cannot apply here because the task escapes + auto task = compute_something(); + store_for_later(std::move(task)); + // end::halo[] +} + +namespace custom_alloc { + +// Stand-in allocator so the fragment's calls resolve +struct frame_allocator +{ + void* allocate(std::size_t n) { return ::operator new(n); } + void deallocate(void* p, std::size_t) { ::operator delete(p); } +}; + +frame_allocator my_allocator; + +// tag::custom_operator_new[] +struct promise_type +{ + // Custom allocation + static void* operator new(std::size_t size) + { + return my_allocator.allocate(size); + } + + static void operator delete(void* ptr, std::size_t size) + { + my_allocator.deallocate(ptr, size); + } + + // ... rest of promise type +}; +// end::custom_operator_new[] + +} // namespace custom_alloc + +// Promise sketches for the unhandled_exception policy fragments +struct terminate_promise +{ + // tag::unhandled_terminate[] + void unhandled_exception() + { + std::terminate(); + } + // end::unhandled_terminate[] +}; + +struct store_promise +{ + std::exception_ptr exception_; + + // tag::unhandled_store[] + void unhandled_exception() + { + exception_ = std::current_exception(); + } + // end::unhandled_store[] +}; + +struct rethrow_promise +{ + // tag::unhandled_rethrow[] + void unhandled_exception() + { + throw; // propagates to whoever resumed the coroutine + } + // end::unhandled_rethrow[] +}; + +struct swallow_promise +{ + // tag::unhandled_swallow[] + void unhandled_exception() + { + // silently ignored - almost always a mistake + } + // end::unhandled_swallow[] +}; + +namespace store_rethrow { + +// Stub handle so the accessor sketch compiles without a real coroutine +template +struct stub_promise +{ + std::exception_ptr exception_; + T result_; +}; + +template +struct stub_handle +{ + stub_promise* p_ = nullptr; + stub_promise& promise() const { return *p_; } +}; + +template +class return_object +{ + stub_handle handle_; + +public: + // tag::store_and_rethrow[] + struct promise_type + { + std::exception_ptr exception_; + + void unhandled_exception() + { + exception_ = std::current_exception(); + } + }; + + // In the return object's result accessor: + T get_result() + { + if (handle_.promise().exception_) + std::rethrow_exception(handle_.promise().exception_); + return std::move(handle_.promise().result_); + } + // end::store_and_rethrow[] +}; + +template class return_object; + +} // namespace store_rethrow + +Generator iota(int n) +{ + for (int i = 0; i < n; ++i) + co_yield i; +} + +struct advanced_test +{ + void testTaskChain() + { + capy::thread_pool pool(1); + capy::run_async(pool.get_executor())(a()); + pool.join(); + } + + void testProductionGenerator() + { + int sum = 0; + for (int v : iota(5)) + sum += v; + BOOST_TEST(sum == 10); + } + + void run() + { + testTaskChain(); + testProductionGenerator(); + } +}; + +} // namespace + +TEST_SUITE(advanced_test, "boost.capy.doc.2d_advanced"); diff --git a/test/doc/snippets/3a_foundations.cpp b/test/doc/snippets/3a_foundations.cpp new file mode 100644 index 000000000..79c43a142 --- /dev/null +++ b/test/doc/snippets/3a_foundations.cpp @@ -0,0 +1,117 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/3.concurrency/3a.foundations.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace { + +std::atomic work_done{false}; + +void do_work() +{ + work_done = true; +} + +std::atomic background_finished{false}; + +// Signals completion so the test can wait for the detached thread +void background_task() +{ + background_finished = true; +} + +void some_function() +{ +} + +struct foundations_test +{ + void + testJoin() + { + // tag::join[] + std::thread t(do_work); + // ... do other things ... + t.join(); // wait for do_work to finish + // end::join[] + BOOST_TEST(work_done); + } + + void + testDetach() + { + // tag::detach[] + std::thread t(background_task); + t.detach(); // thread runs independently + // t is now "empty"—no longer associated with a thread + // end::detach[] + BOOST_TEST(!t.joinable()); + // The detached thread must not outlive the test binary + while (!background_finished) + std::this_thread::yield(); + } + + void + testJoinable() + { + // tag::joinable[] + std::thread t(some_function); + + if (t.joinable()) + { + t.join(); + } + // end::joinable[] + BOOST_TEST(!t.joinable()); + } + + void + run() + { + testJoin(); + testDetach(); + testJoinable(); + } +}; + +} // namespace + +TEST_SUITE(foundations_test, "boost.capy.doc.3a_foundations"); diff --git a/test/doc/snippets/3b_synchronization.cpp b/test/doc/snippets/3b_synchronization.cpp new file mode 100644 index 000000000..4f5dd58a4 --- /dev/null +++ b/test/doc/snippets/3b_synchronization.cpp @@ -0,0 +1,154 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/3.concurrency/3b.synchronization.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace { + +// The #include directives inside the tag expand to nothing here (the +// headers are already included above); they are kept for the page text. +// tag::lock_guard[] +#include +#include +#include + +int counter = 0; +std::mutex counter_mutex; + +void increment_many_times() +{ + for (int i = 0; i < 100000; ++i) + { + std::lock_guard lock(counter_mutex); + ++counter; + // lock is automatically released when it goes out of scope + } +} +// end::lock_guard[] + +std::mutex some_mutex; + +// tag::deadlock[] +std::mutex mutex1, mutex2; + +void thread_a() +{ + std::lock_guard lock1(mutex1); + std::lock_guard lock2(mutex2); // blocks, waiting for B + // ... +} + +void thread_b() +{ + std::lock_guard lock2(mutex2); + std::lock_guard lock1(mutex1); // blocks, waiting for A + // ... +} +// end::deadlock[] + +// Compile-only bug demo: running thread_a and thread_b concurrently +// can deadlock, so nothing ever calls them. +[[maybe_unused]] void (* const deadlock_demo[])() = {thread_a, thread_b}; + +// tag::scoped_lock_multi[] +void safe_function() +{ + std::scoped_lock lock(mutex1, mutex2); // locks both atomically + // ... +} +// end::scoped_lock_multi[] + +struct synchronization_test +{ + void + testLockGuard() + { + std::thread t1(increment_many_times); + std::thread t2(increment_many_times); + t1.join(); + t2.join(); + BOOST_TEST(counter == 200000); + } + + void + testScopedLock() + { + // tag::scoped_lock[] + std::scoped_lock lock(counter_mutex); // C++17 + // end::scoped_lock[] + } + + void + testUniqueLock() + { + // tag::unique_lock[] + std::unique_lock lock(some_mutex, std::defer_lock); + // mutex not yet locked + + lock.lock(); // lock when ready + // ... do work ... + lock.unlock(); // unlock early if needed + // ... do other work ... + // destructor unlocks again if still locked + // end::unique_lock[] + BOOST_TEST(!lock.owns_lock()); + } + + void + testScopedLockMulti() + { + safe_function(); + } + + void + run() + { + testLockGuard(); + testScopedLock(); + testUniqueLock(); + testScopedLockMulti(); + } +}; + +} // namespace + +TEST_SUITE(synchronization_test, "boost.capy.doc.3b_synchronization"); diff --git a/test/doc/snippets/3c_advanced.cpp b/test/doc/snippets/3c_advanced.cpp new file mode 100644 index 000000000..80db9db21 --- /dev/null +++ b/test/doc/snippets/3c_advanced.cpp @@ -0,0 +1,212 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/3.concurrency/3c.advanced.adoc. + +// tag::shared_mutex[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// end::shared_mutex[] + +// tag::thread_safe_cache[] +#include +#include +#include +#include +// end::thread_safe_cache[] + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// tag::shared_mutex[] + +std::shared_mutex rw_mutex; +std::vector data; + +void reader(int id) +{ + std::shared_lock lock(rw_mutex); // shared access + std::cout << "Reader " << id << " sees " << data.size() << " elements\n"; +} + +void writer(int value) +{ + std::unique_lock lock(rw_mutex); // exclusive access + data.push_back(value); + std::cout << "Writer added " << value << "\n"; +} +// end::shared_mutex[] + +// tag::thread_safe_cache[] + +class ThreadSafeCache +{ + std::unordered_map cache_; + mutable std::shared_mutex mutex_; + +public: + std::optional get(std::string const& key) const + { + std::shared_lock lock(mutex_); // readers can proceed in parallel + auto it = cache_.find(key); + if (it != cache_.end()) + return it->second; + return std::nullopt; + } + + void put(std::string const& key, std::string const& value) + { + std::unique_lock lock(mutex_); // exclusive access for writing + cache_[key] = value; + } +}; +// end::thread_safe_cache[] + +// Shows the inefficient polling baseline the page contrasts with condition +// variables. Compile-only: calling it would spin until another thread +// flips `ready`. +[[maybe_unused]] void +busy_wait_demo(bool& ready) +{ + // tag::busy_wait[] + // Inefficient busy-wait + while (!ready) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + // end::busy_wait[] +} + +struct advanced_test +{ + void + testAtomicOps() + { + // tag::atomic_ops[] + std::atomic value{0}; + + value.store(42); // atomic write + int x = value.load(); // atomic read + int old = value.exchange(10); // atomic read-modify-write + value.fetch_add(5); // atomic addition, returns old value + value.fetch_sub(3); // atomic subtraction, returns old value + + // Compare-and-swap (CAS) + int expected = 10; + bool success = value.compare_exchange_strong(expected, 20); + // If value == expected, sets value = 20 and returns true + // Otherwise, sets expected = value and returns false + // end::atomic_ops[] + BOOST_TEST(x == 42); + BOOST_TEST(old == 42); + BOOST_TEST(value.load() == 12); + BOOST_TEST(!success); + BOOST_TEST(expected == 12); + } + + void + testWaitVariants() + { + std::condition_variable cv; + std::mutex m; + std::unique_lock lock(m); + // Predicate is already true, so no variant blocks and no + // notifier thread is needed. + auto predicate = []{ return true; }; + auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(1); + { + // tag::wait_variants[] + // Wait indefinitely + cv.wait(lock, predicate); + + // Wait with timeout + auto status = cv.wait_for(lock, std::chrono::seconds(5), predicate); + // Returns true if predicate is true, false on timeout + // end::wait_variants[] + BOOST_TEST(status); + } + { + // tag::wait_variants[] + + // Wait until specific time point + auto status = cv.wait_until(lock, deadline, predicate); + // end::wait_variants[] + BOOST_TEST(status); + } + } + + void + testSharedMutex() + { + writer(7); + reader(1); + BOOST_TEST(data.size() == 1); + BOOST_TEST(data.front() == 7); + } + + void + testCache() + { + ThreadSafeCache cache; + BOOST_TEST(!cache.get("answer").has_value()); + cache.put("answer", "42"); + auto value = cache.get("answer"); + BOOST_TEST(value.has_value()); + BOOST_TEST(*value == "42"); + } + + void + run() + { + testAtomicOps(); + testWaitVariants(); + testSharedMutex(); + testCache(); + } +}; + +} // namespace + +TEST_SUITE(advanced_test, "boost.capy.doc.3c_advanced"); diff --git a/test/doc/snippets/3d_patterns.cpp b/test/doc/snippets/3d_patterns.cpp new file mode 100644 index 000000000..7b42e63c8 --- /dev/null +++ b/test/doc/snippets/3d_patterns.cpp @@ -0,0 +1,139 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/3.concurrency/3d.patterns.adoc. + +// tag::thread_safe_queue[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// end::thread_safe_queue[] + +#include + +#include "test_suite.hpp" + +namespace { + +// tag::thread_safe_queue[] + +template +class ThreadSafeQueue +{ + std::queue queue_; + std::mutex mutex_; + std::condition_variable cv_; + +public: + void push(T value) + { + { + std::lock_guard lock(mutex_); + queue_.push(std::move(value)); + } + cv_.notify_one(); + } + + T pop() + { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this]{ return !queue_.empty(); }); + T value = std::move(queue_.front()); + queue_.pop(); + return value; + } +}; +// end::thread_safe_queue[] + +// The page's std::async fragments launch this function. +int compute() +{ + return 6 * 7; +} + +struct patterns_test +{ + void + testLaunchPolicies() + { + { + // tag::launch_policies[] + // Force a new thread + auto future = std::async(std::launch::async, compute); + // end::launch_policies[] + BOOST_TEST(future.get() == 42); + } + { + // tag::launch_policies[] + + // Defer execution until get() + auto future = std::async(std::launch::deferred, compute); + // end::launch_policies[] + BOOST_TEST(future.get() == 42); + } + { + // tag::launch_policies[] + + // Let the system decide (default) + auto future = std::async(std::launch::async | std::launch::deferred, compute); + // end::launch_policies[] + BOOST_TEST(future.get() == 42); + } + } + + void + testQueue() + { + ThreadSafeQueue queue; + queue.push(1); + queue.push(2); + BOOST_TEST(queue.pop() == 1); + BOOST_TEST(queue.pop() == 2); + } + + void + run() + { + testLaunchPolicies(); + testQueue(); + } +}; + +} // namespace + +TEST_SUITE(patterns_test, "boost.capy.doc.3d_patterns"); diff --git a/test/doc/snippets/4a_tasks.cpp b/test/doc/snippets/4a_tasks.cpp new file mode 100644 index 000000000..86e96f9d7 --- /dev/null +++ b/test/doc/snippets/4a_tasks.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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4a.tasks.adoc. Pages +// include the tagged regions; scaffolding stays outside the tags. + +// Fragments deliberately leave named results unused; page comments +// explain the values instead. + +// tag::include_task[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// end::include_task[] + +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +// The page's first fragment introduces the umbrella include and the +// using-directive that later fragments rely on for unqualified names. +// tag::declaring[] +// tag::include_umbrella[] +#include +// end::include_umbrella[] +using namespace boost::capy; +// end::declaring[] + +namespace { + +namespace declaring { + +// tag::declaring[] + +task compute_value() +{ + co_return 42; +} + +task fetch_greeting() +{ + co_return "Hello, Capy!"; +} + +task<> do_nothing() // task +{ + co_return; +} +// end::declaring[] + +} // namespace declaring + +namespace returning { + +// tag::returning[] +task add(int a, int b) +{ + int result = a + b; + co_return result; // Completes with value +} + +task<> log_message(std::string msg) +{ + std::cout << msg << "\n"; + co_return; // Completes without value +} +// end::returning[] + +} // namespace returning + +namespace awaiting { + +// tag::awaiting[] +task step_one() +{ + co_return 10; +} + +task step_two(int x) +{ + co_return x * 2; +} + +task full_operation() +{ + int a = co_await step_one(); // Suspends until step_one completes + int b = co_await step_two(a); // Suspends until step_two completes + co_return b + 5; // Final result: 25 +} +// end::awaiting[] + +} // namespace awaiting + +namespace lazy { + +// tag::lazy[] +task compute() +{ + std::cout << "Computing...\n"; // Not printed until awaited + co_return 42; +} + +task<> example() +{ + auto t = compute(); // Task created, but "Computing..." NOT printed yet + std::cout << "Task created\n"; + + int result = co_await std::move(t); // NOW "Computing..." is printed + std::cout << "Result: " << result << "\n"; +} +// end::lazy[] + +} // namespace lazy + +namespace symmetric { + +task<> b(); +task<> c(); + +// tag::chain[] +task<> a() { co_await b(); } +task<> b() { co_await c(); } +task<> c() { co_return; } +// end::chain[] + +} // namespace symmetric + +// Host for the final_suspend awaiter excerpt; compiling is the test. +struct final_suspend_sketch +{ + std::coroutine_handle<> continuation_; + + // tag::final_suspend[] + // Inside task's final_suspend awaiter + std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept + { + return continuation_; // Transfer directly to continuation + } + // end::final_suspend[] +}; + +namespace moving { + +// tag::move_only[] +task compute(); + +task<> example() +{ + auto t1 = compute(); + auto t2 = std::move(t1); // OK: ownership transferred, t1 is now empty + + // auto t3 = t2; // Error: task is not copyable + + int result = co_await std::move(t2); +} +// end::move_only[] + +task compute() +{ + co_return 42; +} + +} // namespace moving + +namespace exceptions { + +// tag::exceptions[] +task might_fail(bool should_fail) +{ + if (should_fail) + throw std::runtime_error("Operation failed"); + co_return 42; +} + +task<> example() +{ + try + { + int result = co_await might_fail(true); + } + catch (std::runtime_error const& e) + { + std::cout << "Caught: " << e.what() << "\n"; + } +} +// end::exceptions[] + +} // namespace exceptions + +struct tasks_test +{ + void + testDeclaring() + { + thread_pool pool(1); + int value = 0; + std::string greeting; + run_async(pool.get_executor(), [&](int v) { value = v; })( + declaring::compute_value()); + run_async(pool.get_executor(), [&](std::string s) { greeting = s; })( + declaring::fetch_greeting()); + run_async(pool.get_executor())(declaring::do_nothing()); + pool.join(); + BOOST_TEST(value == 42); + BOOST_TEST(greeting == "Hello, Capy!"); + } + + void + testReturning() + { + thread_pool pool(1); + int sum = 0; + run_async(pool.get_executor(), [&](int r) { sum = r; })( + returning::add(2, 3)); + run_async(pool.get_executor())(returning::log_message("logged")); + pool.join(); + BOOST_TEST(sum == 5); + } + + void + testAwaiting() + { + thread_pool pool(1); + int result = 0; + run_async(pool.get_executor(), [&](int r) { result = r; })( + awaiting::full_operation()); + pool.join(); + BOOST_TEST(result == 25); + } + + void + testLazy() + { + thread_pool pool(1); + run_async(pool.get_executor())(lazy::example()); + pool.join(); + } + + void + testChain() + { + thread_pool pool(1); + run_async(pool.get_executor())(symmetric::a()); + pool.join(); + } + + void + testMoveOnly() + { + thread_pool pool(1); + run_async(pool.get_executor())(moving::example()); + pool.join(); + } + + void + testExceptions() + { + thread_pool pool(1); + run_async(pool.get_executor())(exceptions::example()); + pool.join(); + } + + void + run() + { + testDeclaring(); + testReturning(); + testAwaiting(); + testLazy(); + testChain(); + testMoveOnly(); + testExceptions(); + } +}; + +} // namespace + +TEST_SUITE(tasks_test, "boost.capy.doc.4a_tasks"); diff --git a/test/doc/snippets/4b_launching.cpp b/test/doc/snippets/4b_launching.cpp new file mode 100644 index 000000000..26d9263cb --- /dev/null +++ b/test/doc/snippets/4b_launching.cpp @@ -0,0 +1,260 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4b.launching.adoc. Pages +// include the tagged regions; scaffolding stays outside the tags. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 { + +// The page teaches with unqualified names after its opening program +// shows `using namespace boost::capy;`. +using namespace boost::capy; + +task compute() +{ + co_return 42; +} + +task expensive_computation() +{ + co_return 6 * 7; +} + +// tag::run_hop[] +task compute_on_pool(thread_pool& pool) +{ + // This task runs on whatever executor we're already on + + // But this child task runs on the pool's executor: + int result = co_await run(pool.get_executor())(expensive_computation()); + + // After co_await, we're back on our original executor + co_return result; +} +// end::run_hop[] + +std::atomic cancellable_saw_token{false}; + +task cancellable_task() +{ + auto token = co_await this_coro::stop_token; + cancellable_saw_token = token.stop_possible(); +} + +std::atomic shared_state{0}; +std::atomic handler_thread_id{}; + +void update_shared_state(int value) +{ + shared_state = value; + handler_thread_id = std::this_thread::get_id(); +} + +// Hosts for the parent() fragments: the shown code refers to a `pool` +// in scope, and the child records what its stop token observed. +struct inherit_fixture +{ + thread_pool pool{1}; + bool child_saw_stop = false; + + task child_task() + { + auto token = co_await this_coro::stop_token; + child_saw_stop = token.stop_requested(); + } + + // tag::inherit_token[] + task parent() + { + // Child automatically receives our stop token + co_await run(pool.get_executor())(child_task()); + } + // end::inherit_token[] +}; + +struct override_fixture +{ + thread_pool pool{1}; + bool child_saw_stop = true; + + task child_task() + { + auto token = co_await this_coro::stop_token; + child_saw_stop = token.stop_requested(); + } + + // tag::override_token[] + task parent() + { + std::stop_source local; + // Child gets local's token, not our caller's + co_await run(pool.get_executor(), local.get_token())(child_task()); + } + // end::override_token[] +}; + +struct launching_test +{ + void + testHandlerOverloads() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + // tag::handlers[] + // Result handler only (an unhandled exception calls std::terminate) + run_async(ex, [](int result) { + std::cout << "Got: " << result << "\n"; + })(compute()); + + // Separate handlers for result and exception + run_async(ex, + [](int result) { std::cout << "Result: " << result << "\n"; }, + [](std::exception_ptr ep) { + try { std::rethrow_exception(ep); } + catch (std::exception const& e) { + std::cout << "Error: " << e.what() << "\n"; + } + } + )(compute()); + // end::handlers[] + pool.join(); + } + + void + testRunHop() + { + thread_pool launch_pool(1); + thread_pool work_pool(1); + int result = 0; + run_async(launch_pool.get_executor(), [&](int r) { result = r; })( + compute_on_pool(work_pool)); + launch_pool.join(); + BOOST_TEST(result == 42); + work_pool.join(); + } + + void + testInjectToken() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + // tag::inject_token[] + std::stop_source source; + run_async(ex, source.get_token())(cancellable_task()); + + // Later, to request cancellation: + source.request_stop(); + // end::inject_token[] + pool.join(); + BOOST_TEST(cancellable_saw_token); + } + + void + testInheritToken() + { + thread_pool launch_pool(1); + inherit_fixture fx; + // A pre-stopped source makes inheritance observable in the child + std::stop_source source; + source.request_stop(); + run_async(launch_pool.get_executor(), source.get_token())( + fx.parent()); + launch_pool.join(); + BOOST_TEST(fx.child_saw_stop); + fx.pool.join(); + } + + void + testOverrideToken() + { + thread_pool launch_pool(1); + override_fixture fx; + // The caller's token is stopped, but the child sees the fresh + // local token instead + std::stop_source source; + source.request_stop(); + run_async(launch_pool.get_executor(), source.get_token())( + fx.parent()); + launch_pool.join(); + BOOST_TEST(!fx.child_saw_stop); + fx.pool.join(); + } + + void + testHandlerThreading() + { + thread_pool pool(4); + // tag::handler_thread[] + // If pool has 4 threads, the handler runs on one of those threads + run_async(pool.get_executor(), [](int result) { + // This runs on a pool thread, NOT the main thread + update_shared_state(result); + })(compute()); + // end::handler_thread[] + pool.join(); + BOOST_TEST(shared_state == 42); + BOOST_TEST(handler_thread_id.load() != std::this_thread::get_id()); + } + + void + run() + { + testHandlerOverloads(); + testRunHop(); + testInjectToken(); + testInheritToken(); + testOverrideToken(); + testHandlerThreading(); + } +}; + +} // namespace + +TEST_SUITE(launching_test, "boost.capy.doc.4b_launching"); diff --git a/test/doc/snippets/4c_executors.cpp b/test/doc/snippets/4c_executors.cpp new file mode 100644 index 000000000..e3dd07b4d --- /dev/null +++ b/test/doc/snippets/4c_executors.cpp @@ -0,0 +1,250 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4c.executors.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// tag::shared_resource[] +#include + +// end::shared_resource[] +#include +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +using namespace boost::capy; + +struct request {}; +struct response {}; + +response process(request const&) { return {}; } + +struct connection +{ + struct { int requests = 0; } stats; + + task read() { co_return request{}; } + task write(response) { co_return; } +}; + +// tag::handle_client[] +task handle_client(connection& conn) +{ + auto req = co_await conn.read(); + auto resp = process(req); + co_await conn.write(resp); + conn.stats.requests++; +} +// end::handle_client[] + +// The page shows the requires-expression; the real concept in +// adds nothrow copy/move +// constructibility, which the prose states separately. +namespace executor_concept_sketch { + +template +// tag::executor_concept[] +concept Executor = requires(E const& ce, E const& ce2, continuation& c) { + // Equality comparable + { ce == ce2 } noexcept -> std::convertible_to; + + // Owning context, returned as an lvalue reference to a type + // derived from execution_context + { ce.context() } noexcept; + + // Work tracking + { ce.on_work_started() } noexcept; + { ce.on_work_finished() } noexcept; + + // Scheduling + { ce.dispatch(c) } -> std::same_as>; + { ce.post(c) }; +}; +// end::executor_concept[] + +} // namespace executor_concept_sketch + +static_assert(executor_concept_sketch::Executor< + thread_pool::executor_type>); +static_assert(executor_concept_sketch::Executor< + strand>); + +// Signature sketch; the real declaration is in +// . +namespace api_sketch { + +class thread_pool +{ +public: + // tag::thread_pool_ctor[] + thread_pool( + std::size_t num_threads = 0, + std::string_view thread_name_prefix = "capy-pool-" + ); + // end::thread_pool_ctor[] +}; + +} // namespace api_sketch + +struct my_executor; + +// tag::my_context[] +class my_context : public execution_context +{ +public: + // ... custom implementation + + my_executor get_executor(); +}; +// end::my_context[] + +// tag::shared_resource[] +class shared_resource +{ + strand strand_; + int counter_ = 0; + +public: + explicit shared_resource(thread_pool& pool) + : strand_(pool.get_executor()) + { + } + + task increment() + { + // All increments are serialized through the strand + co_return co_await run(strand_)(do_increment()); + } + +private: + task do_increment() + { + // No mutex needed—strand ensures exclusive access + ++counter_; + co_return counter_; + } +}; +// end::shared_resource[] + +task independent_task(int i) +{ + co_return i; +} + +struct executors_test +{ + void testHandleClient() + { + thread_pool pool(1); + connection conn; + run_async(pool.get_executor())(handle_client(conn)); + pool.join(); + BOOST_TEST(conn.stats.requests == 1); + } + + void testSharedResource() + { + thread_pool pool(2); + shared_resource sr(pool); + int result = 0; + run_async(pool.get_executor(), [&result](int v) { + result = v; + })(sr.increment()); + pool.join(); + BOOST_TEST(result == 1); + } + + void testSingleThread() + { + // tag::single_thread[] + thread_pool single_thread(1); + auto ex = single_thread.get_executor(); + // All work runs on the single thread + // end::single_thread[] + (void)ex; + } + + void testDataStrand() + { + // tag::data_strand[] + thread_pool pool(4); + strand data_strand(pool.get_executor()); + + // Use data_strand for all access to shared data + // Use pool.get_executor() for independent work + // end::data_strand[] + (void)data_strand; + } + + void testIndependentWork() + { + // tag::independent_tasks[] + thread_pool pool(4); + auto ex = pool.get_executor(); + + // Launch independent tasks directly on the pool + std::vector> tasks; + for (int i = 0; i < 100; ++i) + run_async(ex)(independent_task(i)); + // end::independent_tasks[] + pool.join(); + } + + void run() + { + testHandleClient(); + testSharedResource(); + testSingleThread(); + testDataStrand(); + testIndependentWork(); + } +}; + +} // namespace + +TEST_SUITE(executors_test, "boost.capy.doc.4c_executors"); diff --git a/test/doc/snippets/4d_io_awaitable.cpp b/test/doc/snippets/4d_io_awaitable.cpp new file mode 100644 index 000000000..bf440d1c3 --- /dev/null +++ b/test/doc/snippets/4d_io_awaitable.cpp @@ -0,0 +1,335 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4d.io-awaitable.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +// The page shows the three standard await_suspend forms side by +// side; a single type cannot declare all three, so each lives in +// its own struct contributing a region of the same tag. +struct std_awaiter_void +{ + // tag::std_await_suspend[] + void await_suspend(std::coroutine_handle<> h); + // end::std_await_suspend[] +}; + +struct std_awaiter_bool +{ + // tag::std_await_suspend[] + // or + bool await_suspend(std::coroutine_handle<> h); + // end::std_await_suspend[] +}; + +struct std_awaiter_handle +{ + // tag::std_await_suspend[] + // or + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h); + // end::std_await_suspend[] +}; + +struct io_awaiter_signature +{ + // tag::two_arg_await_suspend[] + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env); + // end::two_arg_await_suspend[] +}; + +// The real definition lives in ; +// the sketch is checked below against types that satisfy (and fail) +// the real concept. +namespace concept_sketch { + +// tag::io_awaitable_concept[] +template +concept IoAwaitable = + requires(A a, std::coroutine_handle<> h, io_env const* env) { + a.await_suspend(h, env); + }; +// end::io_awaitable_concept[] + +} // namespace concept_sketch + +static_assert(concept_sketch::IoAwaitable>); +static_assert(capy::IoAwaitable>); +static_assert(!concept_sketch::IoAwaitable); +static_assert(!capy::IoAwaitable); + +struct caller_promise +{ + io_env const* env_ = nullptr; + + io_env const* environment() const noexcept { return env_; } +}; + +template +struct transform_awaiter_sketch +{ + Awaitable awaitable_; + caller_promise& promise_; + + // tag::context_flow[] + template + auto await_suspend(std::coroutine_handle h) + { + // Forward caller's context to child + return awaitable_.await_suspend(h, promise_.environment()); + } + // end::context_flow[] +}; + +struct context_flow_child +{ + std::coroutine_handle<> await_suspend( + std::coroutine_handle<>, io_env const*) + { + return std::noop_coroutine(); + } +}; + +// Instantiates the sketch; never called. +[[maybe_unused]] std::coroutine_handle<> instantiate_context_flow( + transform_awaiter_sketch& ta, + std::coroutine_handle h) +{ + return ta.await_suspend(h); +} + +using result_type = int; + +// Stand-ins for the async machinery the page leaves unspecified. +void start_operation() {} +void start_async_operation() {} +void store_result() {} + +// tag::my_awaitable[] +struct my_awaitable +{ + io_env const* env_ = nullptr; + continuation cont_; + result_type result_; + + bool await_ready() const noexcept + { + return false; // Or true if result is immediately available + } + + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env) + { + // Store pointer to environment, never copy + env_ = env; + // Wrap the caller's handle in a continuation we own, so it stays + // at a stable address until the executor resumes it. + cont_.h = h; + + // Start async operation... + start_operation(); + + // Return noop to suspend + return std::noop_coroutine(); + } + + result_type await_resume() + { + return result_; + } + +private: + void on_completion() + { + // Resume the caller on its executor. post() takes the + // continuation by reference and queues it; never resume inline + // from a completion callback (it may run on the wrong thread). + env_->executor.post(cont_); + } +}; +// end::my_awaitable[] + +// tag::stoppable_awaitable[] +struct stoppable_awaitable +{ + mutable continuation cont_; + std::optional>> stop_cb_; + + bool await_ready() { return false; } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, io_env const* env) + { + if (env->stop_token.stop_requested()) + return h; // Already cancelled, resume immediately + + // Post through executor when stop is requested + cont_.h = h; + auto ex = env->executor; + stop_cb_.emplace(env->stop_token, + [this, ex]() mutable noexcept { ex.post(cont_); }); + + start_async_operation(); + return std::noop_coroutine(); + } + + void await_resume() { /* ... */ } +}; +// end::stoppable_awaitable[] + +// Compiles on purpose: the page shows this to warn that the broken +// pattern is not caught by the compiler. +struct wrong_stop_callback_demo +{ + std::optional>> stop_cb_; + + void emplace_wrong(std::coroutine_handle<> h, io_env const* env) + { + // tag::wrong_stop_callback[] + // WRONG: resumes coroutine on the calling thread + stop_cb_.emplace(env->stop_token, h); // h is a raw coroutine_handle + // end::wrong_stop_callback[] + } +}; + +// Mirrors the shape of task.hpp's transform_awaitable; the shown +// static_assert sits in the branch taken for non-IoAwaitables. +template +void await_transform_sketch() +{ + if constexpr (capy::IoAwaitable) + { + } + else + { + // tag::reject_plain[] + // In task.hpp, when the awaited type is not an IoAwaitable: + static_assert(sizeof(A) == 0, "requires IoAwaitable"); + // end::reject_plain[] + } +} + +template void await_transform_sketch>(); + +// The "foreign runtime" is played by a plain thread that invokes the +// completion callback, exactly like a callback-based C library would. +std::thread foreign_thread; + +void start_foreign_op(std::function on_complete) +{ + foreign_thread = std::thread(std::move(on_complete)); +} + +// tag::foreign_bridge[] +// Bridge a foreign async operation into a Capy coroutine. +struct foreign_bridge +{ + io_env const* env_ = nullptr; + continuation cont_; + result_type result_; + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, io_env const* env) + { + env_ = env; + cont_.h = h; // stable address; the executor links continuations intrusively + + // Start the foreign operation. Its completion callback may run on + // ANY thread, so it must not resume h directly. Instead it posts + // the continuation back through the caller's executor, restoring + // the same-executor invariant. + start_foreign_op([this]() noexcept { + store_result(); + env_->executor.post(cont_); + }); + + return std::noop_coroutine(); + } + + result_type await_resume() { return std::move(result_); } +}; +// end::foreign_bridge[] + +task use_bridge() +{ + co_return co_await foreign_bridge{}; +} + +struct io_awaitable_test +{ + void testForeignBridge() + { + thread_pool pool(1); + bool done = false; + run_async(pool.get_executor(), [&done](int) { + done = true; + })(use_bridge()); + pool.join(); + if (foreign_thread.joinable()) + foreign_thread.join(); + BOOST_TEST(done); + } + + void run() + { + testForeignBridge(); + } +}; + +} // namespace + +TEST_SUITE(io_awaitable_test, "boost.capy.doc.4d_io_awaitable"); diff --git a/test/doc/snippets/4e_cancellation.cpp b/test/doc/snippets/4e_cancellation.cpp new file mode 100644 index 000000000..44540a937 --- /dev/null +++ b/test/doc/snippets/4e_cancellation.cpp @@ -0,0 +1,536 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4e.cancellation.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// tag::include_stop_token[] +#include +// end::include_stop_token[] +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +void do_work() {} + +// tag::naive_flag[] +std::atomic should_cancel{false}; + +void worker() +{ + while (!should_cancel) + { + do_work(); + } +} +// end::naive_flag[] + +// The #include directives inside the tag expand to nothing here (the +// headers are already included above); they are kept for the page text. +// tag::observer_pattern[] +#include +#include + +void example() +{ + std::stop_source source; + + // Create tokens (distribute notification capability) + std::stop_token token1 = source.get_token(); + std::stop_token token2 = source.get_token(); // Same underlying state + + // Register callbacks (observers) + std::stop_callback cb1(token1, []{ std::cout << "Observer 1 notified\n"; }); + std::stop_callback cb2(token2, []{ std::cout << "Observer 2 notified\n"; }); + + std::cout << "Before signal\n"; + source.request_stop(); // Triggers all callbacks + std::cout << "After signal\n"; +} +// end::observer_pattern[] + +task<> child(); + +// tag::token_propagation[] +task<> parent() +{ + // Our stop token is automatically passed to child + co_await child(); +} + +task<> child() +{ + // Receives parent's stop token via IoAwaitable protocol + auto token = co_await this_coro::stop_token; // Access current token +} +// end::token_propagation[] + +task<> do_chunk_of_work() { co_return; } + +// tag::access_stop_token[] +task<> cancellable_work() +{ + auto token = co_await this_coro::stop_token; + + while (!token.stop_requested()) + { + co_await do_chunk_of_work(); + } +} +// end::access_stop_token[] + +struct Item {}; + +task<> process(Item const&) { co_return; } + +// tag::check_token[] +task<> process_items(std::vector const& items) +{ + auto token = co_await this_coro::stop_token; + + for (auto const& item : items) + { + if (token.stop_requested()) + co_return; // Exit early + + co_await process(item); + } +} +// end::check_token[] + +struct resource_handle {}; + +resource_handle acquire_resource() { return {}; } + +task<> use_resource(resource_handle&) { co_return; } + +// tag::raii_cleanup[] +task<> with_resource() +{ + auto resource = acquire_resource(); // RAII wrapper + auto token = co_await this_coro::stop_token; + + while (!token.stop_requested()) + { + co_await use_resource(resource); + } + // resource destructor runs regardless of how we exit +} +// end::raii_cleanup[] + +task do_fetch() { co_return "payload"; } + +// tag::canceled_convention[] +task fetch_with_cancel() +{ + auto token = co_await this_coro::stop_token; + + if (token.stop_requested()) + { + throw std::system_error( + make_error_code(error::canceled)); + } + + co_return co_await do_fetch(); +} +// end::canceled_convention[] + +// tag::inline_resume_wrong[] +// WRONG — causes use-after-free +std::optional>> stop_cb; + +std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, io_env const* env) +{ + stop_cb.emplace(env->stop_token, h); // Resumes inline! + return std::noop_coroutine(); +} +// end::inline_resume_wrong[] + +// Compile-only bug demo: firing the callback would resume the +// coroutine on the stopping thread, so nothing ever calls it. +[[maybe_unused]] std::coroutine_handle<> (* const wrong_pattern)( + std::coroutine_handle<>, io_env const*) = &await_suspend; + +void start_async_operation(std::coroutine_handle<>, io_env const*) {} + +// tag::stoppable_awaitable[] +struct my_stoppable_awaitable +{ + using stop_cb_t = std::stop_callback>; + + mutable continuation cont_; + std::unique_ptr stop_cb_; + // ... other members for the async operation ... + + bool await_ready() { return false; } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, io_env const* env) + { + if (env->stop_token.stop_requested()) + return h; // Already cancelled + + cont_.h = h; + auto ex = env->executor; + stop_cb_ = std::make_unique(env->stop_token, + [this, ex]() mutable noexcept { ex.post(cont_); }); + + start_async_operation(h, env); + return std::noop_coroutine(); + } + + void await_resume() { /* check result or throw */ } +}; +// end::stoppable_awaitable[] + +task<> use_stoppable() +{ + co_await my_stoppable_awaitable{}; +} + +// tag::racing_deadline[] +struct fetch_channel +{ + capy::async_waker fetch_ready; + std::atomic cancelled{false}; + std::string result; +}; + +// One side of the race: completes when the fetch worker thread +// wakes fetch_ready. If the deadline wins first, when_any's stop +// request cancels the wait; flag the worker so it stops early. +capy::io_task await_fetch(fetch_channel& ch) +{ + auto [ec] = co_await ch.fetch_ready.wait(); + if (ec) + { + ch.cancelled.store(true); + co_return capy::io_result{ec, {}}; + } + co_return capy::io_result{{}, std::move(ch.result)}; +} + +// The other side of the race: completes once whatever plays the +// clock wakes the waker. +capy::io_task<> deadline(capy::async_waker& waker) +{ + auto [ec] = co_await waker.wait(); + co_return capy::io_result<>{ec}; +} +// end::racing_deadline[] + +task<> race(fetch_channel& ch, capy::async_waker& waker, std::size_t& winner) +{ + auto result = co_await when_any(await_fetch(ch), deadline(waker)); + winner = result.index(); +} + +task<> fetch_next_chunk(std::string const&) { co_return; } + +task download(std::string url); + +// tag::user_cancellation[] +class download_manager +{ + executor_ref executor_; + std::stop_source stop_source_; + +public: + void start_download(std::string url) + { + // Token propagated via io_env, not as a function argument + run_async(executor_, stop_source_.get_token())(download(url)); + } + + void cancel() + { + stop_source_.request_stop(); + } +}; + +task download(std::string url) +{ + auto token = co_await this_coro::stop_token; // From run_async's io_env + while (!token.stop_requested()) + { + co_await fetch_next_chunk(url); + } +} +// end::user_cancellation[] + +// download_manager needs a live execution context to launch; the +// class itself is the demonstration, so nothing instantiates it. +[[maybe_unused]] task (* const download_demo)(std::string) = &download; + +struct connection {}; + +task<> process_request(connection&) { co_return; } + +task<> send_goodbye(connection&) { co_return; } + +// tag::graceful_shutdown[] +class server +{ + std::stop_source shutdown_source_; + +public: + void shutdown() + { + shutdown_source_.request_stop(); + // All pending operations receive stop request + } + + task<> handle_connection(connection conn) + { + auto token = shutdown_source_.get_token(); + + while (!token.stop_requested()) + { + co_await process_request(conn); + } + + // Graceful cleanup + co_await send_goodbye(conn); + } +}; +// end::graceful_shutdown[] + +struct cancellation_test +{ + void + testNaiveFlag() + { + should_cancel = true; + worker(); + BOOST_TEST(should_cancel.load()); + } + + void + testObserverPattern() + { + example(); + } + + void + testImmediateInvocation() + { + // tag::immediate_invocation[] + std::stop_source source; + source.request_stop(); // Already signaled + + // Callback runs in constructor, not later + std::stop_callback cb(source.get_token(), []{ + std::cout << "Runs immediately!\n"; + }); + // end::immediate_invocation[] + BOOST_TEST(source.stop_requested()); + } + + void + testResetWorkaround() + { + // tag::reset_workaround[] + std::stop_source source; + auto token = source.get_token(); + + // ... distribute token to workers ... + + source.request_stop(); // Triggered, now permanently signaled + + // To "reset": create new source + source = std::stop_source{}; // New shared state + // Old tokens still see the old, already-signaled state + + // Must redistribute new tokens to ALL holders of the old token + auto new_token = source.get_token(); + // end::reset_workaround[] + BOOST_TEST(token.stop_requested()); + BOOST_TEST(!new_token.stop_requested()); + } + + void + testTokenPropagation() + { + capy::thread_pool pool(1); + capy::run_async(pool.get_executor())(parent()); + pool.join(); + } + + void + testAccessStopToken() + { + capy::thread_pool pool(1); + std::stop_source source; + source.request_stop(); + capy::run_async( + pool.get_executor(), source.get_token())(cancellable_work()); + pool.join(); + } + + void + testCheckToken() + { + capy::thread_pool pool(1); + std::vector items(3); + capy::run_async(pool.get_executor())(process_items(items)); + pool.join(); + } + + void + testRaiiCleanup() + { + capy::thread_pool pool(1); + std::stop_source source; + source.request_stop(); + capy::run_async( + pool.get_executor(), source.get_token())(with_resource()); + pool.join(); + } + + void + testCanceledConvention() + { + capy::thread_pool pool(1); + std::stop_source source; + source.request_stop(); + bool canceled = false; + capy::run_async(pool.get_executor(), source.get_token(), + [](std::string const&) {}, + [&canceled](std::exception_ptr ep) + { + try + { + std::rethrow_exception(ep); + } + catch (std::system_error const& e) + { + canceled = e.code() == capy::cond::canceled; + } + })(fetch_with_cancel()); + pool.join(); + BOOST_TEST(canceled); + } + + void + testStoppableAwaitable() + { + // A pre-signaled token exercises the already-cancelled fast + // path; the armed path would wait on an operation that this + // sketch never starts. + capy::thread_pool pool(1); + std::stop_source source; + source.request_stop(); + capy::run_async( + pool.get_executor(), source.get_token())(use_stoppable()); + pool.join(); + } + + void + testRacingDeadline() + { + capy::thread_pool pool(1); + fetch_channel ch; + capy::async_waker deadline_waker; + std::size_t winner = 0; + capy::run_async( + pool.get_executor())(race(ch, deadline_waker, winner)); + // The test thread plays the clock: an early wake is latched, + // so ordering against the race's suspension is benign. + deadline_waker.wake(); + pool.join(); + BOOST_TEST(winner == 2); + BOOST_TEST(ch.cancelled.load()); + } + + void + testGracefulShutdown() + { + capy::thread_pool pool(1); + server s; + s.shutdown(); + capy::run_async(pool.get_executor())(s.handle_connection({})); + pool.join(); + } + + void + run() + { + testNaiveFlag(); + testObserverPattern(); + testImmediateInvocation(); + testResetWorkaround(); + testTokenPropagation(); + testAccessStopToken(); + testCheckToken(); + testRaiiCleanup(); + testCanceledConvention(); + testStoppableAwaitable(); + testRacingDeadline(); + testGracefulShutdown(); + } +}; + +} // namespace + +TEST_SUITE(cancellation_test, "boost.capy.doc.4e_cancellation"); diff --git a/test/doc/snippets/4f_composition.cpp b/test/doc/snippets/4f_composition.cpp new file mode 100644 index 000000000..5885ca9a1 --- /dev/null +++ b/test/doc/snippets/4f_composition.cpp @@ -0,0 +1,600 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4f.composition.adoc. + +// tag::when_all_basic[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// end::when_all_basic[] +// tag::when_any_basic[] +#include +// end::when_any_basic[] + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +// GCC gives false positive -Wmaybe-uninitialized on structured bindings +// via the tuple protocol inside coroutine frames. +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + +namespace { + +using namespace boost::capy; + +namespace overview { + +std::atomic runs{0}; + +io_task<> task_a() { ++runs; co_return io_result<>{}; } +io_task<> task_b() { ++runs; co_return io_result<>{}; } +io_task<> task_c() { ++runs; co_return io_result<>{}; } + +// tag::sequential[] +task<> sequential() +{ + co_await task_a(); // Wait for A + co_await task_b(); // Then wait for B + co_await task_c(); // Then wait for C +} +// end::sequential[] + +// tag::concurrent[] +task<> concurrent() +{ + // Run A, B, C simultaneously + co_await when_all(task_a(), task_b(), task_c()); +} +// end::concurrent[] + +} // namespace overview + +namespace when_all_basics { + +// tag::when_all_basic[] + +io_task fetch_a() { co_return io_result{{}, 1}; } +io_task fetch_b() { co_return io_result{{}, 2}; } +io_task fetch_c() { co_return io_result{{}, "hello"}; } + +task<> example() +{ + auto [ec, a, b, c] = co_await when_all(fetch_a(), fetch_b(), fetch_c()); + + // ec == std::error_code{} (success) + // a == 1 + // b == 2 + // c == "hello" +} +// end::when_all_basic[] + +} // namespace when_all_basics + +namespace void_mix { + +// tag::when_all_void_mix[] +io_task<> void_task() { co_return io_result<>{}; } +io_task int_task() { co_return io_result{{}, 42}; } + +task<> example() +{ + auto [ec, a, b, c] = co_await when_all(int_task(), void_task(), int_task()); + // a == 42 (int) + // b == tuple<> (from void io_task) + // c == 42 (int) +} +// end::when_all_void_mix[] + +} // namespace void_mix + +namespace all_void { + +io_task<> void_task_a() { co_return io_result<>{}; } +io_task<> void_task_b() { co_return io_result<>{}; } + +// tag::when_all_all_void[] +task<> example() +{ + auto r = co_await when_all(void_task_a(), void_task_b()); + if (r.ec) + { + // handle error + } +} +// end::when_all_all_void[] + +} // namespace all_void + +namespace error_handling { + +io_task task_a() { co_return io_result{{}, 1}; } +io_task task_b() { co_return io_result{error::timeout, 0}; } + +// tag::when_all_error[] +task<> example() +{ + auto [ec, a, b] = co_await when_all(task_a(), task_b()); + if (ec) + std::cerr << "Error: " << ec.message() << "\n"; +} +// end::when_all_error[] + +} // namespace error_handling + +namespace exceptions { + +// tag::when_all_exception[] +io_task might_throw(bool fail) +{ + if (fail) + throw std::runtime_error("failed"); + co_return io_result{{}, 42}; +} + +task<> example() +{ + try + { + co_await when_all(might_throw(true), might_throw(false)); + } + catch (std::runtime_error const& e) + { + // Catches the exception from the failing task + } +} +// end::when_all_exception[] + +} // namespace exceptions + +namespace stop_prop { + +std::atomic iterations{0}; + +io_task<> do_iteration() +{ + ++iterations; + co_return io_result<>{}; +} + +io_task<> fail_fast() { co_return io_result<>{error::timeout}; } + +// tag::stop_propagation[] +io_task<> long_running() +{ + auto token = co_await this_coro::stop_token; + + for (int i = 0; i < 1000; ++i) + { + if (token.stop_requested()) + co_return io_result<>{}; // Exit early when sibling fails + + co_await do_iteration(); + } + co_return io_result<>{}; +} +// end::stop_propagation[] + +} // namespace stop_prop + +namespace any_basic { + +io_task fetch_int() { co_return io_result{{}, 7}; } +io_task fetch_string() { co_return io_result{{}, "s"}; } + +// tag::when_any_basic[] + +task<> example() +{ + auto result = co_await when_any( + fetch_int(), // io_task + fetch_string() // io_task + ); + // result is std::variant + // index 0: all tasks failed (error_code) + // index 1: fetch_int won + // index 2: fetch_string won +} +// end::when_any_basic[] + +} // namespace any_basic + +namespace wrap_translate { + +io_task<> inner() { co_return io_result<>{error::canceled}; } + +// tag::wrap_translate_error[] +// canceled is benign here: translate it to success so when_any picks this child. +io_task<> wrapped() +{ + auto [ec] = co_await inner(); + if (ec == cond::canceled) + co_return io_result<>{}; // success: when_any sees a winner + co_return io_result<>{ec}; // propagate other errors unchanged +} +// end::wrap_translate_error[] + +} // namespace wrap_translate + +namespace wrap_lift { + +io_task<> inner() { co_return io_result<>{error::timeout}; } + +// tag::wrap_lift_error[] +// Always succeeds; the winner's payload carries the original ec. +io_task wrapped() +{ + auto [ec] = co_await inner(); + co_return io_result{{}, ec}; +} + +// when_any(wrapped(), ...) -> variant +// index 0: every child failed +// index i: child i won; std::get(result) is its original ec +// end::wrap_lift_error[] + +} // namespace wrap_lift + +namespace parallel { + +struct page_data +{ + std::string header; + std::string body; + std::string sidebar; +}; + +io_task fetch_header(std::string url) +{ + co_return io_result{{}, url + ":header"}; +} + +io_task fetch_body(std::string url) +{ + co_return io_result{{}, url + ":body"}; +} + +io_task fetch_sidebar(std::string url) +{ + co_return io_result{{}, url + ":sidebar"}; +} + +// tag::parallel_fetch[] +io_task fetch_page_data(std::string url) +{ + auto [ec, header, body, sidebar] = co_await when_all( + fetch_header(url), + fetch_body(url), + fetch_sidebar(url) + ); + if (ec) + co_return io_result{ec, {}}; + + co_return io_result{{}, { + std::move(header), + std::move(body), + std::move(sidebar) + }}; +} +// end::parallel_fetch[] + +} // namespace parallel + +namespace fanout { + +struct item +{ + int value; +}; + +// tag::fan_out[] +io_task process_item(item const& i); + +task process_all(std::vector const& items) +{ + std::vector> tasks; + for (auto const& item : items) + tasks.push_back(process_item(item)); + + auto [ec, results] = co_await when_all(std::move(tasks)); + if (ec) + co_return 0; + + int total = 0; + for (auto v : results) + total += v; + co_return total; +} +// end::fan_out[] + +io_task process_item(item const& i) +{ + co_return io_result{{}, i.value}; +} + +} // namespace fanout + +struct composition_test +{ + void testOverview() + { + overview::runs = 0; + test::run_blocking()(overview::sequential()); + BOOST_TEST_EQ(overview::runs.load(), 3); + test::run_blocking()(overview::concurrent()); + BOOST_TEST_EQ(overview::runs.load(), 6); + } + + void testWhenAllBasic() + { + test::run_blocking()(when_all_basics::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + auto [ec, a, b, c] = co_await when_all( + when_all_basics::fetch_a(), + when_all_basics::fetch_b(), + when_all_basics::fetch_c()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(a, 1); + BOOST_TEST_EQ(b, 2); + BOOST_TEST(c == "hello"); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testVoidMix() + { + test::run_blocking()(void_mix::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + auto [ec, a, b, c] = co_await when_all( + void_mix::int_task(), + void_mix::void_task(), + void_mix::int_task()); + static_assert(std::is_same_v>); + BOOST_TEST(!ec); + BOOST_TEST_EQ(a, 42); + BOOST_TEST_EQ(c, 42); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testAllVoid() + { + test::run_blocking()(all_void::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + auto r = co_await when_all( + all_void::void_task_a(), + all_void::void_task_b()); + BOOST_TEST(!r.ec); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testErrorHandling() + { + test::run_blocking()(error_handling::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + auto [ec, a, b] = co_await when_all( + error_handling::task_a(), + error_handling::task_b()); + BOOST_TEST(ec == cond::timeout); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testException() + { + test::run_blocking()(exceptions::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + bool caught = false; + try + { + co_await when_all( + exceptions::might_throw(true), + exceptions::might_throw(false)); + } + catch (std::runtime_error const&) + { + caught = true; + } + BOOST_TEST(caught); + + auto [ec, x, y] = co_await when_all( + exceptions::might_throw(false), + exceptions::might_throw(false)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(x, 42); + BOOST_TEST_EQ(y, 42); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testStopPropagation() + { + stop_prop::iterations = 0; + + bool checked = false; + auto check = [&]() -> task<> + { + // fail_fast is posted first on the single-threaded test + // context, so its error requests stop before long_running + // starts iterating. + auto r = co_await when_all( + stop_prop::fail_fast(), + stop_prop::long_running()); + BOOST_TEST(r.ec == cond::timeout); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + BOOST_TEST_EQ(stop_prop::iterations.load(), 0); + } + + void testWhenAnyBasic() + { + test::run_blocking()(any_basic::example()); + + bool checked = false; + auto check = [&]() -> task<> + { + auto result = co_await when_any( + any_basic::fetch_int(), + any_basic::fetch_string()); + static_assert(std::is_same_v>); + BOOST_TEST_EQ(result.index(), 1u); + BOOST_TEST_EQ(std::get<1>(result), 7); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testWrapTranslate() + { + bool checked = false; + auto check = [&]() -> task<> + { + auto [ec] = co_await wrap_translate::wrapped(); + BOOST_TEST(!ec); + + auto r = co_await when_any(wrap_translate::wrapped()); + BOOST_TEST_EQ(r.index(), 1u); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testWrapLift() + { + bool checked = false; + auto check = [&]() -> task<> + { + auto r = co_await when_any(wrap_lift::wrapped()); + BOOST_TEST_EQ(r.index(), 1u); + BOOST_TEST(std::get<1>(r) == cond::timeout); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testParallelFetch() + { + bool checked = false; + auto check = [&]() -> task<> + { + auto [ec, pd] = co_await parallel::fetch_page_data("url"); + BOOST_TEST(!ec); + BOOST_TEST(pd.header == "url:header"); + BOOST_TEST(pd.body == "url:body"); + BOOST_TEST(pd.sidebar == "url:sidebar"); + checked = true; + }; + test::run_blocking()(check()); + BOOST_TEST(checked); + } + + void testFanOut() + { + std::vector items = {{1}, {2}, {3}}; + int total = 0; + test::run_blocking([&](int v) { total = v; })( + fanout::process_all(items)); + BOOST_TEST_EQ(total, 6); + } + + void run() + { + testOverview(); + testWhenAllBasic(); + testVoidMix(); + testAllVoid(); + testErrorHandling(); + testException(); + testStopPropagation(); + testWhenAnyBasic(); + testWrapTranslate(); + testWrapLift(); + testParallelFetch(); + testFanOut(); + } +}; + +} // namespace + +TEST_SUITE(composition_test, "boost.capy.doc.4f_composition"); diff --git a/test/doc/snippets/4g_allocators.cpp b/test/doc/snippets/4g_allocators.cpp new file mode 100644 index 000000000..810711e2b --- /dev/null +++ b/test/doc/snippets/4g_allocators.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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4g.allocators.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +std::atomic tasks_completed{0}; + +task<> my_task() +{ + ++tasks_completed; + co_return; +} + +// Minimal hand-rolled coroutine so a raw handle exists to feed +// safe_resume; task<> never exposes its handle. +struct resumable +{ + struct promise_type + { + resumable get_return_object() + { + return {std::coroutine_handle< + promise_type>::from_promise(*this)}; + } + std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() {} + }; + + std::coroutine_handle<> handle; +}; + +resumable clobber_tls(bool& resumed) +{ + resumed = true; + // Simulate another chain's await_resume overwriting TLS + set_current_frame_allocator(nullptr); + co_return; +} + +// tag::frame_alloc_mixin[] +struct my_coroutine +{ + struct promise_type : capy::frame_alloc_mixin + { + // get_return_object, initial_suspend, ... + }; +}; +// end::frame_alloc_mixin[] + +// Shadow copy of the library's task declaration; compiling it here +// proves the shown declaration is the real one. +namespace library_sketch { + +// tag::task_elidable[] +template +struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE + task +{ + // ... +}; +// end::task_elidable[] + +} // namespace library_sketch + +task compute() +{ + co_return 42; +} + +task<> halo_demo(std::vector>& tasks, int& out) +{ + // tag::halo_patterns[] + // HALO can apply: task is awaited immediately + int result = co_await compute(); + + // HALO cannot apply: task escapes to storage + auto t = compute(); + tasks.push_back(std::move(t)); + // end::halo_patterns[] + out = result; +} + +struct item {}; + +int items_processed = 0; + +task<> process(item const&) +{ + ++items_processed; + co_return; +} + +// Bound to a blocking (inline) executor in the test so every frame is +// released before the stack buffer backing the resource goes away. +any_executor executor; + +// tag::batch_allocator[] +void process_batch(std::vector const& items) +{ + std::array buffer; + std::pmr::monotonic_buffer_resource resource( + buffer.data(), buffer.size()); + + for (auto const& item : items) + { + run_async(executor, &resource)(process(item)); + } + // All frames deallocated when resource goes out of scope +} +// end::batch_allocator[] + +struct io_step +{ + std::error_code ec; + std::size_t n = 0; +}; + +struct stream +{ + int reads = 0; + int writes = 0; + + task read_some(char*) + { + ++reads; + co_return io_step{}; + } + + task write_some(char const*) + { + ++writes; + co_return io_step{}; + } +}; + +task<> do_work(char*, std::size_t) +{ + co_return; +} + +void prepare(char*, char const*, std::size_t) {} + +void prepare(char*, std::size_t) {} + +char reply[] = "reply"; + +namespace scope_bad { + +// tag::frame_scope_bad[] +// BAD: buf lives in frame across all subsequent co_awaits +task<> process(stream& s) +{ + char buf[4096]; + auto [ec, n] = co_await s.read_some(buf); + co_await do_work(buf, n); + co_await s.write_some(reply); // buf wastes 4K in frame +} +// end::frame_scope_bad[] + +} // namespace scope_bad + +namespace scope_good { + +// tag::frame_scope_good[] +// GOOD: braces end buf's lifetime before next suspend +task<> process(stream& s) +{ + std::size_t n; + { + char buf[4096]; + auto [ec, n_] = co_await s.read_some(buf); + n = n_; + co_await do_work(buf, n); + } + co_await s.write_some(reply); // 4K saved +} +// end::frame_scope_good[] + +} // namespace scope_good + +namespace overlap_bad { + +// tag::pipeline_overlap_bad[] +// BAD: both arrays in frame simultaneously (8K) +task<> pipeline(stream& in, stream& out) +{ + char read_buf[4096]; + auto [ec1, n] = co_await in.read_some(read_buf); + + char write_buf[4096]; + prepare(write_buf, read_buf, n); + co_await out.write_some(write_buf); +} +// end::pipeline_overlap_bad[] + +} // namespace overlap_bad + +namespace overlap_good { + +// tag::pipeline_overlap_good[] +// GOOD: non-overlapping scopes allow frame reuse (4K) +task<> pipeline(stream& in, stream& out) +{ + std::size_t n; + { + char read_buf[4096]; + auto [ec, n_] = co_await in.read_some(read_buf); + n = n_; + } + { + char write_buf[4096]; + prepare(write_buf, n); + co_await out.write_some(write_buf); + } +} +// end::pipeline_overlap_good[] + +} // namespace overlap_good + +struct allocators_test +{ + void + testTwoCallSyntax() + { + thread_pool pool(1); + auto executor = pool.get_executor(); + int const before = tasks_completed.load(); + // tag::two_call[] + run_async(executor)(my_task()); + // ↑ ↑ + // 1. Sets 2. Task allocated + // TLS using TLS allocator + // end::two_call[] + pool.join(); + BOOST_TEST(tasks_completed.load() == before + 1); + } + + void + testSafeResume() + { + // The sentinel proves TLS survives a resume that clobbers it + std::pmr::monotonic_buffer_resource sentinel; + set_current_frame_allocator(&sentinel); + bool resumed = false; + auto r = clobber_tls(resumed); + std::coroutine_handle<> h = r.handle; + // tag::safe_resume[] + // In your event loop or dispatch path: + capy::safe_resume(h); // saves and restores TLS around h.resume() + // end::safe_resume[] + BOOST_TEST(resumed); + BOOST_TEST(get_current_frame_allocator() == &sentinel); + set_current_frame_allocator(nullptr); + r.handle.destroy(); + } + + void + testRunAsyncPmrAllocator() + { + thread_pool pool(1); + auto executor = pool.get_executor(); + int const before = tasks_completed.load(); + // tag::run_async_pmr_alloc[] + std::pmr::monotonic_buffer_resource resource; + std::pmr::polymorphic_allocator alloc(&resource); + + run_async(executor, alloc)(my_task()); + // end::run_async_pmr_alloc[] + pool.join(); + BOOST_TEST(tasks_completed.load() == before + 1); + } + + void + testRunAsyncMemoryResource() + { + thread_pool pool(1); + auto executor = pool.get_executor(); + int const before = tasks_completed.load(); + // tag::run_async_memory_resource[] + std::pmr::monotonic_buffer_resource resource; + run_async(executor, &resource)(my_task()); + // end::run_async_memory_resource[] + pool.join(); + BOOST_TEST(tasks_completed.load() == before + 1); + } + + void + testHaloPatterns() + { + std::vector> tasks; + int out = 0; + test::run_blocking()(halo_demo(tasks, out)); + BOOST_TEST(out == 42); + BOOST_TEST(tasks.size() == 1u); + // The escaped task is destroyed unawaited when the vector dies + } + + void + testBatchAllocator() + { + test::blocking_context ctx; + executor = ctx.get_executor(); + std::vector items(3); + int const before = items_processed; + process_batch(items); + BOOST_TEST(items_processed == before + 3); + executor = any_executor(); + } + + void + testFrameScope() + { + stream s; + test::run_blocking()(scope_bad::process(s)); + test::run_blocking()(scope_good::process(s)); + BOOST_TEST(s.reads == 2); + BOOST_TEST(s.writes == 2); + } + + void + testPipelineOverlap() + { + stream in; + stream out; + test::run_blocking()(overlap_bad::pipeline(in, out)); + test::run_blocking()(overlap_good::pipeline(in, out)); + BOOST_TEST(in.reads == 2); + BOOST_TEST(out.writes == 2); + } + + void + run() + { + testTwoCallSyntax(); + testSafeResume(); + testRunAsyncPmrAllocator(); + testRunAsyncMemoryResource(); + testHaloPatterns(); + testBatchAllocator(); + testFrameScope(); + testPipelineOverlap(); + } +}; + +} // namespace + +TEST_SUITE(allocators_test, "boost.capy.doc.4g_allocators"); diff --git a/test/doc/snippets/4h_lambda_captures.cpp b/test/doc/snippets/4h_lambda_captures.cpp new file mode 100644 index 000000000..f83068972 --- /dev/null +++ b/test/doc/snippets/4h_lambda_captures.cpp @@ -0,0 +1,283 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/4.coroutines/4h.lambda-captures.adoc. +// The fragments that demonstrate dangling captures are compiled but +// never executed: running them would be undefined behavior. + +// Fragments deliberately leave structured bindings unused; page +// comments explain the values instead. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 capy = boost::capy; + +namespace { + +using namespace boost::capy; + +struct io_step +{ + std::error_code ec; + std::size_t n = 0; +}; + +int socket_reads = 0; + +// Stand-in socket that completes immediately, so the safe fragments +// can run to completion under the blocking test executor. +struct socket +{ + task read_some(mutable_buffer) + { + ++socket_reads; + co_return io_step{}; + } +}; + +any_executor executor; + +int log_count = 0; + +void log(char const*, std::string const&) +{ + ++log_count; +} + +task<> handle_request() +{ + co_return; +} + +namespace dangling_capture { + +// tag::dangling_capture[] +namespace capy = boost::capy; + +void process(socket& sock) +{ + auto task = [&sock]() -> capy::task<> + { + char buf[1024]; + auto [ec, n] = co_await sock.read_some(make_buffer(buf)); + }(); + + run_async(executor)(std::move(task)); +} +// end::dangling_capture[] + +} // namespace dangling_capture + +// Compiles but is never called: the closure dies before the coroutine +// resumes, so running it would be undefined behavior. +[[maybe_unused]] void (* const dangling_capture_demo)(socket&) = + &dangling_capture::process; + +namespace iife_param { + +// tag::iife_parameter[] +namespace capy = boost::capy; + +void process(socket& sock) +{ + auto task = [](socket* s) -> capy::task<> + { + char buf[1024]; + auto [ec, n] = co_await s->read_some(make_buffer(buf)); + }(&sock); + + run_async(executor)(std::move(task)); +} +// end::iife_parameter[] + +} // namespace iife_param + +namespace capture_this { + +// The class compiles but nothing ever calls run(): invoking the +// returned task would access a destroyed closure. +// tag::capture_this_broken[] +class connection_handler +{ + socket sock_; + std::string name_; + +public: + capy::task<> run() + { + // BROKEN: 'this' captured in lambda, lambda destroyed after invoke + return [this]() -> capy::task<> + { + log("Connection from", name_); // UB: 'this' is dangling + co_await handle_request(); + }(); + } +}; +// end::capture_this_broken[] + +} // namespace capture_this + +namespace parameter_self { + +// tag::parameter_self_correct[] +class connection_handler +{ + socket sock_; + std::string name_; + + capy::task<> handle_request(); + +public: + capy::task<> run() + { + // CORRECT: 'self' is a parameter, copied to coroutine frame + return [](connection_handler* self) -> capy::task<> + { + log("Connection from", self->name_); + co_await self->handle_request(); + }(this); + } +}; +// end::parameter_self_correct[] + +capy::task<> connection_handler::handle_request() +{ + co_return; +} + +} // namespace parameter_self + +namespace named_member { + +// tag::named_member_coroutine[] +class connection_handler +{ + socket sock_; + + capy::task<> do_handle() + { + // 'this' is an implicit parameter, handled correctly + char buf[1024]; + co_await sock_.read_some(make_buffer(buf)); + } + +public: + capy::task<> run() + { + return do_handle(); + } +}; +// end::named_member_coroutine[] + +} // namespace named_member + +struct lambda_captures_test +{ + void + testIifeParameter() + { + test::blocking_context ctx; + executor = ctx.get_executor(); + socket sock; + int const before = socket_reads; + iife_param::process(sock); + BOOST_TEST(socket_reads == before + 1); + executor = any_executor(); + } + + void + testParameterSelf() + { + parameter_self::connection_handler h; + int const before = log_count; + test::run_blocking()(h.run()); + BOOST_TEST(log_count == before + 1); + } + + void + testStoredLambda() + { + socket sock; + int const before = socket_reads; + // tag::stored_lambda_safe[] + // SAFE: lambda stored in 'handler', outlives coroutine + auto handler = [&sock]() -> capy::task<> + { + char buf[1024]; + co_await sock.read_some(make_buffer(buf)); + }; + + // Lambda 'handler' still exists here + capy::test::run_blocking()(handler()); // Blocks until coroutine completes + // Lambda destroyed after coroutine finishes + // end::stored_lambda_safe[] + BOOST_TEST(socket_reads == before + 1); + } + + void + testNamedMemberCoroutine() + { + named_member::connection_handler h; + int const before = socket_reads; + test::run_blocking()(h.run()); + BOOST_TEST(socket_reads == before + 1); + } + + void + run() + { + testIifeParameter(); + testParameterSelf(); + testStoredLambda(); + testNamedMemberCoroutine(); + } +}; + +} // namespace + +TEST_SUITE(lambda_captures_test, "boost.capy.doc.4h_lambda_captures"); diff --git a/test/doc/snippets/5a_overview.cpp b/test/doc/snippets/5a_overview.cpp new file mode 100644 index 000000000..bb0994a0e --- /dev/null +++ b/test/doc/snippets/5a_overview.cpp @@ -0,0 +1,115 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/5.buffers/5a.overview.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 { + +using namespace boost::capy; + +// tag::span_signatures[] +void write_data(std::span data); +void read_data(std::span buffer); +// end::span_signatures[] + +// tag::span_of_spans[] +void write_data(std::span const> buffers); +// end::span_of_spans[] + +// tag::span_aliases[] +using HeaderBuffers = std::array, 2>; // 2 buffers +using BodyBuffers = std::array, 3>; // 3 buffers +// end::span_aliases[] + +// tag::concept_signature[] +template +void write_data(Buffers const& buffers); +// end::concept_signature[] + +// Definitions for the declared signatures; the fragments only show +// the declarations. +[[maybe_unused]] void write_data(std::span) +{ +} + +[[maybe_unused]] void read_data(std::span) +{ +} + +// Records the buffer count so the combining fragment is observable. +std::size_t last_write_count = 0; + +void write_data(std::span const> buffers) +{ + last_write_count = buffers.size(); +} + +struct overview_test +{ + void testManualCombine() + { + // tag::span_combine[] + HeaderBuffers headers{ /* ... */ }; + BodyBuffers body{ /* ... */ }; + + // To combine, you MUST allocate a new array: + std::array, 5> combined; + std::copy(headers.begin(), headers.end(), combined.begin()); + std::copy(body.begin(), body.end(), combined.begin() + 2); + + write_data(combined); + // end::span_combine[] + BOOST_TEST(last_write_count == 5); + } + + void run() + { + testManualCombine(); + } +}; + +} // namespace + +TEST_SUITE(overview_test, "boost.capy.doc.5a_overview"); diff --git a/test/doc/snippets/5b_types.cpp b/test/doc/snippets/5b_types.cpp new file mode 100644 index 000000000..881cf812b --- /dev/null +++ b/test/doc/snippets/5b_types.cpp @@ -0,0 +1,275 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/5.buffers/5b.types.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// tag::make_buffer_include[] +#include +// end::make_buffer_include[] + +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +using namespace boost::capy; + +// Interface sketches shown on the page; the sketch namespaces keep them +// from clashing with the real types. Compiling them is the test. +namespace const_buffer_sketch { + +// tag::const_buffer_interface[] +class const_buffer +{ +public: + const_buffer() = default; + const_buffer(void const* data, std::size_t size) noexcept; + const_buffer(mutable_buffer const& b) noexcept; // Implicit conversion + + void const* data() const noexcept; + std::size_t size() const noexcept; + + const_buffer& operator+=(std::size_t n) noexcept; // Remove prefix +}; +// end::const_buffer_interface[] + +} // namespace const_buffer_sketch + +namespace mutable_buffer_sketch { + +// tag::mutable_buffer_interface[] +class mutable_buffer +{ +public: + mutable_buffer() = default; + mutable_buffer(void* data, std::size_t size) noexcept; + + void* data() const noexcept; + std::size_t size() const noexcept; + + mutable_buffer& operator+=(std::size_t n) noexcept; +}; +// end::mutable_buffer_interface[] + +} // namespace mutable_buffer_sketch + +// Records the size seen so the conversion fragment is observable. +std::size_t processed_size = 0; + +void process(const_buffer buf) +{ + processed_size = buf.size(); +} + +// tag::write_data_signature[] +template +void write_data(Buffers const& buffers); +// end::write_data_signature[] + +// Logs the element count of every call so all three calls are observable. +std::vector write_data_lengths; + +template +void write_data(Buffers const& buffers) +{ + write_data_lengths.push_back(buffer_length(buffers)); +} + +// The custom sequence used by the calls fragment. +struct composite_buffers +{ + std::array parts; + auto begin() const noexcept { return parts.begin(); } + auto end() const noexcept { return parts.end(); } +}; + +struct types_test +{ + void testConstruction() + { + // tag::const_buffer_construct[] + // From pointer and size + char data[] = "hello"; + const_buffer buf(data, 5); + + // From mutable_buffer (implicit) + mutable_buffer mbuf(data, 5); + const_buffer cbuf = mbuf; // OK: mutable -> const + // end::const_buffer_construct[] + BOOST_TEST(buf.data() == data); + BOOST_TEST(buf.size() == 5); + BOOST_TEST(cbuf.data() == mbuf.data()); + BOOST_TEST(cbuf.size() == 5); + } + + void testAccessors() + { + char data[] = "hello"; + // tag::const_buffer_accessors[] + const_buffer buf(data, 5); + + void const* ptr = buf.data(); // Pointer to first byte + std::size_t len = buf.size(); // Number of bytes + // end::const_buffer_accessors[] + BOOST_TEST(ptr == data); + BOOST_TEST(len == 5); + } + + void testPrefixRemoval() + { + char data[] = "0123456789"; + // tag::const_buffer_prefix[] + const_buffer buf(data, 10); + + buf += 3; // Remove first 3 bytes + // buf.data() now points 3 bytes later + // buf.size() is now 7 + // end::const_buffer_prefix[] + BOOST_TEST(buf.data() == data + 3); + BOOST_TEST(buf.size() == 7); + } + + void testConversion() + { + char data[8] = {}; + std::size_t size = sizeof(data); + // tag::mutable_to_const[] + void process(const_buffer buf); + + mutable_buffer mbuf(data, size); + process(mbuf); // OK: implicit conversion + // end::mutable_to_const[] + BOOST_TEST(processed_size == size); + } + + void testMakeBuffer() + { + char storage[64]; + void* ptr = storage; + std::size_t size = sizeof(storage); + // tag::make_buffer_sources[] + // From pointer and size + auto buf = make_buffer(ptr, size); + + // From C array + char arr[10]; + auto arr_buf = make_buffer(arr); + + // From std::array + std::array std_arr; + auto std_arr_buf = make_buffer(std_arr); + + // From std::vector + std::vector vec(100); + auto vec_buf = make_buffer(vec); + + // From std::string + std::string str = "hello"; + auto str_buf = make_buffer(str); + + // From std::string_view + std::string_view sv = "hello"; + auto sv_buf = make_buffer(sv); + + // From a span (std::span or boost::span) + std::span sp(arr); + auto sp_buf = make_buffer(sp); + // end::make_buffer_sources[] + BOOST_TEST(buf.data() == storage); + BOOST_TEST(buf.size() == 64); + BOOST_TEST(arr_buf.size() == 10); + BOOST_TEST(std_arr_buf.size() == 10); + BOOST_TEST(vec_buf.size() == 100); + BOOST_TEST(str_buf.size() == 5); + BOOST_TEST(sv_buf.size() == 5); + BOOST_TEST(sp_buf.data() == arr); + BOOST_TEST(sp_buf.size() == 10); + } + + void testSingleAsSequence() + { + const_buffer buf1, buf2, buf3; + composite_buffers my_composite{}; + write_data_lengths.clear(); + // tag::write_data_calls[] + // All of these work: + write_data(make_buffer("hello")); // Single buffer + write_data(std::array{buf1, buf2, buf3}); // Multiple buffers + write_data(my_composite); // Custom sequence + // end::write_data_calls[] + BOOST_TEST(write_data_lengths == + (std::vector{1, 3, 2})); + } + + void testBeginEnd() + { + // tag::begin_end_uniform[] + const_buffer single; + auto it = begin(single); // Returns pointer to single + auto e = end(single); // Returns pointer past single + + std::array multi; + auto it2 = begin(multi); // Returns multi.begin() + auto e2 = end(multi); // Returns multi.end() + // end::begin_end_uniform[] + BOOST_TEST(it == &single); + BOOST_TEST(e == &single + 1); + BOOST_TEST(it2 == multi.begin()); + BOOST_TEST(e2 == multi.end()); + } + + void run() + { + testConstruction(); + testAccessors(); + testPrefixRemoval(); + testConversion(); + testMakeBuffer(); + testSingleAsSequence(); + testBeginEnd(); + } +}; + +} // namespace + +TEST_SUITE(types_test, "boost.capy.doc.5b_types"); diff --git a/test/doc/snippets/5c_sequences.cpp b/test/doc/snippets/5c_sequences.cpp new file mode 100644 index 000000000..6c1a90001 --- /dev/null +++ b/test/doc/snippets/5c_sequences.cpp @@ -0,0 +1,272 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/5.buffers/5c.sequences.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// tag::buffer_slice_include[] +#include +// end::buffer_slice_include[] +// tag::consuming_buffers_include[] +#include +// end::consuming_buffers_include[] + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +// The iteration fragment binds each buffer without using it; the page +// comment explains the loop body instead. The slice fragment discards +// io_result values the same way. + +namespace { + +using namespace boost::capy; + +// The page's concept definitions; the sketch namespace keeps them from +// clashing with the real ones, and the asserts below prove they match. +namespace concept_sketch { + +// tag::const_buffer_sequence_concept[] +template +concept ConstBufferSequence = + std::is_convertible_v || ( + std::ranges::bidirectional_range && + std::is_convertible_v, const_buffer>); +// end::const_buffer_sequence_concept[] + +// tag::mutable_buffer_sequence_concept[] +template +concept MutableBufferSequence = + std::is_convertible_v || ( + std::ranges::bidirectional_range && + std::is_convertible_v, mutable_buffer>); +// end::mutable_buffer_sequence_concept[] + +} // namespace concept_sketch + +static_assert(concept_sketch::ConstBufferSequence == + ConstBufferSequence); +static_assert(concept_sketch::ConstBufferSequence> == + ConstBufferSequence>); +static_assert(concept_sketch::ConstBufferSequence == + ConstBufferSequence); +static_assert(concept_sketch::MutableBufferSequence == + MutableBufferSequence); +static_assert(concept_sketch::MutableBufferSequence == + MutableBufferSequence); + +// tag::send_signature[] +template +void send(Buffers const& bufs); +// end::send_signature[] + +// Logs the element count of every call so all four calls are observable. +std::vector send_lengths; + +template +void send(Buffers const& bufs) +{ + send_lengths.push_back(buffer_length(bufs)); +} + +// The custom type used by the heterogeneous-composition fragment. +struct chained_buffers +{ + std::array parts; + auto begin() const noexcept { return parts.begin(); } + auto end() const noexcept { return parts.end(); } +}; + +// tag::iterate[] +template +void process(Buffers const& bufs) +{ + for (auto it = begin(bufs); it != end(bufs); ++it) + { + const_buffer buf = *it; + // Process buf.data(), buf.size() + } +} +// end::iterate[] + +using Stream = test::stream; + +// tag::read_all[] +template +task read_all(Stream& stream, Buffers buffers) +{ + consuming_buffers consuming(buffers); + std::size_t const total_size = buffer_size(buffers); + std::size_t total = 0; + + while (total < total_size) + { + auto [ec, n] = co_await stream.read_some(consuming.data()); + consuming.consume(n); + total += n; + if (ec) + break; + } + + co_return total; +} +// end::read_all[] + +task<> send_sliced( + Stream& stream, + std::array const& bufs) +{ + // tag::buffer_slice[] + co_await write(stream, buffer_slice(bufs, 0, 16384)); // send only the first 16 KB + auto rest = buffer_slice(bufs, 16384); // everything after the first 16 KB + co_await write(stream, rest); + // end::buffer_slice[] + BOOST_TEST(buffer_size(rest) == buffer_size(bufs) - 16384); +} + +struct sequences_test +{ + void testModels() + { + // tag::concept_models[] + // Single buffers + const_buffer cb; // ConstBufferSequence + mutable_buffer mb; // MutableBufferSequence (and ConstBufferSequence) + + // Standard containers of buffers + std::vector v; // ConstBufferSequence + std::array a; // MutableBufferSequence + + // String types (wrap with make_buffer to get a single buffer) + std::string str; // make_buffer(str) -> mutable_buffer + std::string_view sv; // make_buffer(sv) -> const_buffer + // end::concept_models[] + static_assert(ConstBufferSequence); + static_assert(MutableBufferSequence); + static_assert(ConstBufferSequence); + static_assert(MutableBufferSequence); + static_assert(!ConstBufferSequence); + static_assert(MutableBufferSequence); + static_assert(ConstBufferSequence); + BOOST_TEST(buffer_size(cb) == 0); + BOOST_TEST(mb.size() == 0); + BOOST_TEST(buffer_size(v) == 0); + BOOST_TEST(buffer_size(a) == 0); + BOOST_TEST(make_buffer(str).size() == 0); + BOOST_TEST(make_buffer(sv).size() == 0); + } + + void testHeterogeneous() + { + const_buffer buf1, buf2; + chained_buffers my_custom_buffer_sequence{}; + send_lengths.clear(); + // tag::send_calls[] + // All of these work: + send(make_buffer("Hello")); // string literal + send(make_buffer(std::string_view{"Hello"})); // string_view + send(std::array{buf1, buf2}); // array of buffers + send(my_custom_buffer_sequence); // custom type + // end::send_calls[] + BOOST_TEST(send_lengths == + (std::vector{1, 1, 2, 3})); + } + + void testIterate() + { + char data[4] = {}; + process(make_buffer(data)); + process(std::array{const_buffer(data, 2), const_buffer(data + 2, 2)}); + } + + void testReadAll() + { + auto [a, b] = test::make_stream_pair(); + b.provide("abcdefghijklmnopqrst"); // 20 bytes readable from a + a.set_max_read_size(7); // force several partial reads + + std::vector head(8), tail(12); + std::array bufs{ + make_buffer(head), make_buffer(tail)}; + + std::size_t got = 0; + test::run_blocking([&](std::size_t n) { got = n; })( + read_all(a, bufs)); + + BOOST_TEST(got == 20); + BOOST_TEST(std::string(head.begin(), head.end()) == "abcdefgh"); + BOOST_TEST(std::string(tail.begin(), tail.end()) == "ijklmnopqrst"); + } + + void testBufferSlice() + { + auto [a, b] = test::make_stream_pair(); + std::string part1(10000, 'x'); + std::string part2(10000, 'y'); + std::array bufs{ + make_buffer(part1), make_buffer(part2)}; + + test::run_blocking()(send_sliced(a, bufs)); + + BOOST_TEST(b.data() == part1 + part2); + } + + void run() + { + testModels(); + testHeterogeneous(); + testIterate(); + testReadAll(); + testBufferSlice(); + } +}; + +} // namespace + +TEST_SUITE(sequences_test, "boost.capy.doc.5c_sequences"); diff --git a/test/doc/snippets/5d_system_io.cpp b/test/doc/snippets/5d_system_io.cpp new file mode 100644 index 000000000..7de76d661 --- /dev/null +++ b/test/doc/snippets/5d_system_io.cpp @@ -0,0 +1,157 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/5.buffers/5d.system-io.adoc. The +// OS-level fragments compile only where the platform headers exist; +// guards keep this TU portable while the tags stay extractable. + +// tag::include_buffers[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// end::include_buffers[] + +#include +#include +#include +#include +#include + +#include +#include + +#if __has_include() +#include +#include +#define BOOST_CAPY_DOC_HAS_POSIX_IO +#endif + +#if __has_include() +#include +#endif + +namespace { + +using namespace boost::capy; + +// tag::write_some_signature[] +template +io_task write_some(Buffers buffers); +// end::write_some_signature[] + +#if defined(BOOST_CAPY_DOC_HAS_POSIX_IO) + +// The page fragments deliberately ignore syscall results; the point +// they make is the syscall count, not error handling. + +[[maybe_unused]] void +write_without_gather( + int fd, char const* header, std::size_t header_len, + char const* body, std::size_t body_len) +{ + // tag::two_syscalls[] + write(fd, header, header_len); // syscall 1 + write(fd, body, body_len); // syscall 2 + // end::two_syscalls[] +} + +[[maybe_unused]] void +write_with_gather( + int fd, char* header, std::size_t header_len, + char* body, std::size_t body_len) +{ + // tag::gather_syscall[] + iovec iov[2] = {{header, header_len}, {body, body_len}}; + writev(fd, iov, 2); // single syscall + // end::gather_syscall[] +} + +// tag::cached_iovecs[] +// Build once, use many times +struct message_buffers +{ + std::array iovecs; + + void set_header(void const* p, std::size_t n); + void set_body(void const* p, std::size_t n); + void set_footer(void const* p, std::size_t n); +}; +// end::cached_iovecs[] + +#endif // BOOST_CAPY_DOC_HAS_POSIX_IO + +#if __has_include() + +// Never instantiated: liburing declarations are compile-checked +// without requiring -luring at link time. +template +void +use_registered_buffers( + io_uring* ring, iovec const* buffers, unsigned count, + io_uring_sqe* sqe, int fd, void const* buf, unsigned len, + unsigned long long offset, int buf_index) +{ + // tag::io_uring_fixed[] + // Registration (done once) + io_uring_register_buffers(ring, buffers, count); + + // Use (fast path - no translation) + io_uring_prep_write_fixed(sqe, fd, buf, len, offset, buf_index); + // end::io_uring_fixed[] +} + +#endif // __has_include() + +const_buffer +assemble_message() +{ + static char const msg[] = "assembled message"; + return const_buffer(msg, sizeof(msg) - 1); +} + +[[maybe_unused]] task<> +minimize_buffer_count(test::stream& stream) +{ + // tag::minimize_buffer_count[] + // Prefer: single buffer when possible + auto buf = assemble_message(); // Build in one buffer + co_await write(stream, buf); + + // Avoid: many tiny buffers + std::array tiny_bufs; + co_await write(stream, tiny_bufs); // 100-element translation + // end::minimize_buffer_count[] +} + +} // namespace diff --git a/test/doc/snippets/5e_algorithms.cpp b/test/doc/snippets/5e_algorithms.cpp new file mode 100644 index 000000000..16d094907 --- /dev/null +++ b/test/doc/snippets/5e_algorithms.cpp @@ -0,0 +1,418 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/5.buffers/5e.algorithms.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +#if __has_include() +#include +#endif + +#include "test_suite.hpp" + +namespace { + +using namespace boost::capy; +using namespace std::string_view_literals; + +// The pages show the algorithms with function-style synopses; the real +// entities are function objects with identical call signatures. The +// declarations only need to compile. +namespace synopsis { + +// tag::buffer_size_signature[] +template +std::size_t buffer_size(CB const& buffers); +// end::buffer_size_signature[] + +// tag::buffer_empty_signature[] +template +bool buffer_empty(CB const& buffers); +// end::buffer_empty_signature[] + +// tag::buffer_length_signature[] +template +std::size_t buffer_length(CB const& buffers); +// end::buffer_length_signature[] + +// tag::buffer_copy_signature[] +template +std::size_t buffer_copy(Target const& target, Source const& source, + std::size_t at_most = std::size_t(-1)); +// end::buffer_copy_signature[] + +} // namespace synopsis + +// tag::read_loop[] +template +task read_full(Stream& stream, Buffers buffers) +{ + consuming_buffers remaining(buffers); + std::size_t const total_size = buffer_size(buffers); + std::size_t total = 0; + + while (total < total_size) + { + auto [ec, n] = co_await stream.read_some(remaining.data()); + remaining.consume(n); + total += n; + if (ec) + co_return total; + } + + co_return total; +} +// end::read_loop[] + +// tag::write_loop[] +template +task write_full(Stream& stream, Buffers buffers) +{ + consuming_buffers remaining(buffers); + std::size_t const total_size = buffer_size(buffers); + std::size_t total = 0; + + while (total < total_size) + { + auto [ec, n] = co_await stream.write_some(remaining.data()); + remaining.consume(n); + total += n; + if (ec) + co_return total; + } + + co_return total; +} +// end::write_loop[] + +std::string +build_header() +{ + return "header: "; +} + +std::vector +load_body() +{ + return {'b', 'o', 'd', 'y'}; +} + +[[maybe_unused]] task<> +zero_copy_write(test::stream& stream) +{ + // tag::zero_copy[] + std::string header = build_header(); + std::vector body = load_body(); + + // No copying—header and body are written directly + std::array buffers = {make_buffer(header), make_buffer(body)}; + co_await write(stream, buffers); + // end::zero_copy[] +} + +[[maybe_unused]] task<> +scatter_gather_write( + test::stream& stream, + const_buffer header_buf, const_buffer separator_buf, + const_buffer body_buf, const_buffer footer_buf) +{ + // tag::scatter_gather[] + std::array buffers = {header_buf, separator_buf, body_buf, footer_buf}; + co_await write(stream, buffers); // Single system call + // end::scatter_gather[] +} + +#if __has_include() + +[[maybe_unused]] task<> +send_mapped_file(test::stream& socket, int fd, std::size_t file_size) +{ + // tag::mmap_buffer[] + // Memory-mapped file + void* mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); + const_buffer file_buf(mapped, file_size); + co_await write(socket, file_buf); // Zero-copy network transmission + // end::mmap_buffer[] +} + +#endif // __has_include() + +// Bidirectional iterator presenting each chunk as a const_buffer. Kept +// out of the page fragment, which focuses on the sequence shape. +class chunk_iterator +{ + std::vector>::const_iterator it_; + +public: + using value_type = const_buffer; + using difference_type = std::ptrdiff_t; + + chunk_iterator() = default; + + explicit chunk_iterator( + std::vector>::const_iterator it) + : it_(it) + { + } + + const_buffer operator*() const + { + return make_buffer(*it_); + } + + chunk_iterator& operator++() + { + ++it_; + return *this; + } + + chunk_iterator operator++(int) + { + auto tmp = *this; + ++it_; + return tmp; + } + + chunk_iterator& operator--() + { + --it_; + return *this; + } + + chunk_iterator operator--(int) + { + auto tmp = *this; + --it_; + return tmp; + } + + bool operator==(chunk_iterator const&) const = default; +}; + +// tag::custom_sequence[] +class chunked_buffer_sequence +{ + std::vector> chunks_; + +public: + auto begin() const { return chunk_iterator(chunks_.begin()); } + auto end() const { return chunk_iterator(chunks_.end()); } +}; +// Satisfies ConstBufferSequence—works with all algorithms +// end::custom_sequence[] + +static_assert(ConstBufferSequence); + +task<> +read_full_driver( + test::stream& wr, test::stream& rd, + std::array bufs, std::size_t& got) +{ + co_await write(wr, make_buffer("hello world"sv)); + got = co_await read_full(rd, bufs); +} + +task<> +write_full_driver( + test::stream& wr, std::array bufs, + std::size_t& put) +{ + put = co_await write_full(wr, bufs); +} + +struct algorithms_test +{ + void + testBufferSize() + { + // tag::buffer_size_example[] + auto buf1 = make_buffer("hello"sv); // 5 bytes + auto buf2 = make_buffer("world"sv); // 5 bytes + auto combined = std::array{buf1, buf2}; + + std::size_t total = buffer_size(combined); // 10 + // end::buffer_size_example[] + BOOST_TEST(total == 10); + } + + void + testBufferEmpty() + { + // tag::buffer_empty_example[] + const_buffer empty_buf; + buffer_empty(empty_buf); // true + + const_buffer non_empty("data", 4); + buffer_empty(non_empty); // false + // end::buffer_empty_example[] + BOOST_TEST(buffer_empty(empty_buf)); + BOOST_TEST(! buffer_empty(non_empty)); + } + + void + testBufferLength() + { + const_buffer buf1("one", 3); + const_buffer buf2("two", 3); + const_buffer buf3("three", 5); + // tag::buffer_length_example[] + auto single = make_buffer("hello"sv); + buffer_length(single); // 1 + + auto arr = std::array{buf1, buf2, buf3}; + buffer_length(arr); // 3 + // end::buffer_length_example[] + BOOST_TEST(buffer_length(single) == 1); + BOOST_TEST(buffer_length(arr) == 3); + } + + void + testBufferCopy() + { + // tag::buffer_copy_example[] + char source_data[] = "hello world"; + char dest_data[20]; + + const_buffer src(source_data, 11); + mutable_buffer dst(dest_data, 20); + + std::size_t copied = buffer_copy(dst, src); // 11 + // end::buffer_copy_example[] + BOOST_TEST(copied == 11); + BOOST_TEST(std::memcmp(dest_data, "hello world", 11) == 0); + } + + void + testPartialCopy() + { + char source_data[] = "hello world"; + char dest_data[20] = {}; + const_buffer src(source_data, 11); + mutable_buffer dst(dest_data, 20); + // tag::buffer_copy_at_most[] + std::size_t copied = buffer_copy(dst, src, 5); // Copy at most 5 bytes + // end::buffer_copy_at_most[] + BOOST_TEST(copied == 5); + BOOST_TEST(std::memcmp(dest_data, "hello", 5) == 0); + } + + void + testCrossSequenceCopy() + { + const_buffer buf1("abc", 3); + const_buffer buf2("defg", 4); + const_buffer buf3("hi", 2); + char big_data[6] = {}; + char small_data[2] = {}; + mutable_buffer large_buf(big_data, 6); + mutable_buffer small_buf(small_data, 2); + // tag::buffer_copy_cross[] + // Source: 3 buffers + std::array src = {buf1, buf2, buf3}; + + // Target: 2 buffers with different sizes + std::array dst = {large_buf, small_buf}; + + // Copies across buffer boundaries as needed + std::size_t copied = buffer_copy(dst, src); + // end::buffer_copy_cross[] + BOOST_TEST(copied == 8); + BOOST_TEST(std::memcmp(big_data, "abcdef", 6) == 0); + BOOST_TEST(std::memcmp(small_data, "gh", 2) == 0); + } + + void + testReadFull() + { + auto [a, b] = test::make_stream_pair(); + char storage1[6] = {}; + char storage2[5] = {}; + std::array bufs = { + mutable_buffer(storage1, 6), + mutable_buffer(storage2, 5)}; + std::size_t got = 0; + test::run_blocking()(read_full_driver(a, b, bufs, got)); + BOOST_TEST(got == 11); + BOOST_TEST(std::memcmp(storage1, "hello ", 6) == 0); + BOOST_TEST(std::memcmp(storage2, "world", 5) == 0); + } + + void + testWriteFull() + { + auto [a, b] = test::make_stream_pair(); + std::array bufs = { + make_buffer("hello "sv), + make_buffer("world"sv)}; + std::size_t put = 0; + test::run_blocking()(write_full_driver(a, bufs, put)); + BOOST_TEST(put == 11); + BOOST_TEST(b.data() == "hello world"); + } + + void + run() + { + testBufferSize(); + testBufferEmpty(); + testBufferLength(); + testBufferCopy(); + testPartialCopy(); + testCrossSequenceCopy(); + testReadFull(); + testWriteFull(); + } +}; + +} // namespace + +TEST_SUITE(algorithms_test, "boost.capy.doc.5e_algorithms"); diff --git a/test/doc/snippets/6a_overview.cpp b/test/doc/snippets/6a_overview.cpp new file mode 100644 index 000000000..2bbfbe67a --- /dev/null +++ b/test/doc/snippets/6a_overview.cpp @@ -0,0 +1,162 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/6.streams/6a.overview.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +task<> partial_read(test::stream& stream) +{ + char storage[1024]; + auto buffer = make_buffer(storage); + // tag::read_stream_partial[] + // ReadStream: may return fewer bytes than buffer can hold + auto [ec, n] = co_await stream.read_some(buffer); + // n might be 1, might be 1000, might be buffer_size(buffer) + // end::read_stream_partial[] + BOOST_TEST(! ec); + BOOST_TEST(n >= 1); +} + +task<> partial_write(test::stream& stream) +{ + const_buffer buffers("hello", 5); + // tag::write_stream_partial[] + // WriteStream: may write fewer bytes than provided + auto [ec, n] = co_await stream.write_some(buffers); + // n might be less than buffer_size(buffers) + // end::write_stream_partial[] + BOOST_TEST(! ec); + BOOST_TEST(n == 5); +} + +// tag::any_stream_echo[] +// This function works with any stream implementation +task<> echo(any_stream& stream) +{ + char buf[1024]; + for (;;) + { + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + + auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); + + if (ec) + co_return; + + if (wec) + co_return; + } +} +// end::any_stream_echo[] + +struct overview_test +{ + void + testPartialReadWrite() + { + auto [a, b] = test::make_stream_pair(); + b.provide("hello"); + test::run_blocking()(partial_read(a)); + test::run_blocking()(partial_write(a)); + BOOST_TEST(b.data() == "hello"); + } + + void + testCallerDecides() + { + // tag::caller_decides[] + auto [client, server] = test::make_stream_pair(); + + // Owns the moved-in stream (the by-value form takes ownership) + any_stream s1{std::move(client)}; + + // Wraps the server end by pointer (reference semantics, must outlive s2) + any_stream s2{&server}; + // end::caller_decides[] + + // Feed the client end and signal eof so echo() terminates. + server.provide("ping"); + server.close(); + + // tag::caller_decides[] + + // Same echo() coroutine regardless of how the wrapper was built + test::run_blocking()(echo(s1)); + // end::caller_decides[] + + // The echoed bytes arrive on the server end; read them + // through s2 to exercise the reference-mode wrapper. + test::run_blocking()([](any_stream& s) -> task<> + { + char buf[8]; + auto [ec, n] = co_await s.read_some(make_buffer(buf)); + BOOST_TEST(! ec); + BOOST_TEST(std::string_view(buf, n) == "ping"); + }(s2)); + } + + void + run() + { + testPartialReadWrite(); + testCallerDecides(); + } +}; + +} // namespace + +TEST_SUITE(overview_test, "boost.capy.doc.6a_overview"); diff --git a/test/doc/snippets/6b_streams.cpp b/test/doc/snippets/6b_streams.cpp new file mode 100644 index 000000000..75e54734b --- /dev/null +++ b/test/doc/snippets/6b_streams.cpp @@ -0,0 +1,319 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/6.streams/6b.streams.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 +// tag::any_read_stream_include[] +#include +// end::any_read_stream_include[] +// tag::any_stream_include[] +#include +// end::any_stream_include[] +// tag::any_write_stream_include[] +#include +// end::any_write_stream_include[] +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +namespace definition { + +// tag::read_stream_concept[] +template +concept ReadStream = + requires(T& stream, mutable_buffer_archetype buffers) + { + { stream.read_some(buffers) } -> IoAwaitable; + requires awaitable_decomposes_to< + decltype(stream.read_some(buffers)), + std::error_code, std::size_t>; + }; +// end::read_stream_concept[] + +// tag::write_stream_concept[] +template +concept WriteStream = + requires(T& stream, const_buffer_archetype buffers) + { + { stream.write_some(buffers) } -> IoAwaitable; + requires awaitable_decomposes_to< + decltype(stream.write_some(buffers)), + std::error_code, std::size_t>; + }; +// end::write_stream_concept[] + +// The page's definitions must match the library's. +static_assert(definition::ReadStream); +static_assert(capy::ReadStream); +static_assert(definition::WriteStream); +static_assert(capy::WriteStream); + +} // namespace definition + +// Scaffolds owning the conforming-signature declarations. +struct conforming_read_stream +{ + // tag::read_some_signature[] + template + IoAwaitable auto read_some(Buffers buffers); + // end::read_some_signature[] +}; + +struct conforming_write_stream +{ + // tag::write_some_signature[] + template + IoAwaitable auto write_some(Buffers buffers); + // end::write_some_signature[] +}; + +task<> partial_read(test::stream& stream) +{ + // tag::read_partial[] + char buf[1024]; + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + // n might be 1, might be 500, might be 1024 + // if !ec, then n >= 1 + // end::read_partial[] + BOOST_TEST(! ec); + BOOST_TEST(n >= 1); +} + +// tag::dump_stream[] +template +task<> dump_stream(Stream& stream) +{ + char buf[256]; + + for (;;) + { + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + + std::cout.write(buf, n); + + if (ec) + break; + } +} +// end::dump_stream[] + +task<> partial_write(test::stream& stream, std::string const& large_data) +{ + // tag::write_partial[] + auto [ec, n] = co_await stream.write_some(make_buffer(large_data)); + // n might be less than large_data.size() + // end::write_partial[] + BOOST_TEST(! ec); + BOOST_TEST(n == large_data.size()); +} + +// The page presents each wrapper's constructor signatures; local class +// scaffolds host the declarations so they compile as shown. +namespace synopsis { + +class any_read_stream +{ +public: + // tag::any_read_stream_ctors[] + // Owning: takes ownership of a moved-in stream + template + any_read_stream(S stream); + + // Reference: wraps by pointer without ownership + template + any_read_stream(S* stream); + // end::any_read_stream_ctors[] +}; + +class any_write_stream +{ +public: + // tag::any_write_stream_ctors[] + template + any_write_stream(S stream); // owning + + template + any_write_stream(S* stream); // reference + // end::any_write_stream_ctors[] +}; + +class any_stream +{ +public: + // tag::any_stream_ctors[] + template + requires ReadStream && WriteStream + any_stream(S stream); // owning + + template + requires ReadStream && WriteStream + any_stream(S* stream); // reference + // end::any_stream_ctors[] +}; + +} // namespace synopsis + +// Scaffolding target for the wrapper_usage fragment. +void process_stream(any_stream& stream) +{ + test::run_blocking()([](any_stream& s) -> task<> + { + auto [ec, n] = co_await s.write_some(const_buffer("ok", 2)); + BOOST_TEST(! ec); + BOOST_TEST(n == 2); + }(stream)); +} + +// tag::echo_server[] +// echo.hpp - Header only declares the signature +task<> handle_connection(any_stream& stream); + +// echo.cpp - Implementation in separate translation unit +task<> handle_connection(any_stream& stream) +{ + char buf[1024]; + + for (;;) + { + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + + auto [wec, wn] = co_await write(stream, const_buffer(buf, n)); + + if (ec) + break; + + if (wec) + break; + } +} +// end::echo_server[] + +struct streams_test +{ + void + testPartialRead() + { + auto [a, b] = test::make_stream_pair(); + b.provide("hello"); + test::run_blocking()(partial_read(a)); + } + + void + testDumpStream() + { + auto [a, b] = test::make_stream_pair(); + b.provide("dumped"); + b.close(); + + // Capture std::cout so the fragment's output is observable. + std::ostringstream out; + auto* old = std::cout.rdbuf(out.rdbuf()); + test::run_blocking()(dump_stream(a)); + std::cout.rdbuf(old); + BOOST_TEST(out.str() == "dumped"); + } + + void + testPartialWrite() + { + auto [a, b] = test::make_stream_pair(); + std::string large_data(64, 'x'); + test::run_blocking()(partial_write(a, large_data)); + BOOST_TEST(b.data() == large_data); + } + + void + testWrapperUsage() + { + // tag::wrapper_usage[] + void process_stream(any_stream& stream); + + auto [client, server] = test::make_stream_pair(); + + any_stream wrapped{&client}; // Type erasure, references the existing stream + process_stream(wrapped); // process_stream doesn't know about test::stream + // end::wrapper_usage[] + BOOST_TEST(server.data() == "ok"); + } + + void + testEchoServer() + { + auto [a, b] = test::make_stream_pair(); + b.provide("echo!"); + b.close(); + any_stream stream{&a}; + test::run_blocking()(handle_connection(stream)); + BOOST_TEST(b.data() == "echo!"); + } + + void + run() + { + testPartialRead(); + testDumpStream(); + testPartialWrite(); + testWrapperUsage(); + testEchoServer(); + } +}; + +} // namespace + +TEST_SUITE(streams_test, "boost.capy.doc.6b_streams"); diff --git a/test/doc/snippets/6f_isolation.cpp b/test/doc/snippets/6f_isolation.cpp new file mode 100644 index 000000000..d6df7d826 --- /dev/null +++ b/test/doc/snippets/6f_isolation.cpp @@ -0,0 +1,353 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/6.streams/6f.isolation.adoc. Pages +// include the tagged regions; scaffolding stays outside the tags. The +// page's protocol.hpp and http_client.hpp live next to this file. + +// tag::protocol_impl[] +// protocol.cpp - Implementation isolated here + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +// The user_code fragment deliberately omits the headers field in a +// designated initializer; the default is part of the lesson. +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif +#if defined(__clang__) && defined(__has_warning) +#if __has_warning("-Wmissing-designated-field-initializers") +#pragma clang diagnostic ignored "-Wmissing-designated-field-initializers" +#endif +#endif + +#include "protocol.hpp" +#include +#include +// end::protocol_impl[] + +#include "http_client.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "test_suite.hpp" + +// tag::protocol_impl[] + +task<> handle_protocol(any_stream& stream) +{ + char buf[1024]; + + for (;;) + { + auto [ec, n] = co_await stream.read_some(make_buffer(buf)); + if (ec) + co_return; + + // Process and respond... + std::string response(buf, n); + co_await write(stream, make_buffer(response)); + } +} +// end::protocol_impl[] + +namespace { + +// Stand-in transports: real code would bring a network library. Each +// satisfies WriteSink by forwarding to an in-memory test sink; the +// socket additionally satisfies Stream through a loopback pair. +namespace tcp { + +class socket +{ + std::pair loop_ = + test::make_stream_pair(); + test::fuse f_; + test::write_stream sink_{f_}; + +public: + auto read_some(MutableBufferSequence auto b) + { + return loop_.first.read_some(b); + } + + auto write_some(ConstBufferSequence auto b) + { + return sink_.write_some(b); + } +}; + +} // namespace tcp + +namespace tls { + +class stream +{ + test::fuse f_; + test::write_stream sink_{f_}; + +public: + auto write_some(ConstBufferSequence auto b) + { + return sink_.write_some(b); + } +}; + +} // namespace tls + +struct message +{ + std::string header; + std::string body; +}; + +// tag::send_message[] +// Your library code +task<> send_message(any_write_stream& stream, message const& msg) +{ + co_await write(stream, make_buffer(msg.header)); + co_await write(stream, make_buffer(msg.body)); +} +// end::send_message[] + +task<> use_transports(message const& msg) +{ + { + // tag::callers[] + // TCP socket + tcp::socket socket; + any_write_stream stream{&socket}; // references socket + co_await send_message(stream, msg); + // end::callers[] + } + { + // tag::callers[] + + // TLS stream + tls::stream tls; + any_write_stream stream{&tls}; // references tls + co_await send_message(stream, msg); + // end::callers[] + } + { + test::fuse f; + // tag::callers[] + + // Test mock + test::write_stream mock(f); + any_write_stream stream{&mock}; // references mock + co_await send_message(stream, msg); + // end::callers[] + BOOST_TEST(mock.data() == msg.header + msg.body); + } +} + +} // namespace + +// These namespaces hold the page's declaration-only sketches. They sit +// outside the anonymous namespace so the never-defined declarations do +// not trigger -Wunused-function. +namespace before_after_6f { + +// tag::templates_before[] +// Old approach: template propagates everywhere +template +task<> handle_protocol(Stream& stream); + +// Every caller instantiates for their stream type +// Changes force recompilation of all callers +// end::templates_before[] + +// tag::type_erasure_after[] +// New approach: concrete signature +task<> handle_protocol(any_stream& stream); + +// Implementation compiles once +// Callers only depend on the signature +// end::type_erasure_after[] + +} // namespace before_after_6f + +namespace guidelines_6f { + +// tag::accept_type_erased[] +// Good: accepts any stream +task<> process(any_stream& stream); + +// Avoid: forces specific type +task<> process(tcp::socket& socket); +// end::accept_type_erased[] + +// Definition so the callers below link. +task<> process(any_stream&) +{ + co_return; +} + +// tag::wrap_call_site[] +task<> caller(tcp::socket& socket) +{ + any_stream stream{&socket}; // Wrap by reference here + co_await process(stream); // Call with erased type +} +// end::wrap_call_site[] + +// tag::return_concrete_decl[] +// OK: factory returns concrete type +tcp::socket create_socket(); +// end::return_concrete_decl[] + +tcp::socket create_socket() +{ + return {}; +} + +task<> wrap_if_needed() +{ + // tag::return_concrete_use[] + // Then caller wraps if needed + auto socket = create_socket(); + any_stream stream{&socket}; // reference; socket must outlive stream + // or: any_stream stream{std::move(socket)}; // wrapper takes ownership + // end::return_concrete_use[] + co_await process(stream); +} + +} // namespace guidelines_6f + +namespace { + +// The request literal intentionally leaves `headers` defaulted. +task<> user_code() +{ + // tag::user_code[] + // User code + tcp::socket socket; + // ... connect ... + + any_stream conn{&socket}; // references socket + http_request req{ + .method = "GET", + .url = "/api/data" + }; + auto response = co_await send_request(conn, req); + + // Read body through type-erased source + char storage[4096]; + mutable_buffer buf(storage, sizeof(storage)); + auto [ec, n] = co_await response.body.read_some(buf); + // end::user_code[] + BOOST_TEST(!ec); + BOOST_TEST(response.status_code == 200); + BOOST_TEST(std::string_view(storage, n) == "{}"); +} + +struct isolation_test +{ + void + testHandleProtocol() + { + auto [a, b] = test::make_stream_pair(); + b.provide("ping"); + b.close(); + any_stream stream{&a}; + test::run_blocking()(handle_protocol(stream)); + // The protocol echoes what it read back to the peer. + BOOST_TEST(b.data() == "ping"); + } + + void + testSendMessage() + { + test::fuse f; + test::write_stream mock(f); + any_write_stream stream{&mock}; + test::run_blocking()( + send_message(stream, {"HDR", "BODY"})); + BOOST_TEST(mock.data() == "HDRBODY"); + } + + void + testTransports() + { + test::run_blocking()( + use_transports({"HDR", "BODY"})); + } + + void + testGuidelines() + { + tcp::socket socket; + test::run_blocking()(guidelines_6f::caller(socket)); + test::run_blocking()(guidelines_6f::wrap_if_needed()); + } + + void + testUserCode() + { + test::run_blocking()(user_code()); + } + + void + run() + { + testHandleProtocol(); + testSendMessage(); + testTransports(); + testGuidelines(); + testUserCode(); + } +}; + +} // namespace + +// Definition so the user-code fragment links; a real client would parse +// an HTTP response off the wire. +task send_request(any_stream&, http_request const&) +{ + test::fuse f; + test::read_stream body(f); + body.provide("{}"); + co_return http_response{200, {}, any_read_stream(std::move(body))}; +} + +TEST_SUITE(isolation_test, "boost.capy.doc.6f_isolation"); diff --git a/test/doc/snippets/7a_drivers.cpp b/test/doc/snippets/7a_drivers.cpp new file mode 100644 index 000000000..d1c482de5 --- /dev/null +++ b/test/doc/snippets/7a_drivers.cpp @@ -0,0 +1,410 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/7.testing/7a.drivers.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace { + +// The #include directives inside the tags expand to nothing here (the +// headers are already included above); they are kept for the page text. + +// tag::run_blocking_basic[] +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +task compute(int x) +{ + co_return x * 2; +} + +void test_compute() +{ + int result = 0; + run_blocking([&](int v) { result = v; })(compute(21)); + BOOST_TEST(result == 42); +} +// end::run_blocking_basic[] + +task<> my_task() +{ + co_return; +} + +// tag::fuse_basic[] +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_with_fuse() +{ + fuse f; + auto r = f.armed([](fuse& f) { + auto ec = f.maybe_fail(); + if(ec) + return; // injected error: exit gracefully + + ec = f.maybe_fail(); + if(ec) + return; + }); + BOOST_TEST(r.success); +} +// end::fuse_basic[] + +// The two patterns redeclare [ec, n], so each needs its own scope. +task correct_pattern(read_stream& rs, mutable_buffer buf) +{ + // tag::early_return[] + // Correct: early return on injected error + auto [ec, n] = co_await rs.read_some(buf); + if(ec) + co_return; // fuse injected an error -- exit gracefully + // end::early_return[] +} + +task wrong_pattern(read_stream& rs, mutable_buffer buf) +{ + // tag::early_return[] + + // Wrong: asserting success unconditionally + auto [ec, n] = co_await rs.read_some(buf); + BOOST_TEST(!ec); // fails when fuse injects an error + // end::early_return[] +} + +// tag::canonical_skeleton[] +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +task add(int a, int b) +{ + co_return a + b; +} + +void test_add() +{ + fuse f; + auto r = f.armed([&](fuse&) -> task { + auto sum = co_await add(3, 4); + BOOST_TEST(sum == 7); + }); + BOOST_TEST(r.success); +} +// end::canonical_skeleton[] + +struct drivers_test +{ + void + testRunBlockingBasic() + { + test_compute(); + } + + void + testRunBlockingOverloads() + { + // tag::run_blocking_overloads[] + // Discard result; rethrow on exception + run_blocking()(my_task()); + + // Capture result; rethrow on exception + int out = 0; + run_blocking([&](int v) { out = v; })(compute(21)); + + // Capture result; handle exception separately + run_blocking( + [&](int v) { out = v; }, + [](std::exception_ptr ep) { std::rethrow_exception(ep); } + )(compute(21)); + + // With a stop token (discards result) + std::stop_source src; + run_blocking(src.get_token())(my_task()); + + // With a stop token and a result handler + run_blocking(src.get_token(), [&](int v) { out = v; })(compute(21)); + + // With a stop token and separate handlers + run_blocking( + src.get_token(), + [&](int v) { out = v; }, + [](std::exception_ptr ep) { std::rethrow_exception(ep); } + )(compute(21)); + // end::run_blocking_overloads[] + BOOST_TEST(out == 42); + } + + void + testExercisingCancellation() + { + // tag::run_blocking_cancellation[] + std::stop_source src; + src.request_stop(); + + run_blocking(src.get_token())([&]() -> task<> + { + read_stream rs; + rs.provide("ignored"); + + char buf[32]; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + assert(ec == cond::canceled); // honored the stop token + }()); + // end::run_blocking_cancellation[] + } + + void + testFuseBasic() + { + test_with_fuse(); + } + + void + testInertVsArmed() + { + // tag::inert_vs_armed[] + fuse f; + + // Smoke test: happy path + auto r1 = f.inert([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello"); + + char buf[8]; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST(std::string_view(buf, n) == "hello"); + }); + BOOST_TEST(r1.success); + + // Fault coverage: every error site + auto r2 = f.armed([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello"); + + char buf[8]; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; // fuse injected an error; exit gracefully + BOOST_TEST(std::string_view(buf, n) == "hello"); + }); + BOOST_TEST(r2.success); + // end::inert_vs_armed[] + } + + void + testInertFail() + { + // A const bool with a constant initializer is readable inside + // the capture-less lambda the page shows. + bool const some_condition_failed = false; + // tag::inert_fail[] + fuse f; + auto r = f.inert([](fuse& f) { + auto ec = f.maybe_fail(); // always returns {} + assert(!ec); + + if(some_condition_failed) + f.fail(); // the only way to signal failure in inert mode + }); + BOOST_TEST(r.success); + // end::inert_fail[] + } + + void + testEarlyReturn() + { + fuse f; + auto r = f.armed([&](fuse&) -> task { + read_stream rs(f); + rs.provide("data"); + char arr[8]; + co_await correct_pattern(rs, make_buffer(arr)); + }); + BOOST_TEST(r.success); + + // The wrong pattern asserts success, so only run it un-armed. + fuse f2; + auto r2 = f2.inert([&](fuse&) -> task { + read_stream rs(f2); + rs.provide("data"); + char arr[8]; + co_await wrong_pattern(rs, make_buffer(arr)); + }); + BOOST_TEST(r2.success); + } + + void + testCoroutineSupport() + { + read_stream rs; + rs.provide("data"); + char arr[8]; + auto buf = make_buffer(arr); + // tag::armed_coroutine[] + fuse f; + auto r = f.armed([&](fuse&) -> task { + auto ec = f.maybe_fail(); + if(ec) + co_return; + + auto [ec2, n] = co_await rs.read_some(buf); + if(ec2) + co_return; + }); + BOOST_TEST(r.success); + // end::armed_coroutine[] + } + + void + testCustomFailPoints() + { + // tag::custom_fail_points[] + class widget + { + fuse& f_; + public: + explicit widget(fuse& f) : f_(f) {} + + std::error_code process() + { + auto ec = f_.maybe_fail(); + if(ec) + return ec; + // ... actual work ... + return {}; + } + }; + + fuse f; + widget w(f); + w.process(); // maybe_fail() returns {} + + auto r = f.armed([&](fuse&) { w.process(); }); // both branches exercised + BOOST_TEST(r.success); + // end::custom_fail_points[] + } + + void + testCustomErrorCode() + { + // tag::custom_error_code[] + fuse f(std::make_error_code(std::errc::operation_canceled)); + auto r = f.armed([](fuse& f) { + auto ec = f.maybe_fail(); + if(ec) + { + assert(ec == std::errc::operation_canceled); + return; + } + }); + BOOST_TEST(r.success); + // end::custom_error_code[] + } + + void + testThreadName() + { + // tag::thread_name[] + #include + #include + #include + #include + + using namespace boost::capy; + + thread_pool pool(4); + run_async(pool.get_executor())([]() -> task { + set_current_thread_name("test-worker-0"); + // ... test work runs here; name appears in gdb thread list + co_return; + }()); + pool.join(); + // end::thread_name[] + } + + void + testCanonicalSkeleton() + { + test_add(); + } + + void + run() + { + testRunBlockingBasic(); + testRunBlockingOverloads(); + testExercisingCancellation(); + testFuseBasic(); + testInertVsArmed(); + testInertFail(); + testEarlyReturn(); + testCoroutineSupport(); + testCustomFailPoints(); + testCustomErrorCode(); + testThreadName(); + testCanonicalSkeleton(); + } +}; + +} // namespace + +TEST_SUITE(drivers_test, "boost.capy.doc.7a_drivers"); diff --git a/test/doc/snippets/7b_mock_streams.cpp b/test/doc/snippets/7b_mock_streams.cpp new file mode 100644 index 000000000..0e001ae04 --- /dev/null +++ b/test/doc/snippets/7b_mock_streams.cpp @@ -0,0 +1,322 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/7.testing/7b.mock-streams.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +// GCC gives false positive -Wmaybe-uninitialized on structured bindings +// via the tuple protocol inside coroutine frames. +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +// The #include directives inside the tags expand to nothing here (the +// headers are already included above); they are kept for the page text. + +// tag::read_stream_basic[] +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_read_stream() +{ + fuse f; + read_stream rs(f); + rs.provide("Hello, "); + rs.provide("World!"); + + auto r = f.armed([&](fuse&) -> task { + char buf[32]; + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + BOOST_TEST(std::string_view(buf, n) == "Hello, World!"); + }); + BOOST_TEST(r.success); +} +// end::read_stream_basic[] + +// tag::write_stream_basic[] +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_write_stream() +{ + fuse f; + + auto r = f.armed([&](fuse&) -> task { + write_stream ws(f); + + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello", 5)); + if(ec) + co_return; + BOOST_TEST(ws.data() == "Hello"); + }); + BOOST_TEST(r.success); +} +// end::write_stream_basic[] + +// tag::stream_pair_basic[] +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_stream_pair() +{ + fuse f; + + auto r = f.armed([&](fuse&) -> task { + auto [a, b] = make_stream_pair(f); + + auto [ec, n] = co_await a.write_some( + const_buffer("hello", 5)); + if(ec) + co_return; + + char buf[32]; + auto [ec2, n2] = co_await b.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec2) + co_return; + BOOST_TEST(std::string_view(buf, n2) == "hello"); + }); + BOOST_TEST(r.success); +} +// end::stream_pair_basic[] + +// tag::read_line_test[] +#include +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +// Function under test: read until '\n' or EOF +template +task> +read_line(S& stream) +{ + std::string line; + char ch; + for(;;) + { + auto [ec, n] = co_await stream.read_some( + mutable_buffer(&ch, 1)); + if(ec) + co_return {ec, std::move(line)}; + if(ch == '\n') + break; + line += ch; + } + co_return {std::error_code{}, std::move(line)}; +} + +void test_read_line() +{ + fuse f; + auto r = f.armed([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello\n"); + + auto [ec, line] = co_await read_line(rs); + if(ec) + co_return; // fuse injected an error; exit gracefully + BOOST_TEST(line == "hello"); + }); + BOOST_TEST(r.success); +} +// end::read_line_test[] + +struct mock_streams_test +{ + void + testReadStreamBasic() + { + test_read_stream(); + } + + void + testReadStreamChunked() + { + // tag::read_stream_chunked[] + // At most 4 bytes per read_some call + fuse f; + read_stream rs(f, 4); + rs.provide("Hello, World!"); + + auto r = f.armed([&](fuse&) -> task { + char buf[32]; + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + BOOST_TEST(n == 4); // "Hell" + }); + BOOST_TEST(r.success); + // end::read_stream_chunked[] + } + + void + testReadStreamEof() + { + // tag::read_stream_eof[] + fuse f; + read_stream rs(f); + rs.provide("hi"); + + auto r = f.inert([&](fuse&) -> task { + char buf[8]; + // First read: consumes "hi" + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(!ec); + BOOST_TEST(std::string_view(buf, n) == "hi"); + + // Second read: EOF + auto [ec2, n2] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(ec2 == cond::eof); + BOOST_TEST(n2 == 0); + }); + BOOST_TEST(r.success); + // end::read_stream_eof[] + } + + void + testWriteStreamBasic() + { + test_write_stream(); + } + + void + testWriteStreamChunked() + { + // tag::write_stream_chunked[] + fuse f; + write_stream ws(f, 4); // accept at most 4 bytes per call + + auto r = f.inert([&](fuse&) -> task { + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello", 5)); + BOOST_TEST(!ec); + BOOST_TEST(n == 4); // only "Hell" was accepted + }); + BOOST_TEST(r.success); + // end::write_stream_chunked[] + } + + void + testWriteStreamExpect() + { + // tag::write_stream_expect[] + fuse f; + write_stream ws(f); + ws.expect("Hello World"); + + auto r = f.inert([&](fuse&) -> task { + // Writing matching data succeeds + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello World", 11)); + BOOST_TEST(!ec); + }); + BOOST_TEST(r.success); + // end::write_stream_expect[] + } + + void + testStreamPairBasic() + { + test_stream_pair(); + } + + void + testReadLine() + { + test_read_line(); + } + + void + run() + { + testReadStreamBasic(); + testReadStreamChunked(); + testReadStreamEof(); + testWriteStreamBasic(); + testWriteStreamChunked(); + testWriteStreamExpect(); + testStreamPairBasic(); + testReadLine(); + } +}; + +} // namespace + +TEST_SUITE(mock_streams_test, "boost.capy.doc.7b_mock_streams"); diff --git a/test/doc/snippets/7e_buffer_inspection.cpp b/test/doc/snippets/7e_buffer_inspection.cpp new file mode 100644 index 000000000..77b837bd5 --- /dev/null +++ b/test/doc/snippets/7e_buffer_inspection.cpp @@ -0,0 +1,237 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/7.testing/7e.buffer-inspection.adoc. + +// The all-splits fragment binds both halves without using them; the +// page comment explains the loop body instead. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 { + +// The #include directives inside the tags expand to nothing here (the +// headers are already included above); they are kept for the page text. + +// tag::all_splits[] +#include +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_all_splits() +{ + std::string data = "hello"; + auto cb = make_buffer(data); + + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + BOOST_TEST_EQ(buffer_to_string(b1, b2), data); + } + }); + BOOST_TEST(r.success); +} +// end::all_splits[] + +void test_step_size() +{ + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + // tag::step_size[] + std::string data = "0123456789"; // 10 bytes + auto cb = make_buffer(data); + + bufgrind bg(cb, 3); + // Visits positions: 0, 3, 6, 9, 10 + while(bg) + { + auto [b1, b2] = co_await bg.next(); + // exercise parser at each split point + } + // end::step_size[] + }); + BOOST_TEST(r.success); +} + +void test_mutability() +{ + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + // tag::mutability[] + char data[] = "hello"; + mutable_buffer mb(data, 5); + + bufgrind bg(mb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + // each half is itself a buffer sequence; a mutable input + // yields MutableBufferSequence halves callers may write into + static_assert(MutableBufferSequence); + } + // end::mutability[] + }); + BOOST_TEST(r.success); +} + +// tag::buffer_to_string[] +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_buffer_to_string() +{ + // Single buffer sequence + const_buffer cb(make_buffer(std::string_view("hello"))); + BOOST_TEST_EQ(buffer_to_string(cb), "hello"); + + // Multiple buffer sequences concatenated in order + const_buffer b1(make_buffer(std::string_view("hello"))); + const_buffer b2(make_buffer(std::string_view(" world"))); + BOOST_TEST_EQ(buffer_to_string(b1, b2), "hello world"); +} +// end::buffer_to_string[] + +void test_reconstruct() +{ + // tag::reconstruct[] + std::string original = "hello world"; + auto cb = make_buffer(original); + + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + BOOST_TEST_EQ(buffer_to_string(b1, b2), original); + } + }); + BOOST_TEST(r.success); + // end::reconstruct[] +} + +// tag::parser_all_splits[] +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +// Hypothetical parser: reads all bytes from a ReadStream +task read_all(read_stream& rs) +{ + std::string out; + std::array buf; + for(;;) + { + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return out; + out.append(buf.data(), n); + } +} + +void test_parser_all_splits() +{ + std::string input = "GET / HTTP/1.1\r\n"; + auto cb = make_buffer(input); + + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + + // Feed the split as two discrete reads + read_stream rs(f); + rs.provide(buffer_to_string(b1)); + rs.provide(buffer_to_string(b2)); + + std::string got = co_await read_all(rs); + BOOST_TEST_EQ(got, input); + } + }); + BOOST_TEST(r.success); +} +// end::parser_all_splits[] + +struct buffer_inspection_test +{ + void + run() + { + test_all_splits(); + test_step_size(); + test_mutability(); + test_buffer_to_string(); + test_reconstruct(); + test_parser_all_splits(); + } +}; + +} // namespace + +TEST_SUITE(buffer_inspection_test, "boost.capy.doc.7e_buffer_inspection"); diff --git a/test/doc/snippets/8c_buffer_composition.cpp b/test/doc/snippets/8c_buffer_composition.cpp new file mode 100644 index 000000000..bcb603a48 --- /dev/null +++ b/test/doc/snippets/8c_buffer_composition.cpp @@ -0,0 +1,102 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/8.examples/8c.buffer-composition.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +capy::task<> +send_response( + capy::any_stream& stream, + std::array const& http_response) +{ + // tag::write_gather[] + co_await write(stream, http_response); + // end::write_gather[] +} + +struct buffer_composition_test +{ + void + testWriteGather() + { + std::string status = "HTTP/1.1 200 OK\r\n"; + std::string content_type = "Content-Type: application/json\r\n"; + std::string server = "Server: Capy/1.0\r\n"; + std::string empty_line = "\r\n"; + std::string body = R"({"status":"ok"})"; + + std::array http_response = {{ + capy::make_buffer(status), + capy::make_buffer(content_type), + capy::make_buffer(server), + capy::make_buffer(empty_line), + capy::make_buffer(body) + }}; + + auto [a, b] = capy::test::make_stream_pair(); + capy::any_stream stream{&a}; + + capy::test::run_blocking()(send_response(stream, http_response)); + + BOOST_TEST(b.data() == + status + content_type + server + empty_line + body); + } + + void + run() + { + testWriteGather(); + } +}; + +} // namespace + +TEST_SUITE(buffer_composition_test, "boost.capy.doc.8c_buffer_composition"); diff --git a/test/doc/snippets/8d_mock_stream_testing.cpp b/test/doc/snippets/8d_mock_stream_testing.cpp new file mode 100644 index 000000000..b9415b0c4 --- /dev/null +++ b/test/doc/snippets/8d_mock_stream_testing.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/capy +// + +// Compiled fragments shown in pages/8.examples/8d.mock-stream-testing.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 capy = boost::capy; + +namespace { + +capy::task<> +read_line(capy::any_stream& stream, std::string& out) +{ + char buf[64]; + auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); + (void)ec; + out.assign(buf, n); +} + +struct mock_stream_testing_test +{ + void + testMockStreams() + { + // tag::mock_streams[] + capy::test::fuse f; // test::fuse + auto [a, b] = capy::test::make_stream_pair(f); // connected test::stream pair + b.provide("hello\n"); // supply read input on one end + // end::mock_streams[] + + capy::any_stream stream{&a}; + std::string received; + capy::test::run_blocking()(read_line(stream, received)); + BOOST_TEST(received == "hello\n"); + } + + void + testFuseArmed() + { + // tag::fuse_armed[] + capy::test::fuse f; // test::fuse + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + auto [a, b] = capy::test::make_stream_pair(f); // connected test::stream pair + // ... run test ... + co_return; + }); + // end::fuse_armed[] + + BOOST_TEST(r.success); + } + + void + run() + { + testMockStreams(); + testFuseArmed(); + } +}; + +} // namespace + +TEST_SUITE(mock_stream_testing_test, "boost.capy.doc.8d_mock_stream_testing"); diff --git a/test/doc/snippets/8f_timeout_cancellation.cpp b/test/doc/snippets/8f_timeout_cancellation.cpp new file mode 100644 index 000000000..447ef5ad5 --- /dev/null +++ b/test/doc/snippets/8f_timeout_cancellation.cpp @@ -0,0 +1,80 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/8.examples/8f.timeout-cancellation.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +capy::task<> my_task() +{ + co_return; +} + +struct timeout_cancellation_test +{ + void testTriggerStop() + { + capy::thread_pool pool(1); + auto ex = pool.get_executor(); + // tag::trigger_stop[] + std::stop_source source; + capy::run_async(ex, source.get_token())(my_task()); + + // Later: + source.request_stop(); + // end::trigger_stop[] + pool.join(); + } + + void run() + { + testTriggerStop(); + } +}; + +} // namespace + +TEST_SUITE(timeout_cancellation_test, "boost.capy.doc.8f_timeout_cancellation"); diff --git a/test/doc/snippets/9a_capy_layering.cpp b/test/doc/snippets/9a_capy_layering.cpp new file mode 100644 index 000000000..3f444db85 --- /dev/null +++ b/test/doc/snippets/9a_capy_layering.cpp @@ -0,0 +1,127 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9a.CapyLayering.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +namespace concept_layer { + +// tag::write_signature[] +template +io_task +write(S& stream, CB buffers); +// end::write_signature[] + +} // namespace concept_layer + +// tag::any_stream_echo[] +task<> echo(any_stream& stream) +{ + char buf[1024]; + for(;;) + { + auto [ec, n] = co_await stream.read_some( + make_buffer(buf)); + if(ec) + co_return; + co_await write(stream, const_buffer(buf, n)); + } +} +// end::any_stream_echo[] + +namespace coroutine_layer { + +// tag::connection_base[] +class connection_base { +public: + task<> run(); +protected: + virtual task<> do_handshake() = 0; + virtual task<> do_shutdown() = 0; +}; +// end::connection_base[] + +} // namespace coroutine_layer + +struct capy_layering_test +{ + void + testAnyStreamEcho() + { + capy::test::fuse f; + auto [a, b] = capy::test::make_stream_pair(f); + b.provide("hi"); + // Close a's read direction so echo observes eof after + // draining the buffered bytes. + b.close(); + + capy::any_stream stream{&a}; + capy::test::run_blocking()(echo(stream)); + BOOST_TEST(b.data() == "hi"); + } + + void + run() + { + testAnyStreamEcho(); + } +}; + +} // namespace + +TEST_SUITE(capy_layering_test, "boost.capy.doc.9a_capy_layering"); diff --git a/test/doc/snippets/9c_read_stream.cpp b/test/doc/snippets/9c_read_stream.cpp new file mode 100644 index 000000000..19a472147 --- /dev/null +++ b/test/doc/snippets/9c_read_stream.cpp @@ -0,0 +1,200 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9c.ReadStream.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +namespace definition { + +// tag::concept_definition[] +template +concept ReadStream = + requires(T& stream, mutable_buffer_archetype buffers) + { + { stream.read_some(buffers) } -> IoAwaitable; + requires awaitable_decomposes_to< + decltype(stream.read_some(buffers)), + std::error_code, std::size_t>; + }; +// end::concept_definition[] + +// The page's definition must match the library's. +static_assert(definition::ReadStream); +static_assert(capy::ReadStream); + +} // namespace definition + +// Scaffold owning the conforming-signature declaration. +struct conforming_stream +{ + // tag::read_some_signature[] + template + IoAwaitable auto read_some(Buffers buffers); + // end::read_some_signature[] +}; + +namespace composed { + +// tag::read_signature[] +auto read(ReadStream auto& stream, + MutableBufferSequence auto buffers) + -> io_task; +// end::read_signature[] + +} // namespace composed + +// tag::read_header[] +template +task<> read_header(Stream& stream) +{ + char header[16]; + auto [ec, n] = co_await read( + stream, make_buffer(header)); + if(ec == cond::eof) + co_return; // clean shutdown + if(ec) + co_return; + // header contains exactly 16 bytes +} +// end::read_header[] + +// tag::echo[] +template +task<> echo(Stream& stream, WriteStream auto& dest) +{ + char buf[4096]; + for(;;) + { + auto [ec, n] = co_await stream.read_some( + make_buffer(buf)); + + auto [wec, nw] = co_await dest.write_some( + const_buffer(buf, n)); + + if(ec) + co_return; + + if(wec) + co_return; + } +} +// end::echo[] + +// Scaffold for the canonical advance-then-check loop. +template +task<> read_all(S& s, char* buf, std::size_t size) +{ + std::size_t total = 0; + while(total < size) + { + // tag::canonical_loop[] + auto [ec, n] = co_await s.read_some( + mutable_buffer(buf + total, size - total)); + total += n; + if(ec) + co_return; + // end::canonical_loop[] + } +} + +struct read_stream_test +{ + void + testReadHeader() + { + capy::test::read_stream rs; + rs.provide("0123456789abcdef"); + capy::test::run_blocking()(read_header(rs)); + BOOST_TEST(rs.available() == 0); + } + + void + testEcho() + { + capy::test::read_stream rs; + capy::test::write_stream ws; + rs.provide("hello"); + capy::test::run_blocking()(echo(rs, ws)); + BOOST_TEST(ws.data() == "hello"); + } + + void + testCanonicalLoop() + { + capy::test::read_stream rs; + rs.provide("abcdefgh"); + char buf[8]; + capy::test::run_blocking()(read_all(rs, buf, sizeof(buf))); + BOOST_TEST(std::string_view(buf, 8) == "abcdefgh"); + } + + void + run() + { + testReadHeader(); + testEcho(); + testCanonicalLoop(); + } +}; + +} // namespace + +TEST_SUITE(read_stream_test, "boost.capy.doc.9c_read_stream"); diff --git a/test/doc/snippets/9f_write_stream.cpp b/test/doc/snippets/9f_write_stream.cpp new file mode 100644 index 000000000..c605a5dc6 --- /dev/null +++ b/test/doc/snippets/9f_write_stream.cpp @@ -0,0 +1,194 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9f.WriteStream.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +// The real definition lives in ; +// the sketch is checked below against the real concept. +namespace concept_sketch { + +// tag::write_stream_concept[] +template +concept WriteStream = + requires(T& stream, const_buffer_archetype buffers) + { + { stream.write_some(buffers) } -> IoAwaitable; + requires awaitable_decomposes_to< + decltype(stream.write_some(buffers)), + std::error_code, std::size_t>; + }; +// end::write_stream_concept[] + +} // namespace concept_sketch + +static_assert(concept_sketch::WriteStream); +static_assert(capy::WriteStream); +static_assert(!concept_sketch::WriteStream); +static_assert(!capy::WriteStream); + +// The page shows the conforming member signature in isolation. +struct member_signatures +{ + // tag::write_some_signature[] + template + IoAwaitable auto write_some(Buffers buffers); + // end::write_some_signature[] +}; + +// The real algorithms live in and +// ; these sketches mirror the interface +// the page presents and are checked against the real API below. +namespace api_sketch { + +// tag::write_signature[] +auto write(WriteStream auto& stream, + ConstBufferSequence auto buffers) + -> io_task; +// end::write_signature[] + +// tag::write_now_sketch[] +template +class write_now +{ +public: + explicit write_now(Stream& s) noexcept; + + IoAwaitable auto operator()(ConstBufferSequence auto buffers); +}; +// end::write_now_sketch[] + +} // namespace api_sketch + +static_assert(requires(test::write_stream& s, const_buffer b) { + { capy::write(s, b) } -> std::same_as>; +}); +static_assert( + std::constructible_from< + capy::write_now, test::write_stream&>); + +// tag::relay_with_write_now[] +template +task<> relay_with_write_now(Source& src, Stream& dest) +{ + char buf[65536]; + write_now wn(dest); + + for(;;) + { + // Read a chunk from the source + auto [rec, nr] = co_await src.read_some( + mutable_buffer(buf, sizeof(buf))); + if(rec == cond::eof && nr == 0) + co_return; + + // write_now drains the chunk to completion. + // If the kernel accepts 40KB of 64KB, write_now + // internally calls write_some(24KB) for the + // remainder -- a small write that wastes a + // syscall. The caller cannot top up between + // write_now's internal iterations. + auto [wec, nw] = co_await wn( + const_buffer(buf, nr)); + if(wec) + co_return; + + if(rec == cond::eof) + co_return; + } +} +// end::relay_with_write_now[] + +struct write_stream_test +{ + void + testRelayWithWriteNow() + { + test::fuse f; + test::read_stream src(f); + src.provide("relayed data"); + test::write_stream dest(f); + test::run_blocking()(relay_with_write_now(src, dest)); + BOOST_TEST_EQ(dest.data(), "relayed data"); + } + + void + testRelayChunked() + { + // Partial writes force write_now's internal drain loop + test::fuse f; + test::read_stream src(f); + src.provide("relayed data"); + test::write_stream dest(f, 3); + test::run_blocking()(relay_with_write_now(src, dest)); + BOOST_TEST_EQ(dest.data(), "relayed data"); + } + + void + run() + { + testRelayWithWriteNow(); + testRelayChunked(); + } +}; + +} // namespace + +TEST_SUITE(write_stream_test, "boost.capy.doc.9f_write_stream"); diff --git a/test/doc/snippets/9k_executor.cpp b/test/doc/snippets/9k_executor.cpp new file mode 100644 index 000000000..92e68f0ff --- /dev/null +++ b/test/doc/snippets/9k_executor.cpp @@ -0,0 +1,285 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9k.Executor.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using namespace boost::capy; + +// The page shows the concept exactly as defined in +// ; compiling a copy keeps the page +// in sync with the real definition. +namespace concept_def { + +// tag::executor_concept[] +template +concept Executor = + std::is_nothrow_copy_constructible_v && + std::is_nothrow_move_constructible_v && + requires(E& e, E const& ce, E const& ce2, + continuation c) + { + { ce == ce2 } noexcept -> std::convertible_to; + { ce.context() } noexcept; + requires std::is_lvalue_reference_v< + decltype(ce.context())> && + std::derived_from< + std::remove_reference_t< + decltype(ce.context())>, + execution_context>; + { ce.on_work_started() } noexcept; + { ce.on_work_finished() } noexcept; + + { ce.dispatch(c) } -> std::same_as>; + { ce.post(c) }; + }; +// end::executor_concept[] + +} // namespace concept_def + +static_assert(concept_def::Executor); +static_assert(concept_def::Executor); +static_assert(!concept_def::Executor); + +// Scaffolding context so the conforming dispatch shown on the page +// compiles. +class dispatching_executor +{ + struct + { + bool running_in_this_thread() const noexcept { return true; } + } ctx_; + + void post(continuation&) const {} + +public: + // tag::dispatch_impl[] + std::coroutine_handle<> dispatch(continuation& c) const + { + if(ctx_.running_in_this_thread()) + return c.h; // symmetric transfer + post(c); + return std::noop_coroutine(); + } + // end::dispatch_impl[] +}; + +namespace continuation_def { + +// tag::continuation_struct[] +struct continuation +{ + std::coroutine_handle<> h; + void* reserved = nullptr; +}; +// end::continuation_struct[] + +} // namespace continuation_def + +static_assert(sizeof(continuation_def::continuation) + == sizeof(capy::continuation)); + +// Corosio's contexts implement work tracking over an atomic count; +// this scaffolding reproduces just enough of that shape. +class counting_context +{ + std::atomic outstanding_work_{1}; + bool stopped_ = false; + + void stop() { stopped_ = true; } + +public: + bool stopped() const noexcept { return stopped_; } + + // tag::on_work_finished[] + void on_work_finished() noexcept + { + if(outstanding_work_.fetch_sub( + 1, std::memory_order_acq_rel) == 1) + stop(); + } + // end::on_work_finished[] +}; + +// Stand-in for Corosio's tcp_socket: the page demonstrates how an I/O +// object accepts any executor and extracts the context via the base +// class reference. +class tcp_socket +{ +public: + explicit tcp_socket(execution_context&) {} + + // tag::socket_ctor[] + template + requires capy::Executor + explicit tcp_socket(Ex const& ex) + : tcp_socket(ex.context()) + { + } + // end::socket_ctor[] +}; + +namespace layout_sketch { + +// tag::executor_ref_layout[] +class executor_ref +{ + void const* ex_; // pointer to the executor + detail::executor_vtable const* vt_; // pointer to the vtable +}; +// end::executor_ref_layout[] + +} // namespace layout_sketch + +static_assert(sizeof(layout_sketch::executor_ref) + == sizeof(capy::executor_ref)); + +// I/O awaitable shape: embeds a continuation for the caller's handle +// and an executor_ref for completion dispatch. +struct io_awaitable_sketch +{ + continuation cont_; + executor_ref ex_; + + // tag::capture_at_initiation[] + std::coroutine_handle<> + await_suspend( + std::coroutine_handle<> h, + io_env const* env) noexcept + { + cont_.h = h; + ex_ = env->executor; + // ... initiate I/O operation ... + return std::noop_coroutine(); + } + // end::capture_at_initiation[] + + void complete() + { + // tag::dispatch_at_completion[] + // Timer fires or I/O completes: + ex_.post(cont_); + // end::dispatch_at_completion[] + } +}; + +namespace safe_resume_def { + +// tag::safe_resume[] +inline void +safe_resume(std::coroutine_handle<> h) noexcept +{ + auto* saved = get_current_frame_allocator(); + h.resume(); + set_current_frame_allocator(saved); +} +// end::safe_resume[] + +} // namespace safe_resume_def + +// Declarations are enough for the concept check; the definitions are +// a real implementation's concern. +// tag::minimal_executor[] +class my_executor +{ +public: + execution_context& context() const noexcept; + + void on_work_started() const noexcept; + void on_work_finished() const noexcept; + + std::coroutine_handle<> dispatch(continuation& c) const; + void post(continuation& c) const; + + bool operator==(my_executor const&) const noexcept; +}; +// end::minimal_executor[] + +static_assert(capy::Executor); + +struct executor_design_test +{ + void testDispatchReturnsHandle() + { + dispatching_executor ex; + continuation c; + BOOST_TEST(ex.dispatch(c) == std::coroutine_handle<>()); + } + + void testWorkCountReachesZero() + { + counting_context ctx; + ctx.on_work_finished(); + BOOST_TEST(ctx.stopped()); + } + + void testSocketFromExecutor() + { + thread_pool pool(1); + tcp_socket sock(pool.get_executor()); + (void)sock; + } + + void run() + { + testDispatchReturnsHandle(); + testWorkCountReachesZero(); + testSocketFromExecutor(); + } +}; + +} // namespace + +TEST_SUITE(executor_design_test, "boost.capy.doc.9k_executor"); diff --git a/test/doc/snippets/9l_run_api.cpp b/test/doc/snippets/9l_run_api.cpp new file mode 100644 index 000000000..77195e741 --- /dev/null +++ b/test/doc/snippets/9l_run_api.cpp @@ -0,0 +1,278 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9l.RunApi.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace { + +using namespace boost::capy; + +std::atomic tasks_run{0}; +std::atomic connections_handled{0}; + +task my_task() +{ + ++tasks_run; + co_return; +} + +task cancellable_task() { co_return; } +task compute_value() { co_return 42; } +task compute_on_worker() { co_return 42; } +task subtask() { co_return; } +task cpu_bound_task() { co_return; } + +// Executor a coroutine can hop to; kept alive for the whole test run +// because the run_usage fragments name it at namespace scope. +thread_pool worker_pool(1); +thread_pool::executor_type worker_ex = worker_pool.get_executor(); +std::pmr::memory_resource* my_alloc = std::pmr::new_delete_resource(); + +// tag::run_usage[] +// Switch to a different executor for CPU-bound work +task parent() +{ + int result = co_await run(worker_ex)(compute_on_worker()); + // Completion returns to parent's executor +} + +// Customize stop token, inherit caller's executor +task with_timeout() +{ + std::stop_source source; + co_await run(source.get_token())(subtask()); +} + +// Customize allocator, inherit caller's executor +task with_custom_alloc() +{ + co_await run(my_alloc)(subtask()); +} + +// Switch executor AND customize allocator +task full_control() +{ + co_await run(worker_ex, my_alloc)(cpu_bound_task()); +} +// end::run_usage[] + +// Stand-ins for Corosio's I/O types: the page demonstrates launching +// on a strand, not the socket API. +namespace tcp { +struct socket {}; +} // namespace tcp + +thread_pool& ioc = worker_pool; + +task handle_connection(tcp::socket) +{ + ++connections_handled; + co_return; +} + +// tag::run_async_strand[] +void on_accept(tcp::socket sock) +{ + strand my_strand(ioc.get_executor()); + run_async(my_strand)(handle_connection(std::move(sock))); +} +// end::run_async_strand[] + +// Mirrors the wrapper in ; compiling it +// against the real detail machinery keeps the page in sync. +namespace wrapper_sketch { + +template +class run_async_wrapper +{ + detail::run_async_trampoline tr_; + std::stop_token st_; + std::pmr::memory_resource* saved_tls_; + +public: + // tag::wrapper_tls[] + run_async_wrapper(Ex ex, std::stop_token st, Handlers h, Alloc a) noexcept + : tr_(detail::make_trampoline( + std::move(ex), std::move(h), std::move(a))) + , st_(std::move(st)) + , saved_tls_(get_current_frame_allocator()) // remember prior TLS + { + // Set TLS before task argument is evaluated + set_current_frame_allocator(tr_.h_.promise().get_resource()); + } + + ~run_async_wrapper() + { + // Restore the prior TLS so a stale pointer does not outlive + // the execution context that owns the trampoline's resource. + set_current_frame_allocator(saved_tls_); + } + // end::wrapper_tls[] +}; + +} // namespace wrapper_sketch + +// Instantiating every member checks the shown ctor and dtor bodies. +template class wrapper_sketch::run_async_wrapper< + thread_pool::executor_type, + detail::default_handler, + std::pmr::memory_resource*>; + +// Simplified: the real mixin in +// adds a recycling fast path and stores the resource in the frame for +// deallocation. +struct promise_new_sketch +{ + // tag::operator_new[] + static void* operator new(std::size_t size) + { + auto* mr = get_current_frame_allocator(); + if(!mr) + mr = std::pmr::get_default_resource(); + return mr->allocate(size, alignof(std::max_align_t)); + } + // end::operator_new[] +}; + +struct run_api_test +{ + void testRunAsyncForms() + { + tasks_run = 0; + thread_pool pool(2); + auto ex = pool.get_executor(); + auto* my_pool = std::pmr::new_delete_resource(); + std::stop_token st; + auto* alloc = std::pmr::new_delete_resource(); + auto h1 = [] {}; + auto h2 = [](std::exception_ptr) {}; + + // tag::run_async_usage[] + // Executor only (uses default recycling allocator) + run_async(ex)(my_task()); + + // With a stop token for cooperative cancellation + std::stop_source source; + run_async(ex, source.get_token())(cancellable_task()); + + // With a custom memory resource + run_async(ex, my_pool)(my_task()); + + // With a result handler + run_async(ex, [](int result) { + std::cout << "Got: " << result << "\n"; + })(compute_value()); + + // With separate success and error handlers + run_async(ex, + [](int result) { std::cout << "Got: " << result << "\n"; }, + [](std::exception_ptr ep) { /* handle error */ } + )(compute_value()); + + // Full: executor, stop_token, allocator, success handler, error handler + run_async(ex, st, alloc, h1, h2)(my_task()); + // end::run_async_usage[] + + pool.join(); + BOOST_TEST(tasks_run == 3); + } + + void testRunForms() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + run_async(ex)(parent()); + run_async(ex)(with_timeout()); + run_async(ex)(with_custom_alloc()); + run_async(ex)(full_control()); + pool.join(); + } + + void testPostfixOrder() + { + tasks_run = 0; + thread_pool pool(1); + auto ex = pool.get_executor(); + auto* alloc = std::pmr::new_delete_resource(); + + // tag::postfix_order[] + // Step 1: wrapper constructor sets TLS allocator + // v~~~~~~~~~~~~~~v + run_async(ex, alloc) (my_task()); + // ^~~~~~~~~^ + // Step 2: task frame allocated using TLS allocator + // end::postfix_order[] + + pool.join(); + BOOST_TEST(tasks_run == 1); + } + + // Must run last: joining the worker pool ends it for good. + void testStrandAccept() + { + on_accept(tcp::socket{}); + worker_pool.join(); + BOOST_TEST(connections_handled == 1); + } + + void run() + { + testRunAsyncForms(); + testRunForms(); + testPostfixOrder(); + testStrandAccept(); + } +}; + +} // namespace + +TEST_SUITE(run_api_test, "boost.capy.doc.9l_run_api"); diff --git a/test/doc/snippets/9m_why_not_cobalt.cpp b/test/doc/snippets/9m_why_not_cobalt.cpp new file mode 100644 index 000000000..da82f00b2 --- /dev/null +++ b/test/doc/snippets/9m_why_not_cobalt.cpp @@ -0,0 +1,55 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9m.WhyNotCobalt.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +namespace { + +using boost::capy::io_env; + +// Shows the IoAwaitable protocol signature used for context propagation. +struct context_propagating_op +{ + // tag::await_suspend_env[] + auto await_suspend(std::coroutine_handle<> h, io_env const* env); + // end::await_suspend_env[] +}; + +} // namespace diff --git a/test/doc/snippets/9n_why_not_cobalt_concepts.cpp b/test/doc/snippets/9n_why_not_cobalt_concepts.cpp new file mode 100644 index 000000000..c4557b071 --- /dev/null +++ b/test/doc/snippets/9n_why_not_cobalt_concepts.cpp @@ -0,0 +1,273 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9n.WhyNotCobaltConcepts.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +namespace capy = boost::capy; + +namespace { + +// tag::capy_signature[] +// Capy +capy::task<> my_algo(capy::any_write_stream& stream); +// end::capy_signature[] + +// The concept definitions the page reproduces must stay in sync with +// the shipped ones; local mirrors compile them and the static_asserts +// below compare them against the real concepts. +namespace task_requirements { + +using boost::capy::io_env; + +// tag::io_awaitable_concept[] +template +concept IoAwaitable = + requires(A a, std::coroutine_handle<> h, io_env const* env) + { + a.await_suspend(h, env); + }; +// end::io_awaitable_concept[] + +// tag::io_runnable_concept[] +template +concept IoRunnable = + IoAwaitable && + requires { typename T::promise_type; } && + requires(T& t, T const& ct, typename T::promise_type const& cp, + typename T::promise_type& p) + { + { ct.handle() } noexcept + -> std::same_as>; + { cp.exception() } noexcept -> std::same_as; + { t.release() } noexcept; + { p.set_continuation(std::coroutine_handle<>{}) } noexcept; + { p.set_environment(static_cast(nullptr)) } noexcept; + } && + (std::is_void_v().await_resume())> || + requires(typename T::promise_type& p) { p.result(); }); +// end::io_runnable_concept[] + +static_assert(IoAwaitable> == capy::IoAwaitable>); +static_assert(IoRunnable> == capy::IoRunnable>); +static_assert(IoRunnable> == capy::IoRunnable>); + +} // namespace task_requirements + +namespace context_propagation { + +using boost::capy::io_env; + +// Shows the protocol signature; the declaration is the fragment. +struct child_operation +{ + // tag::await_suspend_env[] + auto await_suspend(std::coroutine_handle<> h, io_env const* env); + // end::await_suspend_env[] +}; + +} // namespace context_propagation + +namespace concept_short { + +using boost::capy::const_buffer_archetype; +using boost::capy::IoAwaitable; + +// tag::write_stream_concept_short[] +template +concept WriteStream = + requires(T& stream, const_buffer_archetype buffers) + { + { stream.write_some(buffers) } -> IoAwaitable; + // ... + }; +// end::write_stream_concept_short[] + +static_assert(WriteStream); + +} // namespace concept_short + +// tag::semantics_quote[] +// From capy/concept/write_stream.hpp + +// Semantic Requirements: +// +// Attempts to write up to buffer_size( buffers ) bytes from +// the buffer sequence to the stream. +// +// If buffer_size( buffers ) > 0: +// +// If !ec, then n >= 1 && n <= buffer_size( buffers ). +// n bytes were written from the buffer sequence. +// If ec, then n >= 0 && n < buffer_size( buffers ). +// n is the number of bytes written before the I/O +// condition arose. +// +// Equivalently, n == buffer_size( buffers ) implies !ec: a +// completion that writes the entire buffer sequence is a success, +// even when the underlying operation also signals a condition. That +// condition is reported on a subsequent write. This lets generic +// composition algorithms such as when_all and when_any distinguish +// a completed transfer from a failure. +// +// If buffer_empty( buffers ) is true, n is 0. The empty +// buffer is not itself a cause for error, but ec may reflect +// the state of the stream. +// +// Buffers in the sequence are consumed in order. +// +// Buffer Lifetime: +// +// The caller must ensure that the memory referenced by buffers +// remains valid until the co_await expression returns. +// end::semantics_quote[] + +// tag::lifetime_warning[] +// Warning: Pass buffer sequences by value. A by-value parameter +// is copied into the coroutine frame (or the awaitable's state), +// so the returned awaitable is self-contained and may be stored, +// moved across threads, or wrapped into a sender without lifetime +// concerns. A by-const-reference parameter binds to caller storage +// and is only safe when the awaitable is consumed immediately by +// co_await in the same scope; storing such an awaitable produces +// a dangling reference. +// end::lifetime_warning[] + +namespace awaitable_storage { + +using boost::capy::ConstBufferSequence; +using boost::capy::WriteStream; + +// Scaffolding mirror of the real class shape so that the tagged +// regions, quoted from capy/io/any_write_stream.hpp, parse exactly +// as the page shows them. +class any_write_stream +{ + struct vtable + { + std::size_t awaitable_size; + }; + + template + struct vtable_for_impl + { + static constexpr vtable value{sizeof(S)}; + }; + + vtable const* vt_ = nullptr; + void* storage_ = nullptr; + void* stream_ = nullptr; + void* cached_awaitable_ = nullptr; + +public: + template + requires (!std::same_as, any_write_stream>) + any_write_stream(S s); + + template + auto write_some(CB buffers); +}; + +// tag::any_write_some_template[] +template +auto +any_write_stream::write_some(CB buffers) +{ + // ... +} +// end::any_write_some_template[] + +// tag::ctor_prealloc[] +// capy/io/any_write_stream.hpp +template + requires (!std::same_as, any_write_stream>) +any_write_stream::any_write_stream(S s) + : vt_(&vtable_for_impl::value) +{ + // ... + storage_ = ::operator new(sizeof(S)); + stream_ = ::new(storage_) S(std::move(s)); + + // Preallocate the awaitable storage + cached_awaitable_ = ::operator new(vt_->awaitable_size); + // ... +} +// end::ctor_prealloc[] + +} // namespace awaitable_storage + +namespace concept_full { + +using boost::capy::awaitable_decomposes_to; +using boost::capy::const_buffer_archetype; +using boost::capy::IoAwaitable; + +// tag::write_stream_concept[] +template +concept WriteStream = + requires(T& stream, const_buffer_archetype buffers) + { + { stream.write_some(buffers) } -> IoAwaitable; + requires awaitable_decomposes_to< + decltype(stream.write_some(buffers)), + std::error_code, std::size_t>; + }; +// end::write_stream_concept[] + +static_assert(WriteStream == + capy::WriteStream); +static_assert(WriteStream); + +} // namespace concept_full + +} // namespace diff --git a/test/doc/snippets/9o_why_not_tmc.cpp b/test/doc/snippets/9o_why_not_tmc.cpp new file mode 100644 index 000000000..3d1845862 --- /dev/null +++ b/test/doc/snippets/9o_why_not_tmc.cpp @@ -0,0 +1,143 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/9.design/9o.WhyNotTMC.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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +using boost::capy::io_env; + +// The page shows the three standard await_suspend forms as one listing; +// they differ only in return type, so each lives in its own scaffolding +// struct and the page stitches the tags together. +struct plain_awaitable_void +{ + // tag::std_suspend_void[] + void await_suspend(std::coroutine_handle<> h); + // end::std_suspend_void[] +}; + +struct plain_awaitable_bool +{ + // tag::std_suspend_bool[] + // or + bool await_suspend(std::coroutine_handle<> h); + // end::std_suspend_bool[] +}; + +struct plain_awaitable_handle +{ + // tag::std_suspend_handle[] + // or + std::coroutine_handle<> await_suspend(std::coroutine_handle<> h); + // end::std_suspend_handle[] +}; + +struct io_awaitable_form +{ + // tag::await_suspend_env[] + auto await_suspend(std::coroutine_handle<> h, io_env const* env); + // end::await_suspend_env[] +}; + +// Mirrors the shape of task.hpp's transform_awaitable(); never +// instantiated with a non-IoAwaitable, so the else branch only +// has to parse. +template +void protocol_strictness_shape() +{ + using A = std::decay_t; + if constexpr (capy::IoAwaitable) + { + } + // tag::strict_static_assert[] + // From task.hpp transform_awaitable() + else + { + static_assert(sizeof(A) == 0, "requires IoAwaitable"); + } + // end::strict_static_assert[] +} + +struct connection { }; + +capy::task<> handle_connection(connection) +{ + co_return; +} + +struct why_not_tmc_test +{ + void + testArenaAllocation() + { + capy::thread_pool pool(1); + auto ex = pool.get_executor(); + connection socket; + using capy::run_async; + // tag::arena[] + std::pmr::monotonic_buffer_resource arena; + run_async(ex, &arena)(handle_connection(socket)); + // On disconnect: entire arena reclaimed instantly + // end::arena[] + pool.join(); + BOOST_TEST(true); + } + + void + run() + { + testArenaAllocation(); + } +}; + +} // namespace + +TEST_SUITE(why_not_tmc_test, "boost.capy.doc.9o_why_not_tmc"); diff --git a/test/doc/snippets/http_client.hpp b/test/doc/snippets/http_client.hpp new file mode 100644 index 000000000..8dbe13722 --- /dev/null +++ b/test/doc/snippets/http_client.hpp @@ -0,0 +1,45 @@ +// +// 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/capy +// + +// The library-API header shown in pages/6.streams/6f.isolation.adoc. +// The page presents this file as http_client.hpp; scaffolding stays +// outside the tags. + +#include +#include + +#include +#include + +// The page shows unqualified names. +using namespace boost::capy; + +// tag::http_client_header[] +// http_client.hpp +#pragma once +#include + +struct http_request +{ + std::string method; + std::string url; + std::map headers; +}; + +struct http_response +{ + int status_code; + std::map headers; + any_read_stream body; // Body is read as a stream +}; + +// Send request, receive response +// Works with any transport that provides any_stream +task send_request(any_stream& conn, http_request const& req); +// end::http_client_header[] diff --git a/test/doc/snippets/index_page.cpp b/test/doc/snippets/index_page.cpp new file mode 100644 index 000000000..054814a2c --- /dev/null +++ b/test/doc/snippets/index_page.cpp @@ -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/capy +// + +// Compiled fragments shown in pages/index.adoc. + +// tag::convention[] +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 + +using namespace boost::capy; +// end::convention[] diff --git a/test/doc/snippets/protocol.hpp b/test/doc/snippets/protocol.hpp new file mode 100644 index 000000000..436ae6243 --- /dev/null +++ b/test/doc/snippets/protocol.hpp @@ -0,0 +1,28 @@ +// +// 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/capy +// + +// The compilation-firewall header shown in +// pages/6.streams/6f.isolation.adoc. The page presents this file as +// protocol.hpp; scaffolding stays outside the tags. + +// tag::protocol_header[] +// protocol.hpp - No template dependencies +#pragma once +#include +#include +// end::protocol_header[] + +// The page shows unqualified names. +using namespace boost::capy; + +// tag::protocol_header[] + +// Declaration only - no implementation details +task<> handle_protocol(any_stream& stream); +// end::protocol_header[] diff --git a/test/doc/snippets/quick_start.cpp b/test/doc/snippets/quick_start.cpp new file mode 100644 index 000000000..9a8953480 --- /dev/null +++ b/test/doc/snippets/quick_start.cpp @@ -0,0 +1,114 @@ +// +// 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/capy +// + +// Compiled fragments shown in pages/quick-start.adoc. Pages include the +// tagged regions; scaffolding stays outside the tags. + +// 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" +// gcc 15 with sanitizers misattributes coroutine frame delete paths +#pragma GCC diagnostic ignored "-Wmismatched-new-delete" +#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 "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +capy::task answer() +{ + co_return 42; +} + +capy::task might_fail() +{ + bool fail = true; + if (fail) + throw std::runtime_error("might_fail"); + co_return 0; +} + +struct quick_start_test +{ + void + testHandlingResults() + { + capy::thread_pool pool(1); + auto executor = pool.get_executor(); + // tag::handler[] + capy::run_async(executor, [](int result) { + std::cout << "Got: " << result << "\n"; + })(answer()); + // end::handler[] + pool.join(); + } + + void + testHandlingErrors() + { + capy::thread_pool pool(1); + auto executor = pool.get_executor(); + // tag::errors[] + capy::run_async(executor, + [](int result) { + std::cout << "Success: " << result << "\n"; + }, + [](std::exception_ptr ep) { + try { + if (ep) std::rethrow_exception(ep); + } catch (std::exception const& e) { + std::cerr << "Error: " << e.what() << "\n"; + } + } + )(might_fail()); + // end::errors[] + pool.join(); + } + + void + run() + { + testHandlingResults(); + testHandlingErrors(); + } +}; + +} // namespace + +TEST_SUITE(quick_start_test, "boost.capy.doc.quick_start"); diff --git a/test/tsan.supp b/test/tsan.supp new file mode 100644 index 000000000..675f24186 --- /dev/null +++ b/test/tsan.supp @@ -0,0 +1,7 @@ +# Apple's libc++ stream internals carry no TSan annotations, so +# concurrent std::cout writes from several threads are reported as a +# data race even though the standard requires them to be race-free +# ([iostream.objects.overview]). Linux/libstdc++ synchronizes through +# paths TSan understands, so these entries match nothing there. +race:__pad_and_output +race:__put_character_sequence