diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc index e98629b68..78cfda5ba 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc @@ -50,13 +50,24 @@ The `promise_type` nested structure provides the minimum scaffolding the compile The presence of `co_return` transforms what looks like a regular function into a coroutine. +[[awaitables_and_awaiters]] == Awaitables and Awaiters -When you write `co_await expr`, the expression `expr` must be an *awaitable*—something that knows how to suspend and resume a coroutine. The awaitable produces an *awaiter* object that implements three methods: +When you see expression `co_await aw`, the sub-expression `aw` is an *awaitable*. +An awaitable conveys state and the type, but on its own it doesn't do much. +When expression `co_await aw` is evaluated, as the first thing, awaitable `aw` +is used to produce an *awaiter*. This is done by first trying to call member function +`await_transform` of the coroutine's promise object, if present, and then calling member +function `operator co_await` -- if present -- on the result. The result is the awaiter. +It is next used to control the coroutine suspension and resumption via its interface +in the form of three member functions: -* `await_ready()` — Returns `true` if the result is immediately available and no suspension is needed -* `await_suspend(handle)` — Called when the coroutine suspends; receives a handle to the coroutine for later resumption -* `await_resume()` — Called when the coroutine resumes; its return value becomes the value of the `co_await` expression +* `await_ready()` — Returns `true` if the result is immediately available and no suspension is needed. +* `await_suspend(handle)` — Called when the coroutine suspends; receives a handle to the coroutine for later resumption. +* `await_resume()` — Called when the coroutine resumes; its return value becomes the value of the `co_await` expression. + +NOTE: In special cases an awaiter and an awaitable can be the same object. + In other special cases, an awaitable can be as simple a type as an `int`. === Example: Understanding the Awaiter Protocol @@ -91,6 +102,11 @@ Study this execution flow: The variable `i` inside `counter` maintains its value across all these suspension and resumption cycles. + +NOTE: While only a coroutine, by definition, can evaluate a `co_await` expression, + the awaiter — that is, the thing being awaited on — need not be a coroutine, or invoke one. + + === Standard Awaiters The {cpp} standard library provides two predefined awaiters: diff --git a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc index 4a9ac57fc..d11e13981 100644 --- a/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc +++ b/doc/modules/ROOT/pages/2.cpp20-coroutines/2c.machinery.adoc @@ -30,7 +30,7 @@ Called if an exception escapes the coroutine body. Typically you either rethrow The compiler transforms your coroutine body into something resembling this pseudocode: -NOTE: The `co_await` keywords below are intentional. This mirrors the {cpp} standard's own description ({cpp}20 [dcl.fct.def.coroutine]/5), which uses `co_await` to express the logical suspension points. The compiler expands each `co_await` into the full awaiter protocol (`await_ready`, `await_suspend`, `await_resume`) as described in xref:2.cpp20-coroutines/2b.syntax.adoc#_awaitables_and_awaiters[Awaitables and Awaiters]. +NOTE: The `co_await` keywords below are intentional. This mirrors the {cpp} standard's own description ({cpp}20 [dcl.fct.def.coroutine]/5), which uses `co_await` to express the logical suspension points. The compiler expands each `co_await` into the full awaiter protocol (`await_ready`, `await_suspend`, `await_resume`) as described in xref:2.cpp20-coroutines/2b.syntax.adoc#awaitables_and_awaiters[Awaitables and Awaiters]. .What the compiler generates [source,cpp,role=pseudocode] diff --git a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc index 0114c7ea1..1cbf3fc81 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc @@ -33,7 +33,12 @@ Capy uses *forward propagation*: the caller passes context to the awaitable thro == The Two-Argument await_suspend -The IoAwaitable protocol extends `await_suspend` to receive context: +In order to make sure — and statically enforce — that every awaitable ``co_await``ed in Capy-coroutines +footnote:[The only awaitables excluded from this constraint are "tag objects" from namespace cpp:this_coro[]: cpp:this_coro::environment[environment], cpp:this_coro::executor[executor], cpp:this_coro::frame_allocator[frame_allocator] and cpp:this_coro::stop_token[stop_token].] +participates in the execution environment propagation, the awaitables +are required to provide member function `await_suspend`, similar +to the one present in C++ awaiters, which takes the second parameter representing +the execution environment: [source,cpp] ---- @@ -48,18 +53,23 @@ This signature receives: ** cpp:io_env::stop_token[env->stop_token] — A stop token for cooperative cancellation ** cpp:io_env::frame_allocator[env->frame_allocator] — An optional frame allocator -Many IoAwaitables return `std::coroutine_handle<>` to enable symmetric transfer. +Following the convention of awaiters in C++, the return type of the function has to be one of: -== IoAwaitable Concept +* `void`, +* `bool`, +* `std::coroutine_handle

