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/channel.h"
 3#include "pw_async2/coro.h"
 4#include "pw_log/log.h"
 5#include "pw_result/result.h"
 6
 7namespace {
 8
 9using ::pw::OkStatus;
10using ::pw::Status;
11using ::pw::async2::Coro;
12using ::pw::async2::CoroContext;
13using ::pw::async2::Receiver;
14using ::pw::async2::Sender;
15
16/// Create a coroutine which asynchronously receives a value from
17/// ``receiver`` and forwards it to ``sender``.
18///
19/// Note: the ``CoroContext`` argument is used by the ``Coro<T>`` internals to
20/// allocate the coroutine state. If this allocation fails, ``Coro<Status>``
21/// will return ``Status::Internal()``.
22Coro<Status> ForwardingCoro(CoroContext&,
23                            Receiver<int> receiver,
24                            Sender<int> sender) {
25  std::optional<int> data = co_await receiver.Receive();
26  if (!data.has_value()) {
27    PW_LOG_ERROR("Receiving failed: channel has closed");
28    co_return Status::Unavailable();
29  }
30
31  if (!(co_await sender.Send(*data))) {
32    PW_LOG_ERROR("Sending failed: channel has closed");
33    co_return Status::Unavailable();
34  }
35
36  co_return OkStatus();
37}
38
39}  // namespace

Any future can be passed to co_await, which will return with a T when the result is ready.

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.