Coroutines#

pw_async2: Cooperative async tasks for embedded

For projects using C++20, pw_async2 provides first-class support for coroutines via pw::async2::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 pw::async2::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 pw::async2::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 pw::async2::CoroContext, which holds a pw::Allocator, must be passed to any function that returns a pw::async2::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 pw::async2::OnceSender and pw::async2::OnceReceiver helpers for sending and receiving a one-time value.

As pw::async2::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 DoPend that invokes the correct member function in the containers (either PendHasSpace or PendNotEmpty).

1auto QueueHasSpace(Queue& queue) {
2  return PendFuncAwaitable(
3      [&queue](Context& cx) { return queue.PendHasSpace(cx); });
4}
5
6auto QueueNotEmpty(Queue& queue) {
7  return PendFuncAwaitable(
8      [&queue](Context& cx) { return queue.PendNotEmpty(cx); });
9}

For sending, the producing coroutine has to wait for there to be space before trying to add to the queue.

1    // Wait for there to be space in the queue before writing the next value.
2    co_await QueueHasSpace(queue);
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 QueueNotEmpty(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