` for any type `P`. -An awaitable satisfies cpp:IoAwaitable[] if `a.await_suspend(h, env)` is a valid expression: +Many types modelling cpp:IoAwaitable[] return `std::coroutine_handle<>` to enable +xref:2.cpp20-coroutines/2d.advanced.adoc#_symmetric_transfer[symmetric transfer]. -[source,cpp] ----- -include::example$snippets/4d_io_awaitable.cpp[tag=io_awaitable_concept] ----- +== IoAwaitable Concept + +Concept cpp:IoAwaitable[] specifies all requirements on a type that can be ``co_await``ed +in Capy-coroutines. Apart from the two-argument `await_suspend`, the type also needs to +provide the two member functions specific to awaiters: `await_ready` and `await_resume`. -The concept constrains only the two-argument `await_suspend` that receives the `io_env`. It does not require `await_ready` or `await_resume`, nor does it constrain the return type of `await_suspend`. A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed. +In order to model cpp:IoAwaitable[] a type need not be associated with any coroutine. In fact, +this is the case for a number of cpp:IoAwaitable[]s in Corosio. == IoRunnable Concept @@ -82,9 +92,8 @@ Capy's cpp:task[task] satisfies this concept. When you write `co_await child_task()` inside a cpp:task[task]: -1. The parent task's `await_transform` intercepts the awaitable -2. It wraps the child in a transform awaiter -3. The transform awaiter's `await_suspend` passes context: +1. Member function `await_transform` of the ``task``'s `promise_type` transforms the awaitable into an awaiter (see xref:2.cpp20-coroutines/2b.syntax.adoc#awaitables_and_awaiters[Awaitables and Awaiters]). +2. The resulting awaiter's `await_suspend` passes context: [source,cpp] ---- diff --git a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc index 92cb5728a..7ec33d334 100644 --- a/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc +++ b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc @@ -28,12 +28,14 @@ IoAwaitable IoRunnable .... -cpp:IoAwaitable[] is the base. It requires a single syntactic property -- the `await_suspend` signature must accept an cpp:io_env[] parameter containing the execution environment: +cpp:IoAwaitable[] is the base. It describes what operations a type must provide +to be able to be ``co_await``ed inside a Capy-coroutine: + + * `await_ready` -- tells if the coroutine needs to be suspended. + * `await_suspend` -- invoked immediately after the coroutine is suspended. + Obtains the execution environment via an cpp:io_env[] parameter and tells which coroutine needs to be now resumed. + * `await_resume` -- tells what value shall be returned when the coroutine is resumed. -[source,cpp] ----- -include::example$snippets/9n_why_not_cobalt_concepts.cpp[tag=io_awaitable_concept] ----- cpp:IoRunnable[] refines cpp:IoAwaitable[] with operations needed to start a task from non-coroutine contexts: cpp:IoRunnable::handle[handle()], cpp:IoRunnable::release[release()], cpp:IoRunnable::exception[exception()], cpp:IoRunnable::result[result()], and the promise-level cpp:IoRunnable::set_continuation[set_continuation()] and cpp:IoRunnable::set_environment[set_environment()]. diff --git a/include/boost/capy/concept/io_awaitable.hpp b/include/boost/capy/concept/io_awaitable.hpp index 835c59480..e3efea326 100644 --- a/include/boost/capy/concept/io_awaitable.hpp +++ b/include/boost/capy/concept/io_awaitable.hpp @@ -19,119 +19,149 @@ namespace boost { namespace capy { -/** Requires `await_suspend` to accept a coroutine handle and an `io_env` pointer. - - An awaitable satisfies `IoAwaitable` if its `await_suspend` accepts - an `io_env`, enabling scheduler affinity, cancellation, and allocator - propagation. This extended signature distinguishes I/O awaitables - from standard C++ awaitables that only take a coroutine handle. - - `IoAwaitable` constrains only this one member function, - `await_suspend(std::coroutine_handle<>, io_env const*)`. It is the - single customization point that receives the `io_env`. It is - therefore the only member that needs the executor, stop token, and - frame allocator used to start, schedule, and cancel the operation. - `await_ready` and `await_resume` operate on state local to the - awaitable and take no `io_env` parameter, so this concept does not - check them. +namespace detail { + + template + constexpr bool is_coroutine_handle = false; + + template + constexpr bool is_coroutine_handle> = true; + + template + concept await_suspend_valid_result = std::same_as || std::same_as || is_coroutine_handle; +} + +/** Describes types that can be `co_await`-ed in Capy-coroutines and + that can be passed the information about the execution environment. + + See the tutorial section _The IoAwaitable Protocol_ for the description + of the execution environment propagation mechanism in Capy. + + @tparam A The awaitable type. - @par Syntactic Requirements + @par `await_ready` - @li `a.await_suspend(h, env)` must be a valid expression where: - - `h` is a `std::coroutine_handle<>` (coroutine handle). - - `env` is an `io_env const*`. + In the context of processing a `co_await` expression, says + if the expression can be computed synchronously, without engaging + the further awaiting machinery. - @par Semantic Requirements + _Returns_: - When `await_suspend` is called: + @li @c true when the operation can be computed synchronously via immediately calling + @c await_resume , + @li @c false when @c await_suspend needs to be called. - @li The awaitable uses `env->executor` to schedule - resumption of the coroutine when the operation completes. - @li The awaitable should monitor `env->stop_token` and - complete early with a cancellation error if stop is - requested. - @li The awaitable may use `env->frame_allocator` for internal - allocations. - @li The awaitable must propagate `env->frame_allocator` faithfully - to any child coroutines it creates. - @li The awaitable may return `std::noop_coroutine()` to - indicate the operation was started asynchronously. + @par `await_suspend` - @par Lifetime + In the context of processing a `co_await` expression, instructs the coroutine machinery, + which coroutine needs to be launched or resumed. - The `io_env` passed to `await_suspend` remains valid for the - lifetime of the awaitable's async operation. @ref run, - @ref run_async and the other functions that start a task - guarantee this. - Awaitables that need to retain access to the environment should - store it as `io_env const*`, never as a copy. Copying is - unnecessary and wasteful because the referent is guaranteed to - outlive the operation. + _preconditions_: `a.await_ready() == false`. - @par Conforming Signatures + _Parameters_: - Only the `await_suspend` overload shown below is checked by - `IoAwaitable`. `await_ready` and `await_resume` are shown for - context. The C++ awaitable protocol (`co_await`) requires the - compiler to find them on the awaiter type. This concept does not - require them. + @li @c h — the handle to the just suspended coroutine, + @li @c env — the execution environment of the just suspended coroutine. - @code - struct A - { - bool await_ready() const noexcept; + _Effects_: If this operations instructs a coroutine to be resumed, it shall make sure that the coroutine's + promise type is passed the @c env parameter, in a manner specific to `A`. - auto await_suspend( - std::coroutine_handle<> h, - io_env const* env ); + _Returns_: The signature has one of the these return types: `void`, `bool` and `std::coroutine_handle

