Async / await
This example collects three checked recipes from the C+ source tree:
docs/examples/recipes/async_compute,
docs/examples/recipes/async_yield_demo, and
docs/examples/recipes/async_fetch.
Together they back the async claim in layers: syntax and coroutine codegen, cooperative scheduling, then method-form async I/O.
| Recipe | What it proves |
|---|---|
async_compute |
async fn returns Future[T]; nested await chains resolve through executor::block_on |
async_yield_demo |
spawn_local and yield_now interleave local tasks through the executor pending queue |
async_fetch |
await stream.write_all_async(...) and await stream.read_async(...) work with a nonblocking TCP stream |
cpc/tests/e2e.rs |
compiler E2E tests build and run all three recipes, including a sidecar TCP server for async_fetch |
Chained await
async_compute is the smallest proof of the language surface. Three coroutines
await each other and the final future is driven from synchronous code:
import "stdlib/future" as future;
import "stdlib/executor" as executor;
async fn step_one() -> i64 {
return 100 as i64;
}
async fn step_two() -> i64 {
let x: i64 = await step_one();
return x +% (200 as i64);
}
async fn step_three() -> i64 {
let y: i64 = await step_two();
return y +% (300 as i64);
}
pub fn main() -> i32 {
let f: future::Future[i64] = step_three();
let total: i64 = executor::block_on::[i64](f);
if total != (600 as i64) { return 1; }
return 0;
}
The expected result is exit code 0; 600 proves the chained awaits returned
their declared values in order.
Cooperative local tasks
async_yield_demo starts three local async tasks. Each task increments a shared
counter, calls executor::yield_now(), and later resumes. The final count must
be 3 * ticks.
async fn step_loop(counter: *i32, ticks: i32) -> i32 {
let mut i: i32 = 0;
while i < ticks {
let cur: i32 = unsafe { *counter };
unsafe { *counter = cur +% 1; }
executor::yield_now();
i = i +% 1;
}
return 0;
}
async fn drive_three(counter: *i32, ticks: i32) -> i32 {
let f1: future::Future[i32] = step_loop(counter, ticks);
let f2: future::Future[i32] = step_loop(counter, ticks);
let f3: future::Future[i32] = step_loop(counter, ticks);
executor::spawn_local::[i32](f1);
executor::spawn_local::[i32](f2);
let _r: i32 = await f3;
while unsafe { *counter } < (3 *% ticks) {
executor::yield_now();
}
return unsafe { *counter };
}
That recipe deliberately avoids I/O. It proves the executor scheduling substrate without depending on a socket or file descriptor.
Method-form async I/O
async_fetch is the stronger I/O proof. It connects to 127.0.0.1, makes the
stream nonblocking, writes one byte, then awaits one byte back:
async fn fetch_one_byte(port: u16) -> i32 {
guard let result::Result[net::TcpStream, result::IoError]::Ok(s) = net::connect_tcp("127.0.0.1", port)
else { return 0 -% 1 as i32; };
let mut stream: net::TcpStream = s;
let _nb: i32 = stream.make_nonblocking();
let req: *u8 = unsafe { malloc(1 as usize) };
unsafe { *req = 0x41 as u8; }
let _w: isize = await stream.write_all_async(req, 1 as usize);
unsafe { free(req); }
let buf: *u8 = unsafe { malloc(1 as usize) };
let n: isize = await stream.read_async(buf, 1 as usize);
if n != (1 as isize) {
unsafe { free(buf); }
return 0 -% 2 as i32;
}
let v: u8 = unsafe { *buf };
unsafe { free(buf); }
return v as i32;
}
The compiler E2E test recipe_async_fetch_runs starts a sidecar TCP server,
sets FETCH_PORT, runs the C+ client, and expects the echoed byte to come back.
Reproduce
From each recipe directory in the C+ source tree:
cpc build
./target/debug/async_compute
cpc build
./target/debug/async_yield_demo
async_fetch needs a listener on 127.0.0.1:$FETCH_PORT; the compiler E2E
suite supplies that sidecar server. Manually:
FETCH_PORT=7878 cpc build
FETCH_PORT=7878 ./target/debug/async_fetch
Expected results:
async_computeexits0after computing600.async_yield_demoexits0after all three local tasks finish.async_fetchprintsgot=65and exits65when the sidecar echoesA.
The boundary is explicit: C+ supports async functions, futures, awaits, local task scheduling, and method-form async I/O. Large fanout patterns depend on the executor's awaiter re-enqueue behavior and are covered separately in compiler E2E tests as that surface evolves.
‹ Back to all examples