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
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ libhal_apply_compile_options(async_context)
libhal_install_library(async_context NAMESPACE libhal)
libhal_add_tests(async_context
TEST_NAMES
basic_context
sync_wait
basics
blocked_by
cancel
exclusive_access
proxy
basics_dep_inject
on_unblock
simple_scheduler

MODULES
tests/util.cppm
Expand All @@ -43,7 +46,6 @@ libhal_add_tests(async_context

if(NOT CMAKE_CROSSCOMPILING)
message(STATUS "Building benchmarks tests!")

find_package(benchmark REQUIRED)
libhal_add_executable(benchmark SOURCES benchmarks/benchmark.cpp)
target_link_libraries(benchmark PRIVATE async_context benchmark::benchmark)
Expand Down
89 changes: 35 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ int main()

Output:

```
```text
Pipeline '🌟 System 1' starting...
['🌟 System 1': Sensor] Starting read...
Pipeline '🔥 System 2' starting...
Expand All @@ -142,9 +142,8 @@ Both pipelines completed successfully!
## Features

- **Stack-based coroutine allocation** - No heap allocations; coroutine frames are allocated from a user-provided stack buffer
- **Cache-line optimized** - Context object fits within `std::hardware_constructive_interference_size` (typically 64 bytes)
- **Blocking state tracking** - Built-in support for time, I/O, sync, and external blocking states
- **Scheduler integration** - Virtual `do_schedule()` method allows custom scheduler implementations
- **Flexible scheduler integration** - Schedulers can poll context state directly, or register an `unblock_listener` for ISR-safe event notification when contexts become unblocked
- **Proxy contexts** - Support for supervised coroutines with timeout capabilities
- **Exception propagation** - Proper exception handling through the coroutine chain
- **Cancellation support** - Clean cancellation with RAII-based resource cleanup
Expand Down Expand Up @@ -222,13 +221,35 @@ work.

## Core Types

### `async::unblock_listener`

An interface for receiving notifications when a context becomes unblocked. This
is the primary mechanism for schedulers to efficiently track which contexts are
ready for execution without polling. Implement this interface and register it
with `context::on_unblock()` to be notified when a context transitions to the
unblocked state.

The `on_unblock()` method is called from within `context::unblock()`, which may
be invoked from ISRs, driver completion handlers, or other threads.
Implementations must be ISR-safe and noexcept.

### `async::context`

The base context class that manages coroutine execution and memory. Derived classes must:
The base context class that manages coroutine execution and memory. Contexts
are initialized with stack memory via their constructor:

1. Provide stack memory via `initialize_stack_memory()`, preferably within the
constructor.
2. Implement `do_schedule()` to handle blocking state notifications
```cpp
std::array<async::uptr, 1024> my_stack{};
async::context ctx(my_stack);
```

> [!CRITICAL]
> The stack memory MUST outlive the context object. The context does not own or
> copy the stack memory—it only stores a reference to it.

Optionally, contexts can register an `unblock_listener` to be notified of state
changes, or the scheduler can poll the context state directly using `state()`
and `pending_delay()`

### `async::future<T>`

Expand Down Expand Up @@ -322,49 +343,10 @@ async::future<int> outer(async::context& p_ctx) {
}
```

### Custom Context Implementation

```cpp
class my_context : public async::context {
public:
std::array<async::uptr, 1024> m_stack{};

my_context() {
initialize_stack_memory(m_stack);
}
~my_context() {
// ‼️ The most derived context must call cancel in its destructor
cancel();
// If memory was allocated, deallocate it here...
}

private:
void do_schedule(async::blocked_by p_state,
async::block_info p_info) noexcept override {
// Notify your scheduler of state changes
}
};
```

#### Initialization

In order to create a usable custom context, the stack memory must be
initialized with a call to `initialize_stack_memory(span)` with a span to the
memory for the stack. There is no requirements of where this memory comes from
except that it be a valid source. Such sources can be array thats member of
this object, dynamically allocated memory that the context has sole ownership
of, or it can point to statically allocated memory that it has sole control and
ownership over.

#### Destruction

The custom context must call `cancel()` before deallocating the stack memory.
Once cancel completes, the stack memory may be deallocated.

### Using `async::basic_context` with `sync_wait()`
### Using `sync_wait()`

```cpp
async::basic_context<512> ctx;
async::inplace_context<512> ctx;
auto future = my_coroutine(ctx);
ctx.sync_wait([](async::sleep_duration p_sleep_time) {
std::this_thread::sleep_for(p_sleep_time);
Expand All @@ -377,14 +359,14 @@ function works best for your systems.
For example, for FreeRTOS this could be:

```C++
// Helper function to convert std::chrono::nanoseconds to FreeRTOS ticks
inline TickType_t ns_to_ticks(const std::chrono::nanoseconds& ns) {
// Convert nanoseconds to milliseconds (rounding to nearest ms)
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(ns).count();
// Helper function to convert microseconds to FreeRTOS ticks
inline TickType_t us_to_ticks(const std::chrono::microseconds& us) {
// Convert microseconds to milliseconds (rounding to nearest ms)
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(us).count();
return pdMS_TO_TICKS(ms);
}
ctx.sync_wait([](async::sleep_duration p_sleep_time) {
xTaskDelay(ns_to_ticks(p_sleep_time));
xTaskDelay(us_to_ticks(p_sleep_time));
});
```

Expand Down Expand Up @@ -577,7 +559,6 @@ To run the benchmarks on their own:
./build/Release/async_benchmark
```


Within the [`CMakeList.txt`](./CMakeLists.txt), you can disable unit test or benchmarking by setting the following to `OFF`:

```cmake
Expand Down
27 changes: 6 additions & 21 deletions benchmarks/benchmark.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,25 +271,10 @@ __attribute__((noinline)) async::future<int> sync_future_level1(
auto f = sync_future_level2(ctx, x);
return sync_wait(f) + 1;
}
struct benchmark_context : public async::context
{
std::array<async::uptr, 8192> m_stack{};

benchmark_context()
{
this->initialize_stack_memory(m_stack);
}

private:
void do_schedule(async::blocked_by, async::block_info) noexcept override
{
// Do nothing for the benchmark
}
};

static void bm_future_sync_return(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
for (auto _ : state) {
Expand Down Expand Up @@ -326,7 +311,7 @@ __attribute__((noinline)) async::future<int> coro_level1(async::context& ctx,

static void bm_future_coroutine(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
for (auto _ : state) {
Expand Down Expand Up @@ -367,7 +352,7 @@ __attribute__((noinline)) async::future<int> sync_in_coro_level1(

static void bm_future_sync_await(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
for (auto _ : state) {
Expand Down Expand Up @@ -408,7 +393,7 @@ __attribute__((noinline)) async::future<int> mixed_coro_level1(

static void bm_future_mixed(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
for (auto _ : state) {
Expand Down Expand Up @@ -449,7 +434,7 @@ void_coro_level1(async::context& ctx, int& out, int x)

static void bm_future_void_coroutine(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
int output = 0;
Expand All @@ -464,7 +449,7 @@ BENCHMARK(bm_future_void_coroutine);

static void bm_future_void_coroutine_context_resume(benchmark::State& state)
{
benchmark_context ctx;
async::inplace_context<1024> ctx;

int input = 42;
int output = 0;
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"doxygenfunction",
"doxygenenum",
"alignof",
"inplace"
],
"ignorePaths": [
"build/",
Expand Down
2 changes: 1 addition & 1 deletion docs/api/async_context.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Defined in namespace `async::v0`
```{doxygenclass} v0::context
```

## async::basic_context
## async::inplace_context

Defined in namespace `async::v0`

Expand Down
Loading
Loading