C+
Realtime

Realtime audio meter

This example is a proof recipe for the #[realtime] claim. It combines an audio-style hot path with the real rt package shapes from the C+ source tree: 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], and the compiler E2E suite checks the diagnostics this page depends on.

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 src/main.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, len, 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/
├── Cplus.toml
├── src/main.cplus
└── bad/
    ├── alloc.cplus
    ├── block.cplus
    └── stack.cplus

Cplus.toml enables the project-wide realtime gate as well as the explicit attribute on the callback. That makes cpc check useful in CI: a new helper function cannot quietly add a heap allocation or a blocking wait.

[package]
name    = "realtime-meter"
version = "0.0.1"
edition = "2026"

[[bin]]
name = "realtime-meter"
path = "src/main.cplus"

[dependencies]
rt = "*"

[profile.realtime]
deny_alloc = true
deny_block = true
deny_unknown_extern = true
stack_limit = 4096

Passing callback

The callback reads a block of samples, applies a gain, writes the output, and publishes a fixed-point peak value into a lock-free SPSC ring. It allocates nothing and has no operation that can wait.

import "rt/rt" as rt;

const GAIN: f32 = 0.80f32;

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

fn peak_to_u64(x: f32) -> u64 {
    let clamped: f32 = if x > 1.0f32 { 1.0f32 } else { x };
    return (clamped * 1000000.0f32) as u64;
}

#[realtime]
#[max_stack(1024)]
fn process_audio(input: f32[], output: f32[], mut peaks: rt::SpscRingU64) -> i32 {
    let n: usize = output.len();
    let mut i: usize = 0 as usize;
    let mut 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 +% (1 as usize);
    }

    let _ok: bool = peaks.push(peak_to_u64(peak));
    return 0;
}

fn main() -> i32 {
    let mut input: f32[8] = [0.10f32, -0.20f32, 0.30f32, -0.40f32, 0.25f32, -0.50f32, 0.75f32, -1.00f32];
    let mut output: f32[8] = [0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32];
    let peaks: rt::SpscRingU64 = rt::SpscRingU64::new();

    return process_audio(input, output, peaks);
}

The ring write is deliberately nonblocking: if the consumer falls behind, push returns false; the callback does not wait.

The package implementation follows the same rule. SpscRingU64::push checks capacity and returns false when full:

#[no_alloc] #[no_block]
pub fn push(mut self, v: u64) -> bool {
    let head: u64 = unsafe { atomic::atomic_load_u64(#addr_of(self.head), atomic::Ordering::Acquire) };
    let tail: u64 = unsafe { atomic::atomic_load_u64(#addr_of(self.tail), atomic::Ordering::Relaxed) };
    if (tail -% head) >= (1024 as u64) {
        return false;
    }
    let idx: usize = (tail % (1024 as u64)) as usize;
    self.buf[idx] = v;
    unsafe { atomic::atomic_store_u64(#addr_of(self.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]
pub fn acquire(mut self) -> option::Option[u32] {
    if self.free_head == pool_none() {
        return option::Option[u32]::None;
    }
    let idx: u32 = self.free_head;
    self.free_head = self.links[idx as usize];
    self.in_use = self.in_use +% (1 as usize);
    return option::Option[u32]::Some(idx);
}

Negative fixture: allocation

This fixture is the kind of change a model might accidentally introduce while trying to make a diagnostic string. to_text() allocates, so it is rejected inside the realtime contract.

#[realtime]
fn process_bad_alloc(x: f32) -> i32 {
    let label = "peak=".to_text();
    if x > 0.0f32 { return label.len() as i32; }
    return 0;
}

Expected diagnostic shape:

bad/alloc.cplus:3:24: error[E0901]: function `process_bad_alloc` is marked `#[no_alloc]` but calls allocating function `to_text`

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.

import "stdlib/thread" as thread;

#[realtime]
fn process_bad_block() -> i32 {
    thread::sleep_ms(1 as u64);
    return 0;
}

Expected diagnostic shape:

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

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;
}

Expected diagnostic shape:

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

Reproduce

cpc check
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