` for any type `P`. + + If the return type is `void`, instructs the coroutine machinery that the control shall be + returned to the resumer of the coroutine that invoked the `co_await` expression. + Takes the ownership for scheduling the + resumption of the coroutine represented by `h` via either `env->executor.post` or `env->executor.dispatch`. + + If the return type is `bool`: + + @li value @c true indicates the behavior equivalent to that of the @c void return type; + + @li value @c false instructs the coroutine machinery to resume the coroutine represented by @c h and to immediately invoke @c await_resume . + + If the return type is `std::coroutine_handle

`, instructs the compiler to resume + the coroutine represented by the returned handle, in a tail call manner (not consuming the stack). + + _Note_: + + @li Returning @c h is equivalent to returning @c false in the @c bool return type signature. + @li Returning @c std::noop_coroutine() is equivalent to using the @c void return type, returning @c true in the @c bool return type signature. + + + _Lifetime_: The object of type @ref io_env pointed to by `env` remains valid + for the duration of the async operation represented by `A`. This is guarantee is maintained + by @ref run, @ref run_async and the other functions that "start a task". + + + @par `await_resume` + + In the context of processing a `co_await` expression, when the suspended coroutine is being resumed or + upon immediate resumption, returns a value — if any — that shall be returned from the `co_await` expression. + + If it throws an exception, the exception is propagated out of the `co_await` expression into the awaiting coroutine's + scope. + + _Returns_: value intended to be returned from the `co_await` expression. The return type determines the type of + the enclosing `co_await` expression and can be `void`. + + - T await_resume(); - }; - @endcode @par Example + The example demonstrates a "leaf" awaitable: one that is associated directly with + a system's I/O operation but no coroutine. + @code - struct my_io_op + class my_awaitable { - io_env const* env_ = nullptr; - continuation cont_; - - auto await_suspend( - std::coroutine_handle<> h, - io_env const* env ) + capy::io_env const* env_ = nullptr; + capy::continuation cont_; + std::error_code ec_ {}; + + public: + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) noexcept { - env_ = env; - cont_ = continuation{h}; - // Pass members by value; capturing this - // risks use-after-free in async callbacks. - // When the async operation completes, resume - // via executor.post(cont_) or executor.dispatch(cont_) - // rather than calling h.resume() directly. - start_async( - env_->stop_token, - env_->executor, - cont_ ); - return std::noop_coroutine(); + env_ = env; // store the pointer, never a copy + cont_ = capy::continuation{h}; + + auto completion = [this](std::error_code ec) noexcept + { + ec_ = ec; // publish result; touch *this + env_->executor.post(cont_); // only before post, never after + }; + + start_my_io_op(env_->stop_token, completion); + + return std::noop_coroutine(); // go back to scheduler } - - bool await_ready() const noexcept { return false; } - void await_resume() {} + + capy::io_result<> await_resume() const noexcept { return {ec_}; } }; @endcode - @see IoRunnable + @par Models + + General-purpose class templates that model `IoAwaitable`: @ref task, @ref quitter, @ref immediate. + + + @see @ref IoRunnable, @ref io_env, @ref executor_ref */ template -concept IoAwaitable = +concept IoAwaitable = std::move_constructible && requires( A a, std::coroutine_handle<> h, io_env const* env) { - a.await_suspend(h, env); + { a.await_ready() } -> std::same_as; + { a.await_suspend(h, env) } -> detail::await_suspend_valid_result; + a.await_resume(); }; /** Names what `co_await a` yields for awaitable type A. diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index 85ba2f752..64a3183b2 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -364,7 +364,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE } else { - static_assert(sizeof(A) == 0, "requires IoAwaitable"); + static_assert(IoAwaitable, "requires IoAwaitable"); } } }; diff --git a/test/doc/snippets/4d_io_awaitable.cpp b/test/doc/snippets/4d_io_awaitable.cpp index 5dc62731e..02f731982 100644 --- a/test/doc/snippets/4d_io_awaitable.cpp +++ b/test/doc/snippets/4d_io_awaitable.cpp @@ -87,7 +87,7 @@ struct std_awaiter_handle struct io_awaiter_signature { // tag::two_arg_await_suspend[] - std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env); + auto await_suspend(std::coroutine_handle<> h, io_env const* env); // end::two_arg_await_suspend[] }; diff --git a/test/unit/detail/await_suspend_helper.cpp b/test/unit/detail/await_suspend_helper.cpp index 2c3583484..70b6eec96 100644 --- a/test/unit/detail/await_suspend_helper.cpp +++ b/test/unit/detail/await_suspend_helper.cpp @@ -43,9 +43,10 @@ class await_suspend_helper_test }; // await_suspend returning a handle: symmetric transfer to it. + template struct handle_awaitable { - std::coroutine_handle<> next; + std::coroutine_handle

next; std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) { @@ -59,22 +60,36 @@ class await_suspend_helper_test { auto const h = std::noop_coroutine(); - // void -> noop_coroutine, and the awaitable was invoked. - void_awaitable va; - BOOST_TEST(call_await_suspend(&va, h, nullptr) == h); - BOOST_TEST(va.suspended); + { + // void -> noop_coroutine, and the awaitable was invoked. + void_awaitable va; + BOOST_TEST(call_await_suspend(&va, h, nullptr) == h); + BOOST_TEST(va.suspended); + } - // bool true -> noop_coroutine (stay suspended). - bool_awaitable bt{true}; - BOOST_TEST(call_await_suspend(&bt, h, nullptr) == h); + { + // bool true -> noop_coroutine (stay suspended). + bool_awaitable bt{true}; + BOOST_TEST(call_await_suspend(&bt, h, nullptr) == h); + } - // bool false -> the original handle (resume). - bool_awaitable bf{false}; - BOOST_TEST(call_await_suspend(&bf, h, nullptr) == h); + { + // bool false -> the original handle (resume). + bool_awaitable bf{false}; + BOOST_TEST(call_await_suspend(&bf, h, nullptr) == h); + } - // handle -> the returned handle. - handle_awaitable ha{h}; - BOOST_TEST(call_await_suspend(&ha, h, nullptr) == h); + { + // handle -> the returned handle. + handle_awaitable hv{h}; + BOOST_TEST(call_await_suspend(&hv, h, nullptr) == h); + } + + { + // handle

-> the returned handle. + handle_awaitable hp{h}; + BOOST_TEST(call_await_suspend(&hp, h, nullptr) == h); + } } };