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..80cf2db9a --- /dev/null +++ b/doc/modules/ROOT/pages/4.coroutines/4i.asio-integration.adoc @@ -0,0 +1,417 @@ +// +// 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: + +[cols="1,1"] +|=== +| Asio Signature | co_await Result + +| `void(error_code, std::size_t)` +| `std::tuple` + +| `void(error_code)` +| `std::tuple` + +| `void()` +| `std::tuple<>` +|=== + +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())( + asio::use_awaitable); + +// 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 exception specification of `await_resume`: + +[cols="2,2"] +|=== +| Coroutine | Completion Signature + +| `io_task` (may throw) +| `void(std::exception_ptr, T)` + +| `io_task` (noexcept) +| `void(T)` + +| `io_task` (may throw) +| `void(std::exception_ptr)` + +| `io_task` (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::io_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::io_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(); +} +---- + +== Reference + +[cols="1,2"] +|=== +| Header | Description + +| `` +| 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) +|=== + +[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 +|=== + 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..30fd52804 --- /dev/null +++ b/include/boost/capy/asio/boost.hpp @@ -0,0 +1,775 @@ +// +// 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 + +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, runnable)(token)`. + * + * @tparam ExecutorType The executor type + * @tparam Runnable The coroutine type + * @tparam Token A Boost.Asio completion token + * @param exec The executor to run on + * @param runnable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(ExecutorType exec, Runnable && runnable, Token token) +{ + return asio_spawn(exec, std::forward(runnable))(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 Runnable The coroutine type + * @tparam Token A Boost.Asio completion token + * @param ctx The execution context + * @param runnable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(Context & ctx, Runnable && runnable, Token token) +{ + return asio_spawn(ctx.get_executor(), std::forward(runnable))(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 &, Runnable &) + { + 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_init_promise_type + : boost_asio_promise_type_allocator_base< + boost::asio::associated_allocator_t> +{ + using args_type = completion_tuple_for_io_runnable; + + boost_asio_init_promise_type( + boost_asio_init &, + Handler & h, + Ex & exec, + Runnable &) + : 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 h_ = std::move(handler); + auto args_ = std::move(args); + asio_executor_adapter ex_ = std::move(ex); + h.destroy(); + + auto handler_ = + std::apply( + [&](auto ... args) + { + return boost::asio::append(std::move(h_), std::move(args)...); + }, + args_); + + auto exec = + boost::asio::get_associated_immediate_executor(handler_, ex_); + 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 + { + Runnable r; + const Ex &ex; + io_env env; + std::stop_source stop_src; + boost::asio::cancellation_slot cancel_slot; + + continuation c; + + wrapper(Runnable && r, const Ex &ex) + : r(std::move(r)), ex(ex) + { + } + + bool await_ready() {return r.await_ready(); } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle tr) + { + // always post in + auto h = r.handle(); + auto & p = h.promise(); + p.set_continuation(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(); + + + p.set_environment(&env); + c.h = h; + return ex.dispatch(c); + } + + completion_tuple_for_io_runnable 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(Runnable & r) + { + return wrapper{std::move(r), ex}; + } +}; + + +struct boost_asio_init +{ + template + void operator()( + Handler , + Ex, + Runnable runnable) + { + auto res = co_await runnable; + co_yield std::move(res); + } +}; + +template + requires + boost::asio::completion_token_for< + Token, + completion_signature_for_io_runnable + > +struct initialize_asio_spawn_helper +{ + template + static auto init(Executor ex, Runnable r, Token && tk) + -> decltype( boost::asio::async_initiate< + Token, + completion_signature_for_io_runnable>( + boost_asio_init{}, + tk, std::move(ex), std::move(r) + )) + { + return boost::asio::async_initiate< + Token, + completion_signature_for_io_runnable>( + 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, + Runnable>; +}; + +#endif //BOOST_CAPY_ASIO_BOOST_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..f198b00dc --- /dev/null +++ b/include/boost/capy/asio/detail/asio_coroutine_unique_handle.hpp @@ -0,0 +1,50 @@ +// +// 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()() + { + std::coroutine_handle::from_address( + handle.release() + ).resume(); + } +}; + +} + +#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..71642808d --- /dev/null +++ b/include/boost/capy/asio/detail/completion_handler.hpp @@ -0,0 +1,224 @@ +// +// 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 + +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), + 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)...); + + if (completed_immediately != nullptr + && *completed_immediately == completed_immediately_t::maybe) + *completed_immediately = completed_immediately_t::yes; + else + std::move(handle)(); + } +}; + + +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 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_; + std::tuple 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..b98d428b2 --- /dev/null +++ b/include/boost/capy/asio/detail/completion_traits.hpp @@ -0,0 +1,72 @@ +// +// 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 + +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 +{ + 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_runnable + = typename completion_traits< + decltype(std::declval().await_resume()), + noexcept(std::declval().await_resume()) + >::signature_type; + +template +using completion_tuple_for_io_runnable + = 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..cadb66cf6 --- /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, Runnable 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, Runnable 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 + * runnable 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 Runnable The coroutine type (must satisfy `IoRunnable`) + * + * @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 runnable The coroutine to spawn + */ + asio_spawn_op(Executor executor, Runnable runnable) + : executor_(std::move(executor)), runnable_(std::move(runnable)) + {} + + /** @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(runnable_), + 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(runnable_), + std::forward(token) + ); + } + + private: + Executor executor_; + Runnable runnable_; +}; + + +/** @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 Runnable The coroutine type (must satisfy `IoRunnable`) + * @param exec The executor to run the coroutine on + * @param runnable 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::io_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, Runnable && runnable) +{ + return asio_spawn_op(std::move(exec), std::forward(runnable)); +} + +/** @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 Runnable The coroutine type (must satisfy `IoRunnable`) + * @param ctx The execution context providing the executor + * @param runnable 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, Runnable && runnable) +{ + return asio_spawn_op(ctx.get_executor(), std::forward(runnable)); +} + +/** @} */ // 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..0c57c4a0b --- /dev/null +++ b/include/boost/capy/asio/standalone.hpp @@ -0,0 +1,735 @@ +// +// 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 + +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 Runnable The coroutine type + * @tparam Token A standalone Asio completion token + * @param exec The executor to run on + * @param runnable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token> +auto asio_spawn(ExecutorType exec, Runnable && runnable, Token token) +{ + return asio_spawn(exec, std::forward(runnable))(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 Runnable The coroutine type + * @tparam Token A standalone Asio completion token + * @param ctx The execution context + * @param runnable The coroutine to spawn + * @param token The completion token + * @return Depends on the token type + */ +template + > Token + > +auto asio_spawn(Context & ctx, Runnable && runnable, Token token) +{ + return asio_spawn(ctx.get_executor(), std::forward(runnable)) + (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 &, Runnable &) + { + 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_init_promise_type + : boost_asio_standalone_promise_type_allocator_base< + ::asio::associated_allocator_t + > +{ + using args_type = completion_tuple_for_io_runnable; + + boost_asio_standalone_init_promise_type( + boost_asio_standalone_init &, + Handler & h, + Ex & exec, + Runnable &) + : 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 h_ = std::move(handler); + auto args_ = std::move(args); + asio_executor_adapter ex_ = std::move(ex); + h.destroy(); + + auto handler_ = + std::apply( + [&](auto ... args) + { + return ::asio::append(std::move(h_), std::move(args)...); + }, + args_); + + auto exec = ::asio::get_associated_immediate_executor(handler_, ex_); + ::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 + { + Runnable r; + const Ex &ex; + io_env env; + std::stop_source stop_src; + ::asio::cancellation_slot cancel_slot; + + wrapper(Runnable && r, const Ex &ex) + : r(std::move(r)), ex(ex) + { + } + + continuation c; + + bool await_ready() {return r.await_ready(); } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle tr) + { + // always post in + auto h = r.handle(); + auto & p = h.promise(); + p.set_continuation(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(); + + + p.set_environment(&env); + c.h = h; + return ex.dispatch(c); + } + + completion_tuple_for_io_runnable 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(Runnable & r) + { + return wrapper{std::move(r), ex}; + } +}; + + + +struct boost_asio_standalone_init +{ + template + void operator()( + Handler , + Ex, + Runnable runnable) + { + auto res = co_await runnable; + co_yield std::move(res); + } +}; + +template + requires + ::asio::completion_token_for< + Token, + completion_signature_for_io_runnable + > +struct initialize_asio_standalone_spawn_helper +{ + template + static auto init(Executor ex, Runnable r, Token && tk) + -> decltype(::asio::async_initiate< + Token, + completion_signature_for_io_runnable>( + boost_asio_standalone_init{}, + tk, std::move(ex), std::move(r) + )) + { + return ::asio::async_initiate< + Token, + completion_signature_for_io_runnable>( + 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, + Runnable> +{ + using promise_type + = boost::capy::detail::boost_asio_standalone_init_promise_type< + Handler, + Executor, + Runnable>; +}; + +#endif // BOOST_CAPY_ASIO_STANDALONE_HPP + 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..0386ac7fb --- /dev/null +++ b/test/unit/asio.cpp @@ -0,0 +1,189 @@ +// +// 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 "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 run() + { + testExecutor(); + testFromExecutor(); + testFromAnyIOExecutor(); + testAsIoAwaitable(); + testAsioSpawn(); + testTimer(); + } +}; + + +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..831490b6e --- /dev/null +++ b/test/unit/asio_standalone.cpp @@ -0,0 +1,186 @@ +// +// 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 "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 run() + { + testExecutor(); + testFromExecutor(); + testFromAnyIOExecutor(); + testAsIoAwaitable(); + testAsioSpawn(); + testTimer(); + } +}; + + +TEST_SUITE( + asio_standalone_test, + "boost.capy.asio.standalone"); + +} // namespace capy +} // namespace boost + +#endif + diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp index ef070f9ac..4911cae1b 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