C+
Realtime

Realtime audio meter

This example is a proof recipe for the #[realtime] claim. Its passing file is a standalone audio-style hot path that cpc check accepts without imports. The real-time package in the C+ source tree supplies the same bounded building blocks for applications: vendor/rt/src/rt.cplus ships SpscRingU64, and vendor/rt/src/pool.cplus ships FixedPoolU64. Their hot-path methods are annotated #[no_alloc] and #[no_block].

The important property is not the audio math. The important property is that the compiler rejects code paths that would allocate, block, or exceed the stack budget before the callback can ship.

Claim How this example checks it
#[realtime] accepts arithmetic-only hot paths passing.cplus checks cleanly
allocation is rejected bad/alloc.cplus reports E0901
blocking is rejected bad/block.cplus reports E0907
oversized stack frames are rejected bad/stack.cplus reports E0908

Compiler-backed evidence

The source tree has two layers of evidence behind this recipe:

Evidence What it proves
vendor/rt/src/rt.cplus SpscRingU64::new, count, is_empty, is_full, push, and pop are fixed-capacity, nonblocking, allocation-free operations marked #[no_alloc] #[no_block]
vendor/rt/src/pool.cplus FixedPoolU64 provides an allocation-free fixed pool with acquire, release, get, and set marked #[no_alloc] #[no_block]
cpc/tests/e2e.rs compiler tests reject allocation with E0901, blocking calls with E0907, oversized frames with E0908, and JSON realtime reports with nonzero exit on violations

Representative E2E test names include realtime_profile_rejects_local_allocation, realtime_profile_clean_program_passes, no_alloc_rejects_allocating_method_through_receiver, no_alloc_allows_marked_method_through_receiver, no_alloc_rejects_to_string, no_block_rejects_blocking_method_through_receiver, realtime_report_json_flags_violations, and realtime_report_clean_exits_zero.

Project layout

realtime_meter/
├── passing.cplus
└── bad/
    ├── alloc.cplus
    ├── block.cplus
    └── stack.cplus

Passing callback

The callback reads a block of samples through explicit pointers, applies a gain, writes the output, and returns the peak. It allocates nothing and has no operation that can wait.

const GAIN: f32 = 0.80f32;

#[realtime]
fn abs_f32(x: f32) -> f32 {
    if x < 0.0f32 { return 0.0f32 - x; }
    return x;
}

#[realtime]
#[max_stack(1024)]
fn process_audio(n: usize, restrict input: *f32,
        restrict output: *f32) -> f32 {
    var i: usize = 0usize;
    var peak: f32 = 0.0f32;

    while i < n {
        let y: f32 = input[i] * GAIN;
        output[i] = y;

        let a: f32 = abs_f32(y);
        if a > peak { peak = a; }

        i = i +% 1usize;
    }
    return peak;
}

fn main() -> i32 {
    var input: [f32; 8] = [0.10f32, -0.20f32, 0.30f32, -0.40f32,
        0.25f32, -0.50f32, 0.75f32, -1.00f32];
    var output: [f32; 8] = [0.0f32; 8];
    let peak: f32 = process_audio(8usize, #addr_of(input[0]),
        #addr_of(output[0]));
    if peak == 1.0f32 { return 0; }
    return 1;
}

For an application that publishes the peak to another thread, the rt package follows the same rule. SpscRingU64::push checks capacity and returns false when full; it never waits:

#[no_alloc] #[no_block]
fn push(ref this, v: u64) -> bool {
    let head: u64 = { atomic::load_u64(#addr_of(this._head), atomic::Ordering::Acquire) };
    let tail: u64 = { atomic::load_u64(#addr_of(this._tail), atomic::Ordering::Relaxed) };
    if (tail -% head) >= (1024 as u64) {
        return false;
    }
    let idx: usize = (tail % (1024 as u64)) as usize;
    this._buf[idx] = v;
    { atomic::store_u64(#addr_of(this._tail), tail +% (1 as u64), atomic::Ordering::Release); }
    return true;
}

FixedPoolU64::acquire uses the same contract style: bounded state, no heap, and failure as a value instead of a wait.

#[no_alloc] #[no_block]
fn acquire(ref this) -> option::Option[u32] {
    if this.free_head == pool_none() {
        return option::Option[u32]::None;
    }
    let idx: u32 = this.free_head;
    this.free_head = this.links[idx as usize];
    this.in_use = this.in_use +% (1 as usize);
    return option::Option[u32]::Some(idx);
}

Negative fixture: allocation

This fixture calls an allocator directly. The compiler rejects it inside the realtime contract before a binary is produced.

extern fn malloc(n: usize) -> *u8;

#[realtime]
fn process_bad_alloc() -> i32 {
    let p: *u8 = malloc(16usize);
    if p.is_null() { return 1; }
    return 0;
}

fn main() -> i32 { return process_bad_alloc(); }

Expected diagnostic shape:

bad/alloc.cplus:5:18: error[E0901]: function `process_bad_alloc` is marked `#[no_alloc]` but calls allocating function `malloc`

Negative fixture: blocking

The callback cannot sleep, wait on a timer, block on a mutex, join a thread, or perform blocking I/O. This fixture should be rejected with E0907.

#[no_alloc]
extern fn sleep(seconds: u32) -> u32;

#[realtime]
fn process_bad_block() -> i32 {
    let _rc: u32 = sleep(1u32);
    return 0;
}

fn main() -> i32 { return process_bad_block(); }

Expected diagnostic shape:

bad/block.cplus:6:20: error[E0907]: function `process_bad_block` is marked `#[no_block]` but calls blocking function `sleep`

Negative fixture: stack budget

Large locals are visible to #[max_stack]. This fixture intentionally exceeds a small budget.

#[realtime]
#[max_stack(256)]
fn process_bad_stack() -> i32 {
    let scratch: [u8; 2048] = [0u8; 2048];
    return scratch[0] as i32;
}

fn main() -> i32 { return process_bad_stack(); }

Expected diagnostic shape:

bad/stack.cplus:2:1: error[E0908]: function `process_bad_stack` stack frame exceeds 256 bytes

Reproduce

cpc check passing.cplus
cpc check bad/alloc.cplus --diagnostics=json
cpc check bad/block.cplus --diagnostics=json
cpc check bad/stack.cplus --diagnostics=json

The first command should pass. The three bad/ commands should fail, and the JSON diagnostics should include E0901, E0907, and E0908 respectively.

That is the proof boundary: the claim is not "the programmer remembered not to allocate." The claim is that the compiler rejects the callback if allocation, blocking, or an oversized frame becomes reachable. The public recipe is small, but the backing checks live in the compiler suite and in the rt package source.


‹ Back to all examples