C+
Async

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:

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