Real-time
Audio callbacks, control loops, and frame hot paths cannot afford a hidden allocation or a lock. C+ lets you mark such a function and have the compiler prove the property holds across the entire transitive call graph. A violation is a compile error, not a lint you can ignore.
The near-term target is soft real-time: audio callbacks, game frame loops, robotics control loops, market-data hot paths, and embedded-style firmware on a normal OS. Hard real-time needs scheduler, interrupt, and hardware guarantees that live below the language.
The #[realtime] bundle
#[realtime] is sugar for a bundle of smaller contracts:
#[no_alloc]— no heap allocation#[no_block]— no blocking call or lock wait#[bounded_recursion]— no unbounded recursion
Bounded stack usage is checked alongside it. Each smaller attribute is available on its own, because many code paths need only one part of the contract.
#[realtime]
fn process_frame(input: f32x4[], output: f32x4[]) {
let n: usize = output.len();
let mut i: usize = 0 as usize;
while i < n {
output[i] = input[i] * GAIN; // pure SIMD math
i = i +% (1 as usize);
}
// a Vec::new, lock, channel recv, sleep, or unknown extern here
// would not compile (E0901 / E0907)
return;
}
#[no_alloc] — proven allocation-free (E0901)
A #[no_alloc] function rejects any path that reaches the heap. Direct malloc, calloc, realloc, aligned_alloc, and free reject, as do calls through #[link_name = "malloc"]. The check is resolved from sema information, not text, so it also catches allocation through a method call, through string interpolation ("...${x}..."), and through the Vec / HashMap / Box / Arc / Text constructors. The blessed to_string() is rejected at its call site because it allocates.
The contract is transitive: a function called from a #[no_alloc] body must itself be #[no_alloc], and an unknown extern is treated as "not proven" and rejected. The escape hatch is to vouch for a known-safe extern explicitly:
#[no_alloc]
extern fn sinf(x: f32) -> f32;
The check reaches implicit drop-glue too. If a #[no_alloc] function lets a value drop at scope exit and that destructor would allocate or free — a Text / Vec / Box local, or a type whose drop is not itself #[no_alloc] — it is rejected (E0901), reaching through struct fields, enum payloads, and array elements. So a hidden free at the closing brace is caught the same way a direct free call is.
This now covers more than let locals: an owned drop-carrying parameter (a move x, a move-by-default non-Copy struct, or move self) that drops at the end of the function, and a discarded drop-carrying temporary (a returned-but-unused owning value), are both caught as well. Anywhere the compiler would insert a drop that allocates or frees, the contract sees it.
#[no_block] — proven non-blocking (E0907)
A #[no_block] function rejects mutex locks, condition-variable waits, thread joins, sleep and timer waits, blocking file and socket I/O, unbounded channel recv, and unknown externs. It allows plain arithmetic, stack memory, atomics, atomic_thread_fence, #cpu_relax, and nonblocking try-style APIs. Like #[no_alloc], it composes transitively.
#[max_stack(N)] — bounded frame size (E0908)
#[max_stack(4096)]
fn callback(...) { ... }
The checker estimates the frame from the parameters and typed locals across all nested blocks, using ABI-accurate sizes for primitives, pointers, arrays, structs, enums, and SIMD types, and reports E0908 when the estimate exceeds N. Large local arrays and by-value temporaries become visible in the diagnostic.
Project-wide: [profile.realtime]
Rather than annotate every function, a project can opt in globally from its manifest:
[profile.realtime]
deny_alloc = true
deny_block = true
deny_unknown_extern = true
stack_limit = 4096
When present, the driver synthesizes the matching contracts onto every function defined in this package (dependencies are exempt), and the same E0901 / E0907 / E0908 diagnostics do the enforcement. cpc check runs the whole-project front-end gate without codegen, the fast CI check, and cpc check --diagnostics=json emits one machine-readable diagnostic per line for editor and CI tooling.
Threads: Send and Sync (E0502)
The marker types are enforced structurally. Rc[T] is !Send and !Sync, and MutexGuard[T] is !Send, so handing one to a Send-bounded site such as thread::spawn is E0502. Arc[T] stays Send + Sync as the thread-safe sibling.
A nominal type that transitively hides a raw pointer is !Send and !Sync by default, and the same !Send propagates structurally through a struct that holds an Rc. When a type is in fact safe to move or share, opt back in with a manual unsafe impl Send / unsafe impl Sync — see Threads & atomics for the marker syntax and its conditional, generic form.
Building blocks
The contracts say what a hot path may not do; these packages give it allocation-free, lock-free tools to work with:
- rt — a lock-free SPSC ring (
SpscRingU64) and a fixed-capacity object pool (FixedPoolU64). Every method is#[no_alloc]and#[no_block], so they are callable from inside a#[realtime]body. - static-arena — a fixed-size, stack-resident arena with zero
mallocand zerofree. rt_darwin(see the rt page) — the macOS platform controls: a monotonic clock, audio-QoS thread priority, and page locking, each returningResult.rt_linuxandrt_posixwill mirror it.