Skip to content

Commit a8d8b38

Browse files
committed
♻️ Decouple context from virtual callback API via unblock_listener
Separate the unblock notification mechanism from the context class into a dedicated `unblock_listener` interface. This eliminates the need for virtual function on context itself, allowing each context to be a simple, concrete object. The callback pattern is now only used for the unblock event—the only notification that must happen asynchronously (in ISRs or other threads). The scheduler can directly inspect context state to determine readiness and sleep duration without callbacks. Changes: - Extract unblock notification into `unblock_listener` interface - Remove virtual methods from context class - Update sleep_duration to use microseconds (u32) instead of nanoseconds to save 4 bytes, reduce the timing requirements on the scheduler, and to provide a more realistic delay range for most systems. - Simplify context state inspection for scheduler decision-making - Add `on_unblock()` listener registration/clearing on context - Update tests to use new listener-based design
1 parent a7e45b7 commit a8d8b38

16 files changed

Lines changed: 975 additions & 632 deletions

CMakeLists.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ libhal_apply_compile_options(async_context)
2626
libhal_install_library(async_context NAMESPACE libhal)
2727
libhal_add_tests(async_context
2828
TEST_NAMES
29-
basic_context
29+
sync_wait
3030
basics
3131
blocked_by
3232
cancel
3333
exclusive_access
3434
proxy
35+
basics_dep_inject
36+
on_unblock
37+
simple_scheduler
3538

3639
MODULES
3740
tests/util.cppm
@@ -43,7 +46,6 @@ libhal_add_tests(async_context
4346

4447
if(NOT CMAKE_CROSSCOMPILING)
4548
message(STATUS "Building benchmarks tests!")
46-
4749
find_package(benchmark REQUIRED)
4850
libhal_add_executable(benchmark SOURCES benchmarks/benchmark.cpp)
4951
target_link_libraries(benchmark PRIVATE async_context benchmark::benchmark)

README.md

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ int main()
117117
118118
Output:
119119
120-
```
120+
```text
121121
Pipeline '🌟 System 1' starting...
122122
['🌟 System 1': Sensor] Starting read...
123123
Pipeline '🔥 System 2' starting...
@@ -142,9 +142,8 @@ Both pipelines completed successfully!
142142
## Features
143143

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

223222
## Core Types
224223

224+
### `async::unblock_listener`
225+
226+
An interface for receiving notifications when a context becomes unblocked. This
227+
is the primary mechanism for schedulers to efficiently track which contexts are
228+
ready for execution without polling. Implement this interface and register it
229+
with `context::on_unblock()` to be notified when a context transitions to the
230+
unblocked state.
231+
232+
The `on_unblock()` method is called from within `context::unblock()`, which may
233+
be invoked from ISRs, driver completion handlers, or other threads.
234+
Implementations must be ISR-safe and noexcept.
235+
225236
### `async::context`
226237

227-
The base context class that manages coroutine execution and memory. Derived classes must:
238+
The base context class that manages coroutine execution and memory. Contexts
239+
are initialized with stack memory via their constructor:
228240

229-
1. Provide stack memory via `initialize_stack_memory()`, preferably within the
230-
constructor.
231-
2. Implement `do_schedule()` to handle blocking state notifications
241+
```cpp
242+
std::array<async::uptr, 1024> my_stack{};
243+
async::context ctx(my_stack);
244+
```
245+
246+
> [!CRITICAL]
247+
> The stack memory MUST outlive the context object. The context does not own or
248+
> copy the stack memory—it only stores a reference to it.
249+
250+
Optionally, contexts can register an `unblock_listener` to be notified of state
251+
changes, or the scheduler can poll the context state directly using `state()`
252+
and `pending_delay()`
232253
233254
### `async::future<T>`
234255
@@ -322,49 +343,10 @@ async::future<int> outer(async::context& p_ctx) {
322343
}
323344
```
324345
325-
### Custom Context Implementation
326-
327-
```cpp
328-
class my_context : public async::context {
329-
public:
330-
std::array<async::uptr, 1024> m_stack{};
331-
332-
my_context() {
333-
initialize_stack_memory(m_stack);
334-
}
335-
~my_context() {
336-
// ‼️ The most derived context must call cancel in its destructor
337-
cancel();
338-
// If memory was allocated, deallocate it here...
339-
}
340-
341-
private:
342-
void do_schedule(async::blocked_by p_state,
343-
async::block_info p_info) noexcept override {
344-
// Notify your scheduler of state changes
345-
}
346-
};
347-
```
348-
349-
#### Initialization
350-
351-
In order to create a usable custom context, the stack memory must be
352-
initialized with a call to `initialize_stack_memory(span)` with a span to the
353-
memory for the stack. There is no requirements of where this memory comes from
354-
except that it be a valid source. Such sources can be array thats member of
355-
this object, dynamically allocated memory that the context has sole ownership
356-
of, or it can point to statically allocated memory that it has sole control and
357-
ownership over.
358-
359-
#### Destruction
360-
361-
The custom context must call `cancel()` before deallocating the stack memory.
362-
Once cancel completes, the stack memory may be deallocated.
363-
364-
### Using `async::basic_context` with `sync_wait()`
346+
### Using `sync_wait()`
365347
366348
```cpp
367-
async::basic_context<512> ctx;
349+
async::inplace_context<512> ctx;
368350
auto future = my_coroutine(ctx);
369351
ctx.sync_wait([](async::sleep_duration p_sleep_time) {
370352
std::this_thread::sleep_for(p_sleep_time);
@@ -377,14 +359,14 @@ function works best for your systems.
377359
For example, for FreeRTOS this could be:
378360

379361
```C++
380-
// Helper function to convert std::chrono::nanoseconds to FreeRTOS ticks
381-
inline TickType_t ns_to_ticks(const std::chrono::nanoseconds& ns) {
382-
// Convert nanoseconds to milliseconds (rounding to nearest ms)
383-
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(ns).count();
362+
// Helper function to convert microseconds to FreeRTOS ticks
363+
inline TickType_t us_to_ticks(const std::chrono::microseconds& us) {
364+
// Convert microseconds to milliseconds (rounding to nearest ms)
365+
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(us).count();
384366
return pdMS_TO_TICKS(ms);
385367
}
386368
ctx.sync_wait([](async::sleep_duration p_sleep_time) {
387-
xTaskDelay(ns_to_ticks(p_sleep_time));
369+
xTaskDelay(us_to_ticks(p_sleep_time));
388370
});
389371
```
390372
@@ -577,7 +559,6 @@ To run the benchmarks on their own:
577559
./build/Release/async_benchmark
578560
```
579561

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

583564
```cmake

benchmarks/benchmark.cpp

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -271,25 +271,10 @@ __attribute__((noinline)) async::future<int> sync_future_level1(
271271
auto f = sync_future_level2(ctx, x);
272272
return sync_wait(f) + 1;
273273
}
274-
struct benchmark_context : public async::context
275-
{
276-
std::array<async::uptr, 8192> m_stack{};
277-
278-
benchmark_context()
279-
{
280-
this->initialize_stack_memory(m_stack);
281-
}
282-
283-
private:
284-
void do_schedule(async::blocked_by, async::block_info) noexcept override
285-
{
286-
// Do nothing for the benchmark
287-
}
288-
};
289274

290275
static void bm_future_sync_return(benchmark::State& state)
291276
{
292-
benchmark_context ctx;
277+
async::inplace_context<1024> ctx;
293278

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

327312
static void bm_future_coroutine(benchmark::State& state)
328313
{
329-
benchmark_context ctx;
314+
async::inplace_context<1024> ctx;
330315

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

368353
static void bm_future_sync_await(benchmark::State& state)
369354
{
370-
benchmark_context ctx;
355+
async::inplace_context<1024> ctx;
371356

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

409394
static void bm_future_mixed(benchmark::State& state)
410395
{
411-
benchmark_context ctx;
396+
async::inplace_context<1024> ctx;
412397

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

450435
static void bm_future_void_coroutine(benchmark::State& state)
451436
{
452-
benchmark_context ctx;
437+
async::inplace_context<1024> ctx;
453438

454439
int input = 42;
455440
int output = 0;
@@ -464,7 +449,7 @@ BENCHMARK(bm_future_void_coroutine);
464449

465450
static void bm_future_void_coroutine_context_resume(benchmark::State& state)
466451
{
467-
benchmark_context ctx;
452+
async::inplace_context<1024> ctx;
468453

469454
int input = 42;
470455
int output = 0;

docs/api/async_context.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Defined in namespace `async::v0`
99
```{doxygenclass} v0::context
1010
```
1111

12-
## async::basic_context
12+
## async::inplace_context
1313

1414
Defined in namespace `async::v0`
1515

0 commit comments

Comments
 (0)