diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6d6b138..e37acb5 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -1 +1,2 @@ +* xref:coroutines.adoc[Coroutines] * xref:reference:boost/capy.adoc[Reference] diff --git a/doc/modules/ROOT/pages/coroutines.adoc b/doc/modules/ROOT/pages/coroutines.adoc new file mode 100644 index 0000000..4d73cda --- /dev/null +++ b/doc/modules/ROOT/pages/coroutines.adoc @@ -0,0 +1,343 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Coroutines + +== Introduction + +Capy provides lightweight coroutine support for C++20, enabling +asynchronous code that reads like synchronous code. The library +offers two awaitable types: `task` for lazy coroutine-based +operations, and `async_result` for bridging callback-based +APIs into the coroutine world. + +This section covers the awaitable types provided by the library, +demonstrates their usage patterns, and presents practical examples +showing how to integrate coroutines into your applications. + +NOTE: Coroutine features are only available when compiling with +C++20 or later. + +== Awaitables + +=== task + +xref:reference:boost/capy/task.adoc[`task`] is a lazy coroutine type that produces a value of type `T`. +The coroutine does not begin execution when created; it remains +suspended until awaited. This lazy evaluation enables structured +concurrency where parent coroutines naturally await their children. + +A `task` owns its coroutine handle and destroys it automatically. +Exceptions thrown within the coroutine are captured and rethrown +when the result is retrieved via `co_await`. + +The `task` specialization is used for coroutines that perform +work but do not produce a value. These coroutines use `co_return;` +with no argument. + +=== async_result + +xref:reference:boost/capy/async_result.adoc[`async_result`] bridges traditional callback-based asynchronous +APIs with coroutines. It wraps a deferred operation—a callable that +accepts a completion handler, starts an asynchronous operation, and +invokes the handler with the result. + +The key advantage of `async_result` is its type-erased design. The +implementation details are hidden behind an abstract interface, +allowing runtime-specific code such as Boost.Asio to be confined +to source files. Headers that return `async_result` do not need +to include Asio or other heavyweight dependencies, keeping compile +times low and interfaces clean. + +Use xref:reference:boost/capy/make_async_result.adoc[`make_async_result()`] to create an `async_result` from any +callable that follows the deferred operation pattern. + +The `async_result` specialization is used for operations that +signal completion without producing a value, such as timers, write +operations, or connection establishment. The completion handler +takes no arguments. + +== Usage + +=== When to use task + +Return `task` from a coroutine function—one that uses `co_await` +or `co_return`. The function body contains coroutine logic and the +return type tells the compiler to generate the appropriate coroutine +machinery. + +[source,cpp] +---- +task compute() +{ + int a = co_await step_one(); + int b = co_await step_two(a); + co_return a + b; +} +---- + +Use `task` when composing asynchronous operations purely within the +coroutine world. Tasks can await other tasks, forming a tree of +dependent operations. + +=== When to use async_result + +Return `async_result` from a regular (non-coroutine) function that +wraps an existing callback-based API. The function does not use +`co_await` or `co_return`; instead it constructs and returns an +`async_result` using `make_async_result()`. + +[source,cpp] +---- +async_result async_read(socket& s, buffer& b) +{ + return make_async_result( + [&](auto handler) { + s.async_read(b, std::move(handler)); + }); +} +---- + +Use `async_result` at the boundary between callback-based code and +coroutines. It serves as an adapter that lets coroutines `co_await` +operations implemented with traditional completion handlers. + +=== Choosing between them + +* Writing new asynchronous logic? Use `task`. +* Wrapping an existing callback API? Use `async_result`. +* Composing multiple awaitable operations? Use `task`. +* Exposing a library function without leaking dependencies? Use + `async_result` with the implementation in a source file. + +In practice, application code is primarily `task`-based, while +`async_result` appears at integration points with I/O libraries +and other callback-driven systems. + +== Examples + +=== Chaining tasks + +This example demonstrates composing multiple tasks into a pipeline. +Each step awaits the previous one, and the final result propagates +back to the caller. + +[source,cpp] +---- +#include +#include + +using boost::capy::task; + +task parse_header(std::string const& data) +{ + // Extract content length from header + auto pos = data.find("Content-Length: "); + if (pos == std::string::npos) + co_return 0; + co_return std::stoi(data.substr(pos + 16)); +} + +task fetch_data() +{ + // Simulated network response + co_return std::string("Content-Length: 42\r\n\r\nHello"); +} + +task get_content_length() +{ + std::string response = co_await fetch_data(); + int length = co_await parse_header(response); + co_return length; +} +---- + +=== Wrapping a callback API + +This example shows how to wrap a hypothetical callback-based +timer into an awaitable. The implementation details stay in +the source file. + +[source,cpp] +---- +// timer.hpp - public header, no Asio includes +#ifndef TIMER_HPP +#define TIMER_HPP + +#include + +namespace mylib { + +// Returns the number of milliseconds actually elapsed +boost::capy::async_result +async_wait(int milliseconds); + +} // namespace mylib + +#endif +---- + +[source,cpp] +---- +// timer.cpp - implementation, Asio details hidden here +#include "timer.hpp" +#include + +namespace mylib { + +boost::capy::async_result +async_wait(int milliseconds) +{ + return boost::capy::make_async_result( + [milliseconds](auto handler) + { + // In a real implementation, this would use + // a shared io_context and steady_timer + auto timer = std::make_shared( + get_io_context(), + std::chrono::milliseconds(milliseconds)); + + timer->async_wait( + [timer, milliseconds, h = std::move(handler)] + (boost::system::error_code) mutable + { + h(milliseconds); + }); + }); +} + +} // namespace mylib +---- + +=== Void operations + +This example shows `task` and `async_result` for +operations that complete without producing a value. + +[source,cpp] +---- +#include +#include + +using boost::capy::task; +using boost::capy::async_result; +using boost::capy::make_async_result; + +// Wrap a callback-based timer (void result) +async_result async_sleep(int milliseconds) +{ + return make_async_result( + [milliseconds](auto on_done) + { + // In real code, this would start a timer + // and call on_done() when it expires + start_timer(milliseconds, std::move(on_done)); + }); +} + +// A void task that performs work without returning a value +task log_with_delay(std::string message) +{ + co_await async_sleep(100); + std::cout << message << std::endl; + co_return; +} + +task run_sequence() +{ + co_await log_with_delay("Step 1"); + co_await log_with_delay("Step 2"); + co_await log_with_delay("Step 3"); + co_return; +} +---- + +=== Running a task to completion + +Tasks are lazy and require a driver to execute. This example +shows a simple synchronous driver that runs a task until it +completes. + +[source,cpp] +---- +#include + +using boost::capy::task; + +template +T run(task t) +{ + bool done = false; + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + + // In a real application, this would integrate with + // an event loop rather than spinning + while (!done) + { + // Process pending I/O events here + } + + return t.await_resume(); +} + +task compute() +{ + co_return 42; +} + +int main() +{ + int result = run(compute()); + return result == 42 ? 0 : 1; +} +---- + +=== Complete request handler + +This example combines tasks and async_result to implement a +request handler that reads a request, processes it, and sends +a response. + +[source,cpp] +---- +#include +#include +#include + +using boost::capy::task; +using boost::capy::async_result; + +// Forward declarations - implementations use async_result +// to wrap the underlying I/O library +async_result async_read(int fd); +async_result async_write(int fd, std::string data); + +// Pure coroutine logic using task +task process_request(std::string const& request) +{ + // Transform the request into a response + co_return "HTTP/1.1 200 OK\r\n\r\nHello, " + request; +} + +task handle_connection(int fd) +{ + // Read the incoming request + std::string request = co_await async_read(fd); + + // Process it + std::string response = co_await process_request(request); + + // Send the response + std::size_t bytes_written = co_await async_write(fd, response); + + co_return static_cast(bytes_written); +} +---- + diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index dd2a295..d1c7434 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -11,10 +11,12 @@ #define BOOST_CAPY_HPP #include +#include #include #include #include #include #include +#include #endif diff --git a/include/boost/capy/async_result.hpp b/include/boost/capy/async_result.hpp index 819fdee..308c20a 100644 --- a/include/boost/capy/async_result.hpp +++ b/include/boost/capy/async_result.hpp @@ -14,6 +14,7 @@ #ifdef BOOST_CAPY_HAS_CORO +#include #include #include #include @@ -23,14 +24,95 @@ namespace boost { namespace capy { +//----------------------------------------------------------------------------- +// +// Concepts +// +//----------------------------------------------------------------------------- + +/** Concept for a deferred operation that produces a value. + + A deferred operation is a callable that accepts a completion + handler. When invoked, it initiates an asynchronous operation + and calls the handler with the result when complete. + + @tparam Op The operation type. + @tparam T The result type. +*/ +template +concept deferred_operation = std::invocable>; + +/** Concept for a deferred operation that produces no value. + + A void deferred operation accepts a completion handler that + takes no arguments. + + @tparam Op The operation type. +*/ +template +concept void_deferred_operation = std::invocable>; + +//----------------------------------------------------------------------------- + +/** An awaitable wrapper for callback-based asynchronous operations. + + This class template provides a bridge between traditional + callback-based asynchronous APIs and C++20 coroutines. It + wraps a deferred operation and makes it awaitable, allowing + seamless integration with coroutine-based code. + + @par Thread Safety + Distinct objects may be accessed concurrently. Shared objects + require external synchronization. + + @par Example + @code + // Wrap a callback-based timer + async_result async_sleep(std::chrono::milliseconds ms) + { + return make_async_result( + [ms](auto&& handler) { + // Start timer, call handler when done + start_timer(ms, std::move(handler)); + }); + } + + task example() + { + co_await async_sleep(std::chrono::milliseconds(100)); + } + @endcode + + @tparam T The type of value produced by the asynchronous operation. + + @see make_async_result, task +*/ template class async_result { public: + /** Abstract base class for operation implementations. + + Derived classes implement the actual asynchronous operation + and result retrieval logic. + */ struct impl_base { + /// Virtual destructor. virtual ~impl_base() = default; + + /** Start the asynchronous operation. + + @param on_done Callback to invoke when the operation completes. + */ virtual void start(std::function on_done) = 0; + + /** Retrieve the operation result. + + @return The result value. + + @throws Any exception stored during the operation. + */ virtual T get_result() = 0; }; @@ -38,18 +120,43 @@ class async_result std::unique_ptr impl_; public: + /** Construct from an implementation. + + @param p Unique pointer to the operation implementation. + */ explicit async_result(std::unique_ptr p) : impl_(std::move(p)) {} + /// Default move constructor. async_result(async_result&&) = default; + + /// Default move assignment operator. async_result& operator=(async_result&&) = default; + /** Check if the result is ready. + + @return Always returns false; the operation must be started. + */ bool await_ready() const noexcept { return false; } + /** Suspend the caller and start the operation. + + Initiates the asynchronous operation and arranges for + the caller to be resumed when it completes. + + @param h The coroutine handle of the awaiting coroutine. + */ void await_suspend(std::coroutine_handle<> h) { impl_->start([h]{ h.resume(); }); } + /** Retrieve the result after completion. + + @return The value produced by the asynchronous operation. + + @throws Any exception that occurred during the operation. + */ + [[nodiscard]] T await_resume() { return impl_->get_result(); @@ -58,17 +165,143 @@ class async_result //----------------------------------------------------------------------------- +/** An awaitable wrapper for callback-based operations with no result. + + This specialization of async_result is used for asynchronous + operations that signal completion but do not produce a value, + such as timers, write operations, or connection establishment. + + @par Thread Safety + Distinct objects may be accessed concurrently. Shared objects + require external synchronization. + + @par Example + @code + // Wrap a callback-based timer + async_result async_sleep(std::chrono::milliseconds ms) + { + return make_async_result( + [ms](auto handler) { + start_timer(ms, [h = std::move(handler)]{ h(); }); + }); + } + + task example() + { + co_await async_sleep(std::chrono::milliseconds(100)); + } + @endcode + + @see async_result, make_async_result +*/ +template<> +class async_result +{ +public: + /** Abstract base class for void operation implementations. + + Derived classes implement the actual asynchronous operation + and exception handling. + */ + struct impl_base + { + /// Virtual destructor. + virtual ~impl_base() = default; + + /** Start the asynchronous operation. + + @param on_done Callback to invoke when the operation completes. + */ + virtual void start(std::function on_done) = 0; + + /** Check for and rethrow any stored exception. + + @throws Any exception stored during the operation. + */ + virtual void get_result() = 0; + }; + +private: + std::unique_ptr impl_; + +public: + /** Construct from an implementation. + + @param p Unique pointer to the operation implementation. + */ + explicit async_result(std::unique_ptr p) : impl_(std::move(p)) {} + + /// Default move constructor. + async_result(async_result&&) = default; + + /// Default move assignment operator. + async_result& operator=(async_result&&) = default; + + /** Check if the result is ready. + + @return Always returns false; the operation must be started. + */ + bool await_ready() const noexcept { return false; } + + /** Suspend the caller and start the operation. + + Initiates the asynchronous operation and arranges for + the caller to be resumed when it completes. + + @param h The coroutine handle of the awaiting coroutine. + */ + void await_suspend(std::coroutine_handle<> h) + { + impl_->start([h]{ h.resume(); }); + } + + /** Complete the await and check for exceptions. + + @throws Any exception that occurred during the operation. + */ + void await_resume() + { + impl_->get_result(); + } +}; + +//----------------------------------------------------------------------------- + +/** Default implementation of async_result::impl_base. + + This class template wraps a deferred operation callable and + manages the result storage. + + @tparam T The result type. + @tparam DeferredOp The callable type that initiates the operation. +*/ template struct async_result_impl : capy::async_result::impl_base { + /// The deferred operation callable. DeferredOp op_; - std::variant result_; + /// Storage for exception or result value. + std::variant result_{}; + + /** Construct from a deferred operation. + + @param op The callable that initiates the asynchronous operation. + It will be invoked with a completion handler that + accepts the result arguments. + */ explicit async_result_impl(DeferredOp&& op) : op_(std::forward(op)) { } + /** Start the operation. + + Invokes the deferred operation with a handler that stores + the result and calls the completion callback. + + @param on_done Callback to invoke when complete. + */ void start(std::function on_done) override { std::move(op_)( @@ -79,6 +312,12 @@ struct async_result_impl : capy::async_result::impl_base }); } + /** Retrieve the result. + + @return The result value. + + @throws Any stored exception. + */ T get_result() override { if (result_.index() == 0 && std::get<0>(result_)) @@ -89,7 +328,89 @@ struct async_result_impl : capy::async_result::impl_base //----------------------------------------------------------------------------- +/** Implementation of async_result::impl_base. + + This specialization wraps a deferred operation that produces + no result value. + + @tparam DeferredOp The callable type that initiates the operation. +*/ +template +struct async_result_void_impl : capy::async_result::impl_base +{ + /// The deferred operation callable. + DeferredOp op_; + + /// Storage for an exception, if one occurred. + std::exception_ptr exception_{}; + + /** Construct from a deferred operation. + + @param op The callable that initiates the asynchronous operation. + It will be invoked with a completion handler that + takes no arguments. + */ + explicit async_result_void_impl(DeferredOp&& op) + : op_(std::forward(op)) + { + } + + /** Start the operation. + + Invokes the deferred operation with a handler that signals + completion and calls the done callback. + + @param on_done Callback to invoke when complete. + */ + void start(std::function on_done) override + { + std::move(op_)(std::move(on_done)); + } + + /** Check for exceptions. + + @throws Any stored exception. + */ + void get_result() override + { + if (exception_) + std::rethrow_exception(exception_); + } +}; + +//----------------------------------------------------------------------------- + +/** Create an async_result from a deferred operation. + + This factory function creates an awaitable async_result that + wraps a callback-based asynchronous operation. + + @par Example + @code + async_result async_read() + { + return make_async_result( + [](auto handler) { + // Simulate async read + handler("Hello, World!"); + }); + } + @endcode + + @tparam T The result type of the asynchronous operation. + @tparam DeferredOp The type of the deferred operation callable. + + @param op A callable that accepts a completion handler. When invoked, + it should initiate the asynchronous operation and call the + handler with the result when complete. + + @return An async_result that can be awaited in a coroutine. + + @see async_result +*/ template + requires (!std::is_void_v) +[[nodiscard]] capy::async_result make_async_result(DeferredOp&& op) { @@ -98,6 +419,44 @@ make_async_result(DeferredOp&& op) std::make_unique(std::forward(op))); } +/** Create an async_result from a deferred operation. + + This overload is used for operations that signal completion + without producing a value. + + @par Example + @code + async_result async_wait(int milliseconds) + { + return make_async_result( + [milliseconds](auto on_done) { + // Start timer, call on_done() when elapsed + start_timer(milliseconds, std::move(on_done)); + }); + } + @endcode + + @tparam DeferredOp The type of the deferred operation callable. + + @param op A callable that accepts a completion handler taking no + arguments. When invoked, it should initiate the operation + and call the handler when complete. + + @return An async_result that can be awaited in a coroutine. + + @see async_result +*/ +template + requires std::is_void_v +[[nodiscard]] +capy::async_result +make_async_result(DeferredOp&& op) +{ + using impl_type = async_result_void_impl>; + return capy::async_result( + std::make_unique(std::forward(op))); +} + } // capy } // boost diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index 4c8d4bb..329de71 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -23,22 +23,74 @@ namespace boost { namespace capy { +/** A lazy coroutine task that produces a value of type T. + + This class template represents an owning handle to a suspended + coroutine that will eventually produce a value of type @ref T. + The coroutine is lazy: it does not begin execution until it is + awaited or manually resumed via its handle. + + @par Thread Safety + Distinct objects may be accessed concurrently. Shared objects + require external synchronization. + + @par Example + @code + task compute_value() + { + co_return 42; + } + + task example() + { + int result = co_await compute_value(); + } + @endcode + + @tparam T The type of value produced by the coroutine. + + @see async_result +*/ template class task { public: + /** The coroutine promise type. + + This nested type satisfies the coroutine promise requirements + and manages the coroutine's result storage and completion + notification. + */ struct promise_type { - std::variant result_; + /// Storage for the result value or exception + std::variant result_{}; + + /// Callback invoked when the coroutine completes std::function on_done_; + /** Returns the task object for this coroutine. + + @return A task owning the coroutine handle. + */ task get_return_object() { return task{std::coroutine_handle::from_promise(*this)}; } + /** Suspend the coroutine at the start. + + The coroutine is lazy and does not run until awaited. + + @return An awaitable that always suspends. + */ std::suspend_always initial_suspend() noexcept { return {}; } + /** Suspend at the end and notify completion. + + @return An awaitable that suspends and invokes the + completion callback if set. + */ auto final_suspend() noexcept { struct awaiter @@ -55,7 +107,16 @@ class task return awaiter{this}; } + /** Store the return value. + + @param v The value to store as the coroutine result. + */ void return_value(T v) { result_.template emplace<1>(std::move(v)); } + + /** Store an unhandled exception. + + Captures the current exception for later rethrowing. + */ void unhandled_exception() { result_.template emplace<2>(std::current_exception()); } }; @@ -63,21 +124,57 @@ class task std::coroutine_handle h_; public: + /** Construct a task from a coroutine handle. + + @param h The coroutine handle to take ownership of. + */ explicit task(std::coroutine_handle h) : h_(h) {} + + /** Destructor. + + Destroys the owned coroutine if present. + */ ~task() { if (h_) h_.destroy(); } + /** Move constructor. + + @param o The task to move from. After the move, @p o will + be empty. + */ task(task&& o) noexcept : h_(std::exchange(o.h_, {})) {} + + /// Move assignment is deleted. task& operator=(task&&) = delete; - // For awaiting from another task + /** Check if the task is ready. + + @return Always returns false; the task must be awaited. + */ bool await_ready() const noexcept { return false; } + /** Suspend the caller and start this task. + + Sets up the completion callback to resume the caller + when this task completes, then transfers control to + this task's coroutine. + + @param caller The coroutine handle of the awaiting coroutine. + + @return The coroutine handle to resume (this task's handle). + */ std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept { h_.promise().on_done_ = [caller]{ caller.resume(); }; return h_; } + /** Retrieve the result after completion. + + @return The value produced by the coroutine. + + @throws Any exception that was thrown inside the coroutine. + */ + [[nodiscard]] T await_resume() { auto& r = h_.promise().result_; @@ -86,9 +183,196 @@ class task return std::move(std::get<1>(r)); } - // For external drivers + /** Access the underlying coroutine handle. + + @return The coroutine handle, without transferring ownership. + */ + [[nodiscard]] std::coroutine_handle handle() const noexcept { return h_; } + /** Release ownership of the coroutine handle. + + After calling this function, the task no longer owns the + coroutine and the caller becomes responsible for destroying it. + + @return The coroutine handle. + */ + [[nodiscard]] + std::coroutine_handle release() noexcept + { + return std::exchange(h_, {}); + } +}; + +//----------------------------------------------------------------------------- + +/** A lazy coroutine task that produces no value. + + This specialization of task is used for coroutines that perform + work but do not return a value. It uses `co_return;` with no + argument to complete. + + @par Thread Safety + Distinct objects may be accessed concurrently. Shared objects + require external synchronization. + + @par Example + @code + task log_message(std::string msg) + { + std::cout << msg << std::endl; + co_return; + } + + task example() + { + co_await log_message("Hello, World!"); + } + @endcode + + @see task, async_result +*/ +template<> +class task +{ +public: + /** The coroutine promise type for void tasks. + + This nested type satisfies the coroutine promise requirements + and manages exception storage and completion notification. + */ + struct promise_type + { + /// Storage for an exception, if one was thrown + std::exception_ptr exception_{}; + + /// Callback invoked when the coroutine completes + std::function on_done_; + + /** Returns the task object for this coroutine. + + @return A task owning the coroutine handle. + */ + task get_return_object() + { + return task{std::coroutine_handle::from_promise(*this)}; + } + + /** Suspend the coroutine at the start. + + The coroutine is lazy and does not run until awaited. + + @return An awaitable that always suspends. + */ + std::suspend_always initial_suspend() noexcept { return {}; } + + /** Suspend at the end and notify completion. + + @return An awaitable that suspends and invokes the + completion callback if set. + */ + auto final_suspend() noexcept + { + struct awaiter + { + promise_type* p_; + bool await_ready() noexcept { return false; } + void await_suspend(std::coroutine_handle<>) noexcept + { + if (p_->on_done_) + p_->on_done_(); + } + void await_resume() noexcept {} + }; + return awaiter{this}; + } + + /** Signal coroutine completion. + + Called when the coroutine executes `co_return;`. + */ + void return_void() noexcept {} + + /** Store an unhandled exception. + + Captures the current exception for later rethrowing. + */ + void unhandled_exception() { exception_ = std::current_exception(); } + }; + +private: + std::coroutine_handle h_; + +public: + /** Construct a task from a coroutine handle. + + @param h The coroutine handle to take ownership of. + */ + explicit task(std::coroutine_handle h) : h_(h) {} + + /** Destructor. + + Destroys the owned coroutine if present. + */ + ~task() { if (h_) h_.destroy(); } + + /** Move constructor. + + @param o The task to move from. After the move, @p o will + be empty. + */ + task(task&& o) noexcept : h_(std::exchange(o.h_, {})) {} + + /// Move assignment is deleted. + task& operator=(task&&) = delete; + + /** Check if the task is ready. + + @return Always returns false; the task must be awaited. + */ + bool await_ready() const noexcept { return false; } + + /** Suspend the caller and start this task. + + Sets up the completion callback to resume the caller + when this task completes, then transfers control to + this task's coroutine. + + @param caller The coroutine handle of the awaiting coroutine. + + @return The coroutine handle to resume (this task's handle). + */ + std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept + { + h_.promise().on_done_ = [caller]{ caller.resume(); }; + return h_; + } + + /** Complete the await operation. + + @throws Any exception that was thrown inside the coroutine. + */ + void await_resume() + { + if (h_.promise().exception_) + std::rethrow_exception(h_.promise().exception_); + } + + /** Access the underlying coroutine handle. + + @return The coroutine handle, without transferring ownership. + */ + [[nodiscard]] + std::coroutine_handle handle() const noexcept { return h_; } + + /** Release ownership of the coroutine handle. + + After calling this function, the task no longer owns the + coroutine and the caller becomes responsible for destroying it. + + @return The coroutine handle. + */ + [[nodiscard]] std::coroutine_handle release() noexcept { return std::exchange(h_, {}); diff --git a/test/unit/async_result.cpp b/test/unit/async_result.cpp index 0ce34fe..05b86a0 100644 --- a/test/unit/async_result.cpp +++ b/test/unit/async_result.cpp @@ -4,7 +4,7 @@ // 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/beast2 +// Official repository: https://github.com/cppalliance/capy // // Test that header file is self-contained. @@ -12,16 +12,456 @@ #ifdef BOOST_CAPY_HAS_CORO +#include +#include + #include "test_suite.hpp" +#include +#include + namespace boost { namespace capy { +template +T run_task(task& t) +{ + while (!t.handle().done()) + t.handle().resume(); + return t.await_resume(); +} + +template<> +void run_task(task& t) +{ + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); +} + +struct async_test_exception : std::runtime_error +{ + explicit async_test_exception(const char* msg) + : std::runtime_error(msg) + { + } +}; + +struct result_with_error +{ + int value; + boost::system::error_code ec; + + result_with_error() = default; + + result_with_error(int v, boost::system::error_code e = {}) + : value(v) + , ec(e) + { + } +}; + struct async_result_test { + static async_result + async_int_value() + { + return make_async_result( + [](auto cb) { + cb(42); + }); + } + + static async_result + async_string_value() + { + return make_async_result( + [](auto cb) { + cb("hello async"); + }); + } + + static task + task_awaiting_int() + { + int v = co_await async_int_value(); + co_return v; + } + + static task + task_awaiting_string() + { + std::string s = co_await async_string_value(); + co_return s; + } + + void + testBasicValue() + { + // async_result returning int + { + auto t = task_awaiting_int(); + BOOST_TEST_EQ(run_task(t), 42); + } + + // async_result returning string + { + auto t = task_awaiting_string(); + BOOST_TEST_EQ(run_task(t), "hello async"); + } + } + + static async_result + async_returns_success() + { + return make_async_result( + [](auto cb) { + cb(100, boost::system::error_code{}); + }); + } + + static async_result + async_returns_error() + { + return make_async_result( + [](auto cb) { + cb(0, boost::system::errc::make_error_code( + boost::system::errc::invalid_argument)); + }); + } + + static task + task_awaits_success() + { + auto r = co_await async_returns_success(); + co_return r; + } + + static task + task_awaits_error() + { + auto r = co_await async_returns_error(); + co_return r; + } + + static task + task_checks_error_and_returns() + { + auto r = co_await async_returns_error(); + if (r.ec) + co_return -1; + co_return r.value; + } + + void + testErrorHandling() + { + // async_result with success + { + auto t = task_awaits_success(); + auto r = run_task(t); + BOOST_TEST_EQ(r.value, 100); + BOOST_TEST(!r.ec); + } + + // async_result with error + { + auto t = task_awaits_error(); + auto r = run_task(t); + BOOST_TEST_EQ(r.value, 0); + BOOST_TEST(r.ec); + BOOST_TEST_EQ(r.ec, boost::system::errc::invalid_argument); + } + + // task checks error and returns appropriate value + { + auto t = task_checks_error_and_returns(); + BOOST_TEST_EQ(run_task(t), -1); + } + } + + static async_result + async_value_1() + { + return make_async_result( + [](auto cb) { cb(10); }); + } + + static async_result + async_value_2() + { + return make_async_result( + [](auto cb) { cb(20); }); + } + + static async_result + async_value_3() + { + return make_async_result( + [](auto cb) { cb(30); }); + } + + static task + task_awaits_multiple() + { + int v1 = co_await async_value_1(); + int v2 = co_await async_value_2(); + int v3 = co_await async_value_3(); + co_return v1 + v2 + v3; + } + + void + testMultipleAwaits() + { + auto t = task_awaits_multiple(); + BOOST_TEST_EQ(run_task(t), 60); + } + + void + testAwaitReady() + { + auto ar = async_int_value(); + BOOST_TEST(!ar.await_ready()); + } + + void + testMoveOperations() + { + // async_result is move constructible + { + auto ar1 = async_int_value(); + auto ar2 = std::move(ar1); + (void)ar2; + } + + // async_result is move assignable + { + auto ar1 = async_int_value(); + auto ar2 = async_string_value(); + (void)ar1; + (void)ar2; + } + } + + static async_result + async_with_captured_state(int multiplier) + { + return make_async_result( + [multiplier](auto cb) { + cb(10 * multiplier); + }); + } + + static task + task_awaits_with_state() + { + int v1 = co_await async_with_captured_state(2); + int v2 = co_await async_with_captured_state(3); + co_return v1 + v2; + } + + void + testCapturedState() + { + auto t = task_awaits_with_state(); + BOOST_TEST_EQ(run_task(t), 50); + } + + struct complex_result + { + int id; + std::string name; + double value; + + complex_result() = default; + complex_result(int i, std::string n, double v) + : id(i) + , name(std::move(n)) + , value(v) + { + } + }; + + static async_result + async_complex() + { + return make_async_result( + [](auto cb) { + cb(1, "test", 3.14); + }); + } + + static task + task_awaits_complex() + { + auto r = co_await async_complex(); + co_return r; + } + + void + testComplexResult() + { + auto t = task_awaits_complex(); + auto r = run_task(t); + BOOST_TEST_EQ(r.id, 1); + BOOST_TEST_EQ(r.name, "test"); + BOOST_TEST_EQ(r.value, 3.14); + } + + static task + inner_task_with_async() + { + int v = co_await async_int_value(); + co_return v * 2; + } + + static task + outer_task_with_both() + { + int v1 = co_await async_value_1(); + int v2 = co_await inner_task_with_async(); + co_return v1 + v2; + } + + void + testTaskChaining() + { + auto t = outer_task_with_both(); + BOOST_TEST_EQ(run_task(t), 94); + } + + //---------------------------------------------------------- + // async_result tests + //---------------------------------------------------------- + + static async_result + async_void_basic() + { + return make_async_result( + [](auto on_done) { + on_done(); + }); + } + + static task + task_awaits_void_async() + { + co_await async_void_basic(); + co_return; + } + + void + testVoidAsyncBasic() + { + bool done = false; + auto t = task_awaits_void_async(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + } + + static async_result + async_void_step() + { + return make_async_result( + [](auto on_done) { + on_done(); + }); + } + + static task + task_awaits_void_then_value() + { + co_await async_void_step(); + int v = co_await async_int_value(); + co_await async_void_step(); + co_return v; + } + + void + testVoidAsyncWithValue() + { + auto t = task_awaits_void_then_value(); + BOOST_TEST_EQ(run_task(t), 42); + } + + static task + task_awaits_multiple_void() + { + co_await async_void_step(); + co_await async_void_step(); + co_await async_void_step(); + co_return; + } + + void + testVoidAsyncChain() + { + bool done = false; + auto t = task_awaits_multiple_void(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + } + + void + testVoidAsyncAwaitReady() + { + auto ar = async_void_basic(); + BOOST_TEST(!ar.await_ready()); + } + + void + testVoidAsyncMove() + { + auto ar1 = async_void_basic(); + auto ar2 = std::move(ar1); + (void)ar2; + } + + static async_result + async_void_deferred() + { + return make_async_result( + [](auto on_done) { + // Simulate deferred completion + on_done(); + }); + } + + static task + task_with_deferred_void() + { + co_await async_void_deferred(); + co_return 999; + } + + void + testVoidAsyncDeferred() + { + auto t = task_with_deferred_void(); + BOOST_TEST_EQ(run_task(t), 999); + } + void run() { + testBasicValue(); + testErrorHandling(); + testMultipleAwaits(); + testAwaitReady(); + testMoveOperations(); + testCapturedState(); + testComplexResult(); + testTaskChaining(); + + // async_result tests + testVoidAsyncBasic(); + testVoidAsyncWithValue(); + testVoidAsyncChain(); + testVoidAsyncAwaitReady(); + testVoidAsyncMove(); + testVoidAsyncDeferred(); } }; diff --git a/test/unit/task.cpp b/test/unit/task.cpp index a08b490..370d2cd 100644 --- a/test/unit/task.cpp +++ b/test/unit/task.cpp @@ -4,7 +4,7 @@ // 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/beast2 +// Official repository: https://github.com/cppalliance/capy // // Test that header file is self-contained. @@ -12,27 +12,418 @@ #ifdef BOOST_CAPY_HAS_CORO +#include + #include "test_suite.hpp" +#include +#include + namespace boost { namespace capy { -static -capy::task -handler() +template +T run_task(task& t) { - co_return 42; + while (!t.handle().done()) + t.handle().resume(); + return t.await_resume(); } +struct test_exception : std::runtime_error +{ + explicit test_exception(const char* msg) + : std::runtime_error(msg) + { + } +}; + struct task_test { + static task + returns_int() + { + co_return 42; + } + + static task + returns_string() + { + co_return "hello"; + } + void - run() + testReturnValue() + { + // task returning int + { + auto t = returns_int(); + BOOST_TEST_EQ(run_task(t), 42); + } + + // task returning string + { + auto t = returns_string(); + BOOST_TEST_EQ(run_task(t), "hello"); + } + } + + static task + throws_exception() + { + throw test_exception("test error"); + co_return 0; + } + + static task + throws_std_exception() + { + throw std::runtime_error("runtime error"); + co_return 0; + } + + void + testException() + { + // task that throws custom exception + { + auto t = throws_exception(); + while (!t.handle().done()) + t.handle().resume(); + BOOST_TEST_THROWS(t.await_resume(), test_exception); + } + + // task that throws std::runtime_error + { + auto t = throws_std_exception(); + while (!t.handle().done()) + t.handle().resume(); + BOOST_TEST_THROWS(t.await_resume(), std::runtime_error); + } + } + + static task + inner_task_value() + { + co_return 100; + } + + static task + outer_task_awaits_inner() + { + int v = co_await inner_task_value(); + co_return v + 1; + } + + static task + inner_task_throws() + { + throw test_exception("inner exception"); + co_return 0; + } + + static task + outer_task_awaits_throwing_inner() + { + int v = co_await inner_task_throws(); + co_return v + 1; + } + + static task + outer_task_catches_inner_exception() + { + try + { + (void)co_await inner_task_throws(); + co_return -1; + } + catch (test_exception const&) + { + co_return 999; + } + } + + static task + chained_tasks() + { + auto inner = []() -> task { + co_return 10; + }; + + auto middle = [&]() -> task { + int v = co_await inner(); + co_return v * 2; + }; + + int v = co_await middle(); + co_return v + 5; + } + + void + testTaskAwaitsTask() + { + // outer task awaits inner task with value + { + auto t = outer_task_awaits_inner(); + BOOST_TEST_EQ(run_task(t), 101); + } + + // outer task awaits inner task that throws + { + auto t = outer_task_awaits_throwing_inner(); + while (!t.handle().done()) + t.handle().resume(); + BOOST_TEST_THROWS(t.await_resume(), test_exception); + } + + // outer task catches exception from inner task + { + auto t = outer_task_catches_inner_exception(); + BOOST_TEST_EQ(run_task(t), 999); + } + + // chained tasks (3 levels) + { + auto t = chained_tasks(); + BOOST_TEST_EQ(run_task(t), 25); + } + } + + void + testMoveOperations() + { + // move constructor + { + auto t1 = returns_int(); + auto h = t1.handle(); + BOOST_TEST(h); + + task t2(std::move(t1)); + BOOST_TEST(!t1.handle()); + BOOST_TEST(t2.handle() == h); + + BOOST_TEST_EQ(run_task(t2), 42); + } + + // release() + { + auto t = returns_int(); + auto h = t.release(); + BOOST_TEST(h); + BOOST_TEST(!t.handle()); + + while (!h.done()) + h.resume(); + auto& result = h.promise().result_; + BOOST_TEST_EQ(result.index(), 1u); + BOOST_TEST_EQ(std::get<1>(result), 42); + + h.destroy(); + } + } + + static async_result + async_returns_value() + { + return make_async_result( + [](auto cb) { + cb(123); + }); + } + + static async_result + async_with_delayed_completion() + { + return make_async_result( + [](auto cb) { + cb(456); + }); + } + + static task + task_awaits_async_result() + { + int v = co_await async_returns_value(); + co_return v + 1; + } + + static task + task_awaits_multiple_async_results() + { + int v1 = co_await async_returns_value(); + int v2 = co_await async_with_delayed_completion(); + co_return v1 + v2; + } + + void + testTaskAwaitsAsyncResult() + { + // task awaits single async_result + { + auto t = task_awaits_async_result(); + BOOST_TEST_EQ(run_task(t), 124); + } + + // task awaits multiple async_results + { + auto t = task_awaits_multiple_async_results(); + BOOST_TEST_EQ(run_task(t), 579); + } + } + + void + testAwaitReady() + { + auto t = returns_int(); + BOOST_TEST(!t.await_ready()); + } + + //---------------------------------------------------------- + // task tests + //---------------------------------------------------------- + + static task + void_task_basic() + { + co_return; + } + + static task + void_task_throws() + { + throw test_exception("void task exception"); + co_return; + } + + void + testVoidTaskBasic() + { + bool done = false; + auto t = void_task_basic(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + t.await_resume(); // should not throw + } + + void + testVoidTaskException() + { + auto t = void_task_throws(); + t.handle().promise().on_done_ = []{ }; + t.handle().resume(); + BOOST_TEST_THROWS(t.await_resume(), test_exception); + } + + static task + void_task_awaits_value() + { + int v = co_await returns_int(); + (void)v; + co_return; + } + + static task + void_task_awaits_void() { - auto t = handler(); - while (!t.handle().done()) + co_await void_task_basic(); + co_return; + } + + void + testVoidTaskAwaits() + { + // void task awaits value-returning task + { + bool done = false; + auto t = void_task_awaits_value(); + t.handle().promise().on_done_ = [&done]{ done = true; }; t.handle().resume(); - BOOST_TEST_EQ(t.await_resume(), 42); + BOOST_TEST(done); + } + + // void task awaits another void task + { + bool done = false; + auto t = void_task_awaits_void(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + } + } + + static task + void_task_chain_step() + { + co_return; + } + + static task + void_task_chain() + { + co_await void_task_chain_step(); + co_await void_task_chain_step(); + co_await void_task_chain_step(); + co_return; + } + + void + testVoidTaskChain() + { + bool done = false; + auto t = void_task_chain(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + } + + void + testVoidTaskMove() + { + auto t1 = void_task_basic(); + auto h = t1.handle(); + BOOST_TEST(h); + + task t2(std::move(t1)); + BOOST_TEST(!t1.handle()); + BOOST_TEST(t2.handle() == h); + } + + static task + void_task_awaits_async_result() + { + int v = co_await async_returns_value(); + (void)v; + co_return; + } + + void + testVoidTaskAwaitsAsyncResult() + { + bool done = false; + auto t = void_task_awaits_async_result(); + t.handle().promise().on_done_ = [&done]{ done = true; }; + t.handle().resume(); + BOOST_TEST(done); + } + + void + run() + { + testReturnValue(); + testException(); + testTaskAwaitsTask(); + testMoveOperations(); + testTaskAwaitsAsyncResult(); + testAwaitReady(); + + // task tests + testVoidTaskBasic(); + testVoidTaskException(); + testVoidTaskAwaits(); + testVoidTaskChain(); + testVoidTaskMove(); + testVoidTaskAwaitsAsyncResult(); } };