Coroutines#

pw_async2: Cooperative async tasks for embedded

For projects using C++20, pw_async2 provides first-class support for coroutines via Coro. This allows you to write asynchronous logic in a sequential, synchronous style, eliminating the need to write explicit state machines. The co_await keyword is used to suspend execution until an asynchronous operation is Ready.

Coro<Status> ReadAndSend(Reader& reader, Writer& writer) {
  // co_await suspends the coroutine until the Read operation completes.
  Result<Data> data = co_await reader.Read();
  if (!data.ok()) {
    co_return data.status();
  }

  // The coroutine resumes here and continues.
  co_await writer.Write(*data);
  co_return OkStatus();
}

See also Pigweed Blog #5: C++20 coroutines without heap allocation, a blog post on how Pigweed implements coroutines without heap allocation, and challenges encountered along the way.

Define tasks#

The following code example demonstrates basic usage:

 1#include "pw_allocator/allocator.h"
 2#include "pw_async2/coro.h"
 3#include "pw_log/log.h"
 4#include "pw_result/result.h"
 5
 6namespace {
 7
 8using ::pw::OkStatus;
 9using ::pw::Result;
10using ::pw::Status;
11using ::pw::async2::Coro;
12using ::pw::async2::CoroContext;
13
14class MyReceiver;
15class MySender;
16
17/// Create a coroutine which asynchronously receives a value from
18/// ``receiver`` and forwards it to ``sender``.
19///
20/// Note: the ``CoroContext`` argument is used by the ``Coro<T>`` internals to
21/// allocate the coroutine state. If this allocation fails, ``Coro<Status>``
22/// will return ``Status::Internal()``.
23Coro<Status> ReceiveAndSendCoro(CoroContext&,
24                                MyReceiver receiver,
25                                MySender sender) {
26  Result<MyData> data = co_await receiver.Receive();
27  if (!data.ok()) {
28    PW_LOG_ERROR("Receiving failed: %s", data.status().str());
29    co_return Status::Unavailable();
30  }
31  Status sent = co_await sender.Send(std::move(*data));
32  if (!sent.ok()) {
33    PW_LOG_ERROR("Sending failed: %s", sent.str());
34    co_return Status::Unavailable();
35  }
36  co_return OkStatus();
37}
38
39}  // namespace

Any value with a Poll<T> Pend(Context&) method can be passed to co_await, which will return with a T when the result is ready. The PendFuncAwaitable class can also be used to co_await on a provided delegate function.

To return from a coroutine, co_return <expression> must be used instead of the usual return <expression> syntax. Because of this, the PW_TRY and PW_TRY_ASSIGN macros are not usable within coroutines. PW_CO_TRY and PW_CO_TRY_ASSIGN should be used instead.

For a more detailed explanation of Pigweed’s coroutine support, see Coro.

Memory#

When using C++20 coroutines, the compiler generates code to save the coroutine’s state (including local variables) across suspension points (co_await). pw_async2 hooks into this mechanism to control where this state is stored.

A CoroContext, which holds a pw::Allocator, must be passed to any function that returns a Coro. This allocator is used to allocate the coroutine frame. If allocation fails, the resulting Coro will be invalid and will immediately return a Ready(Status::Internal()) result when polled. This design makes coroutine memory usage explicit and controllable.

Passing data between coroutines#

Just like when Passing data between tasks, there are two patterns for sending data between coroutines, with very much the same solutions.

This section just briefly describes how to co_await the data, as all the details around construction and sending a value are the same as Passing data between tasks.

Single values#

As with the non-coroutine case, pw_async2 provides the OnceSender and OnceReceiver helpers for sending and receiving a one-time value.

As OnceReceiver satisfies the The pendable function interface requirement, this means your coroutine can just co_await the receiver instance to obtain the value.

1  Result<int> value = co_await int_receiver;
2  if (!value.ok()) {
3    PW_LOG_ERROR(
4        "OnceSender was destroyed without sending a message! Outrageous :(");
5    co_return Status::Cancelled();
6  }
7  PW_LOG_INFO("Got an int: %d", *value);

Like in the non-coroutine case, the value is wrapped as a Result<T> in case of error.

Multiple values#

To use pw::InlineAsyncQueue or pw::InlineAsyncDeque with co_await, an adapter is needed that exposes a Pend that invokes the correct member function in the containers (either PendHasSpace or PendNotEmpty).

PendableFor does this for you, and supports member functions or free functions.

For sending, the producing coroutine has to wait for there to be space before trying to add to the queue. Here we co_await the result of PendableFor<&Queue::PendHasSpace>(queue, 1) to wait for space for one element to be available.

1    // Wait for there to be space in the queue before writing the next value.
2    co_await PendableFor<&Queue::PendHasSpace>(queue, 1);
3    queue.push(value);

Receiving values is similar. The receiving has to wait for there to be values before trying to remove them from the queue.

1    // Wait for there to be something to read.
2    co_await PendableFor<&Queue::PendNotEmpty>(queue);
3    const int result = queue.front();
4    queue.pop();

A complete example for using pw::InlineAsyncQueue this way can be found in pw_async2/examples/inline_async_queue_with_coro_test.cc, and you can try it yourself with:

bazelisk run --config=cxx20 //pw_async2/examples:inline_async_queue_with_coro_test