Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions doc/modules/ROOT/pages/2.cpp20-coroutines/2b.syntax.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
33 changes: 21 additions & 12 deletions doc/modules/ROOT/pages/4.coroutines/4d.io-awaitable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
----
Expand All @@ -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<P>` 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

Expand All @@ -82,9 +92,8 @@ Capy's cpp:task[task<T>] satisfies this concept.

When you write `co_await child_task()` inside a cpp:task[task<T>]:

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]
----
Expand Down
12 changes: 7 additions & 5 deletions doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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()].

Expand Down
194 changes: 112 additions & 82 deletions include/boost/capy/concept/io_awaitable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
constexpr bool is_coroutine_handle = false;

template <typename T>
constexpr bool is_coroutine_handle<std::coroutine_handle<T>> = true;

template <typename T>
concept await_suspend_valid_result = std::same_as<T, void> || std::same_as<T, bool> || is_coroutine_handle<T>;
}

/** 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<P>` 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<P>`, 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<typename A>
concept IoAwaitable =
concept IoAwaitable = std::move_constructible<A> &&
requires(
A a,
std::coroutine_handle<> h,
io_env const* env)
{
a.await_suspend(h, env);
{ a.await_ready() } -> std::same_as<bool>;
{ a.await_suspend(h, env) } -> detail::await_suspend_valid_result;
a.await_resume();
};

/** Names what `co_await a` yields for awaitable type A.
Expand Down
2 changes: 1 addition & 1 deletion include/boost/capy/task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
}
else
{
static_assert(sizeof(A) == 0, "requires IoAwaitable");
static_assert(IoAwaitable<A>, "requires IoAwaitable");
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion test/doc/snippets/4d_io_awaitable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
};

Expand Down
Loading
Loading