diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6f070d251..fe51d9408 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -20,6 +20,7 @@ ** xref:4.coroutines/4f.composition.adoc[Concurrent Composition] ** xref:4.coroutines/4g.allocators.adoc[Frame Allocators] ** xref:4.coroutines/4h.lambda-captures.adoc[Lambda Coroutine Captures] +** xref:4.coroutines/4i.asio-integration.adoc[Asio Integration] * xref:5.buffers/5.intro.adoc[Buffer Sequences] ** xref:5.buffers/5a.overview.adoc[Why Concepts, Not Spans] ** xref:5.buffers/5b.types.adoc[Buffer Types] diff --git a/doc/modules/ROOT/pages/4.coroutines/4i.asio-integration.adoc b/doc/modules/ROOT/pages/4.coroutines/4i.asio-integration.adoc new file mode 100644 index 000000000..4ad097cb9 --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4i.asio-integration.adoc @@ -0,0 +1,672 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Asio Integration + +Capy provides seamless integration with both Boost.Asio and standalone Asio. This chapter explains how to use Asio's I/O facilities within capy coroutines and how to spawn capy coroutines from Asio code. + +== Prerequisites + +* Completed xref:4.coroutines/4a.tasks.adoc[The task Type] +* Completed xref:4.coroutines/4b.launching.adoc[Launching Coroutines] +* Familiarity with Asio's async model and completion tokens + +== Overview + +Capy's Asio integration provides two directions of interoperability: + +1. **Capy to Asio**: Await Asio async operations inside capy coroutines using `as_io_awaitable` +2. **Asio to Capy**: Spawn capy coroutines from Asio code using `asio_spawn` + +Additionally, you can: + +* Wrap Asio executors for use with capy (`wrap_asio_executor`) +* Wrap capy executors for use with Asio (`asio_executor_adapter`) + +== Choosing the Right Header + +[cols="1,2"] +|=== +| Header | Use When + +| `` +| Using Boost.Asio (`boost::asio` namespace) + +| `` +| Using standalone Asio (`asio` namespace) +|=== + +Both headers provide identical functionality, just targeting different Asio variants. + +== Awaiting Asio Operations in Capy Coroutines + +The `as_io_awaitable` completion token allows you to `co_await` any Asio async operation directly within a capy `io_task`. + +=== Basic Usage + +[source,cpp] +---- +#include +#include + +namespace capy = boost::capy; +namespace asio = boost::asio; + +capy::io_task read_some(asio::ip::tcp::socket& socket) +{ + std::array buffer; + + // Await the Asio operation using as_io_awaitable + auto [ec, bytes_read] = co_await socket.async_read_some( + asio::buffer(buffer), + capy::as_io_awaitable); + + if (ec) + throw std::system_error(ec); + + co_return bytes_read; +} +---- + +The `as_io_awaitable` token transforms the operation into an awaitable that: + +* Suspends the coroutine until the operation completes +* Returns the completion arguments as a tuple +* Propagates cancellation from the capy stop token + +=== Completion Signatures + +The result type depends on the Asio operation's completion signature. +When the signature begins with `error_code`, the result is an `io_result` +which bundles the error code with any additional values and supports structured bindings. + +[cols="1,1"] +|=== +| Asio Signature | co_await Result + +| `void(error_code, std::size_t)` +| `io_result` + +| `void(error_code)` +| `io_result<>` + +| `void(std::exception_ptr, std::size_t)` +| `std::size_t` + +| `void(std::exception_ptr)` +| `void` + +| `void()` +| `void` +|=== + +Use structured bindings for clean syntax: + +[source,cpp] +---- +auto [ec, bytes] = co_await socket.async_read_some(buffer, capy::as_io_awaitable); +---- + +=== Setting as Default Token + +To avoid repeating `as_io_awaitable` on every call, rebind your I/O objects: + +[source,cpp] +---- +// Method 1: Type alias +using awaitable_socket = capy::as_io_awaitable_t::as_default_on_t< + asio::ip::tcp::socket>; + +awaitable_socket socket(io_context); +auto [ec, n] = co_await socket.async_read_some(buffer); // No token needed + +// Method 2: Runtime rebinding +auto socket = capy::as_io_awaitable_t::as_default_on( + asio::ip::tcp::socket(io_context)); +---- + +=== Cancellation + +When the capy coroutine receives a stop request (via its stop token), a terminal cancellation signal is sent to the Asio operation: + +[source,cpp] +---- +capy::io_task cancellable_read(asio::ip::tcp::socket& socket) +{ + std::array buffer; + + // If stop is requested, this operation will be cancelled + auto [ec, n] = co_await socket.async_read_some( + buffer, capy::as_io_awaitable); + + if (ec == asio::error::operation_aborted) + { + // Cancelled via stop token + co_return; + } + // ... +} +---- + +== Spawning Capy Coroutines from Asio + +The `asio_spawn` function allows you to run capy coroutines from Asio code, using any Asio completion token to handle the result. + +=== Basic Usage + +[source,cpp] +---- +#include +#include + +capy::io_task compute() +{ + // ... async work ... + co_return 42; +} + +int main() +{ + asio::io_context io; + + // Wrap the Asio executor for capy + auto exec = capy::wrap_asio_executor(io.get_executor()); + + // Spawn the coroutine, fire-and-forget + capy::asio_spawn(exec, compute())(asio::detached); + + io.run(); +} +---- + +=== Handling Results + +Use any Asio completion token to receive the result: + +[source,cpp] +---- +// Callback handler +capy::asio_spawn(exec, compute())( + [](std::exception_ptr ep, int result) { + if (ep) + std::rethrow_exception(ep); + std::cout << "Result: " << result << "\n"; + }); + +// With use_awaitable (in an Asio coroutine) +auto [ep, result] = co_await capy::asio_spawn(exec, compute())); + +// With use_future +auto future = capy::asio_spawn(exec, compute(), asio::use_future); +auto [ep, result] = future.get(); +---- + +=== Completion Signature + +The completion signature depends on the coroutine's return type +and the `noexcept` specification of `await_resume`: + +[cols="2,2"] +|=== +| Coroutine | Completion Signature + +| `IoAwaitable` (may throw) +| `void(std::exception_ptr, T)` + +| `IoAwaitable` (noexcept) +| `void(T)` + +| `IoAwaitable` (may throw) +| `void(std::exception_ptr)` + +| `IoAwaitable` (noexcept) +| `void()` +|=== + +=== Three-Argument Form + +A convenience overload accepts the token directly: + +[source,cpp] +---- +// Two-step form +capy::asio_spawn(exec, compute())(asio::detached); + +// Equivalent three-argument form +capy::asio_spawn(exec, compute(), asio::detached); +---- + +== Wrapping Executors + +=== Asio Executor to Capy + +Use `wrap_asio_executor` to adapt any Asio executor for capy: + +[source,cpp] +---- +asio::io_context io; +auto capy_exec = capy::wrap_asio_executor(io.get_executor()); + +// Now use with capy's run/run_async +capy::run_async(capy_exec)(my_task()); +---- + +The function automatically detects and handles: + +* Legacy Networking TS executors (with `on_work_started`/`on_work_finished`) +* Modern Boost.Asio standard executors +* Standalone Asio standard executors + +=== Capy Executor to Asio + +The `asio_executor_adapter` wraps a capy executor for use with Asio: + +[source,cpp] +---- +capy::thread_pool pool; +capy::asio_executor_adapter<> asio_exec(pool.get_executor()); + +// Use with Asio operations +asio::post(asio_exec, []{ std::cout << "Hello!\n"; }); +---- + +The adapter supports Asio's execution properties: + +* `blocking`: `possibly`, `never`, or `always` +* `outstanding_work`: `tracked` or `untracked` +* `allocator`: Custom allocator for handler allocation + +[source,cpp] +---- +// Require non-blocking execution +auto never_blocking = asio::require(asio_exec, + asio::execution::blocking.never); + +// Require work tracking +auto tracked = asio::require(asio_exec, + asio::execution::outstanding_work.tracked); +---- + +== Complete Example: Echo Server + +This example demonstrates a complete echo server using capy coroutines with Boost.Asio: + +[source,cpp] +---- +#include +#include +#include +#include + +namespace capy = boost::capy; +namespace asio = boost::asio; +using tcp = asio::ip::tcp; + +// Rebind socket to use as_io_awaitable by default +using socket_type = capy::as_io_awaitable_t::as_default_on_t; + +capy::task handle_client(socket_type socket) +{ + std::array buffer; + + for (;;) + { + // Read some data + auto [read_ec, bytes_read] = co_await socket.async_read_some( + asio::buffer(buffer)); + + if (read_ec) + { + if (read_ec != asio::error::eof) + std::cerr << "Read error: " << read_ec.message() << "\n"; + co_return; + } + + // Echo it back + auto [write_ec, bytes_written] = co_await asio::async_write( + socket, + asio::buffer(buffer.data(), bytes_read)); + + if (write_ec) + { + std::cerr << "Write error: " << write_ec.message() << "\n"; + co_return; + } + } +} + +capy::task accept_loop(tcp::acceptor& acceptor) +{ + auto exec = co_await capy::this_coro::executor; + + for (;;) + { + // Accept a connection + auto [ec, socket] = co_await acceptor.async_accept( + capy::as_io_awaitable); + + if (ec) + { + std::cerr << "Accept error: " << ec.message() << "\n"; + continue; + } + + // Spawn handler for this client (detached) + capy::asio_spawn(exec, handle_client( + socket_type(std::move(socket))))(asio::detached); + } +} + +int main() +{ + asio::io_context io; + + tcp::acceptor acceptor(io, {tcp::v4(), 8080}); + std::cout << "Listening on port 8080...\n"; + + auto exec = capy::wrap_asio_executor(io.get_executor()); + capy::asio_spawn(exec, accept_loop(acceptor))(asio::detached); + + io.run(); +} +---- + +== Buffer Sequence Integration + +Capy provides utilities for seamless bidirectional conversion between Asio buffer sequences and capy's native buffer types. This allows: + +* Using capy's `MutableBufferSequence` and `ConstBufferSequence` with Asio I/O operations +* Using Asio buffer sequences with capy's buffer concepts and algorithms + +=== Buffer Types + +Capy provides Asio-compatible buffer wrappers that inherit from capy's native buffer types: + +[cols="1,2"] +|=== +| Type | Description + +| `asio_const_buffer` +| Extends `const_buffer` with implicit conversions to `asio::const_buffer` and `boost::asio::const_buffer` + +| `asio_mutable_buffer` +| Extends `mutable_buffer` with implicit conversions to both mutable and const Asio buffer types +|=== + +These types allow capy buffers to be used directly with Asio operations: + +[source,cpp] +---- +capy::asio_mutable_buffer buf(data, size); + +// Works with Asio operations +co_await socket.async_read_some(buf, capy::as_io_awaitable); +---- + +=== The as_asio_buffer_sequence Function + +The `as_asio_buffer_sequence` function provides bidirectional conversion between Asio and capy buffer sequences. The returned range satisfies both Asio's buffer sequence requirements and capy's `MutableBufferSequence`/`ConstBufferSequence` concepts. + +==== Capy to Asio + +Convert capy buffer sequences for use with Asio operations: + +[source,cpp] +---- +// Single buffers -> array of 1 +auto seq1 = capy::as_asio_buffer_sequence(my_mutable_buffer); +auto seq2 = capy::as_asio_buffer_sequence(my_const_buffer); + +// Capy buffer sequences -> wrapped with transforming range +capy::mutable_buffer_pair buffers = /* ... */; +auto seq3 = capy::as_asio_buffer_sequence(buffers); + +// Use with Asio +co_await socket.async_read_some(seq3, capy::as_io_awaitable); +---- + +==== Asio to Capy + +Convert Asio buffer sequences for use with capy's buffer concepts: + +[source,cpp] +---- +// Asio buffer sequences -> wrapped with transforming range +std::vector asio_buffers = /* ... */; +auto seq = capy::as_asio_buffer_sequence(asio_buffers); + +// Now satisfies capy::MutableBufferSequence +static_assert(capy::MutableBufferSequence); + +// Use with capy buffer algorithms +std::size_t total = capy::buffer_size(seq); +---- + +==== Pass-Through + +Ranges that already contain `asio_const_buffer` or `asio_mutable_buffer` elements are forwarded unchanged: + +[source,cpp] +---- +std::vector already_compatible = /* ... */; +auto seq = capy::as_asio_buffer_sequence(already_compatible); +// Returns the range as-is, no wrapping needed +---- + +=== Example: Scatter/Gather I/O + +Using buffer sequences enables efficient scatter/gather I/O: + +[source,cpp] +---- +#include +#include + +capy::io_task read_message( + boost::asio::ip::tcp::socket& socket) +{ + // Header + body buffers + std::array header; + std::array body; + + capy::mutable_buffer_pair buffers( + capy::mutable_buffer(header.data(), header.size()), + capy::mutable_buffer(body.data(), body.size())); + + // Scatter read into both buffers + auto [ec, n] = co_await socket.async_read_some( + capy::as_asio_buffer_sequence(buffers), + capy::as_io_awaitable); + + if (ec) + throw std::system_error(ec); + + co_return n; +} +---- + +== Stream Adapters + +Capy provides bidirectional stream adapters for converting between Asio's `AsyncReadStream`/`AsyncWriteStream` types and capy's `ReadStream`/`WriteStream` concepts. + +=== Capy Streams to Asio + +The `async_read_stream`, `async_write_stream`, and `async_stream` classes wrap capy streams to provide Asio-style `async_read_some` and `async_write_some` operations with completion token support. + +[source,cpp] +---- +#include + +// Wrap a capy stream for Asio +capy::any_stream my_stream = ...; +auto exec = capy::wrap_asio_executor(io.get_executor()); + +capy::async_stream asio_compatible(std::move(my_stream), exec); + +// Use with Asio-style async operations +auto [ec, n] = co_await asio_compatible.async_read_some( + buffer, capy::as_io_awaitable); +---- + +These adapters are useful when you have a capy stream but need to use it with code expecting Asio's async model. + +[cols="1,2"] +|=== +| Class | Description + +| `async_read_stream` +| Wraps a capy `ReadStream`, provides `async_read_some` + +| `async_write_stream` +| Wraps a capy `WriteStream`, provides `async_write_some` + +| `async_stream` +| Wraps a capy `Stream`, provides both operations +|=== + +=== Asio Streams to Capy + +The `asio_read_stream`, `asio_write_stream`, and `asio_stream` classes wrap Asio streams to satisfy capy's stream concepts, using `as_io_awaitable` internally. + +[source,cpp] +---- +#include + +// Wrap an Asio socket for capy +boost::asio::ip::tcp::socket socket(io); +capy::asio_stream stream(std::move(socket)); + +// Use with capy's stream interface +auto result = co_await stream.read_some(buffer); +auto [ec, bytes] = result; +---- + +These adapters allow Asio I/O objects to be used with capy's stream algorithms and abstractions. + +[cols="1,2"] +|=== +| Class | Description + +| `asio_read_stream` +| Wraps an Asio `AsyncReadStream`, provides `read_some` + +| `asio_write_stream` +| Wraps an Asio `AsyncWriteStream`, provides `write_some` + +| `asio_stream` +| Wraps an Asio stream, provides both operations +|=== + +=== Example: Bridging Stream Types + +[source,cpp] +---- +#include +#include + +// Convert Asio socket to capy stream +capy::task process_connection(boost::asio::ip::tcp::socket socket) +{ + // Wrap the Asio socket as a capy stream + capy::asio_stream stream(std::move(socket)); + + // Now use capy stream algorithms + std::array buf; + capy::mutable_buffer buffer(buf.data(), buf.size()); + + while (true) + { + auto [ec, n] = co_await stream.read_some(buffer); + if (ec) + break; + + // Echo back + co_await stream.write_some(capy::const_buffer(buf.data(), n)); + } +} +---- + +== Reference + +[cols="1,2"] +|=== +| Header | Description + +| `` +| Buffer types, iterator, and `as_asio_buffer_sequence` (included by boost.hpp/standalone.hpp) + +| `` +| Complete Boost.Asio integration + +| `` +| Complete standalone Asio integration + +| `` +| The `as_io_awaitable` completion token (included by above) + +| `` +| The `asio_spawn` function (included by above) + +| `` +| The `asio_executor_adapter` class (included by above) + +| `` +| The `wrap_asio_executor` function (included by above) + +| `` +| Stream adapters for bidirectional Asio/capy conversion +|=== + +[cols="1,2"] +|=== +| Symbol | Description + +| `as_io_awaitable` +| Completion token for awaiting Asio operations + +| `as_io_awaitable_t::as_default_on` +| Rebind I/O objects to default to `as_io_awaitable` + +| `asio_spawn` +| Spawn capy coroutines with Asio completion tokens + +| `wrap_asio_executor` +| Adapt Asio executors for capy + +| `asio_executor_adapter` +| Adapt capy executors for Asio + +| `asio_const_buffer` +| Capy const buffer with Asio conversion operators + +| `asio_mutable_buffer` +| Capy mutable buffer with Asio conversion operators + +| `as_asio_buffer_sequence` +| Convert any buffer sequence for Asio compatibility + +| `async_read_stream` +| Wrap capy ReadStream for Asio async operations + +| `async_write_stream` +| Wrap capy WriteStream for Asio async operations + +| `async_stream` +| Wrap capy Stream for Asio async operations + +| `asio_read_stream` +| Wrap Asio AsyncReadStream for capy ReadStream concept + +| `asio_write_stream` +| Wrap Asio AsyncWriteStream for capy WriteStream concept + +| `asio_stream` +| Wrap Asio stream for capy Stream concept +|=== + diff --git a/example/asio/CMakeLists.txt b/example/asio/CMakeLists.txt index 003e1c5e0..96aa93f13 100644 --- a/example/asio/CMakeLists.txt +++ b/example/asio/CMakeLists.txt @@ -7,137 +7,13 @@ # Official repository: https://github.com/cppalliance/capy # -#------------------------------------------------- -# -# Find or fetch Boost.Asio -# -#------------------------------------------------- -if (NOT TARGET Boost::asio) - # Use BOOST_SRC_DIR if already defined, else check env, else use default - if (NOT DEFINED BOOST_SRC_DIR) - if (DEFINED ENV{BOOST_SRC_DIR}) - set(BOOST_SRC_DIR "$ENV{BOOST_SRC_DIR}" CACHE STRING "Boost source dir") - else () - set(BOOST_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." CACHE STRING "Boost source dir") - endif () - endif () - - # Find absolute BOOST_SRC_DIR - if (NOT IS_ABSOLUTE ${BOOST_SRC_DIR}) - set(BOOST_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${BOOST_SRC_DIR}") - endif () - - # Validate BOOST_SRC_DIR - set(BOOST_SRC_DIR_IS_VALID ON) - foreach (F "CMakeLists.txt" "Jamroot" "boost-build.jam" "bootstrap.sh" "libs") - if (NOT EXISTS "${BOOST_SRC_DIR}/${F}") - set(BOOST_SRC_DIR_IS_VALID OFF) - break() - endif () - endforeach () - - if (BOOST_SRC_DIR_IS_VALID) - message(STATUS "Building with valid Boost src dir") - # From BOOST_SRC_DIR - set(BOOST_INCLUDE_LIBRARIES asio) - set(BOOST_EXCLUDE_LIBRARIES capy) - set(PREV_BUILD_TESTING ${BUILD_TESTING}) - set(BUILD_TESTING OFF CACHE BOOL "Build the tests." FORCE) - add_subdirectory(${BOOST_SRC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/_deps/boost EXCLUDE_FROM_ALL) - set(BUILD_TESTING ${PREV_BUILD_TESTING} CACHE BOOL "Build the tests." FORCE) - else () - # Try installed Boost package - find_package(Boost QUIET) - if (Boost_FOUND) - message(STATUS "Using installed Boost package for asio example") - if (NOT TARGET Boost::asio) - add_library(Boost::asio ALIAS Boost::headers) - endif () - else () - # Fallback: FetchContent - message(STATUS "No local Boost found, using FetchContent to download Boost") - include(FetchContent) - - # Policy CMP0135 (introduced in CMake 3.24) controls the timestamp behavior - # for files extracted from archives downloaded via FetchContent/ExternalProject. - # - # OLD behavior: Extracted files keep their original timestamps from the archive. - # This can cause build issues because archive timestamps are often - # older than dependent build files, triggering unnecessary rebuilds - # or causing "file has modification time in the future" warnings. - # - # NEW behavior: Extracted files get the current time (download time) as their - # timestamp, which avoids these timing-related build problems. - # - # We set NEW if the policy exists. On older CMake versions (< 3.24), the policy - # doesn't exist, so this block is skipped and FetchContent works normally. - # This avoids using DOWNLOAD_EXTRACT_TIMESTAMP directly in FetchContent_Declare, - # which would cause a parse error on CMake < 3.24 (unrecognized keyword). - if (POLICY CMP0135) - cmake_policy(SET CMP0135 NEW) - endif() - - set(BOOST_INCLUDE_LIBRARIES asio) - FetchContent_Declare( - boost - URL https://github.com/boostorg/boost/releases/download/boost-1.88.0/boost-1.88.0-cmake.tar.xz - ) - set(PREV_BUILD_TESTING ${BUILD_TESTING}) - set(BUILD_TESTING OFF CACHE BOOL "Build the tests." FORCE) - FetchContent_MakeAvailable(boost) - set(BUILD_TESTING ${PREV_BUILD_TESTING} CACHE BOOL "Build the tests." FORCE) - endif () - endif () -endif () - -#------------------------------------------------- -# -# Example executables -# -#------------------------------------------------- -file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp - CMakeLists.txt - Jamfile) - -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) - -# any_stream example (coroutine-based) -add_executable(capy_example_any_stream - any_stream.cpp - api/capy_streams.cpp - api/capy_streams.hpp) - -set_property(TARGET capy_example_any_stream - PROPERTY FOLDER "examples") - -target_link_libraries(capy_example_any_stream - Boost::capy - Boost::asio) - -# asio_callbacks example (callback-based with uni_stream) -add_executable(capy_example_asio_callbacks - asio_callbacks.cpp - api/capy_streams.cpp - api/capy_streams.hpp - api/uni_stream.hpp) - -set_property(TARGET capy_example_asio_callbacks - PROPERTY FOLDER "examples") - -target_link_libraries(capy_example_asio_callbacks - Boost::capy - Boost::asio) - # use_capy example (use_capy completion token) -add_executable(capy_example_use_capy - use_capy_example.cpp - api/capy_streams.cpp - api/capy_streams.hpp - api/use_capy.hpp) +add_executable(capy_example_asio_echo_server + echo_server.cpp) -set_property(TARGET capy_example_use_capy +set_property(TARGET capy_example_asio_echo_server PROPERTY FOLDER "examples") -target_link_libraries(capy_example_use_capy +target_link_libraries(capy_example_asio_echo_server Boost::capy Boost::asio) diff --git a/example/asio/Jamfile b/example/asio/Jamfile index ca138e351..935e6d871 100644 --- a/example/asio/Jamfile +++ b/example/asio/Jamfile @@ -14,17 +14,7 @@ project . ; -exe any_stream : - any_stream.cpp - api/capy_streams.cpp +exe echo_server : + echo_server.cpp ; -exe asio_callbacks : - asio_callbacks.cpp - api/capy_streams.cpp - ; - -exe use_capy_example : - use_capy_example.cpp - api/capy_streams.cpp - ; diff --git a/example/asio/echo_server.cpp b/example/asio/echo_server.cpp new file mode 100644 index 000000000..f14494e17 --- /dev/null +++ b/example/asio/echo_server.cpp @@ -0,0 +1,136 @@ +// +// Copyright (c) 2026 Klemens Morgenstern +// +// 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 +// + +// +// Echo Server Example (Boost.Asio) +// +// A complete echo server using Boost.Asio TCP sockets with capy coroutines. +// Demonstrates how to use capy::as_io_awaitable to await ASIO async operations. +// + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace net = boost::asio; +namespace capy = boost::capy; + +// Socket type with as_io_awaitable as the default completion token. +// This allows omitting the completion token in async calls. +using tcp_socket = capy::as_io_awaitable_t::as_default_on_t< + net::ip::tcp::socket>; + +using tcp_acceptor = capy::as_io_awaitable_t::as_default_on_t< + net::ip::tcp::acceptor>; + +// Handle a single client session. +// Reads data from the socket and echoes it back until the connection closes. +capy::io_task<> echo_session(tcp_socket sock) +{ + char buf[1024]; + + for (;;) + { + // Read some data from the client. + auto res = co_await sock.async_read_some(net::buffer(buf)); + + if (boost::system::error_code ec = res.ec; ec) + { + if (ec != net::error::eof) + std::cerr << "Read error: " << ec.message() << "\n"; + break; + } + + // Write the data back to the client. + res = co_await net::async_write( + sock, net::buffer(buf, std::get<0>(res.values))); + + if (res.ec) + { + std::cerr << "Write error: " << res.ec.message() << "\n"; + break; + } + } + + boost::system::error_code ec; + sock.shutdown(net::ip::tcp::socket::shutdown_both, ec); + sock.close(ec); + + co_return {}; +} + +// Accept loop - accepts connections and spawns echo sessions. +capy::io_task<> accept_loop(tcp_acceptor& acceptor) +{ + auto exec = capy::wrap_asio_executor(acceptor.get_executor()); + auto ep = acceptor.local_endpoint(); + std::cout << "Listening on port " << ep.port() << "\n"; + + for (;;) + { + // Accept a new connection. + auto [ec, sock] = co_await acceptor.async_accept(); + + if (ec) + { + std::cerr << "Accept error: " << ec.message() << "\n"; + continue; + } + + auto remote = sock.remote_endpoint(); + std::cout << "Connection from " << remote.address() + << ":" << remote.port() << "\n"; + + // Spawn an echo session for this connection. + // Convert the socket to use as_io_awaitable as default. + capy::run_async(exec)( + echo_session(capy::as_io_awaitable_t::as_default_on(std::move(sock)))); + } +} + +int main(int argc, char* argv[]) +{ + unsigned short port = 8080; + if (argc > 1) + port = static_cast(std::atoi(argv[1])); + + try + { + net::io_context ioc; + + // Create the acceptor with as_io_awaitable as default. + tcp_acceptor acceptor( + ioc, + net::ip::tcp::endpoint(net::ip::tcp::v4(), port)); + + // Wrap the ASIO executor for use with capy. + auto exec = capy::wrap_asio_executor(ioc.get_executor()); + + // Spawn the accept loop. + capy::run_async(exec)(accept_loop(acceptor)); + + // Run the I/O context. + ioc.run(); + } + catch (std::exception const& e) + { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} diff --git a/include/boost/capy/asio/as_io_awaitable.hpp b/include/boost/capy/asio/as_io_awaitable.hpp new file mode 100644 index 000000000..9795bb0e2 --- /dev/null +++ b/include/boost/capy/asio/as_io_awaitable.hpp @@ -0,0 +1,173 @@ +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_DETAIL_AS_IO_AWAITABLE_HPP +#define BOOST_CAPY_ASIO_DETAIL_AS_IO_AWAITABLE_HPP + +#include +#include + +namespace boost::capy +{ + +/** @defgroup asio Asio Integration + * @brief Components for integrating capy coroutines with Asio + * + * This module provides seamless integration between capy's coroutine + * framework and both Boost.Asio and standalone Asio. It enables: + * - Using capy coroutines as Asio completion tokens + * - Wrapping Asio executors for use with capy + * - Spawning capy coroutines from Asio contexts + * @{ + */ + +/** @brief Completion token for awaiting Asio async operations in capy coroutines. + * + * `as_io_awaitable_t` is a completion token type that allows Asio async + * operations to be `co_await`ed within capy's IO coroutines. When used as a + * completion token, the async operation returns an io-awaitable. + * + * @par Example + * @code + * capy::io_task read_some(asio::ip::tcp::socket& socket) + * { + * std::array buffer; + * auto [ec, n] = co_await socket.async_read_some( + * asio::buffer(buffer), + * capy::as_io_awaitable); + * if (ec) + * throw std::system_error(ec); + * co_return n; + * } + * @endcode + * + * @par Cancellation + * When the capy coroutine receives a stop request, a terminal cancellation + * signal is emitted to the underlying Asio operation. + * + * @see as_io_awaitable The global instance of this type + * @see executor_with_default For setting this as the default token + */ +struct as_io_awaitable_t +{ + /// Default constructor. + constexpr as_io_awaitable_t() + { + } + + /** @brief Executor adapter that sets `as_io_awaitable_t` as the default token. + * + * This nested class template wraps an executor and specifies + * `as_io_awaitable_t` as the default completion token type. I/O objects + * using this executor will default to returning awaitables when no + * completion token is explicitly provided. + * + * @tparam InnerExecutor The underlying executor type to wrap + * + * @par Example + * @code + * using socket_type = asio::basic_stream_socket< + * asio::ip::tcp, + * as_io_awaitable_t::executor_with_default>; + * + * // Now async operations default to returning awaitables: + * auto bytes = co_await socket.async_read_some(buffer); + * @endcode + */ + template + struct executor_with_default : InnerExecutor + { + /// The default completion token type for I/O objects using this executor. + typedef as_io_awaitable_t default_completion_token_type; + + executor_with_default(const InnerExecutor& ex) noexcept + : InnerExecutor(ex) + { + } + + /// Construct the adapted executor from the inner executor type. + template + executor_with_default( + const InnerExecutor1& ex, + typename std::enable_if< + std::conditional< + !std::is_same::value, + std::is_convertible, + std::false_type + >::type::value>::type = 0) noexcept + : InnerExecutor(ex) + { + } + }; + + /** @brief Type alias to rebind an I/O object to use `as_io_awaitable_t` as default. + * + * Given an I/O object type `T` (e.g., `asio::ip::tcp::socket`), this alias + * produces a new type that uses `executor_with_default` and thus defaults + * to `as_io_awaitable_t` for all async operations. + * + * @tparam T The I/O object type to adapt (must support `rebind_executor`) + * + * @par Example + * @code + * using awaitable_socket = as_io_awaitable_t::as_default_on_t< + * asio::ip::tcp::socket>; + * @endcode + */ + template + using as_default_on_t = typename T::template rebind_executor< + executor_with_default >::other; + + /** @brief Adapts an I/O object instance to use `as_io_awaitable_t` as default. + * + * This function takes an existing I/O object and returns a new object of + * the same kind but rebound to use `executor_with_default`, making + * `as_io_awaitable_t` the default completion token. + * + * @tparam T The I/O object type (deduced) + * @param object The I/O object to adapt + * @return A new I/O object with `as_io_awaitable_t` as the default token + * + * @par Example + * @code + * asio::ip::tcp::socket raw_socket(io_context); + * auto socket = as_io_awaitable_t::as_default_on(std::move(raw_socket)); + * + * // Now you can omit the completion token: + * auto bytes = co_await socket.async_read_some(buffer); + * @endcode + */ + template + static typename std::decay_t::template rebind_executor< + executor_with_default::executor_type> + >::other + as_default_on(T && object) + { + return typename std::decay_t::template rebind_executor< + executor_with_default::executor_type> + >::other(std::forward(object)); + } +}; + +/** @brief Global instance of `as_io_awaitable_t` for convenient use. + * + * Use this constant as a completion token to await Asio async operations. + * + * @par Example + * @code + * auto result = co_await socket.async_read_some(buffer, as_io_awaitable); + * @endcode + */ +constexpr as_io_awaitable_t as_io_awaitable; + +/** @} */ // end of asio group + +} + +#endif + diff --git a/include/boost/capy/asio/boost.hpp b/include/boost/capy/asio/boost.hpp new file mode 100644 index 000000000..1439bf19e --- /dev/null +++ b/include/boost/capy/asio/boost.hpp @@ -0,0 +1,910 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_BOOST_HPP +#define BOOST_CAPY_ASIO_BOOST_HPP + +/** @file boost.hpp + * @brief Boost.Asio integration for capy coroutines. + * + * This header provides complete integration between capy's coroutine framework + * and Boost.Asio. Include this header when using capy with Boost.Asio. + * + * @par Features + * - Property query/require support for `asio_executor_adapter` + * - `asio_boost_standard_executor` wrapper for Boost.Asio executors + * - `async_result` specialization for `as_io_awaitable_t` + * - Three-argument `asio_spawn` overloads with completion tokens + * + * @par Example + * @code + * #include + * #include + * + * capy::io_task my_coro(boost::asio::ip::tcp::socket& sock) { + * char buf[1024]; + * auto [n] = co_await sock.async_read_some( + * boost::asio::buffer(buf), capy::as_io_awaitable); + * // ... + * } + * + * int main() { + * boost::asio::io_context io; + * auto exec = capy::wrap_asio_executor(io.get_executor()); + * capy::asio_spawn(exec, my_coro(socket))(boost::asio::detached); + * io.run(); + * } + * @endcode + * + * @see standalone.hpp For standalone Asio support + * @ingroup asio + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::capy +{ + +/** @addtogroup asio + * @{ + */ + +/// @name Execution Property Queries for asio_executor_adapter +/// @{ + +/** @brief Queries the execution context from an asio_executor_adapter. + * + * Returns the Boost.Asio execution context associated with the adapter. + * The context is provided via an adapter service that bridges capy's + * execution_context with Asio's. + * + * @param exec The executor adapter to query + * @return Reference to the associated `boost::asio::execution_context` + */ +template +boost::asio::execution_context& + query(const asio_executor_adapter & exec, + boost::asio::execution::context_t) noexcept +{ + using service = detail::asio_adapter_context_service< + boost::asio::execution_context>; + return exec.context(). + template use_service(); +} + +/** @brief Queries the blocking property from an asio_executor_adapter. + * @param exec The executor adapter to query + * @return The current blocking property value + */ +template +constexpr boost::asio::execution::blocking_t + query(const asio_executor_adapter &, + boost::asio::execution::blocking_t) noexcept +{ + using ex = asio_executor_adapter; + + switch (Bits & ex::blocking_mask) + { + case ex::blocking_never: + return boost::asio::execution::blocking.never; + case ex::blocking_always: + return boost::asio::execution::blocking.always; + case ex::blocking_possibly: + return boost::asio::execution::blocking.possibly; + default: return {}; + } +} + +/// @} + +/// @name Execution Property Requirements for asio_executor_adapter +/// @{ + +/** @brief Requires blocking.possibly property. + * @return New adapter with blocking.possibly set + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::blocking_t::possibly_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_possibly; + return asio_executor_adapter(exec); +} + +/** @brief Requires blocking.never property. + * @return New adapter that never blocks + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::blocking_t::never_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_never; + return asio_executor_adapter(exec); +} + +/** @brief Requires blocking.always property. + * @return New adapter that always blocks until execution completes + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::blocking_t::always_t) +{ + using ex = asio_executor_adapter; +constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_always; + return asio_executor_adapter(exec); +} + +/** @brief Queries the outstanding_work property. + * @param exec The executor adapter to query + * @return The current work tracking setting + */ +template +static constexpr boost::asio::execution::outstanding_work_t query( + const asio_executor_adapter &, + boost::asio::execution::outstanding_work_t) noexcept +{ + using ex = asio_executor_adapter; + switch (Bits & ex::work_mask) + { + case ex::work_tracked: + return boost::asio::execution::outstanding_work.tracked; + case ex::work_untracked: + return boost::asio::execution::outstanding_work.untracked; + default: return {}; + } +} + +/** @brief Requires outstanding_work.tracked property. + * @return New adapter that tracks outstanding work + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::outstanding_work_t::tracked_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::work_mask) | ex::work_tracked; + return asio_executor_adapter(exec); +} + +/** @brief Requires outstanding_work.untracked property. + * @return New adapter that does not track outstanding work + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::outstanding_work_t::untracked_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::work_mask) | ex::work_untracked; + return asio_executor_adapter(exec); +} + +/** @brief Queries the allocator property. + * @return The adapter's current allocator + */ +template +constexpr Allocator query( + const asio_executor_adapter & exec, + boost::asio::execution::allocator_t) noexcept +{ + return exec.get_allocator(); +} + +/** @brief Requires a specific allocator. + * @param a The allocator property containing the new allocator + * @return New adapter using the specified allocator + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::allocator_t a) +{ + return asio_executor_adapter( + exec, a.value() + ); +} + +/** @brief Requires the default allocator (uses frame allocator). + * @return New adapter using the frame allocator from the context + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + boost::asio::execution::allocator_t) + noexcept(std::is_nothrow_move_constructible_v) +{ + return asio_executor_adapter< + Executor, + std::pmr::polymorphic_allocator, + Bits> + ( + exec, + exec.context().get_frame_allocator() + ); +} + +/// @} + +namespace detail +{ + +template +struct asio_work_tracker_service : boost::asio::execution_context::service +{ + static boost::asio::execution_context::id id; + + asio_work_tracker_service(boost::asio::execution_context & ctx) + : boost::asio::execution_context::service(ctx) {} + + using tracked_executor = + typename boost::asio::prefer_result< + Executor, + boost::asio::execution::outstanding_work_t::tracked_t + >::type; + + alignas(tracked_executor) char buffer[sizeof(tracked_executor) ]; + + + std::mutex mutex; + std::size_t work = 0u; + + void shutdown() + { + std::lock_guard _(mutex); + if (std::exchange(work, 0) > 0u) + reinterpret_cast(buffer)->~tracked_executor(); + } + + + void work_started(const Executor & exec) + { + std::lock_guard _(mutex); + if (work ++ == 0u) + new (buffer) tracked_executor( + boost::asio::prefer(exec, + boost::asio::execution::outstanding_work.tracked)); + } + + void work_finished() + { + std::lock_guard _(mutex); + if (--work == 0u) + reinterpret_cast(buffer)->~tracked_executor(); + } + +}; + + +template +boost::asio::execution_context::id asio_work_tracker_service::id; + + +} + +/** @brief Wraps a Boost.Asio standard executor for use with capy. + * + * This class adapts Boost.Asio executors that follow the P0443/P2300 + * standard executor model to be usable as capy executors. It provides + * work tracking through an `asio_work_tracker_service` and integrates + * with capy's execution context system. + * + * @tparam Executor A Boost.Asio executor satisfying `AsioBoostStandardExecutor` + * + * @par Example + * @code + * boost::asio::io_context io; + * auto wrapped = asio_boost_standard_executor(io.get_executor()); + * + * // Use with capy coroutines + * capy::run(wrapped, my_io_task()); + * @endcode + * + * @see wrap_asio_executor For automatic executor type detection + * @see asio_net_ts_executor For legacy Networking TS executors + */ +template +struct asio_boost_standard_executor +{ + /** @brief Constructs from a Boost.Asio executor. + * @param executor The Boost.Asio executor to wrap + */ + asio_boost_standard_executor(Executor executor) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(executor)) + { + } + + /** @brief Move constructor. */ + asio_boost_standard_executor(asio_boost_standard_executor && rhs) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(rhs.executor_)) + { + } + + /** @brief Copy constructor. */ + asio_boost_standard_executor(const asio_boost_standard_executor & rhs) + noexcept(std::is_nothrow_copy_constructible_v) + : executor_(rhs.executor_) + { + } + + /** @brief Returns the associated capy execution context. + * @return Reference to the capy execution_context + */ + execution_context& context() const noexcept + { + auto & ec = boost::asio::query(executor_, boost::asio::execution::context); + return boost::asio::use_service< + detail::asio_context_service + >(ec); + } + + /** @brief Notifies that work has started. + * + * Delegates to the work tracker service to maintain a tracked executor + * while work is outstanding. + */ + void on_work_started() const noexcept + { + auto & ec = boost::asio::query(executor_, boost::asio::execution::context); + boost::asio::use_service< + detail::asio_work_tracker_service + >(ec).work_started(executor_); + } + + /** @brief Notifies that work has finished. + * + * Decrements the work counter in the tracker service. + */ + void on_work_finished() const noexcept + { + auto & ec = boost::asio::query(executor_, boost::asio::execution::context); + boost::asio::use_service< + detail::asio_work_tracker_service + >(ec).work_finished(); + } + + /** @brief Dispatches a continuation for execution. + * + * May execute inline if the executor allows, otherwise posts. + * Uses the context's frame allocator for handler allocation. + * + * @param c The continuation to dispatch + * @return A noop coroutine handle + */ + std::coroutine_handle<> dispatch(continuation & c) const + { + boost::asio::prefer( + executor_, + boost::asio::execution::allocator( + std::pmr::polymorphic_allocator( + context().get_frame_allocator() + ) + ) + ).execute(detail::asio_coroutine_unique_handle(c.h)); + return std::noop_coroutine(); + } + + /** @brief Posts a continuation for deferred execution. + * + * The continuation will never be executed inline. Uses blocking.never + * and relationship.fork properties for proper async behavior. + * + * @param c The continuation to post + */ + void post(continuation & c) const + { + boost::asio::prefer( + boost::asio::require(executor_, boost::asio::execution::blocking.never), + boost::asio::execution::relationship.fork, + boost::asio::execution::allocator( + std::pmr::polymorphic_allocator( + context().get_frame_allocator() + ) + ) + ).execute(detail::asio_coroutine_unique_handle(c.h)); + } + + /** @brief Equality comparison. */ + bool operator==(const asio_boost_standard_executor & rhs) const noexcept + { + return executor_ == rhs.executor_; + } + + /** @brief Inequality comparison. */ + bool operator!=(const asio_boost_standard_executor & rhs) const noexcept + { + return executor_ != rhs.executor_; + } + + private: + Executor executor_; +}; + + +/** @brief Spawns a capy coroutine with a Boost.Asio completion token (executor overload). + * + * Convenience overload that combines the two-step spawn process into one call. + * Equivalent to `asio_spawn(exec, awaitable)(token)`. + * + * @tparam ExecutorType The executor type + * @tparam Awaitable The coroutine type + * @tparam Token A Boost.Asio completion token + * @param exec The executor to run on + * @param awaitable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(ExecutorType exec, Awaitable && awaitable, Token token) +{ + return asio_spawn(exec, std::forward(awaitable))(std::move(token)); +} + +/** @brief Spawns a capy coroutine with a Boost.Asio completion token (context overload). + * + * Convenience overload that extracts the executor from a context. + * + * @tparam Context The execution context type + * @tparam Awaitable The coroutine type + * @tparam Token A Boost.Asio completion token + * @param ctx The execution context + * @param awaitable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(Context & ctx, Awaitable && awaitable, Token token) +{ + return asio_spawn(ctx.get_executor(), std::forward(awaitable))(std::move(token)); +} + +/** @} */ // end of asio group + +} + +template +struct boost::asio::async_result + : boost::capy::detail::async_result_impl< + boost::asio::cancellation_signal, + boost::asio::cancellation_type, + Ts...> +{ +}; + + +namespace boost::capy::detail +{ + + +struct boost_asio_init; + + +template +struct boost_asio_promise_type_allocator_base +{ + template + void * operator new (std::size_t n, boost_asio_init &, + Handler & handler, + Ex &, Awaitable &) + { + using allocator_type = std::allocator_traits + ::template rebind_alloc; + allocator_type allocator(boost::asio::get_associated_allocator(handler)); + + // round n up to max_align + if (const auto d = n % sizeof(std::max_align_t); d > 0u) + n += (sizeof(std::max_align_t) - d); + + auto mem = std::allocator_traits:: + template rebind_traits:: + allocate(allocator, n + sizeof(allocator_type)); + + void* p = static_cast(mem) + n; + new (p) allocator_type(std::move(allocator)); + + return mem; + } + void operator delete(void * ptr, std::size_t n) + { + if (const auto d = n % sizeof(std::max_align_t); d > 0u) + n += (sizeof(std::max_align_t) - d); + + using allocator_type = std::allocator_traits + ::template rebind_alloc; + auto allocator_p = reinterpret_cast( + static_cast(ptr) + n); + auto allocator = std::move(*allocator_p); + + allocator_p->~allocator_type(); + allocator.deallocate(static_cast(ptr), n + sizeof(allocator_type)); + } +}; + +template<> +struct boost_asio_promise_type_allocator_base> +{ +}; + +template +struct boost_asio_init_promise_type + : boost_asio_promise_type_allocator_base< + boost::asio::associated_allocator_t> +{ + using args_type = completion_tuple_for_io_awaitable; + + boost_asio_init_promise_type( + boost_asio_init &, + Handler & h, + Ex & exec, + Awaitable &) + : handler(h), ex(exec) + { + } + + Handler & handler; + Ex &ex; + + void get_return_object() {} + void unhandled_exception() {throw;} + void return_void() {} + + std::suspend_never initial_suspend() noexcept {return {};} + std::suspend_never final_suspend() noexcept {return {};} + + struct completer + { + Handler handler; + Ex ex; + args_type args; + + + bool await_ready() const {return false;} + + auto await_suspend( + std::coroutine_handle h) + { + auto handler_ = + std::apply( + [&](auto ... args) + { + return boost::asio::append(std::move(handler), + std::move(args)...); + }, + detail::decomposed_types(std::move(args))); + + auto exec = boost::asio::get_associated_immediate_executor( + handler_, + asio_executor_adapter(std::move(ex))); + h.destroy(); + boost::asio::dispatch(exec, std::move(handler_)); + } + void await_resume() const {} + }; + + completer yield_value(args_type value) + { + return {std::move(handler), std::move(ex), std::move(value)}; + } + + struct wrapper + { + Awaitable r; + const Ex &ex; + io_env env; + std::stop_source stop_src; + boost::asio::cancellation_slot cancel_slot; + + continuation c; + + wrapper(Awaitable && r, const Ex &ex) + : r(std::move(r)), ex(ex) + { + } + + bool await_ready() {return r.await_ready(); } + + auto await_suspend( + std::coroutine_handle tr) + { + env.executor = ex; + env.stop_token = stop_src.get_token(); + cancel_slot = + boost::asio::get_associated_cancellation_slot(tr.promise().handler); + + if (cancel_slot.is_connected()) + cancel_slot.assign( + [this](boost::asio::cancellation_type ct) + { + if ((ct & boost::asio::cancellation_type::terminal) + != boost::asio::cancellation_type::none) + stop_src.request_stop(); + }); + env.frame_allocator = get_current_frame_allocator(); + + using suspend_kind = decltype(r.await_suspend(tr, &env)); + if constexpr (std::is_void_v) + r.await_suspend(tr, &env); + else if constexpr (std::same_as) + return r.await_suspend(tr, &env); + else + { + c.h = r.await_suspend(tr, &env); + return ex.dispatch(c); + } + } + + completion_tuple_for_io_awaitable await_resume() + { + if (cancel_slot.is_connected()) + cancel_slot.clear(); + + using type = decltype(r.await_resume()); + if constexpr (!noexcept(r.await_resume())) + { + if constexpr (std::is_void_v) + try + { + r.await_resume(); + return {std::exception_ptr()}; + } + catch (...) + { + return std::current_exception(); + } + else + try + { + return {std::current_exception(), r.await_resume()}; + } + catch (...) + { + return {std::current_exception(), type()}; + } + } + else + { + if constexpr (std::is_void_v) + { + r.await_resume(); + return {}; + } + else + return {r.await_resume()}; + } + } + }; + + wrapper await_transform(Awaitable & r) + { + return wrapper{std::move(r), ex}; + } +}; + + +struct boost_asio_init +{ + template + void operator()( + Handler , + Ex, + Awaitable awaitable) + { + auto res = co_await awaitable; + co_yield std::move(res); + } +}; + +template + requires + boost::asio::completion_token_for< + Token, + completion_signature_for_io_awaitable + > +struct initialize_asio_spawn_helper +{ + template + static auto init(Executor ex, Awaitable r, Token && tk) + -> decltype( boost::asio::async_initiate< + Token, + completion_signature_for_io_awaitable>( + boost_asio_init{}, + tk, std::move(ex), std::move(r) + )) + { + return boost::asio::async_initiate< + Token, + completion_signature_for_io_awaitable>( + boost_asio_init{}, + tk, std::move(ex), std::move(r) + ); + } +}; + + +} + + +template +struct std::coroutine_traits +{ + using promise_type + = boost::capy::detail::boost_asio_init_promise_type< + Handler, + Executor, + Awaitable>; +}; + + + +namespace boost::capy +{ + + +namespace detail +{ + +template +class asio_boost_buffer_range +{ +public: + using sequence_type = Sequence; + using iterator = asio_buffer_iterator< + decltype(boost::asio::buffer_sequence_begin(std::declval()))>; + using const_iterator = iterator; + + asio_boost_buffer_range() = default; + + explicit asio_boost_buffer_range(Sequence seq) + noexcept(std::is_nothrow_move_constructible_v) + : seq_(std::move(seq)) + { + } + + asio_boost_buffer_range(const asio_boost_buffer_range&) = default; + asio_boost_buffer_range(asio_boost_buffer_range&&) = default; + asio_boost_buffer_range& operator=(const asio_boost_buffer_range&) = default; + asio_boost_buffer_range& operator=(asio_boost_buffer_range&&) = default; + + iterator begin() const + noexcept(noexcept(boost::asio::buffer_sequence_begin(std::declval()))) + { + return iterator(boost::asio::buffer_sequence_begin(seq_)); + } + + iterator end() const + noexcept(noexcept(boost::asio::buffer_sequence_end(std::declval()))) + { + return iterator(boost::asio::buffer_sequence_end(seq_)); + } + + /// Returns the underlying sequence. + const Sequence& base() const noexcept { return seq_; } + +private: + Sequence seq_{}; +}; + +// Deduction guide +template +asio_boost_buffer_range(Sequence) -> asio_boost_buffer_range; + + +asio_mutable_buffer asio_buffer_transformer_t:: + operator()(const boost::asio::mutable_buffer &mb) const noexcept +{ + return {mb.data(), mb.size()}; +} + +asio_const_buffer asio_buffer_transformer_t:: + operator()(const boost::asio::const_buffer &cb) const noexcept +{ + return {cb.data(), cb.size()}; +} + +} + +/** Convert a Boost.Asio buffer sequence for bidirectional Asio/capy compatibility. + + Wraps a Boost.Asio buffer sequence in a transforming range that converts + each buffer element to asio_const_buffer or asio_mutable_buffer. + The returned range satisfies both Boost.Asio's buffer sequence requirements + and capy's buffer sequence concepts. + + This overload uses `boost::asio::buffer_sequence_begin` and + `boost::asio::buffer_sequence_end` for iteration. + + @par Example: Asio to Capy + @code + std::vector asio_bufs = ...; + auto seq = capy::as_asio_buffer_sequence(asio_bufs); + std::size_t total = capy::buffer_size(seq); // Use capy algorithms + @endcode + + @param seq The Boost.Asio buffer sequence to convert + @return A transforming range over the buffer sequence + + @see as_asio_buffer_sequence in buffers.hpp for capy buffer sequences +*/ +template + requires requires (const T & seq) + { + {boost::asio::buffer_sequence_begin(seq)} -> std::bidirectional_iterator; + {boost::asio::buffer_sequence_end(seq)} -> std::bidirectional_iterator; + {*boost::asio::buffer_sequence_begin(seq)} -> std::convertible_to; + {*boost::asio::buffer_sequence_end(seq)} -> std::convertible_to; + } +auto as_asio_buffer_sequence(const T & seq) +{ + return detail::asio_boost_buffer_range(seq); +} + +asio_const_buffer::operator boost::asio::const_buffer() const +{ + return {data(), size()}; +} + +asio_mutable_buffer::operator boost::asio::const_buffer() const +{ + return {data(), size()}; +} + +asio_mutable_buffer::operator boost::asio::mutable_buffer() const +{ + return {data(), size()}; +} + +} + +#endif //BOOST_CAPY_ASIO_BOOST_HPP + diff --git a/include/boost/capy/asio/buffers.hpp b/include/boost/capy/asio/buffers.hpp new file mode 100644 index 000000000..d74375571 --- /dev/null +++ b/include/boost/capy/asio/buffers.hpp @@ -0,0 +1,453 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_BUFFERS_HPP +#define BOOST_CAPY_ASIO_BUFFERS_HPP + +#include +#include +#include + +namespace asio +{ + +class mutable_buffer; +class const_buffer; + +} + +namespace boost::asio +{ + +class mutable_buffer; +class const_buffer; + +} + +namespace boost::capy +{ + +/** A const buffer type compatible with both capy and Asio. + + Extends capy's `const_buffer` with implicit conversion operators + to both standalone Asio (`::asio::const_buffer`) and Boost.Asio + (`boost::asio::const_buffer`) buffer types. + + This allows seamless use of capy buffers with Asio I/O operations + without explicit conversion. + + @par Example + @code + capy::asio_const_buffer buf(data, size); + + // Works directly with Asio operations + co_await socket.async_write_some(buf, capy::as_io_awaitable); + @endcode + + @see asio_mutable_buffer, as_asio_buffer_sequence +*/ +struct asio_const_buffer : const_buffer +{ + using const_buffer::const_buffer; + using const_buffer::operator=; + + /// Construct from a capy const_buffer. + asio_const_buffer(const_buffer cb) : const_buffer(cb) {} + + /// Convert to standalone Asio const_buffer. + inline + operator ::asio::const_buffer() const; + + /// Convert to Boost.Asio const_buffer. + inline + operator boost::asio::const_buffer() const; +}; + +/** A mutable buffer type compatible with both capy and Asio. + + Extends capy's `mutable_buffer` with implicit conversion operators + to both standalone Asio and Boost.Asio buffer types. Supports + conversion to both mutable and const buffer types. + + This allows seamless use of capy buffers with Asio I/O operations + without explicit conversion. + + @par Example + @code + char data[1024]; + capy::asio_mutable_buffer buf(data, sizeof(data)); + + // Works directly with Asio read operations + auto [ec, n] = co_await socket.async_read_some(buf, capy::as_io_awaitable); + @endcode + + @see asio_const_buffer, as_asio_buffer_sequence +*/ +struct asio_mutable_buffer : mutable_buffer +{ + using mutable_buffer::mutable_buffer; + using mutable_buffer::operator=; + + /// Construct from a capy mutable_buffer. + asio_mutable_buffer(mutable_buffer mb) : mutable_buffer(mb) {} + + /// Convert to standalone Asio mutable_buffer. + inline + operator ::asio::mutable_buffer() const; + + /// Convert to Boost.Asio mutable_buffer. + inline + operator boost::asio::mutable_buffer() const; + + /// Convert to standalone Asio const_buffer. + inline + operator ::asio::const_buffer() const; + + /// Convert to Boost.Asio const_buffer. + inline + operator boost::asio::const_buffer() const; +}; + + +namespace detail +{ + +struct asio_buffer_transformer_t +{ + inline + asio_mutable_buffer operator()(const boost::asio::mutable_buffer &mb) const noexcept; + + inline + asio_const_buffer operator()(const boost::asio::const_buffer &cb) const noexcept; + + inline + asio_mutable_buffer operator()(const ::asio::mutable_buffer &mb) const noexcept; + + inline + asio_const_buffer operator()(const ::asio::const_buffer &cb) const noexcept; + + + asio_mutable_buffer operator()(const mutable_buffer &mb) const noexcept + { + return mb; + } + + asio_const_buffer operator()(const const_buffer &cb) const noexcept + { + return cb; + } + + asio_buffer_transformer_t() noexcept = default; + asio_buffer_transformer_t(const asio_buffer_transformer_t &) noexcept = default; +}; + +constexpr static asio_buffer_transformer_t asio_buffer_transformer; + +/** A bidirectional iterator that transforms buffer types using asio_buffer_transformer. + * + * Wraps an underlying bidirectional iterator and applies asio_buffer_transformer + * to each dereferenced value, converting between Asio buffer types and capy + * asio_const_buffer/asio_mutable_buffer types. + * + * @tparam Iterator The underlying bidirectional iterator type + */ +template +class asio_buffer_iterator +{ +public: + using iterator_type = Iterator; + using iterator_category = std::bidirectional_iterator_tag; + using difference_type = typename std::iterator_traits::difference_type; + using underlying_value = typename std::iterator_traits::value_type; + using value_type = decltype(asio_buffer_transformer(std::declval())); + using reference = value_type; + using pointer = void; + + asio_buffer_iterator() = default; + + explicit asio_buffer_iterator(Iterator it) + noexcept(std::is_nothrow_move_constructible_v) + : it_(std::move(it)) + { + } + + asio_buffer_iterator(const asio_buffer_iterator&) = default; + asio_buffer_iterator(asio_buffer_iterator&&) = default; + asio_buffer_iterator& operator=(const asio_buffer_iterator&) = default; + asio_buffer_iterator& operator=(asio_buffer_iterator&&) = default; + + reference operator*() const + noexcept(noexcept(asio_buffer_transformer(*std::declval()))) + { + return asio_buffer_transformer(*it_); + } + + asio_buffer_iterator& operator++() + noexcept(noexcept(++std::declval())) + { + ++it_; + return *this; + } + + asio_buffer_iterator operator++(int) + noexcept(noexcept(std::declval()++)) + { + asio_buffer_iterator tmp(*this); + ++it_; + return tmp; + } + + asio_buffer_iterator& operator--() + noexcept(noexcept(--std::declval())) + { + --it_; + return *this; + } + + asio_buffer_iterator operator--(int) + noexcept(noexcept(std::declval()--)) + { + asio_buffer_iterator tmp(*this); + --it_; + return tmp; + } + + bool operator==(const asio_buffer_iterator& other) const + noexcept(noexcept(std::declval() == std::declval())) + { + return it_ == other.it_; + } + + bool operator!=(const asio_buffer_iterator& other) const + noexcept(noexcept(std::declval() != std::declval())) + { + return it_ != other.it_; + } + + /// Returns the underlying iterator. + Iterator base() const noexcept(std::is_nothrow_copy_constructible_v) + { + return it_; + } + +private: + Iterator it_{}; +}; + +// Deduction guide +template +asio_buffer_iterator(Iterator) -> asio_buffer_iterator; + + +/** A bidirectional range that transforms buffer sequences using asio_buffer_transformer. + * + * Wraps a buffer sequence and provides begin/end iterators that transform + * buffer elements via asio_buffer_transformer. Satisfies the requirements + * of std::ranges::bidirectional_range. + * + * @tparam Sequence The underlying buffer sequence type + */ +template +class asio_buffer_range +{ +public: + using sequence_type = Sequence; + using iterator = asio_buffer_iterator< + decltype(std::ranges::begin(std::declval()))>; + using const_iterator = iterator; + + asio_buffer_range() = default; + + explicit asio_buffer_range(Sequence seq) + noexcept(std::is_nothrow_move_constructible_v) + : seq_(std::move(seq)) + { + } + + asio_buffer_range(const asio_buffer_range&) = default; + asio_buffer_range(asio_buffer_range&&) = default; + asio_buffer_range& operator=(const asio_buffer_range&) = default; + asio_buffer_range& operator=(asio_buffer_range&&) = default; + + iterator begin() const + noexcept(noexcept(std::ranges::begin(std::declval()))) + { + return iterator(std::ranges::begin(seq_)); + } + + iterator end() const + noexcept(noexcept(std::ranges::end(std::declval()))) + { + return iterator(std::ranges::end(seq_)); + } + + /// Returns true if the range is empty. + bool empty() const + noexcept(noexcept(std::ranges::empty(std::declval()))) + { + return std::ranges::empty(seq_); + } + + /// Returns the underlying sequence. + const Sequence& base() const noexcept { return seq_; } + +private: + Sequence seq_{}; +}; + +// Deduction guide +template +asio_buffer_range(Sequence) -> asio_buffer_range; + +} + + +/** @defgroup as_asio_buffer_sequence as_asio_buffer_sequence + + Bidirectional conversion between Asio and capy buffer sequences. + + The `as_asio_buffer_sequence` function provides seamless conversion + in both directions: + + - **Capy to Asio**: Convert capy buffer sequences for use with Asio + I/O operations. The returned range satisfies Asio's + `MutableBufferSequence` or `ConstBufferSequence` requirements. + + - **Asio to Capy**: Convert Asio buffer sequences for use with capy's + buffer concepts. The returned range satisfies capy's + `MutableBufferSequence` or `ConstBufferSequence` concepts. + + @par Example: Capy to Asio + @code + capy::mutable_buffer_pair buffers = ...; + auto seq = capy::as_asio_buffer_sequence(buffers); + co_await socket.async_read_some(seq, capy::as_io_awaitable); + @endcode + + @par Example: Asio to Capy + @code + std::vector asio_bufs = ...; + auto seq = capy::as_asio_buffer_sequence(asio_bufs); + std::size_t total = capy::buffer_size(seq); // Use capy algorithms + @endcode + + @{ +*/ + +/** Pass through a range already containing asio_const_buffer elements. + + @param rng A bidirectional range of asio_const_buffer + @return The range forwarded unchanged +*/ +template Range> + requires std::same_as, asio_const_buffer> +auto as_asio_buffer_sequence(Range && rng) +{ + return std::forward(rng); +} + +/** Pass through a range already containing asio_mutable_buffer elements. + + @param rng A bidirectional range of asio_mutable_buffer + @return The range forwarded unchanged +*/ +template Range> + requires std::same_as, asio_mutable_buffer> +auto as_asio_buffer_sequence(Range && rng) +{ + return std::forward(rng); +} + +/** Convert a single asio_mutable_buffer to a buffer sequence. + + @param mb The mutable buffer + @return A single-element array containing the buffer +*/ +inline +auto as_asio_buffer_sequence(asio_mutable_buffer mb) +{ + return std::array{mb}; +} + +/** Convert a single asio_const_buffer to a buffer sequence. + + @param cb The const buffer + @return A single-element array containing the buffer +*/ +inline +auto as_asio_buffer_sequence(asio_const_buffer cb) +{ + return std::array{cb}; +} + +/** Convert a single capy mutable_buffer to a buffer sequence. + + @param mb The mutable buffer + @return A single-element array containing an asio_mutable_buffer +*/ +inline +auto as_asio_buffer_sequence(mutable_buffer mb) +{ + return std::array{mb}; +} + +/** Convert a buffer type that might support both mutable & const buffer + to an asio buffer sequence. + + @param buf The const buffer + @return A single-element array containing an asio_mutable_buffer + +*/ +template Buffer> +inline +auto as_asio_buffer_sequence(const Buffer & buf) +{ + return as_asio_buffer_sequence(static_cast(buf)); +} + +/** Convert a single capy const_buffer to a buffer sequence. + @param cb The const buffer + @return A single-element array containing an asio_const_buffer + +*/ +inline +auto as_asio_buffer_sequence(const_buffer cb) +{ + return std::array{cb}; +} + +/** Convert any buffer sequence for bidirectional Asio/capy compatibility. + + Wraps the buffer sequence in a transforming range that converts + each buffer element to asio_const_buffer or asio_mutable_buffer. + The returned range satisfies both Asio's buffer sequence requirements + and capy's buffer sequence concepts. + + This overload handles: + - Capy buffer sequences (for use with Asio operations) + - Asio buffer sequences (for use with capy concepts/algorithms) + + @param seq The buffer sequence to convert + @return A transforming range over the buffer sequence +*/ +template + requires (!std::convertible_to && !std::convertible_to) +inline +auto as_asio_buffer_sequence(const Seq & seq) +{ + return detail::asio_buffer_range(seq); +} + +/** @} */ + +} + + +#endif // BOOST_CAPY_ASIO_BUFFERS_HPP + diff --git a/include/boost/capy/asio/detail/asio_context_service.hpp b/include/boost/capy/asio/detail/asio_context_service.hpp new file mode 100644 index 000000000..271231b01 --- /dev/null +++ b/include/boost/capy/asio/detail/asio_context_service.hpp @@ -0,0 +1,38 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_DETAIL_ASIO_CONTEXT_SERVICE +#define BOOST_CAPY_ASIO_DETAIL_ASIO_CONTEXT_SERVICE + +#include + +namespace boost::capy::detail +{ + +template +struct asio_context_service + : Context::service + , capy::execution_context +{ + static Context::id id; + + asio_context_service(Context & ctx) + : Context::service(ctx) {} + void shutdown() override {capy::execution_context::shutdown();} +}; + + +// asio_context_service is templated for this id. +template +Context::id asio_context_service::id; + +} + +#endif + diff --git a/include/boost/capy/asio/detail/asio_coroutine_unique_handle.hpp b/include/boost/capy/asio/detail/asio_coroutine_unique_handle.hpp new file mode 100644 index 000000000..e7189d5bd --- /dev/null +++ b/include/boost/capy/asio/detail/asio_coroutine_unique_handle.hpp @@ -0,0 +1,55 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + + +#ifndef BOOST_CAPY_ASIO_DETAIL_ASIO_COROUTINE_UNIQUE_HANDLE +#define BOOST_CAPY_ASIO_DETAIL_ASIO_COROUTINE_UNIQUE_HANDLE + +#include +#include + +namespace boost::capy::detail +{ + +struct asio_coroutine_unique_handle +{ + struct deleter + { + deleter() = default; + void operator()(void * h) const + { + std::coroutine_handle::from_address(h).destroy(); + } + }; + std::unique_ptr handle; + + asio_coroutine_unique_handle( + std::coroutine_handle h) : handle(h.address()) {} + + asio_coroutine_unique_handle( + asio_coroutine_unique_handle && + ) noexcept = default; + + void operator()() + { + release().resume(); + } + + std::coroutine_handle<> release() + { + return std::coroutine_handle::from_address( + handle.release() + ); + } +}; + +} + +#endif + diff --git a/include/boost/capy/asio/detail/completion_handler.hpp b/include/boost/capy/asio/detail/completion_handler.hpp new file mode 100644 index 000000000..bd5284bc7 --- /dev/null +++ b/include/boost/capy/asio/detail/completion_handler.hpp @@ -0,0 +1,263 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_DETAIL_COMPLETION_HANDLER_HPP +#define BOOST_CAPY_ASIO_DETAIL_COMPLETION_HANDLER_HPP + +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include + +namespace boost::capy::detail +{ + +struct asio_immediate_executor_helper +{ + enum completed_immediately_t + { + no, maybe, yes, initiating + }; + + executor_ref exec; + completed_immediately_t * completed_immediately = nullptr; + + template + void execute(Fn && fn) const + { + // only allow it when we're still initializing + if (completed_immediately && + ((*completed_immediately == initiating) + || (*completed_immediately == maybe))) + { + // only use this indicator if the fn will actually call our handler + // otherwise this was a single op in a composed operation + *completed_immediately = maybe; + fn(); + + if (*completed_immediately != yes) + *completed_immediately = initiating; + } + else + { + exec.post( + make_continuation( + std::forward(fn), + std::pmr::polymorphic_allocator( + exec.context().get_frame_allocator()))); + } + } + + friend bool operator==(const asio_immediate_executor_helper& lhs, + const asio_immediate_executor_helper& rhs) noexcept + { + return lhs.exec == rhs.exec; + } + + friend bool operator!=(const asio_immediate_executor_helper& lhs, + const asio_immediate_executor_helper& rhs) noexcept + { + return lhs.exec != rhs.exec; + } + + asio_immediate_executor_helper( + const asio_immediate_executor_helper & rhs) noexcept = default; + + asio_immediate_executor_helper( + executor_ref inner, + completed_immediately_t * completed_immediately + ) : exec(std::move(inner)), completed_immediately(completed_immediately) + { + } +}; + + +template +struct asio_coroutine_completion_handler +{ + asio_coroutine_unique_handle handle; + std::optional> & result; + const capy::io_env * env; + CancellationSlot slot; + using completed_immediately_t = asio_immediate_executor_helper::completed_immediately_t; + + completed_immediately_t * completed_immediately = nullptr; + + using allocator_type = std::pmr::polymorphic_allocator; + allocator_type get_allocator() const {return env->frame_allocator;} + + using executor_type = asio_executor_adapter; + executor_type get_executor() const {return env->executor;} + + using cancellation_slot_type = CancellationSlot; + cancellation_slot_type get_cancellation_slot() const {return slot;} + + using immediate_executor_type = asio_immediate_executor_helper; + immediate_executor_type get_immediate_executor() const + { + return immediate_executor_type{env->executor, completed_immediately }; + }; + + asio_coroutine_completion_handler( + std::coroutine_handle h, + std::optional> & result, + const capy::io_env * env, + CancellationSlot slot = {}, + completed_immediately_t * ci = nullptr) + : handle(h) + , result(result) + , env(env) + , slot(slot), completed_immediately(ci) + {} + + asio_coroutine_completion_handler( + asio_coroutine_completion_handler && + ) noexcept = default; + + void operator()(Args ... args) + { + result.emplace(std::forward(args)...); + + auto h = handle.release(); + + if (completed_immediately != nullptr + && *completed_immediately == completed_immediately_t::maybe) + { + *completed_immediately = completed_immediately_t::yes; + } + else + h(); + } +}; + +template +struct async_result_impl_result_tuple +{ + using type = std::tuple; +}; + + +template + requires std::constructible_from, T0, Ts...> +struct async_result_impl_result_tuple +{ + using type = io_result; +}; + +inline std::tuple<> make_async_result(const std::tuple<> &) { return {}; } + +template +inline auto make_async_result(std::tuple && tup) +{ + if constexpr (std::convertible_to) + return std::apply( + [](auto &&e, auto &&... args) + { + return io_result(std::move(e), std::move(args)...); + }, + std::move(tup)); + + else + return std::move(tup); +} + + +template +struct async_result_impl +{ + template + struct awaitable_t + { + using completed_immediately_t + = asio_immediate_executor_helper::completed_immediately_t; + + CancellationSignal signal; + completed_immediately_t completed_immediately; + + struct cb + { + CancellationSignal &signal; + cb(CancellationSignal &signal) : signal(signal) {} + void operator()() {signal.emit(CancellationType::terminal); } + }; + std::optional> stopper; + + bool await_ready() const {return false;} + + bool await_suspend(std::coroutine_handle<> h, const capy::io_env * env) + { + completed_immediately = completed_immediately_t::initiating; + stopper.emplace(env->stop_token, signal); + using slot_t = std::decay_t; + capy::detail::asio_coroutine_completion_handler ch( + h, result_, env, + signal.slot(), + &completed_immediately); + + std::apply( + [&](auto ... args) + { + std::move(init_)( + std::move(ch), + std::move(args)...); + }, + std::move(args_)); + + if (completed_immediately == completed_immediately_t::initiating) + completed_immediately = completed_immediately_t::no; + return completed_immediately != completed_immediately_t::yes; + } + + auto await_resume() + { + return make_async_result(std::move(*result_)); + } + + + awaitable_t(Initiation init, std::tuple args) + : init_(std::move(init)), args_(std::move(args)) + { + } + + awaitable_t(awaitable_t && rhs) noexcept + : init_(std::move(rhs.init_)) + , args_(std::move(rhs.args_)) + , result_(std::move(rhs.result_)) {} + private: + Initiation init_; + // if Args<0> == error_code this needs to be an io_result. + typename async_result_impl_result_tuple::type args_; + std::optional> result_; + }; + + template + static auto initiate(Initiation&& initiation, + RawToken&&, Args&&... args) + { + return awaitable_t< + std::decay_t, + std::decay_t...>( + std::forward(initiation), + std::make_tuple(std::forward(args)...)); + } +}; + + +} + +#endif //BOOST_CAPY_ASIO_DETAIL_COMPLETION_HANDLER + diff --git a/include/boost/capy/asio/detail/completion_traits.hpp b/include/boost/capy/asio/detail/completion_traits.hpp new file mode 100644 index 000000000..8b8b24e5f --- /dev/null +++ b/include/boost/capy/asio/detail/completion_traits.hpp @@ -0,0 +1,82 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_DETAIL_COMPLETION_TRAITS_HPP +#define BOOST_CAPY_ASIO_DETAIL_COMPLETION_TRAITS_HPP + +#include +#include + +#include +#include + +namespace boost { +namespace capy { +namespace detail { + + +template +struct completion_traits +{ + using signature_type = void(std::exception_ptr, ResultType); + using result_type = std::tuple; + +}; + +template +struct completion_traits +{ + using signature_type = void(ResultType); + using result_type = std::tuple; +}; + + +template +struct completion_traits, true> +{ + using signature_type = void(std::error_code, Ts...); + using result_type = io_result; +}; + + +template<> +struct completion_traits +{ + using signature_type = void(std::exception_ptr); + using result_type = std::tuple; +}; + +template<> +struct completion_traits +{ + using signature_type = void(); + using result_type = std::tuple<>; +}; + +template +using completion_signature_for_io_awaitable + = typename completion_traits< + decltype(std::declval().await_resume()), + noexcept(std::declval().await_resume()) + >::signature_type; + +template +using completion_tuple_for_io_awaitable + = typename completion_traits< + decltype(std::declval().await_resume()), + noexcept(std::declval().await_resume()) + >::result_type; + + +} +} +} + +#endif + diff --git a/include/boost/capy/asio/detail/continuation.hpp b/include/boost/capy/asio/detail/continuation.hpp new file mode 100644 index 000000000..fb99f2c0e --- /dev/null +++ b/include/boost/capy/asio/detail/continuation.hpp @@ -0,0 +1,153 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_CONTINUATION_HPP +#define BOOST_CAPY_ASIO_CONTINUATION_HPP + +#include +#include + +#include + +namespace boost::capy +{ + +namespace detail +{ + +template +struct continuation_handle_promise_base_ +{ + using alloc_t = std::allocator_traits + ::template rebind_alloc; + + template + void * operator new(std::size_t n, Func &, Allocator &allocator_) + { + alloc_t alloc(allocator_); + std::size_t m = n; + if (n % alignof(alloc_t) > 0) + m += alignof(alloc_t) - (n % alignof(alloc_t)); + + char * mem = alloc.allocate(m + sizeof(alloc)); + + new (mem + m) alloc_t(std::move(alloc)); + return mem; + } + + void operator delete(void * p, std::size_t n) + { + std::size_t m = n; + if (n % alignof(alloc_t) > 0) + m += alignof(alloc_t) - (n % alignof(alloc_t)); + + auto * a = reinterpret_cast(static_cast(p) + m); + + alloc_t alloc(std::move(*a)); + a->~alloc_t(); + + alloc.deallocate(static_cast(p), n); + } +}; + + +template<> +struct continuation_handle_promise_base_> +{ +}; + +template +struct continuation_handle_promise_type + : continuation_handle_promise_base_ +{ + + struct initial_aw_t + { + bool await_ready() const {return false;} + void await_suspend( + std::coroutine_handle> h) + { + auto & c = h.promise().cont; + c.h = h; + c.next = nullptr; + } + void await_resume() {} + }; + + initial_aw_t initial_suspend() const noexcept + { + return initial_aw_t{}; + } + std::suspend_never final_suspend() const noexcept {return {};} + + template + auto yield_value(Function & func) + { + struct yielder + { + Function func; + + bool await_ready() const {return false;} + void await_suspend(std::coroutine_handle<> h) + { + auto f = std::move(func); + h.destroy(); + std::move(f)(); + } + void await_resume() {} + }; + + return yielder{std::move(func)}; + } + + void unhandled_exception() { throw; } + void return_void() {} + + continuation cont; + + struct helper + { + continuation * cont; + using promise_type = continuation_handle_promise_type; + }; + + helper get_return_object() + { + return helper{&cont}; + } +}; + + +template Function, typename Allocator> +auto make_continuation_helper( + Function func, + Allocator) + -> continuation_handle_promise_type::helper +{ + co_yield func; +} + +template Function, typename Allocator> +continuation & make_continuation( + Function && func, + Allocator && alloc) +{ + continuation * c = detail::make_continuation_helper( + std::forward(func), + std::forward(alloc)).cont; + return *c; +} + +} + + +} + +#endif + diff --git a/include/boost/capy/asio/detail/fwd.hpp b/include/boost/capy/asio/detail/fwd.hpp new file mode 100644 index 000000000..bbb843a3e --- /dev/null +++ b/include/boost/capy/asio/detail/fwd.hpp @@ -0,0 +1,53 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_DETAIL_FWD_HPP +#define BOOST_CAPY_ASIO_DETAIL_FWD_HPP + +namespace boost::asio +{ + +class execution_context; + +namespace execution::detail +{ + +template +struct context_t; + +} + +template +struct query_result; + +} + + +namespace asio +{ + +class execution_context; + +namespace execution::detail +{ + +template +struct context_t; + +} + +template +struct query_result; + +} + + + +#endif // BOOST_CAPY_ASIO_DETAIL_FWD_HPP + diff --git a/include/boost/capy/asio/executor_adapter.hpp b/include/boost/capy/asio/executor_adapter.hpp new file mode 100644 index 000000000..6b34978c2 --- /dev/null +++ b/include/boost/capy/asio/executor_adapter.hpp @@ -0,0 +1,327 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_EXECUTOR_ADAPTER_HPP +#define BOOST_CAPY_ASIO_EXECUTOR_ADAPTER_HPP + +#include +#include +#include +#include + + +namespace boost { +namespace capy { + +/** @addtogroup asio + * @{ + */ + +namespace detail +{ + +/** @brief Service that bridges capy's execution_context with Asio's. + * @internal + * + * This service inherits from both capy's service and the target Asio + * execution context, allowing capy executors wrapped in `asio_executor_adapter` + * to be queried for their Asio execution context. + * + * @tparam ExecutionContext The Asio execution_context type (boost::asio or standalone) + */ +template +struct asio_adapter_context_service + : execution_context::service, + // shutdown is protected + ExecutionContext +{ + asio_adapter_context_service(boost::capy::execution_context &) {} + void shutdown() override {ExecutionContext::shutdown();} +}; + +} + + +/** @brief Adapts a capy executor to be usable with Asio. + * + * `asio_executor_adapter` wraps a capy executor and exposes it as an + * Asio-compatible executor. This allows capy coroutines and executors to + * interoperate seamlessly with Asio's async operations. + * + * The adapter tracks execution properties (blocking behavior and work tracking) + * as compile-time template parameters for zero-overhead property queries. + * + * @tparam Executor The underlying capy executor type (default: `capy::any_executor`) + * @tparam Allocator The allocator type for handler allocation + * (default: `std::pmr::polymorphic_allocator`) + * @tparam Bits Compile-time bitfield encoding blocking and work-tracking properties + * + * @par Execution Properties + * The adapter supports the standard Asio execution properties: + * - `blocking`: `possibly` (default), `never`, or `always` + * - `outstanding_work`: `untracked` (default) or `tracked` + * - `allocator`: Custom allocator for handler allocation + * - `context`: Returns the associated capy execution_context + * + * @par Example + * @code + * // Wrap a capy executor for use with Asio + * capy::any_executor capy_exec = ...; + * asio_executor_adapter<> asio_exec(capy_exec); + * + * // Use with Asio operations + * asio::post(asio_exec, []{ std::cout << "Hello from capy!\n"; }); + * + * // Require non-blocking execution + * auto never_blocking = asio::require(asio_exec, + * asio::execution::blocking.never); + * @endcode + * + * @see wrap_asio_executor For the reverse direction (Asio to capy) + */ +template, + int Bits = 0> +struct asio_executor_adapter +{ + /// @name Blocking Property Constants + /// @{ + constexpr static int blocking_possibly = 0b000; ///< May block the caller + constexpr static int blocking_never = 0b001; ///< Never blocks the caller + constexpr static int blocking_always = 0b010; ///< Always blocks until complete + constexpr static int blocking_mask = 0b011; ///< Mask for blocking bits + /// @} + + /// @name Work Tracking Property Constants + /// @{ + constexpr static int work_untracked = 0b000; ///< Work is not tracked + constexpr static int work_tracked = 0b100; ///< Outstanding work is tracked + constexpr static int work_mask = 0b100; ///< Mask for work tracking bit + /// @} + + + /// @name Constructors + /// @{ + + /** @brief Copy constructor from adapter with different property bits. + * + * Creates a copy with potentially different execution properties. + * If this adapter tracks work, `on_work_started()` is called. + * + * @tparam Bits_ The source adapter's property bits + * @param rhs The source adapter to copy from + */ + template + asio_executor_adapter( + const asio_executor_adapter & rhs) + noexcept(std::is_nothrow_copy_constructible_v) + : executor_(rhs.executor_), allocator_(rhs.allocator_) + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_started(); + } + + /** @brief Move constructor from adapter with different property bits. + * + * Moves from another adapter with potentially different properties. + * If this adapter tracks work, `on_work_started()` is called. + * + * @tparam Bits_ The source adapter's property bits + * @param rhs The source adapter to move from + */ + template + asio_executor_adapter( + asio_executor_adapter && rhs) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(rhs.executor_)) + , allocator_(std::move(rhs.allocator_)) + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_started(); + } + + /** @brief Constructs from executor and allocator. + * + * @param executor The capy executor to wrap + * @param alloc The allocator for handler allocation + */ + asio_executor_adapter(Executor executor, const Allocator & alloc) + noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_copy_constructible_v) + : executor_(std::move(executor)), allocator_(alloc) + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_started(); + } + + /** @brief Constructs from adapter with different allocator. + * + * @tparam OtherAllocator The source adapter's allocator type + * @param executor The source adapter + * @param alloc The new allocator to use + */ + template + explicit asio_executor_adapter( + asio_executor_adapter executor, + const Allocator & alloc) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_copy_constructible_v) + : executor_(std::move(executor.executor_)), allocator_(alloc) + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_started(); + } + + + /** @brief Constructs from a capy executor. + * + * The allocator is obtained from the executor's context frame allocator. + * + * @param executor The capy executor to wrap + */ + asio_executor_adapter(Executor executor) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(executor)) + , allocator_(executor_.context().get_frame_allocator()) + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_started(); + } + + /** @brief Destructor. + * + * If work tracking is enabled, calls `on_work_finished()`. + */ + ~asio_executor_adapter() + { + if constexpr((Bits & work_mask) == work_tracked) + executor_.on_work_finished(); + } + + /// @} + + /// @name Assignment + /// @{ + + /** @brief Copy assignment from adapter with different property bits. + * + * Properly handles work tracking when changing executors. + * + * @tparam Bits_ The source adapter's property bits + * @param rhs The source adapter + * @return Reference to this adapter + */ + template + asio_executor_adapter & operator=( + const asio_executor_adapter & rhs) + { + + if constexpr((Bits & work_mask) == work_tracked) + if (rhs.executor_ != executor_) + { + rhs.executor_.on_work_started(); + executor_.on_work_finished(); + } + + executor_ = rhs.executor_; + allocator_ = rhs.allocator_; + } + + /// @} + + /// @name Comparison + /// @{ + + /** @brief Equality comparison. + * @param rhs The adapter to compare with + * @return `true` if both executor and allocator are equal + */ + bool operator==(const asio_executor_adapter & rhs) const noexcept + { + return executor_ == rhs.executor_ + && allocator_ == rhs.allocator_; + } + + /** @brief Inequality comparison. + * @param rhs The adapter to compare with + * @return `true` if executor or allocator differs + */ + bool operator!=(const asio_executor_adapter & rhs) const noexcept + { + return executor_ != rhs.executor_ + && allocator_ != rhs.allocator_; + } + + /// @} + + /// @name Execution + /// @{ + + /** @brief Executes a function according to the blocking property. + * + * The execution behavior depends on the `Bits` template parameter: + * - `blocking_never`: Posts the function for deferred execution + * - `blocking_possibly`: Dispatches (may run inline or post) + * - `blocking_always`: Executes the function inline immediately + * + * @tparam Function The callable type + * @param f The function to execute + */ + template + void execute(Function&& f) const + { + if constexpr ((Bits & blocking_mask) == blocking_never) + executor_.post( + detail::make_continuation(std::forward(f), allocator_)); + else if constexpr((Bits & blocking_mask) == blocking_possibly) + executor_.dispatch( + detail::make_continuation(std::forward(f), allocator_) + ).resume(); + else if constexpr((Bits & blocking_mask) == blocking_always) + std::forward(f)(); + } + + /// @} + + /// @name Accessors + /// @{ + + /** @brief Returns the associated execution context. + * @return Reference to the capy execution_context + */ + execution_context & context() const {return executor_.context(); } + + /** @brief Returns the associated allocator. + * @return Copy of the allocator + */ + Allocator get_allocator() const noexcept {return allocator_;} + + /** @brief Returns the underlying capy executor. + * @return Copy of the wrapped executor + */ + const Executor get_capy_executor() const {return executor_;} + + /// @} + private: + + template + friend struct asio_executor_adapter; + Executor executor_; + [[no_unique_address]] Allocator allocator_; + +}; + +/** @} */ // end of asio group + +} +} + + +#endif //BOOST_CAPY_ASIO_EXECUTOR_ADAPTER_HPP + diff --git a/include/boost/capy/asio/executor_from_asio.hpp b/include/boost/capy/asio/executor_from_asio.hpp new file mode 100644 index 000000000..948e0584e --- /dev/null +++ b/include/boost/capy/asio/executor_from_asio.hpp @@ -0,0 +1,299 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_EXECUTOR_FROM_ASIO_HPP +#define BOOST_CAPY_ASIO_EXECUTOR_FROM_ASIO_HPP + +#include +#include +#include +#include + + +#include +#include + +namespace boost { +namespace capy { + +/** @addtogroup asio + * @{ + */ + +namespace detail +{ + +/** @brief Concept for legacy Networking TS style Asio executors. + * @internal + * + * Matches executors that provide the original Networking TS executor interface + * with explicit work counting (`on_work_started`/`on_work_finished`) and + * `dispatch`/`post` methods that take a handler and allocator. + * + * @tparam Executor The type to check + */ +template +concept AsioNetTsExecutor = requires (Executor exec, + std::coroutine_handle<> h, + std::pmr::polymorphic_allocator a) + { + exec.on_work_started(); + exec.on_work_finished(); + exec.dispatch(h, a); + exec.post(h, a); + exec.context(); + } ; + +/** @brief Concept for Boost.Asio standard executors. + * @internal + * + * Matches executors compatible with the P0443/P2300 executor model as + * implemented in Boost.Asio. These executors use the property query/require + * mechanism and return `boost::asio::execution_context&` from context queries. + * + * @tparam Executor The type to check + */ +template +concept AsioBoostStandardExecutor = std::same_as< + typename boost::asio::query_result< + Executor, + boost::asio::execution::detail::context_t<0>>::type, + boost::asio::execution_context&>; + +/** @brief Concept for standalone Asio standard executors. + * @internal + * + * Matches executors compatible with the P0443/P2300 executor model as + * implemented in standalone Asio. These executors return + * `::asio::execution_context&` from context queries. + * + * @tparam Executor The type to check + */ +template +concept AsioStandaloneStandardExecutor = std::same_as< + typename ::asio::query_result< + Executor, + ::asio::execution::detail::context_t<0>>::type, + ::asio::execution_context&>; + +} + + +/** @brief Wraps a legacy Networking TS executor for use with capy. + * + * This class adapts Asio executors that follow the original Networking TS + * executor model (with `on_work_started`/`on_work_finished` and + * `dispatch`/`post` methods) to be usable as capy executors. + * + * @tparam Executor An executor type satisfying `AsioNetTsExecutor` + * + * @par Example + * @code + * boost::asio::io_context io; + * auto wrapped = asio_net_ts_executor(io.get_executor()); + * + * // Use with capy coroutines + * capy::run(wrapped, my_coroutine()); + * @endcode + * + * @see wrap_asio_executor For automatic executor type detection + */ +template +struct asio_net_ts_executor +{ + /** @brief Constructs from an Asio Net.TS executor. + * @param executor The Asio executor to wrap + */ + asio_net_ts_executor(Executor executor) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(executor)) + { + } + + /** @brief Move constructor. */ + asio_net_ts_executor(asio_net_ts_executor && rhs) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(rhs.executor_)) + { + } + + /** @brief Copy constructor. */ + asio_net_ts_executor(const asio_net_ts_executor & rhs) + noexcept(std::is_nothrow_copy_constructible_v) + : executor_(rhs.executor_) + { + } + + /** @brief Returns the associated capy execution context. + * + * The context is obtained via an `asio_context_service` registered + * with the underlying Asio execution context. + * + * @return Reference to the capy execution_context + */ + execution_context& context() const noexcept + { + using ex_t = std::remove_reference_t; + return use_service> + ( + executor_.context() + ); + } + + /** @brief Notifies that work has started. + * + * Forwards to the underlying executor's `on_work_started()`. + */ + void on_work_started() const noexcept + { + executor_.on_work_started(); + } + + /** @brief Notifies that work has finished. + * + * Forwards to the underlying executor's `on_work_finished()`. + */ + void on_work_finished() const noexcept + { + executor_.on_work_finished(); + } + + /** @brief Dispatches a continuation for execution. + * + * May execute inline if allowed by the executor, otherwise posts. + * + * @param c The continuation to dispatch + * @return A noop coroutine handle (execution is delegated to Asio) + */ + std::coroutine_handle<> dispatch(continuation & c) const + { + executor_.dispatch( + detail::asio_coroutine_unique_handle(c.h), + std::pmr::polymorphic_allocator( + boost::capy::get_current_frame_allocator())); + + return std::noop_coroutine(); + } + + /** @brief Posts a continuation for deferred execution. + * + * The continuation will never be executed inline. + * + * @param c The continuation to post + */ + void post(continuation & c) const + { + executor_.post( + detail::asio_coroutine_unique_handle(c.h), + std::pmr::polymorphic_allocator( + boost::capy::get_current_frame_allocator())); + } + + /** @brief Equality comparison. */ + bool operator==(const asio_net_ts_executor & rhs) const noexcept + { + return executor_ == rhs.executor_; + } + + /** @brief Inequality comparison. */ + bool operator!=(const asio_net_ts_executor & rhs) const noexcept + { + return executor_ != rhs.executor_; + } + + private: + Executor executor_; +}; + + +/** @brief Wraps a Boost.Asio standard executor for use with capy. + * + * Forward declaration; defined in `boost.hpp`. + * + * @tparam Executor An executor type satisfying `AsioBoostStandardExecutor` + * @see asio_net_ts_executor For legacy executor wrapping + */ +template +struct asio_boost_standard_executor; + +/** @brief Wraps a standalone Asio standard executor for use with capy. + * + * Forward declaration; defined in `standalone.hpp`. + * + * @tparam Executor An executor type satisfying `AsioStandaloneStandardExecutor` + * @see asio_net_ts_executor For legacy executor wrapping + */ +template +struct asio_standalone_standard_executor; + + +/** @brief Automatically wraps any Asio executor for use with capy. + * + * This function detects the type of Asio executor and returns the appropriate + * capy-compatible wrapper: + * - Legacy Net.TS executors -> `asio_net_ts_executor` + * - Boost.Asio standard executors -> `asio_boost_standard_executor` + * - Standalone Asio standard executors -> `asio_standalone_standard_executor` + * + * @tparam Executor The Asio executor type (deduced) + * @param exec The Asio executor to wrap + * @return A capy-compatible executor wrapping the input + * + * @par Example + * @code + * boost::asio::io_context io; + * auto capy_exec = wrap_asio_executor(io.get_executor()); + * + * // Now use with capy + * capy::run(capy_exec, my_io_task()); + * @endcode + * + * @note Fails to compile with a static_assert if the executor type is not recognized. + */ +template +auto wrap_asio_executor(Executor && exec) +{ + using executor_t = std::decay_t; + if constexpr (detail::AsioNetTsExecutor) + return asio_net_ts_executor( + std::forward(exec) + ); + else if constexpr (detail::AsioBoostStandardExecutor) + return asio_boost_standard_executor( + std::forward(exec) + ); + else if constexpr (detail::AsioStandaloneStandardExecutor) + return asio_standalone_standard_executor( + std::forward(exec) + ); + else + static_assert(sizeof(Executor) == 0, "Unknown executor type"); +}; + + +/** @brief Type alias for the result of `wrap_asio_executor`. + * + * Given an Asio executor type, this alias yields the corresponding + * capy wrapper type. + * + * @tparam Executor The Asio executor type + */ +template +using wrap_asio_executor_t + = decltype(wrap_asio_executor(std::declval())); + +/** @} */ // end of asio group + + + +} +} + + +#endif //BOOST_CAPY_ASIO_EXECUTOR_ADAPTER_HPP diff --git a/include/boost/capy/asio/spawn.hpp b/include/boost/capy/asio/spawn.hpp new file mode 100644 index 000000000..550d2f57c --- /dev/null +++ b/include/boost/capy/asio/spawn.hpp @@ -0,0 +1,217 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_SPAWN_HPP +#define BOOST_CAPY_ASIO_SPAWN_HPP + + +#include +#include +#include +#include +#include +#include + + +namespace boost::capy +{ + +/** @addtogroup asio + * @{ + */ + +namespace detail +{ + +/** @brief Helper for initializing spawned coroutines with Boost.Asio tokens. + * @internal + * + * Specialized in `boost.hpp` for actual Boost.Asio completion tokens. + */ +template +struct initialize_asio_spawn_helper; + +/** @brief Concept for valid Boost.Asio spawn completion tokens. + * @internal + * + * A token satisfies this concept if `initialize_asio_spawn_helper` can + * initialize the spawn operation with it. + */ +template +concept asio_spawn_token = + requires (Token && tk, Executor ex, Awaitable rn) + { + initialize_asio_spawn_helper:: + init(std::move(ex), std::move(rn), std::forward(tk)); + }; + +/** @brief Helper for initializing spawned coroutines with standalone Asio tokens. + * @internal + * + * Specialized in `standalone.hpp` for actual standalone Asio completion tokens. + */ +template +struct initialize_asio_standalone_spawn_helper; + +/** @brief Concept for valid standalone Asio spawn completion tokens. + * @internal + * + * A token satisfies this concept if `initialize_asio_standalone_spawn_helper` + * can initialize the spawn operation with it. + */ +template +concept asio_standalone_spawn_token = + requires (Token && tk, Executor ex, Awaitable rn) + { + initialize_asio_standalone_spawn_helper:: + init(std::move(ex), std::move(rn), std::forward(tk)); + }; + + +} + +/** @brief Deferred spawn operation that can be initiated with a completion token. + * + * This class represents a spawn operation that has captured an executor and + * awaitable but hasn't been initiated yet. Call `operator()` with a completion + * token to start the operation. + * + * @tparam Executor The executor type for running the coroutine + * @tparam Awaitable The coroutine type (must satisfy `IoAwaitable`) + * + * @par Example + * @code + * auto op = asio_spawn(executor, my_coroutine()); + * + * // Initiate with different tokens: + * op(asio::detached); // Fire and forget + * op([](std::exception_ptr) { ... }); // Callback on completion + * co_await op(asio::use_awaitable); // Await in another coroutine + * @endcode + * + * @see asio_spawn Factory function to create spawn operations + */ +template +struct asio_spawn_op +{ + /** @brief Constructs the spawn operation. + * @param executor The executor to run on + * @param awaitable The coroutine to spawn + */ + asio_spawn_op(Executor executor, Awaitable awaitable) + : executor_(std::move(executor)), awaitable_(std::move(awaitable)) + {} + + /** @brief Initiates the spawn with a Boost.Asio completion token. + * + * @tparam Token A valid Boost.Asio completion token + * @param token The completion token determining how to handle completion + * @return Depends on the token (e.g., void for callbacks, awaitable for use_awaitable) + */ + template Token> + auto operator()(Token && token) + { + return detail::initialize_asio_spawn_helper::init( + std::move(executor_), + std::move(awaitable_), + std::forward(token) + ); + } + + /** @brief Initiates the spawn with a standalone Asio completion token. + * + * @tparam Token A valid standalone Asio completion token + * @param token The completion token determining how to handle completion + * @return Depends on the token + */ + template Token> + auto operator()(Token && token) + { + return detail::initialize_asio_standalone_spawn_helper::init + ( + std::move(executor_), + std::move(awaitable_), + std::forward(token) + ); + } + + private: + Executor executor_; + Awaitable awaitable_; +}; + + +/** @brief Spawns a capy coroutine for execution with an Asio completion token. + * + * Creates a deferred spawn operation that can be initiated with any Asio + * completion token. The coroutine will run on the specified executor. + * + * @tparam ExecutorType The executor type (must satisfy `Executor`) + * @tparam Awaitable The coroutine type (must satisfy `IoAwaitable`) + * @param exec The executor to run the coroutine on + * @param awaitable The coroutine to spawn + * @return An `asio_spawn_op` that can be called with a completion token + * + * @par Completion Signature + * The completion signature depends on the coroutine's return type: + * - `void` return, noexcept: `void()` + * - `void` return, may throw: `void(std::exception_ptr)` + * - `T` return, noexcept: `void(T)` + * - `T` return, may throw: `void(std::exception_ptr, T)` + * + * @par Example + * @code + * capy::task compute() { co_return 42; } + * + * // Using with Boost.Asio + * asio_spawn(executor, compute())( + * [](std::exception_ptr ep, int result) { + * if (!ep) std::cout << "Result: " << result << "\n"; + * }); + * + * // Using with asio::use_awaitable + * auto [ep, result] = co_await asio_spawn(executor, compute())(asio::use_awaitable); + * @endcode + * + * @see asio_spawn_op The returned operation type + */ +template +auto asio_spawn(ExecutorType exec, Awaitable && awaitable) +{ + return asio_spawn_op(std::move(exec), std::forward(awaitable)); +} + +/** @brief Spawns a capy coroutine using a context's executor. + * + * Convenience overload that extracts the executor from an execution context. + * + * @tparam Context The execution context type (must satisfy `ExecutionContext`) + * @tparam Awaitable The coroutine type (must satisfy `IoAwaitable`) + * @param ctx The execution context providing the executor + * @param awaitable The coroutine to spawn + * @return An `asio_spawn_op` that can be called with a completion token + * + * @par Example + * @code + * boost::asio::io_context io; + * asio_spawn(io, my_coroutine())(asio::detached); + * io.run(); + * @endcode + */ +template +auto asio_spawn(Context & ctx, Awaitable && awaitable) +{ + return asio_spawn_op(ctx.get_executor(), std::forward(awaitable)); +} + +/** @} */ // end of asio group + +} + +#endif diff --git a/include/boost/capy/asio/standalone.hpp b/include/boost/capy/asio/standalone.hpp new file mode 100644 index 000000000..06b50ed66 --- /dev/null +++ b/include/boost/capy/asio/standalone.hpp @@ -0,0 +1,885 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_ASIO_STANDALONE_HPP +#define BOOST_CAPY_ASIO_STANDALONE_HPP + +/** @file standalone.hpp + * @brief Standalone Asio integration for capy coroutines. + * + * This header provides complete integration between capy's coroutine framework + * and standalone Asio (without Boost). Include this header when using capy + * with standalone Asio. + * + * @par Features + * - Property query/require support for `asio_executor_adapter` + * - `asio_standalone_standard_executor` wrapper for standalone Asio executors + * - `async_result` specialization for `as_io_awaitable_t` + * - Three-argument `asio_spawn` overloads with completion tokens + * + * @par Example + * @code + * #include + * #include + * + * capy::io_task my_coro(asio::ip::tcp::socket& sock) { + * char buf[1024]; + * auto [n] = co_await sock.async_read_some( + * asio::buffer(buf), capy::as_io_awaitable); + * // ... + * } + * + * int main() { + * asio::io_context io; + * auto exec = capy::wrap_asio_executor(io.get_executor()); + * capy::asio_spawn(exec, my_coro(socket))(asio::detached); + * io.run(); + * } + * @endcode + * + * @see boost.hpp For Boost.Asio support + * @ingroup asio + */ + +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::capy +{ + +/** @addtogroup asio + * @{ + */ + +/// @name Execution Property Queries for asio_executor_adapter (Standalone Asio) +/// @{ + +/** @brief Queries the execution context from an asio_executor_adapter. + * @param exec The executor adapter to query + * @return Reference to the associated `::asio::execution_context` + */ +template +::asio::execution_context& + query(const asio_executor_adapter & exec, + ::asio::execution::context_t) noexcept +{ + using service = detail::asio_adapter_context_service< + ::asio::execution_context>; + return exec.context(). + template use_service(); +} + +/** @brief Queries the blocking property. + * @param exec The executor adapter to query + * @return The current blocking property value + */ +template +constexpr ::asio::execution::blocking_t + query(const asio_executor_adapter &, + ::asio::execution::blocking_t) noexcept +{ + using ex = asio_executor_adapter; + switch (Bits & ex::blocking_mask) + { + case ex::blocking_never: return ::asio::execution::blocking.never; + case ex::blocking_always: return ::asio::execution::blocking.always; + case ex::blocking_possibly: return ::asio::execution::blocking.possibly; + default: return {}; + } +} + +/// @} + +/// @name Execution Property Requirements for asio_executor_adapter (Standalone Asio) +/// @{ + +/** @brief Requires blocking.possibly property. + * @return New adapter with blocking.possibly set + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::blocking_t::possibly_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_possibly; + return asio_executor_adapter(exec); +} + +/** @brief Requires blocking.never property. + * @return New adapter that never blocks + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::blocking_t::never_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_never; + return asio_executor_adapter(exec); +} + +/** @brief Requires blocking.always property. + * @return New adapter that always blocks until execution completes + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::blocking_t::always_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::blocking_mask) | ex::blocking_always; + return asio_executor_adapter(exec); +} + +/** @brief Queries the outstanding_work property. + * @param exec The executor adapter to query + * @return The current work tracking setting + */ +template +static constexpr ::asio::execution::outstanding_work_t query( + const asio_executor_adapter &, + ::asio::execution::outstanding_work_t) noexcept +{ + using ex = asio_executor_adapter; + switch (Bits & ex::work_mask) + { + case ex::work_tracked: + return ::asio::execution::outstanding_work.tracked; + case ex::work_untracked: + return ::asio::execution::outstanding_work.untracked; + default: return {}; + } +} + +/** @brief Requires outstanding_work.tracked property. + * @return New adapter that tracks outstanding work + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::outstanding_work_t::tracked_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::work_mask) | ex::work_tracked; + return asio_executor_adapter(exec); +} + +/** @brief Requires outstanding_work.untracked property. + * @return New adapter that does not track outstanding work + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::outstanding_work_t::untracked_t) +{ + using ex = asio_executor_adapter; + constexpr int new_bits = (Bits & ~ex::work_mask) | ex::work_untracked; + return asio_executor_adapter(exec); +} + +/** @brief Queries the allocator property. + * @return The adapter's current allocator + */ +template +constexpr Allocator query( + const asio_executor_adapter & exec, + ::asio::execution::allocator_t) noexcept +{ + return exec.get_allocator(); +} + +/** @brief Requires a specific allocator. + * @param a The allocator property containing the new allocator + * @return New adapter using the specified allocator + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::allocator_t a) +{ + return asio_executor_adapter( + exec, a.value() + ); +} + +/** @brief Requires the default allocator (uses frame allocator). + * @return New adapter using the frame allocator from the context + */ +template +constexpr auto + require(const asio_executor_adapter & exec, + ::asio::execution::allocator_t a) + noexcept(std::is_nothrow_move_constructible_v) +{ + return asio_executor_adapter< + Executor, + std::pmr::polymorphic_allocator, + Bits> + ( + exec, + exec.context().get_frame_allocator() + ); +} + +/// @} + +namespace detail +{ + +template +struct asio_standalone_work_tracker_service : + ::asio::execution_context::service +{ + static ::asio::execution_context::id id; + + asio_standalone_work_tracker_service(::asio::execution_context & ctx) + : ::asio::execution_context::service(ctx) {} + + using tracked_executor = + typename ::asio::prefer_result< + Executor, + ::asio::execution::outstanding_work_t::tracked_t + >::type; + + alignas(tracked_executor) char buffer[sizeof(tracked_executor) ]; + + std::mutex mutex; + std::size_t work = 0u; + + void shutdown() + { + std::lock_guard _(mutex); + if (std::exchange(work, 0) > 0u) + reinterpret_cast(buffer)->~tracked_executor(); + } + + + void work_started(const Executor & exec) + { + std::lock_guard _(mutex); + if (work ++ == 0u) + new (buffer) tracked_executor( + ::asio::prefer(exec, + ::asio::execution::outstanding_work.tracked)); + } + + void work_finished() + { + std::lock_guard _(mutex); + if (--work == 0u) + reinterpret_cast(buffer)->~tracked_executor(); + } +}; + + +template +::asio::execution_context::id + asio_standalone_work_tracker_service::id; + + +} + +template +struct asio_standalone_standard_executor +{ + + asio_standalone_standard_executor(Executor executor) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(executor)) + { + } + + asio_standalone_standard_executor(asio_standalone_standard_executor && rhs) + noexcept(std::is_nothrow_move_constructible_v) + : executor_(std::move(rhs.executor_)) + { + } + + asio_standalone_standard_executor( + const asio_standalone_standard_executor & rhs) + noexcept(std::is_nothrow_copy_constructible_v) + : executor_(rhs.executor_) + { + } + + execution_context& context() const noexcept + { + auto & ec = ::asio::query(executor_, ::asio::execution::context); + return ::asio::use_service< + detail::asio_context_service<::asio::execution_context> + >(ec); + } + + /** @brief Notifies that work has started. */ + void on_work_started() const noexcept + { + auto & ec = ::asio::query(executor_, ::asio::execution::context); + ::asio::use_service< + detail::asio_standalone_work_tracker_service + >(ec).work_started(executor_); + } + + /** @brief Notifies that work has finished. */ + void on_work_finished() const noexcept + { + auto & ec = ::asio::query(executor_, ::asio::execution::context); + ::asio::use_service< + detail::asio_standalone_work_tracker_service + >(ec).work_finished(); + } + + /** @brief Dispatches a continuation for execution. + * @param c The continuation to dispatch + * @return A noop coroutine handle + */ + std::coroutine_handle<> dispatch(continuation & c) const + { + ::asio::prefer( + executor_, + ::asio::execution::allocator( + std::pmr::polymorphic_allocator( + context().get_frame_allocator() + ) + ) + ).execute(detail::asio_coroutine_unique_handle(c.h)); + return std::noop_coroutine(); + } + + /** @brief Posts a continuation for deferred execution. + * @param c The continuation to post + */ + void post(continuation & c) const + { + ::asio::prefer( + ::asio::require(executor_, ::asio::execution::blocking.never), + ::asio::execution::relationship.fork, + ::asio::execution::allocator( + std::pmr::polymorphic_allocator( + context().get_frame_allocator() + ) + ) + ).execute(detail::asio_coroutine_unique_handle(c.h)); + } + + /** @brief Equality comparison. */ + bool operator==( + const asio_standalone_standard_executor & rhs) const noexcept + { + return executor_ == rhs.executor_; + } + + /** @brief Inequality comparison. */ + bool operator!=( + const asio_standalone_standard_executor & rhs) const noexcept + { + return executor_ != rhs.executor_; + } + + private: + Executor executor_; +}; + + +/** @brief Spawns a capy coroutine with a standalone Asio completion token (executor overload). + * + * Convenience overload that combines the two-step spawn process into one call. + * + * @tparam ExecutorType The executor type + * @tparam Awaitable The coroutine type + * @tparam Token A standalone Asio completion token + * @param exec The executor to run on + * @param awaitable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(ExecutorType exec, Awaitable && awaitable, Token token) +{ + return asio_spawn(exec, std::forward(awaitable))(std::move(token)); +} + +/** @brief Spawns a capy coroutine with a standalone Asio completion token (context overload). + * + * Convenience overload that extracts the executor from a context. + * + * @tparam Context The execution context type + * @tparam Awaitable The coroutine type + * @tparam Token A standalone Asio completion token + * @param ctx The execution context + * @param awaitable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token + > +auto asio_spawn(Context & ctx, Awaitable && awaitable, Token token) +{ + return asio_spawn(ctx.get_executor(), std::forward(awaitable)) + (std::move(token)); +} + +/** @} */ // end of asio group + +} + +/** @brief Standalone Asio async_result specialization for as_io_awaitable_t. + * + * This specialization enables `as_io_awaitable` to be used as a completion + * token with any standalone Asio async operation. + * + * @tparam Ts The completion signature argument types + */ +template +struct asio::async_result + : boost::capy::detail::async_result_impl + < + ::asio::cancellation_signal, + ::asio::cancellation_type, + Ts... + > +{ +}; + + +namespace boost::capy::detail +{ + +struct boost_asio_standalone_init; + +template +struct boost_asio_standalone_promise_type_allocator_base +{ + template + void * operator new (std::size_t n, boost_asio_standalone_init &, + Handler & handler, + Ex &, Awaitable &) + { + using allocator_type = std::allocator_traits + ::template rebind_alloc; + allocator_type allocator(::asio::get_associated_allocator(handler)); + + // round n up to max_align + if (const auto d = n % sizeof(std::max_align_t); d > 0u) + n += (sizeof(std::max_align_t) - d); + + auto mem = std::allocator_traits:: + template rebind_traits:: + allocate(allocator, n + sizeof(allocator_type)); + + void* p = static_cast(mem) + n; + new (p) allocator_type(std::move(allocator)); + return mem; + } + void operator delete(void * ptr, std::size_t n) + { + if (const auto d = n % sizeof(std::max_align_t); d > 0u) + n += (sizeof(std::max_align_t) - d); + + using allocator_type = std::allocator_traits + ::template rebind_alloc; + auto allocator_p = reinterpret_cast( + static_cast(ptr) + n); + auto allocator = std::move(*allocator_p); + + allocator_p->~allocator_type(); + allocator.deallocate(static_cast(ptr), n + sizeof(allocator_type)); + } +}; + +template<> +struct boost_asio_standalone_promise_type_allocator_base> +{ +}; + + +template +struct boost_asio_standalone_init_promise_type + : boost_asio_standalone_promise_type_allocator_base< + ::asio::associated_allocator_t + > +{ + using args_type = completion_tuple_for_io_awaitable; + + boost_asio_standalone_init_promise_type( + boost_asio_standalone_init &, + Handler & h, + Ex & exec, + Awaitable &) + : handler(h), ex(exec) {} + + Handler & handler; + Ex &ex; + + void get_return_object() {} + void unhandled_exception() {throw;} + void return_void() {} + + std::suspend_never initial_suspend() noexcept {return {};} + std::suspend_never final_suspend() noexcept {return {};} + + struct completer + { + Handler handler; + Ex ex; + args_type args; + + bool await_ready() const {return false;} + void await_suspend( + std::coroutine_handle h) + { + auto handler_ = + std::apply( + [&](auto ... args) + { + return ::asio::append(std::move(handler), + std::move(args)...); + }, + detail::decomposed_types(std::move(args))); + + auto exec = ::asio::get_associated_immediate_executor( + handler_, + asio_executor_adapter(std::move(ex))); + + h.destroy(); + ::asio::dispatch(exec, std::move(handler_)); + } + void await_resume() const {} + }; + + completer yield_value(args_type value) + { + return {std::move(handler), std::move(ex), std::move(value)}; + } + + struct wrapper + { + Awaitable r; + const Ex &ex; + io_env env; + std::stop_source stop_src; + ::asio::cancellation_slot cancel_slot; + + wrapper(Awaitable && r, const Ex &ex) + : r(std::move(r)), ex(ex) + { + } + + continuation c; + + bool await_ready() {return r.await_ready(); } + + auto await_suspend( + std::coroutine_handle tr) + { + env.executor = ex; + + env.stop_token = stop_src.get_token(); + cancel_slot = + ::asio::get_associated_cancellation_slot(tr.promise().handler); + if (cancel_slot.is_connected()) + cancel_slot.assign( + [this](::asio::cancellation_type ct) + { + if ((ct & ::asio::cancellation_type::terminal) + != ::asio::cancellation_type::none) + stop_src.request_stop(); + }); + env.frame_allocator = get_current_frame_allocator(); + + + using suspend_kind = decltype(r.await_suspend(tr, &env)); + if constexpr (std::is_void_v) + r.await_suspend(tr, &env); + else if constexpr (std::same_as) + return r.await_suspend(tr, &env); + else + { + c.h = r.await_suspend(tr, &env); + return ex.dispatch(c); + } + } + + completion_tuple_for_io_awaitable await_resume() + { + if (cancel_slot.is_connected()) + cancel_slot.clear(); + + using type = decltype(r.await_resume()); + if constexpr (!noexcept(r.await_resume())) + { + if constexpr (std::is_void_v) + try + { + r.await_resume(); + return {std::exception_ptr()}; + } + catch (...) + { + return std::current_exception(); + } + else + try + { + return {std::exception_ptr(), r.await_resume()}; + } + catch (...) + { + return {std::current_exception(), type()}; + } + } + else + { + if constexpr (std::is_void_v) + { + r.await_resume(); + return {}; + } + else + return {r.await_resume()}; + } + } + }; + + wrapper await_transform(Awaitable & r) + { + return wrapper{std::move(r), ex}; + } +}; + + + +struct boost_asio_standalone_init +{ + template + void operator()( + Handler , + Ex, + Awaitable awaitable) + { + auto res = co_await awaitable; + co_yield std::move(res); + } +}; + +template + requires + ::asio::completion_token_for< + Token, + completion_signature_for_io_awaitable + > +struct initialize_asio_standalone_spawn_helper +{ + template + static auto init(Executor ex, Awaitable r, Token && tk) + -> decltype(::asio::async_initiate< + Token, + completion_signature_for_io_awaitable>( + boost_asio_standalone_init{}, + tk, std::move(ex), std::move(r) + )) + { + return ::asio::async_initiate< + Token, + completion_signature_for_io_awaitable>( + boost_asio_standalone_init{}, + tk, std::move(ex), std::move(r) + ); + } +}; + +} + + +template +struct std::coroutine_traits< + void, + boost::capy::detail::boost_asio_standalone_init&, + Handler, + Executor, + Awaitable> +{ + using promise_type + = boost::capy::detail::boost_asio_standalone_init_promise_type< + Handler, + Executor, + Awaitable>; +}; + +namespace boost::capy +{ + + +namespace detail +{ + + +template +struct asio_standalone_buffer_sequence_wrapper +{ + Sequence seq; + auto begin() const {return ::asio::buffer_sequence_begin(seq); } + auto end() const {return ::asio::buffer_sequence_end(seq); } +}; + +/** A bidirectional range that transforms buffer sequences using asio_buffer_transformer. + * + * Wraps an asio buffer sequence and provides begin/end iterators that transform + * buffer elements via asio_buffer_transformer. Uses ::asio::buffer_sequence_begin/end. + * + * @tparam Sequence The underlying buffer sequence type + */ +template +class asio_standalone_buffer_range +{ +public: + using sequence_type = Sequence; + using iterator = asio_buffer_iterator< + decltype(::asio::buffer_sequence_begin(std::declval()))>; + using const_iterator = iterator; + + asio_standalone_buffer_range() = default; + + explicit asio_standalone_buffer_range(Sequence seq) + noexcept(std::is_nothrow_move_constructible_v) + : seq_(std::move(seq)) + { + } + + asio_standalone_buffer_range(const asio_standalone_buffer_range&) = default; + asio_standalone_buffer_range(asio_standalone_buffer_range&&) = default; + asio_standalone_buffer_range& operator=(const asio_standalone_buffer_range&) = default; + asio_standalone_buffer_range& operator=(asio_standalone_buffer_range&&) = default; + + iterator begin() const + noexcept(noexcept(::asio::buffer_sequence_begin(std::declval()))) + { + return iterator(::asio::buffer_sequence_begin(seq_)); + } + + iterator end() const + noexcept(noexcept(::asio::buffer_sequence_end(std::declval()))) + { + return iterator(::asio::buffer_sequence_end(seq_)); + } + + /// Returns the underlying sequence. + const Sequence& base() const noexcept { return seq_; } + +private: + Sequence seq_{}; +}; + +// Deduction guide +template +asio_standalone_buffer_range(Sequence) -> asio_standalone_buffer_range; + + +asio_mutable_buffer asio_buffer_transformer_t:: + operator()(const ::asio::mutable_buffer &mb) const noexcept +{ + return {mb.data(), mb.size()}; +} + +asio_const_buffer asio_buffer_transformer_t:: + operator()(const ::asio::const_buffer &cb) const noexcept +{ + return {cb.data(), cb.size()}; +} + +} + +/** Convert a standalone Asio buffer sequence for bidirectional Asio/capy compatibility. + + Wraps a standalone Asio buffer sequence in a transforming range that converts + each buffer element to asio_const_buffer or asio_mutable_buffer. + The returned range satisfies both standalone Asio's buffer sequence requirements + and capy's buffer sequence concepts. + + This overload uses `::asio::buffer_sequence_begin` and + `::asio::buffer_sequence_end` for iteration. + + @par Example: Asio to Capy + @code + std::vector asio_bufs = ...; + auto seq = capy::as_asio_buffer_sequence(asio_bufs); + std::size_t total = capy::buffer_size(seq); // Use capy algorithms + @endcode + + @param seq The standalone Asio buffer sequence to convert + @return A transforming range over the buffer sequence + + @see as_asio_buffer_sequence in buffers.hpp for capy buffer sequences +*/ +template + requires requires (const T & seq) + { + {::asio::buffer_sequence_begin(seq)} -> std::bidirectional_iterator; + {::asio::buffer_sequence_end(seq)} -> std::bidirectional_iterator; + {*::asio::buffer_sequence_begin(seq)} -> std::convertible_to<::asio::const_buffer>; + {*::asio::buffer_sequence_end(seq)} -> std::convertible_to<::asio::const_buffer>; + } +auto as_asio_buffer_sequence(const T & seq) +{ + return detail::asio_standalone_buffer_range(seq); +} + +asio_const_buffer::operator ::asio::const_buffer() const +{ + return {data(), size()}; +} + +asio_mutable_buffer::operator ::asio::const_buffer() const +{ + return {data(), size()}; +} + +asio_mutable_buffer::operator ::asio::mutable_buffer() const +{ + return {data(), size()}; +} + + +} + +#endif // BOOST_CAPY_ASIO_STANDALONE_HPP + diff --git a/include/boost/capy/asio/stream.hpp b/include/boost/capy/asio/stream.hpp new file mode 100644 index 000000000..45445cc90 --- /dev/null +++ b/include/boost/capy/asio/stream.hpp @@ -0,0 +1,511 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// +#ifndef BOOST_CAPY_ASIO_STREAM_HPP +#define BOOST_CAPY_ASIO_STREAM_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::capy +{ + +/** Wraps a capy ReadStream for use with Asio async operations. + + Adapts a capy `ReadStream` to provide Asio-style `async_read_some` + operations with completion token support. + + @tparam Stream The underlying capy ReadStream type + @tparam Exec The executor type + + @par Example + @code + capy::any_read_stream stream = ...; + auto exec = capy::wrap_asio_executor(io.get_executor()); + capy::async_read_stream reader(std::move(stream), exec); + + auto [ec, n] = co_await reader.async_read_some(buffer, capy::as_io_awaitable); + @endcode +*/ +template +struct async_read_stream +{ + using executor_type = asio_executor_adapter; + using next_layer_type = Stream; + + /// Default constructor. + async_read_stream() = default; + + /// Construct from a stream and executor. + async_read_stream(Stream stream, Exec executor) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + , executor_(std::move(executor)) + { + } + + async_read_stream(const async_read_stream&) + noexcept(std::is_nothrow_copy_constructible_v && + std::is_nothrow_copy_constructible_v) = default; + async_read_stream(async_read_stream&&) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) = default; + async_read_stream& operator=(const async_read_stream&) + noexcept(std::is_nothrow_copy_assignable_v && + std::is_nothrow_copy_assignable_v) = default; + async_read_stream& operator=(async_read_stream&&) + noexcept(std::is_nothrow_move_assignable_v && + std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor adapter for Asio compatibility. + executor_type get_executor() const { return executor_; } + + /// Initiates an async read, returning a spawn handle. + template + auto async_read_some(Buffer && buffer) + { + return asio_spawn( + executor_, + stream_.read_some(as_asio_buffer_sequence(std::forward(buffer)))); + } + + /// Initiates an async read with a completion token. + template + auto async_read_some(Buffer && buffer, CompletionToken && token) + { + return asio_spawn( + executor_, + stream_.read_some(as_asio_buffer_sequence(std::forward(buffer))), + std::forward(token)); + } + + /// Returns a reference inter to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + Stream stream_; + Exec executor_; +}; + +// Deduction guides +template +async_read_stream(Stream, Exec) -> async_read_stream; + + +/** Wraps a capy WriteStream for use with Asio async operations. + + Adapts a capy `WriteStream` to provide Asio-style `async_write_some` + operations with completion token support. + + @tparam Stream The underlying capy WriteStream type + @tparam Exec The executor type + + @par Example + @code + capy::any_write_stream stream = ...; + auto exec = capy::wrap_asio_executor(io.get_executor()); + capy::async_write_stream writer(std::move(stream), exec); + + auto [ec, n] = co_await writer.async_write_some(buffer, capy::as_io_awaitable); + @endcode +*/ +template +struct async_write_stream +{ + using executor_type = asio_executor_adapter; + using next_layer_type = Stream; + + /// Default constructor. + async_write_stream() = default; + + /// Construct from a stream and executor. + async_write_stream(Stream stream, Exec executor) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + , executor_(std::move(executor)) + { + } + + async_write_stream(const async_write_stream&) + noexcept(std::is_nothrow_copy_constructible_v && + std::is_nothrow_copy_constructible_v) = default; + async_write_stream(async_write_stream&&) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) = default; + async_write_stream& operator=(const async_write_stream&) + noexcept(std::is_nothrow_copy_assignable_v && + std::is_nothrow_copy_assignable_v) = default; + async_write_stream& operator=(async_write_stream&&) + noexcept(std::is_nothrow_move_assignable_v && + std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor adapter for Asio compatibility. + executor_type get_executor() const { return executor_; } + + /// Initiates an async write, returning a spawn handle. + template + auto async_write_some(Buffer && buffer) + { + + return asio_spawn( + executor_, + stream_.write_some(as_asio_buffer_sequence(std::forward(buffer)))); + } + + /// Initiates an async write with a completion token. + template + auto async_write_some(Buffer && buffer, CompletionToken && token) + { + return asio_spawn( + executor_, + stream_.write_some(as_asio_buffer_sequence(std::forward(buffer))), + std::forward(token)); + } + + /// Returns a reference to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + Stream stream_; + Exec executor_; +}; + +// Deduction guides +template +async_write_stream(Stream, Exec) -> async_write_stream; + + +/** Wraps a capy Stream for use with Asio async operations. + + Adapts a capy `Stream` (supporting both read and write) to provide + Asio-style `async_read_some` and `async_write_some` operations + with completion token support. + + @tparam Stream The underlying capy Stream type + @tparam Exec The executor type + + @par Example + @code + capy::any_stream stream = ...; + auto exec = capy::wrap_asio_executor(io.get_executor()); + capy::async_stream sock(std::move(stream), exec); + + // Read + auto [ec1, n1] = co_await sock.async_read_some(read_buf, capy::as_io_awaitable); + + // Write + auto [ec2, n2] = co_await sock.async_write_some(write_buf, capy::as_io_awaitable); + @endcode +*/ +template +struct async_stream +{ + using executor_type = asio_executor_adapter; + using next_layer_type = Stream; + + /// Default constructor. + async_stream() = default; + + /// Construct from a stream and executor. + async_stream(Stream stream, Exec executor) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + , executor_(std::move(executor)) + { + } + + async_stream(const async_stream&) + noexcept(std::is_nothrow_copy_constructible_v && + std::is_nothrow_copy_constructible_v) = default; + async_stream(async_stream&&) + noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) = default; + async_stream& operator=(const async_stream&) + noexcept(std::is_nothrow_copy_assignable_v && + std::is_nothrow_copy_assignable_v) = default; + async_stream& operator=(async_stream&&) + noexcept(std::is_nothrow_move_assignable_v && + std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor adapter for Asio compatibility. + executor_type get_executor() const { return executor_; } + + /// Initiates an async read, returning a spawn handle. + template + auto async_read_some(Buffer && buffer) + { + return asio_spawn( + executor_, + stream_.read_some(as_asio_buffer_sequence(std::forward(buffer)))); + } + + /// Initiates an async read with a completion token. + template + auto async_read_some(Buffer && buffer, CompletionToken && token) + { + return asio_spawn( + executor_, + stream_.read_some(as_asio_buffer_sequence(std::forward(buffer))), + std::forward(token)); + } + + /// Initiates an async write, returning a spawn handle. + template + auto async_write_some(Buffer && buffer) + { + return asio_spawn( + executor_, + stream_.write_some(as_asio_buffer_sequence(std::forward(buffer)))); + } + + /// Initiates an async write with a completion token. + template + auto async_write_some(Buffer && buffer, CompletionToken && token) + { + return asio_spawn( + executor_, + stream_.write_some(as_asio_buffer_sequence(std::forward(buffer))), + std::forward(token)); + } + + /// Returns a reference to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + Stream stream_; + Exec executor_; +}; + +// Deduction guides +template +async_stream(StreamT, Exec) -> async_stream; + +/** Wraps an Asio AsyncReadStream for use with capy's ReadStream concept. + + Adapts an Asio `AsyncReadStream` to provide capy-style `read_some` + operations that return an IoAwaitable. + + @tparam AsyncReadStream The underlying Asio AsyncReadStream type + + @par Example + @code + boost::asio::ip::tcp::socket socket(io); + capy::asio_read_stream reader(std::move(socket)); + + auto result = co_await reader.read_some(buffer); + @endcode +*/ +template +struct asio_read_stream +{ + using executor_type = typename AsyncReadStream::executor_type; + using next_layer_type = AsyncReadStream; + + /// Default constructor. + asio_read_stream() = default; + + /// Construct from an Asio stream. + explicit asio_read_stream(AsyncReadStream stream) + noexcept(std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + { + } + + asio_read_stream(const asio_read_stream&) + noexcept(std::is_nothrow_copy_constructible_v) = default; + asio_read_stream(asio_read_stream&&) + noexcept(std::is_nothrow_move_constructible_v) = default; + asio_read_stream& operator=(const asio_read_stream&) + noexcept(std::is_nothrow_copy_assignable_v) = default; + asio_read_stream& operator=(asio_read_stream&&) + noexcept(std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor from the underlying stream. + executor_type get_executor() const { return stream_.get_executor(); } + + /// Initiates an async read, returning an IoAwaitable. + template + auto read_some(Seq buffers) + { + return stream_.async_read_some(as_asio_buffer_sequence(buffers), as_io_awaitable); + } + + /// Returns a reference to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + AsyncReadStream stream_; +}; + +// Deduction guide +template +asio_read_stream(AsyncReadStream) -> asio_read_stream; + + +/** Wraps an Asio AsyncWriteStream for use with capy's WriteStream concept. + + Adapts an Asio `AsyncWriteStream` to provide capy-style `write_some` + operations that return an IoAwaitable. + + @tparam AsyncWriteStream The underlying Asio AsyncWriteStream type + + @par Example + @code + boost::asio::ip::tcp::socket socket(io); + capy::asio_write_stream writer(std::move(socket)); + + auto result = co_await writer.write_some(buffer); + @endcode +*/ +template +struct asio_write_stream +{ + using executor_type = typename AsyncWriteStream::executor_type; + using next_layer_type = AsyncWriteStream; + + /// Default constructor. + asio_write_stream() = default; + + /// Construct from an Asio stream. + explicit asio_write_stream(AsyncWriteStream stream) + noexcept(std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + { + } + + asio_write_stream(const asio_write_stream&) + noexcept(std::is_nothrow_copy_constructible_v) = default; + asio_write_stream(asio_write_stream&&) + noexcept(std::is_nothrow_move_constructible_v) = default; + asio_write_stream& operator=(const asio_write_stream&) + noexcept(std::is_nothrow_copy_assignable_v) = default; + asio_write_stream& operator=(asio_write_stream&&) + noexcept(std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor from the underlying stream. + executor_type get_executor() const { return stream_.get_executor(); } + + /// Initiates an async write, returning an IoAwaitable. + template + auto write_some(Seq buffers) + { + return stream_.async_write_some(as_asio_buffer_sequence(buffers), as_io_awaitable); + } + + /// Returns a reference to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + AsyncWriteStream stream_; +}; + +// Deduction guide +template +asio_write_stream(AsyncWriteStream) -> asio_write_stream; + + +/** Wraps an Asio AsyncStream for use with capy's Stream concept. + + Adapts an Asio stream (supporting both read and write) to provide + capy-style `read_some` and `write_some` operations that return + an IoAwaitable. + + @tparam AsyncStream The underlying Asio stream type + + @par Example + @code + boost::asio::ip::tcp::socket socket(io); + capy::asio_stream stream(std::move(socket)); + + // Read + auto read_result = co_await stream.read_some(read_buf); + + // Write + auto write_result = co_await stream.write_some(write_buf); + @endcode +*/ +template +struct asio_stream +{ + using executor_type = typename AsyncStream::executor_type; + using next_layer_type = AsyncStream; + + /// Default constructor. + asio_stream() = default; + + /// Construct from an Asio stream. + explicit asio_stream(AsyncStream stream) + noexcept(std::is_nothrow_move_constructible_v) + : stream_(std::move(stream)) + { + } + + asio_stream(const asio_stream&) + noexcept(std::is_nothrow_copy_constructible_v) = default; + asio_stream(asio_stream&&) + noexcept(std::is_nothrow_move_constructible_v) = default; + asio_stream& operator=(const asio_stream&) + noexcept(std::is_nothrow_copy_assignable_v) = default; + asio_stream& operator=(asio_stream&&) + noexcept(std::is_nothrow_move_assignable_v) = default; + + /// Returns the executor from the underlying stream. + executor_type get_executor() const { return stream_.get_executor(); } + + /// Initiates an async read, returning an IoAwaitable. + template + auto read_some(Seq buffers) + { + return stream_.async_read_some(as_asio_buffer_sequence(buffers), as_io_awaitable); + } + + /// Initiates an async write, returning an IoAwaitable. + template + auto write_some(Seq buffers) + { + return stream_.async_write_some(as_asio_buffer_sequence(buffers), as_io_awaitable); + } + + /// Returns a reference to the underlying stream. + next_layer_type& next_layer() { return stream_; } + /// Returns a const reference to the underlying stream. + const next_layer_type& next_layer() const { return stream_; } + + private: + AsyncStream stream_; +}; + +// Deduction guide +template +asio_stream(AsyncStream) -> asio_stream; + +} + +#endif //BOOST_CAPY_ASIO_STREAM_HPP + diff --git a/include/boost/capy/buffers/asio.hpp b/include/boost/capy/buffers/asio.hpp deleted file mode 100644 index a2b8be26c..000000000 --- a/include/boost/capy/buffers/asio.hpp +++ /dev/null @@ -1,329 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_CAPY_BUFFERS_ASIO_HPP -#define BOOST_CAPY_BUFFERS_ASIO_HPP - -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -namespace detail { - -// true when BS itself or its range element is convertible -// to asio::const_buffer (i.e. it is an asio-native sequence) -template -constexpr bool is_native_asio_v = false; - -template -constexpr bool is_native_asio_v>> = true; - -template -constexpr bool is_native_asio_v && - std::ranges::bidirectional_range && - std::is_convertible_v< - std::ranges::range_value_t, - asio::const_buffer>>> = true; - -/** Adapts a buffer sequence so its iterators yield the other library's buffer type. - - When the source is an asio-native sequence the adaptor's - iterators produce `capy::mutable_buffer` or `capy::const_buffer`. - Otherwise the source is a capy sequence and the iterators - produce `asio::mutable_buffer` or `asio::const_buffer`. - - @tparam BufferSequence The source buffer sequence type. - @tparam IsMutable Whether the target buffer type is mutable. -*/ -template< - class BufferSequence, - bool IsMutable> -class buffer_sequence_adaptor -{ - static constexpr bool native_asio = - is_native_asio_v; - - static auto get_begin(BufferSequence const& bs) noexcept - { - if constexpr (native_asio) - return asio::buffer_sequence_begin(bs); - else - return capy::begin(bs); - } - - using source_iterator = decltype( - get_begin(std::declval())); - - using source_value_type = std::conditional_t< - native_asio, - std::conditional_t, - std::conditional_t>; - -public: - using value_type = std::conditional_t< - native_asio, - std::conditional_t, - std::conditional_t>; - - class const_iterator - { - source_iterator it_{}; - - public: - using value_type = buffer_sequence_adaptor::value_type; - using reference = value_type; - using pointer = void; - using difference_type = std::ptrdiff_t; - - using iterator_category = std::conditional_t< - std::random_access_iterator, - std::random_access_iterator_tag, - std::bidirectional_iterator_tag>; - - const_iterator() = default; - - explicit const_iterator( - source_iterator it) noexcept - : it_(it) - { - } - - reference operator*() const noexcept - { - source_value_type b(*it_); - return value_type(b.data(), b.size()); - } - - const_iterator& - operator++() noexcept - { - ++it_; - return *this; - } - - const_iterator - operator++(int) noexcept - { - auto tmp = *this; - ++*this; - return tmp; - } - - const_iterator& - operator--() noexcept - { - --it_; - return *this; - } - - const_iterator - operator--(int) noexcept - { - auto tmp = *this; - --*this; - return tmp; - } - - bool operator==( - const_iterator const& other) const noexcept - { - return it_ == other.it_; - } - - bool operator!=( - const_iterator const& other) const noexcept - { - return it_ != other.it_; - } - - //--- random access (conditional) --- - - const_iterator& - operator+=(difference_type n) noexcept - requires std::random_access_iterator - { - it_ += n; - return *this; - } - - const_iterator& - operator-=(difference_type n) noexcept - requires std::random_access_iterator - { - it_ -= n; - return *this; - } - - reference - operator[](difference_type n) const noexcept - requires std::random_access_iterator - { - return *(*this + n); - } - - friend const_iterator - operator+( - const_iterator it, - difference_type n) noexcept - requires std::random_access_iterator - { - it += n; - return it; - } - - friend const_iterator - operator+( - difference_type n, - const_iterator it) noexcept - requires std::random_access_iterator - { - it += n; - return it; - } - - friend const_iterator - operator-( - const_iterator it, - difference_type n) noexcept - requires std::random_access_iterator - { - it -= n; - return it; - } - - friend difference_type - operator-( - const_iterator const& a, - const_iterator const& b) noexcept - requires std::random_access_iterator - { - return a.it_ - b.it_; - } - - auto operator<=>( - const_iterator const& other) const noexcept - requires std::random_access_iterator - { - return it_ <=> other.it_; - } - }; - - template - explicit buffer_sequence_adaptor(BS&& bs) - noexcept(std::is_nothrow_constructible_v< - BufferSequence, BS&&>) - : bs_(std::forward(bs)) - { - } - - const_iterator - begin() const noexcept - { - if constexpr (native_asio) - return const_iterator( - asio::buffer_sequence_begin(bs_)); - else - return const_iterator( - capy::begin(bs_)); - } - - const_iterator - end() const noexcept - { - if constexpr (native_asio) - return const_iterator( - asio::buffer_sequence_end(bs_)); - else - return const_iterator( - capy::end(bs_)); - } - -private: - BufferSequence bs_; -}; - -} // detail - -/** Adapt a capy buffer sequence for use with Asio. - - Returns a wrapper whose iterators dereference to - `asio::mutable_buffer` or `asio::const_buffer`, - depending on whether the source is a - `MutableBufferSequence`. - - @param bs The capy buffer sequence to adapt. - @return An adapted buffer sequence usable with Asio. -*/ -template - requires ConstBufferSequence> -auto -to_asio(BS&& bs) -{ - using Seq = std::remove_cvref_t; - constexpr bool is_mutable = - MutableBufferSequence; - - return detail::buffer_sequence_adaptor< - Seq, is_mutable>( - std::forward(bs)); -} - -/** Adapt an Asio buffer sequence for use with capy. - - Returns a wrapper whose iterators dereference to - `capy::mutable_buffer` or `capy::const_buffer`, - depending on whether the source is convertible to - `asio::mutable_buffer`. Accepts single asio buffers - and ranges of asio buffers. - - @param bs The Asio buffer sequence to adapt. - @return An adapted buffer sequence usable with capy. -*/ -template - requires std::is_convertible_v< - std::remove_cvref_t, asio::const_buffer> || - std::ranges::bidirectional_range> -auto -from_asio(BS&& bs) -{ - using Seq = std::remove_cvref_t; - - constexpr bool is_mutable = [] { - if constexpr (std::is_convertible_v) - return true; - else if constexpr (std::is_convertible_v) - return false; - else - return std::is_convertible_v< - std::ranges::range_value_t, - asio::mutable_buffer>; - }(); - - return detail::buffer_sequence_adaptor< - Seq, is_mutable>( - std::forward(bs)); -} - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/test/read_stream.hpp b/include/boost/capy/test/read_stream.hpp index d31143507..997f35314 100644 --- a/include/boost/capy/test/read_stream.hpp +++ b/include/boost/capy/test/read_stream.hpp @@ -160,7 +160,7 @@ class read_stream } io_result - await_resume() + await_resume() noexcept { // Empty buffer is a no-op regardless of // stream state or fuse. diff --git a/include/boost/capy/test/stream.hpp b/include/boost/capy/test/stream.hpp index 32078e678..6edf223af 100644 --- a/include/boost/capy/test/stream.hpp +++ b/include/boost/capy/test/stream.hpp @@ -237,7 +237,7 @@ class stream } io_result - await_resume() + await_resume() noexcept { if(buffer_empty(buffers_)) return {{}, 0}; diff --git a/include/boost/capy/test/write_stream.hpp b/include/boost/capy/test/write_stream.hpp index 6e36abd3a..93c26a501 100644 --- a/include/boost/capy/test/write_stream.hpp +++ b/include/boost/capy/test/write_stream.hpp @@ -177,7 +177,7 @@ class write_stream } io_result - await_resume() + await_resume() noexcept { if(buffer_empty(buffers_)) return {{}, 0}; diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 14774345b..5a0c31c85 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -12,6 +12,7 @@ list(APPEND PFILES CMakeLists.txt Jamfile) + source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) add_executable(boost_capy_tests ${PFILES}) diff --git a/test/unit/asio.cpp b/test/unit/asio.cpp new file mode 100644 index 000000000..42483d687 --- /dev/null +++ b/test/unit/asio.cpp @@ -0,0 +1,320 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + + + +#include +#include +#if __has_include() +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + + +#include +#include +#include +#include "test_helpers.hpp" +#include "test_suite.hpp" + + + +namespace boost { +namespace capy { + +struct boost_asio_test +{ + void testExecutor() + { + int dispatch_count = 0; + int work_cnt = 0; + test_executor exec{0, dispatch_count, work_cnt}; + boost::capy::asio_executor_adapter wrapped_te{exec}; + + bool ran = false; + boost::asio::post(wrapped_te, [&]{ran = true;}); + BOOST_TEST_EQ(dispatch_count, 0); + BOOST_TEST(ran); + + + ran = false; + boost::asio::dispatch(wrapped_te, [&]{ran = true;}); + BOOST_TEST_EQ(dispatch_count, 1); + BOOST_TEST(ran); + + BOOST_TEST(work_cnt == 0); + { + auto wk = boost::asio::require(wrapped_te, boost::asio::execution::outstanding_work.tracked); + BOOST_TEST_EQ(work_cnt, 1); + + } + BOOST_TEST_EQ(work_cnt, 0); + boost::asio::any_io_executor aio{wrapped_te}; + BOOST_TEST_EQ(work_cnt, 0); + aio = boost::asio::prefer(aio, boost::asio::execution::outstanding_work.tracked); + BOOST_TEST_EQ(work_cnt, 1); + aio = nullptr; + BOOST_TEST_EQ(work_cnt, 0); + } + + void testFromExecutor() + { + boost::asio::io_context ctx; + auto exec = wrap_asio_executor(ctx.get_executor()); + + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + done = true; + co_return ; + }; + + boost::capy::run_async(exec)(tsk()); + BOOST_TEST(!done); + ctx.run(); + BOOST_TEST(done); + } + + + void testFromAnyIOExecutor() + { + boost::asio::io_context ctx; + boost::asio::any_io_executor any_exec{ctx.get_executor()}; + auto exec = wrap_asio_executor(any_exec); + + + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + done = true; + co_return ; + }; + + boost::capy::run_async(exec)(tsk()); + BOOST_TEST(!done); + ctx.run(); + BOOST_TEST(done); + } + + void testAsIoAwaitable() + { + + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + boost::capy::asio_executor_adapter wrapped_te{co_await capy::this_coro::executor}; + + co_await boost::asio::post(wrapped_te, as_io_awaitable); + done = true; + }; + int dispatch_count = 0; + int work_cnt = 0; + test_executor exec{0, dispatch_count, work_cnt}; + boost::capy::run_async(exec)(tsk()); + + BOOST_TEST(done); + } + + void testAsioSpawn() + { + int dispatch_count = 0; + test_executor exec{0, dispatch_count}; + bool done = false; + auto tsk = [&]() -> + boost::capy::task + { + done = true; + co_return ; + }; + + auto ft = asio_spawn(exec, tsk(), boost::asio::use_future); + + ft.get(); + BOOST_TEST(done); + BOOST_TEST(dispatch_count == 1); + } + + void testTimer() + { + int dispatch_count = 0; + test_executor te{dispatch_count}; + boost::capy::asio_executor_adapter wrapped_te{te}; + + boost::asio::steady_timer t{wrapped_te}; + t.expires_after(std::chrono::milliseconds(1)); + + bool done = false; + t.async_wait( + [&](auto ec) + { + BOOST_TEST(!ec); + done = true; + }); + + while (!done) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + + void staticTestBuffer(boost::asio::writable_pipe & wp) + { + boost::asio::mutable_buffer boofer; + boost::capy::MutableBufferSequence auto seq + = boost::capy::as_asio_buffer_sequence(boofer); + + using seq_t = decltype(seq); + + boost::capy::ConstBufferSequence auto cseq + = boost::capy::as_asio_buffer_sequence(boofer); + + using cseq_t = decltype(cseq); + + boost::capy::mutable_buffer_pair p; + auto seq2 = boost::capy::as_asio_buffer_sequence(p); + using seq2_t = decltype(seq2); + + auto s = seq; + auto cs = cseq; + auto s2 = seq2; + + wp.write_some(seq2); + + static_assert(boost::asio::is_mutable_buffer_sequence::value); + static_assert(boost::asio:: is_const_buffer_sequence::value); + + + static_assert(boost::asio::is_mutable_buffer_sequence::value); + static_assert(boost::asio:: is_const_buffer_sequence::value); + + + static_assert(boost::asio::is_mutable_buffer_sequence::value); + static_assert(boost::asio:: is_const_buffer_sequence::value); + } + + void testStreamToAsio() + { + thread_pool tp; + + async_write_stream ws{test::write_stream(), tp.get_executor()}; + + std::atomic done{0}; + + boost::asio::async_write( + ws, + boost::asio::buffer("Test", 4), + [&](boost::system::error_code ec, std::size_t n) + { + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4); + done ++; + }); + + BOOST_TEST_EQ(ws.next_layer().data(), "Test"); + + async_read_stream rs{test::read_stream(), tp.get_executor()}; + rs.next_layer().provide("foobar"); + + + char data[100]; + boost::asio::async_read( + rs, + boost::asio::buffer(data), + [&](boost::system::error_code ec, std::size_t n) + { + BOOST_TEST_EQ(ec, boost::capy::error::eof); + BOOST_TEST_EQ(n, 6); + BOOST_TEST_EQ(std::string_view(data, n), "foobar"); + done ++; + }); + + while (done.load() < 2u); + + tp.join(); + } + + void testStreamFromAsio() + { + boost::asio::io_context ctx; + boost::asio::readable_pipe rp{ctx}; + boost::asio::writable_pipe wp{ctx}; + boost::asio::connect_pipe(rp, wp); + + any_read_stream rs{asio_read_stream(std::move(rp)) }; + any_write_stream ws{asio_write_stream(std::move(wp))}; + + bool done = false; + + auto t = + [&]() -> task + { + std::string rb; + rb.resize(10); + + auto [r1, r2, r3] = co_await capy::when_all( + ws.write_some(make_buffer("hello pipe", 10)), + rs.read_some(make_buffer(rb)) + ); + + BOOST_TEST(!r1); + BOOST_TEST_EQ(r2, r3); + BOOST_TEST_EQ(rb, "hello pipe"); + }; + + run_async(wrap_asio_executor(ctx.get_executor()), [&]{done = true;})(t()); + + ctx.run(); + BOOST_TEST(done); + } + + void run() + { + testExecutor(); + testFromExecutor(); + testFromAnyIOExecutor(); + testAsIoAwaitable(); + testAsioSpawn(); + testTimer(); + testStreamToAsio(); + testStreamFromAsio(); + } +}; + + +TEST_SUITE( + boost_asio_test, + "boost.capy.asio.boost"); + +} // namespace capy +} // namespace boost + +#endif + diff --git a/test/unit/asio_both.cpp b/test/unit/asio_both.cpp new file mode 100644 index 000000000..3c942e720 --- /dev/null +++ b/test/unit/asio_both.cpp @@ -0,0 +1,190 @@ + +#if __has_include() && __has_include() + +#include +#include + + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "test_helpers.hpp" +#include "test_suite.hpp" + +namespace boost { +namespace capy { + + +struct boost_asio_both_test +{ + + boost::asio::awaitable aw_boost(executor_ref exec, task t) + { + int i = co_await asio_spawn(exec, std::move(t)); + BOOST_TEST(i == 42); + co_return i; + } + + + ::asio::awaitable aw_standalone(executor_ref exec, task t) + { + int i = co_await asio_spawn(exec, std::move(t)); + BOOST_TEST(i == 42); + co_return i; + } + + + boost::asio::awaitable aw_boost_tuple(executor_ref exec, task t) + { + auto [ep, i] = co_await asio_spawn(exec, std::move(t))(boost::asio::as_tuple); + BOOST_TEST(i == 42); + co_return i; + } + + + ::asio::awaitable aw_standalone_tuple(executor_ref exec, task t) + { + auto [ep, i] = co_await asio_spawn(exec, std::move(t))(::asio::as_tuple); + BOOST_TEST(i == 42); + co_return i; + } + + task foo() {co_return 42;} + task bar() {throw 42; co_return 0;} + + + void testBoost() + { + boost::asio::io_context ctx; + + int dispatch_count = 0; + test_executor te{dispatch_count}; + std::exception_ptr ep; + int i = 0; + + boost::asio::co_spawn( + ctx, + aw_boost(te, foo()), + [&](std::exception_ptr ep_, int i_) + { + ep = ep_; + i = i_; + }); + + BOOST_TEST(i == 0); + ctx.run(); + + BOOST_TEST(i == 42); + BOOST_TEST(!ep); + BOOST_TEST(dispatch_count == 1); + } + + + void testBoostTuple() + { + boost::asio::io_context ctx; + + int dispatch_count = 0; + test_executor te{dispatch_count}; + std::exception_ptr ep; + int i = 0; + + boost::asio::co_spawn( + ctx, + aw_boost_tuple(te, foo()), + [&](std::exception_ptr ep_, int i_) + { + ep = ep_; + i = i_; + }); + + BOOST_TEST(i == 0); + ctx.run(); + + BOOST_TEST(i == 42); + BOOST_TEST(!ep); + BOOST_TEST(dispatch_count == 1); + } + + void testStandalone() + { + ::asio::io_context ctx; + + int dispatch_count = 0; + test_executor te{dispatch_count}; + std::exception_ptr ep; + int i = 0; + + ::asio::co_spawn( + ctx, + aw_standalone(te, foo()), + [&](std::exception_ptr ep_, int i_) + { + ep = ep_; + i = i_; + }); + + BOOST_TEST(i == 0); + ctx.run(); + + BOOST_TEST(i == 42); + BOOST_TEST(!ep); + BOOST_TEST(dispatch_count == 1); + } + + + void testStandaloneTuple() + { + ::asio::io_context ctx; + + int dispatch_count = 0; + test_executor te{dispatch_count}; + std::exception_ptr ep; + int i = 0; + + ::asio::co_spawn( + ctx, + aw_standalone_tuple(te, foo()), + [&](std::exception_ptr ep_, int i_) + { + ep = ep_; + i = i_; + }); + + BOOST_TEST(i == 0); + ctx.run(); + + BOOST_TEST(i == 42); + BOOST_TEST(!ep); + BOOST_TEST(dispatch_count == 1); + } + + void run() + { + testBoost(); + testBoostTuple(); + + testStandalone(); + + testStandaloneTuple(); + } + +}; + +TEST_SUITE( + boost_asio_both_test, + "boost.capy.asio.both"); + +} // namespace capy +} // namespace boost + +#endif + + diff --git a/test/unit/asio_standalone.cpp b/test/unit/asio_standalone.cpp new file mode 100644 index 000000000..4e299f272 --- /dev/null +++ b/test/unit/asio_standalone.cpp @@ -0,0 +1,314 @@ +// +// Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#if __has_include() +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include "test_helpers.hpp" +#include "test_suite.hpp" + + +namespace boost { +namespace capy { + + +struct asio_standalone_test +{ + void testExecutor() + { + int dispatch_count = 0; + int work_cnt = 0; + test_executor exec{0, dispatch_count, work_cnt}; + boost::capy::asio_executor_adapter wrapped_te{exec}; + + bool ran = false; + ::asio::post(wrapped_te, [&]{ran = true;}); + BOOST_TEST_EQ(dispatch_count, 0); + BOOST_TEST(ran); + + + ran = false; + ::asio::dispatch(wrapped_te, [&]{ran = true;}); + BOOST_TEST_EQ(dispatch_count, 1); + BOOST_TEST(ran); + + BOOST_TEST(work_cnt == 0); + { + auto wk = ::asio::require(wrapped_te, ::asio::execution::outstanding_work.tracked); + BOOST_TEST_EQ(work_cnt, 1); + + } + BOOST_TEST_EQ(work_cnt, 0); + ::asio::any_io_executor aio{wrapped_te}; + BOOST_TEST_EQ(work_cnt, 0); + aio = ::asio::prefer(aio, ::asio::execution::outstanding_work.tracked); + BOOST_TEST_EQ(work_cnt, 1); + aio = nullptr; + BOOST_TEST_EQ(work_cnt, 0); + } + + + void testFromExecutor() + { + ::asio::io_context ctx; + auto exec = wrap_asio_executor(ctx.get_executor()); + + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + done = true; + co_return ; + }; + + boost::capy::run_async(exec)(tsk()); + BOOST_TEST(!done); + ctx.run(); + BOOST_TEST(done); + } + + + void testFromAnyIOExecutor() + { + ::asio::io_context ctx; + ::asio::any_io_executor any_exec{ctx.get_executor()}; + auto exec = wrap_asio_executor(any_exec); + + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + done = true; + co_return ; + }; + + boost::capy::run_async(exec)(tsk()); + BOOST_TEST(!done); + ctx.run(); + BOOST_TEST(done); + } + + void testAsIoAwaitable() + { + bool done = false; + auto tsk = [&]() -> boost::capy::task + { + boost::capy::asio_executor_adapter wrapped_te{co_await capy::this_coro::executor}; + co_await ::asio::post(wrapped_te, as_io_awaitable); + done = true; + }; + int dispatch_count = 0; + int work_cnt = 0; + test_executor exec{0, dispatch_count, work_cnt}; + boost::capy::run_async(exec)(tsk()); + + BOOST_TEST(done); + } + + + void testAsioSpawn() + { + int dispatch_count = 0; + test_executor exec{0, dispatch_count}; + bool done = false; + auto tsk = [&]() -> + boost::capy::task + { + done = true; + co_return ; + }; + + auto ft = asio_spawn(exec, tsk(), ::asio::use_future); + + ft.get(); + BOOST_TEST(done); + BOOST_TEST(dispatch_count == 1); + } + + + void testTimer() + { + int dispatch_count = 0; + test_executor te{dispatch_count}; + boost::capy::asio_executor_adapter wrapped_te{te}; + + ::asio::steady_timer t{wrapped_te}; + t.expires_after(std::chrono::milliseconds(1)); + + bool done = false; + t.async_wait( + [&](auto ec) + { + BOOST_TEST(!ec); + done = true; + }); + + while (!done) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + void staticTestBuffer(::asio::writable_pipe & wp) + { + ::asio::mutable_buffer boofer; + boost::capy::MutableBufferSequence auto seq + = boost::capy::as_asio_buffer_sequence(boofer); + + using seq_t = decltype(seq); + + boost::capy::ConstBufferSequence auto cseq + = boost::capy::as_asio_buffer_sequence(boofer); + + using cseq_t = decltype(cseq); + + boost::capy::mutable_buffer_pair p; + auto seq2 = boost::capy::as_asio_buffer_sequence(p); + using seq2_t = decltype(seq2); + + auto s = seq; + auto cs = cseq; + auto s2 = seq2; + + wp.write_some(seq2); + + static_assert(::asio::is_mutable_buffer_sequence::value); + static_assert(::asio:: is_const_buffer_sequence::value); + + + static_assert(::asio::is_mutable_buffer_sequence::value); + static_assert(::asio:: is_const_buffer_sequence::value); + + + static_assert(::asio::is_mutable_buffer_sequence::value); + static_assert(::asio:: is_const_buffer_sequence::value); + } + + + + void testStreamToAsio() + { + thread_pool tp; + + async_write_stream ws{test::write_stream(), tp.get_executor()}; + + std::atomic done{0}; + + ::asio::async_write( + ws, + ::asio::buffer("Test", 4), + [&](std::error_code ec, std::size_t n) + { + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4); + done ++; + }); + + BOOST_TEST_EQ(ws.next_layer().data(), "Test"); + + async_read_stream rs{test::read_stream(), tp.get_executor()}; + rs.next_layer().provide("foobar"); + + + char data[100]; + ::asio::async_read( + rs, + ::asio::buffer(data), + [&](std::error_code ec, std::size_t n) + { + BOOST_TEST_EQ(ec, boost::capy::error::eof); + BOOST_TEST_EQ(n, 6); + BOOST_TEST_EQ(std::string_view(data, n), "foobar"); + done ++; + }); + + while (done.load() < 2u); + + tp.join(); + } + + + void testStreamFromAsio() + { + ::asio::io_context ctx; + ::asio::readable_pipe rp{ctx}; + ::asio::writable_pipe wp{ctx}; + ::asio::connect_pipe(rp, wp); + + any_read_stream rs{asio_read_stream(std::move(rp)) }; + any_write_stream ws{asio_write_stream(std::move(wp))}; + + bool done = false; + + auto t = + [&]() -> task + { + std::string rb; + rb.resize(10); + + auto [r1, r2, r3] = co_await capy::when_all( + ws.write_some(make_buffer("hello pipe", 10)), + rs.read_some(make_buffer(rb)) + ); + + BOOST_TEST(!r1); + BOOST_TEST_EQ(r2, r3); + BOOST_TEST_EQ(rb, "hello pipe"); + }; + + run_async(wrap_asio_executor(ctx.get_executor()), [&]{done = true;})(t()); + + ctx.run(); + BOOST_TEST(done); + } + + + void run() + { + testExecutor(); + testFromExecutor(); + testFromAnyIOExecutor(); + testAsIoAwaitable(); + testAsioSpawn(); + testTimer(); + testStreamToAsio(); + testStreamFromAsio(); + } +}; + + +TEST_SUITE( + asio_standalone_test, + "boost.capy.asio.standalone"); + +} // namespace capy +} // namespace boost + +#endif + diff --git a/test/unit/buffers/asio.cpp b/test/unit/buffers/asio.cpp deleted file mode 100644 index 2be261807..000000000 --- a/test/unit/buffers/asio.cpp +++ /dev/null @@ -1,329 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -#if BOOST_CAPY_HAS_ASIO - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "test_buffers.hpp" - -namespace boost { -namespace capy { - -//------------------------------------------------ -// to_asio result satisfies asio buffer sequence traits - -using to_asio_const_t = decltype( - to_asio(std::declval const&>())); -using to_asio_mutable_t = decltype( - to_asio(std::declval const&>())); - -static_assert(asio::is_const_buffer_sequence::value); -static_assert(asio::is_const_buffer_sequence::value); -static_assert(!asio::is_mutable_buffer_sequence::value); -static_assert(asio::is_mutable_buffer_sequence::value); - -//------------------------------------------------ -// from_asio result satisfies capy buffer sequence concepts - -using from_asio_const_t = decltype( - from_asio(std::declval>())); -using from_asio_mutable_t = decltype( - from_asio(std::declval>())); - -static_assert(ConstBufferSequence); -static_assert(ConstBufferSequence); -static_assert(!MutableBufferSequence); -static_assert(MutableBufferSequence); - -//------------------------------------------------ -// to_asio iterator dereferences to asio buffer types - -static_assert(std::is_same_v< - to_asio_const_t::const_iterator::value_type, - asio::const_buffer>); -static_assert(std::is_same_v< - to_asio_mutable_t::const_iterator::value_type, - asio::mutable_buffer>); - -//------------------------------------------------ -// from_asio iterator dereferences to capy buffer types - -static_assert(std::is_same_v< - from_asio_const_t::const_iterator::value_type, - capy::const_buffer>); -static_assert(std::is_same_v< - from_asio_mutable_t::const_iterator::value_type, - capy::mutable_buffer>); - -//------------------------------------------------ - -struct asio_test -{ - void - test_to_asio_const() - { - char const data[] = "Hello"; - const_buffer cb(data, 5); - - auto adapted = to_asio(cb); - auto it = adapted.begin(); - auto end = adapted.end(); - BOOST_TEST(it != end); - - asio::const_buffer ab = *it; - BOOST_TEST_EQ(ab.data(), cb.data()); - BOOST_TEST_EQ(ab.size(), cb.size()); - - ++it; - BOOST_TEST(it == end); - } - - void - test_to_asio_mutable() - { - char data[] = "Hello"; - mutable_buffer mb(data, 5); - - auto adapted = to_asio(mb); - auto it = adapted.begin(); - auto end = adapted.end(); - BOOST_TEST(it != end); - - asio::mutable_buffer ab = *it; - BOOST_TEST_EQ(ab.data(), mb.data()); - BOOST_TEST_EQ(ab.size(), mb.size()); - - ++it; - BOOST_TEST(it == end); - } - - void - test_to_asio_array() - { - char d1[] = "abc"; - char d2[] = "defgh"; - mutable_buffer_array<4> bufs; - bufs = mutable_buffer_array<4>( - std::array{{ - mutable_buffer(d1, 3), - mutable_buffer(d2, 5) - }}); - - auto adapted = to_asio(bufs); - auto it = adapted.begin(); - auto end = adapted.end(); - - asio::mutable_buffer ab0 = *it; - BOOST_TEST_EQ(ab0.data(), static_cast(d1)); - BOOST_TEST_EQ(ab0.size(), 3u); - ++it; - - asio::mutable_buffer ab1 = *it; - BOOST_TEST_EQ(ab1.data(), static_cast(d2)); - BOOST_TEST_EQ(ab1.size(), 5u); - ++it; - - BOOST_TEST(it == end); - } - - void - test_from_asio_const() - { - char const data[] = "Hello"; - asio::const_buffer ab(data, 5); - std::span sp(&ab, 1); - - auto adapted = from_asio(sp); - auto it = adapted.begin(); - auto end = adapted.end(); - BOOST_TEST(it != end); - - const_buffer cb = *it; - BOOST_TEST_EQ(cb.data(), ab.data()); - BOOST_TEST_EQ(cb.size(), ab.size()); - - ++it; - BOOST_TEST(it == end); - } - - void - test_from_asio_mutable() - { - char data[] = "Hello"; - asio::mutable_buffer ab(data, 5); - std::span sp(&ab, 1); - - auto adapted = from_asio(sp); - auto it = adapted.begin(); - auto end = adapted.end(); - BOOST_TEST(it != end); - - mutable_buffer mb = *it; - BOOST_TEST_EQ(mb.data(), ab.data()); - BOOST_TEST_EQ(mb.size(), ab.size()); - - ++it; - BOOST_TEST(it == end); - } - - void - test_from_asio_array() - { - char d1[] = "abc"; - char d2[] = "defgh"; - std::array asio_bufs{{ - asio::mutable_buffer(d1, 3), - asio::mutable_buffer(d2, 5) - }}; - - auto adapted = from_asio(asio_bufs); - auto it = adapted.begin(); - auto end = adapted.end(); - - mutable_buffer mb0 = *it; - BOOST_TEST_EQ(mb0.data(), static_cast(d1)); - BOOST_TEST_EQ(mb0.size(), 3u); - ++it; - - mutable_buffer mb1 = *it; - BOOST_TEST_EQ(mb1.data(), static_cast(d2)); - BOOST_TEST_EQ(mb1.size(), 5u); - ++it; - - BOOST_TEST(it == end); - } - - void - test_roundtrip() - { - // capy -> asio -> capy preserves data/size - char data[] = "roundtrip"; - mutable_buffer mb(data, 9); - - auto asio_adapted = to_asio(mb); - auto it = asio_adapted.begin(); - asio::mutable_buffer ab = *it; - - std::span sp(&ab, 1); - auto capy_adapted = from_asio(sp); - auto it2 = capy_adapted.begin(); - mutable_buffer mb2 = *it2; - - BOOST_TEST_EQ(mb2.data(), mb.data()); - BOOST_TEST_EQ(mb2.size(), mb.size()); - } - - void - test_bidirectional() - { - char d1[] = "ab"; - char d2[] = "cd"; - std::array asio_bufs{{ - asio::const_buffer(d1, 2), - asio::const_buffer(d2, 2) - }}; - - auto adapted = from_asio(asio_bufs); - auto it = adapted.end(); - auto beg = adapted.begin(); - - --it; - const_buffer cb1 = *it; - BOOST_TEST_EQ(cb1.data(), static_cast(d2)); - - --it; - const_buffer cb0 = *it; - BOOST_TEST_EQ(cb0.data(), static_cast(d1)); - - BOOST_TEST(it == beg); - } - - void - test_random_access() - { - char d1[] = "ab"; - char d2[] = "cd"; - char d3[] = "ef"; - std::array asio_bufs{{ - asio::const_buffer(d1, 2), - asio::const_buffer(d2, 2), - asio::const_buffer(d3, 2) - }}; - - auto adapted = from_asio(asio_bufs); - auto beg = adapted.begin(); - auto end = adapted.end(); - - BOOST_TEST_EQ(end - beg, 3); - BOOST_TEST(beg < end); - - auto mid = beg + 1; - const_buffer cb = *mid; - BOOST_TEST_EQ(cb.data(), static_cast(d2)); - - cb = beg[2]; - BOOST_TEST_EQ(cb.data(), static_cast(d3)); - - mid += 1; - cb = *mid; - BOOST_TEST_EQ(cb.data(), static_cast(d3)); - - mid -= 2; - cb = *mid; - BOOST_TEST_EQ(cb.data(), static_cast(d1)); - } - - void - test_move_semantics() - { - char d1[] = "abc"; - mutable_buffer_array<4> bufs; - bufs = mutable_buffer_array<4>( - mutable_buffer(d1, 3)); - - auto adapted = to_asio(std::move(bufs)); - auto it = adapted.begin(); - asio::mutable_buffer ab = *it; - BOOST_TEST_EQ(ab.data(), static_cast(d1)); - BOOST_TEST_EQ(ab.size(), 3u); - } - - void - run() - { - test_to_asio_const(); - test_to_asio_mutable(); - test_to_asio_array(); - test_from_asio_const(); - test_from_asio_mutable(); - test_from_asio_array(); - test_roundtrip(); - test_bidirectional(); - test_random_access(); - test_move_semantics(); - } -}; - -TEST_SUITE( - asio_test, - "boost.capy.buffers.asio"); - -} // capy -} // boost - -#endif // BOOST_CAPY_HAS_ASIO diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp index 08f421f5c..1c3d6a38b 100644 --- a/test/unit/test_helpers.hpp +++ b/test/unit/test_helpers.hpp @@ -71,8 +71,9 @@ struct test_executor { int id_ = 0; int* dispatch_count_ = nullptr; + int* work_count_ = nullptr; test_io_context* ctx_ = nullptr; - + test_executor() = default; explicit @@ -87,12 +88,26 @@ struct test_executor { } + explicit + test_executor(int& count, int & work) noexcept + : dispatch_count_(&count) + , work_count_(&work) + { + } + test_executor(int id, int& count) noexcept : id_(id) , dispatch_count_(&count) { } + test_executor(int id, int& count, int& work) noexcept + : id_(id) + , dispatch_count_(&count) + , work_count_(&work) + { + } + bool operator==(test_executor const& other) const noexcept { @@ -104,8 +119,8 @@ struct test_executor test_io_context& context() const noexcept; - void on_work_started() const noexcept {} - void on_work_finished() const noexcept {} + void on_work_started() const noexcept {if (work_count_) (*work_count_)++;} + void on_work_finished() const noexcept {if (work_count_) (*work_count_)--;} std::coroutine_handle<> dispatch(continuation& c) const