Skip to content

Commit d7bffe7

Browse files
authored
📝 Add README.md (#55)
1 parent 6082078 commit d7bffe7

File tree

2 files changed

+342
-5
lines changed

2 files changed

+342
-5
lines changed

CMakeLists.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ cmake_minimum_required(VERSION 3.28)
1717
# Generate compile commands for anyone using our libraries.
1818
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
1919
set(CMAKE_COLOR_DIAGNOSTICS ON)
20+
set(BUILD_UNIT_TESTS ON)
21+
set(BUILD_BENCHMARKS ON)
2022

2123
project(async_context LANGUAGES CXX)
2224

@@ -71,7 +73,7 @@ add_custom_target(copy_compile_commands ALL
7173
# Unit testing
7274
# ==============================================================================
7375

74-
if(TRUE)
76+
if(BUILD_UNIT_TESTS)
7577
if(CMAKE_CROSSCOMPILING)
7678
message(STATUS "Cross compiling, skipping unit test execution")
7779
else()
@@ -111,13 +113,13 @@ else()
111113

112114
add_custom_target(run_tests ALL DEPENDS async_unit_test COMMAND async_unit_test)
113115
endif()
114-
endif()
116+
endif() # BUILD_UNIT_TESTS
115117

116118
# ==============================================================================
117119
# Benchmarking
118120
# ==============================================================================
119121

120-
if(TRUE)
122+
if(BUILD_BENCHMARKS)
121123
if(CMAKE_CROSSCOMPILING)
122124
message(STATUS "Cross compiling, skipping benchmarks")
123125
else()
@@ -143,4 +145,4 @@ else()
143145

144146
add_custom_target(run_benchmark ALL DEPENDS async_benchmark COMMAND async_benchmark)
145147
endif()
146-
endif()
148+
endif() # BUILD_BENCHMARKS

README.md

Lines changed: 336 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,336 @@
1-
# async_scope
1+
# async_context
2+
3+
A lightweight, C++23 coroutine library for embedded systems and
4+
resource-constrained environments. Built with stack-based allocation to avoid
5+
heap usage and designed to fit within a single cache line for optimal
6+
performance.
7+
8+
## Features
9+
10+
- **Stack-based coroutine allocation** - No heap allocations; coroutine frames are allocated from a user-provided stack buffer
11+
- **Cache-line optimized** - Context object fits within `std::hardware_constructive_interference_size` (typically 64 bytes)
12+
- **Blocking state tracking** - Built-in support for time, I/O, sync, and external blocking states
13+
- **Scheduler integration** - Virtual `do_schedule()` method allows custom scheduler implementations
14+
- **Proxy contexts** - Support for supervised coroutines with timeout capabilities
15+
- **Exception propagation** - Proper exception handling through the coroutine chain
16+
- **Cancellation support** - Clean cancellation with RAII-based resource cleanup
17+
18+
> [!WARNING]
19+
>
20+
> Cancellation support is not implemented yet.
21+
22+
## Requirements
23+
24+
- C++23 compiler with coroutine support
25+
- Tested with Clang 18+
26+
27+
## Stack-Based Allocation
28+
29+
Unlike typical coroutine implementations that allocate frames on the heap,
30+
`async_context` uses a stack-based allocation scheme. Each context owns a
31+
contiguous buffer of memory that grows upward as coroutines are called.
32+
33+
### Memory Layout
34+
35+
```mermaid
36+
block-beta
37+
columns 8
38+
39+
block:stack:8
40+
columns 8
41+
A["&m_stack_pointer"]:1
42+
B["Coroutine Frame A"]:3
43+
C["&m_stack_pointer"]:1
44+
D["Coroutine Frame B"]:2
45+
E["..."]:1
46+
end
47+
48+
space:5
49+
F["m_stack_pointer"]:1
50+
space:2
51+
52+
F --> E
53+
54+
style A fill:#4a9,stroke:#333
55+
style C fill:#4a9,stroke:#333
56+
style B fill:#69b,stroke:#333
57+
style D fill:#69b,stroke:#333
58+
style E fill:#ddd,stroke:#333
59+
style F fill:#f96,stroke:#333
60+
```
61+
62+
### How Allocation Works
63+
64+
1. **Allocation**: When a coroutine is created, the promise's `operator new`
65+
requests memory from the context. The context:
66+
- Stores the address of `m_stack_pointer` at the current position
67+
- Returns the next address as the coroutine frame location
68+
- Advances `m_stack_pointer` past the allocated frame
69+
70+
2. **Deallocation**: When a coroutine completes, `operator delete`:
71+
- Reads the stored `&m_stack_pointer` from just before the frame
72+
- Resets `m_stack_pointer` back to that position
73+
74+
This creates a strict LIFO (stack) discipline—coroutines must complete in
75+
reverse order of their creation, which naturally matches how `co_await` chains
76+
work.
77+
78+
### Allocation Sequence
79+
80+
```mermaid
81+
sequenceDiagram
82+
participant Caller
83+
participant Context
84+
participant Stack
85+
86+
Note over Stack: Initial state: m_stack_pointer at start
87+
88+
Caller->>Context: Call coroutine A
89+
Context->>Stack: Store &m_stack_pointer
90+
Context->>Stack: Allocate Frame A
91+
Note over Stack: m_stack_pointer advances
92+
93+
Caller->>Context: A calls coroutine B
94+
Context->>Stack: Store &m_stack_pointer
95+
Context->>Stack: Allocate Frame B
96+
Note over Stack: m_stack_pointer advances
97+
98+
Note over Caller: B completes (co_return)
99+
Context->>Stack: Read &m_stack_pointer from before Frame B
100+
Context->>Stack: Reset m_stack_pointer (deallocate B)
101+
102+
Note over Caller: A completes (co_return)
103+
Context->>Stack: Read &m_stack_pointer from before Frame A
104+
Context->>Stack: Reset m_stack_pointer (deallocate A)
105+
106+
Note over Stack: Back to initial state
107+
```
108+
109+
### Benefits
110+
111+
- **No heap allocation**: Ideal for embedded systems without dynamic memory
112+
- **Deterministic**: Memory usage is bounded by the stack buffer size
113+
- **Cache-friendly**: Coroutine frames are contiguous in memory
114+
- **Fast**: Simple pointer arithmetic instead of malloc/free
115+
116+
## Core Types
117+
118+
### `async::context`
119+
120+
The base context class that manages coroutine execution and memory. Derived classes must:
121+
122+
1. Provide stack memory via `initialize_stack_memory()`, preferably within the
123+
constructor.
124+
2. Implement `do_schedule()` to handle blocking state notifications
125+
126+
### `async::future<T>`
127+
128+
A coroutine return type containing either a value, asynchronous operation, or
129+
an `std::exception_ptr`. If this object is contains a coroutine handle, then it
130+
the future must be resumed until the future object is converted into the value
131+
of type `T`.
132+
133+
- Synchronous returns (no coroutine frame allocation)
134+
- `co_await` for composing asynchronous operations
135+
- `co_return` for returning values
136+
- Move semantics (non-copyable)
137+
138+
### `async::task`
139+
140+
An alias for `async::future<void>` - an async operation with no return value.
141+
142+
### `async::blocked_by`
143+
144+
An enum describing what a coroutine is blocked by:
145+
146+
- `nothing` - Ready to run
147+
- `io` - Blocked by I/O operation
148+
- `sync` - Blocked by resource contention (mutex, semaphore)
149+
- `external` - Blocked by external coroutine system
150+
- `time` - Blocked until a duration elapses
151+
152+
The state of this can be found from the `async::context::state()`. All states
153+
besides time are safe to resume at any point. If a context has been blocked by
154+
time, then it must defer calling resume until that time has elapsed.
155+
156+
## Usage
157+
158+
### Basic Coroutine
159+
160+
```cpp
161+
import async_context;
162+
163+
async::future<int> compute(async::context& p_ctx) {
164+
co_return 42;
165+
}
166+
```
167+
168+
### Awaiting Time
169+
170+
```cpp
171+
async::future<void> delay_example(async::context& p_ctx) {
172+
using namespace std::chrono_literals;
173+
co_await 100ms; // Request the scheduler resume this coroutine >= 100ms
174+
co_return;
175+
}
176+
```
177+
178+
### Awaiting I/O
179+
180+
```cpp
181+
async::future<void> io_example(async::context& p_ctx) {
182+
dma_controller.on_completion([&ctx]() {
183+
ctx.unblock();
184+
});
185+
186+
// Start DMA transaction...
187+
188+
while (!dma_complete) {
189+
co_await ctx.block_by_io();
190+
}
191+
co_return;
192+
}
193+
```
194+
195+
Please note that this coroutine has a loop where it continually reports that
196+
its blocked by IO. It is important that any coroutine blocking by IO check if
197+
the IO has completed before proceeding. If not, it must
198+
`co_await ctx.block_by_io();` at some point to give control back to the resumer.
199+
200+
### Composing Coroutines
201+
202+
```cpp
203+
async::future<int> inner(async::context& p_ctx) {
204+
co_return 10;
205+
}
206+
207+
async::future<int> outer(async::context& p_ctx) {
208+
int value = co_await inner(p_ctx);
209+
co_return value * 2;
210+
}
211+
```
212+
213+
### Custom Context Implementation
214+
215+
```cpp
216+
class my_context : public async::context {
217+
public:
218+
std::array<async::uptr, 1024> m_stack{};
219+
220+
my_context() {
221+
initialize_stack_memory(m_stack);
222+
}
223+
224+
private:
225+
void do_schedule(async::blocked_by p_state,
226+
async::block_info p_info) noexcept override {
227+
// Notify your scheduler of state changes
228+
}
229+
};
230+
```
231+
232+
### Using basic_context with sync_wait
233+
234+
```cpp
235+
class simple_context : public async::basic_context {
236+
public:
237+
std::array<async::uptr, 8192> m_stack{};
238+
239+
simple_context() {
240+
initialize_stack_memory(m_stack);
241+
}
242+
};
243+
244+
simple_context ctx;
245+
auto future = my_coroutine(ctx);
246+
ctx.sync_wait([](async::sleep_duration p_sleep_time) {
247+
std::this_thread::sleep_for(p_sleep_time);
248+
});
249+
```
250+
251+
### Proxy Context for Timeouts
252+
253+
```cpp
254+
async::future<int> supervised(async::context& p_ctx) {
255+
auto proxy = async::proxy_context::from(p_ctx);
256+
auto child_future = child_coroutine(proxy);
257+
258+
int timeout = 10;
259+
while (!child_future.done() && timeout-- > 0) {
260+
child_future.resume();
261+
co_await std::suspend_always{};
262+
}
263+
264+
if (timeout <= 0) {
265+
throw timed_out();
266+
}
267+
co_return child_future.value();
268+
}
269+
```
270+
271+
## Exception Handling
272+
273+
Exceptions thrown in coroutines are propagated through the coroutine chain
274+
until it reaches the top level coroutine. When the top level is reached, the
275+
exception will be thrown from a call to `.resume()`.
276+
277+
```cpp
278+
async::future<void> may_throw(async::context& ctx) {
279+
throw std::runtime_error("error");
280+
co_return;
281+
}
282+
283+
async::future<void> just_calls(async::context& ctx) {
284+
co_await may_throw(ctx);
285+
co_return;
286+
}
287+
288+
auto future = may_throw(ctx);
289+
try {
290+
future.resume();
291+
} catch (const std::runtime_error& e) {
292+
// Handle exception
293+
}
294+
```
295+
296+
## Creating the package
297+
298+
Before getting started, if you haven't used libhal before, follow the
299+
[Getting Started](https://libhal.github.io/latest/getting_started/) guide.
300+
301+
To create the library package call:
302+
303+
```bash
304+
conan create . -pr hal/tc/llvm-20 -pr hal/os/mac --version=<insert-version>
305+
```
306+
307+
Replace `mac` with `linux` or `windows` if that is what you are building on.
308+
309+
This will build and run unit tests, benchmarks, and a test package to confirm
310+
that the package was built correctly.
311+
312+
To run tests on their own:
313+
314+
```bash
315+
./build/Release/async_context_tests
316+
```
317+
318+
To run the benchmarks on their own:
319+
320+
```bash
321+
./build/Release/async_benchmark
322+
```
323+
324+
325+
Within the [`CMakeList.txt`](./CMakeLists.txt), you can disable unit test or benchmarking by setting the following to `OFF`:
326+
327+
```cmake
328+
set(BUILD_UNIT_TESTS OFF)
329+
set(BUILD_BENCHMARKS OFF)
330+
```
331+
332+
## License
333+
334+
Apache License 2.0 - See [LICENSE](LICENSE) for details.
335+
336+
Copyright 2024 - 2025 Khalil Estell and the libhal contributors

0 commit comments

Comments
 (0)