FFI, real-time, and performance in C+
You can model data and manage ownership (guide two). This is the systems layer: talking to C in both directions, giving the optimizer what it needs to vectorize, and asking the compiler to prove a hot path is real-time safe. Every command and every output below was run as written.
The C ABI goes both ways
Most languages can call C. C+ does that, and it also goes the other way: cpc emits standard relocatable object files with plain C symbols, so a C or C++ build links C+ like any other .o. That second direction is what lets you adopt C+ inside an existing codebase one function at a time.
Direction one: C+ calls C
Declare the C function with extern fn and call it inside unsafe. There is no binding generator and no glue layer:
extern fn abs(x: i32) -> i32;
fn main() -> i32 {
let n: i32 = unsafe { abs(0 -% 5) };
println(n); // 5
return 0;
}
The unsafe is the point where you take responsibility: the compiler cannot check what a foreign function does, so you mark the call site and own the contract. Everything outside that block stays under the usual rules.
Direction two: C calls C+
Mark a function pub extern fn and it is exported with an unmangled C symbol. Here is a tiny library, with no main:
pub extern fn cplus_add(a: i32, b: i32) -> i32 {
return a +% b;
}
pub extern fn cplus_scale(x: i32, by: i32) -> i32 {
return x *% by;
}
Compile it to an object file, and generate a matching C header:
$ cpc --emit-obj mathx.cplus -o mathx.o
$ cpc --emit-header mathx.cplus > mathx.h
The header cpc writes is exactly what a C compiler expects:
// Generated by cpc — public C ABI for `cplus_lib`. Do not edit.
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int32_t cplus_add(int32_t a, int32_t b);
int32_t cplus_scale(int32_t x, int32_t by);
#ifdef __cplusplus
} // extern "C"
#endif
And the symbols in the object file are plain C symbols, nothing mangled:
$ nm mathx.o | grep cplus
0000000000000000 T _cplus_add
0000000000000020 T _cplus_scale
So an ordinary C program includes the header and links the object:
#include <stdio.h>
#include "mathx.h"
int main(void) {
printf("cplus_add(40, 2) = %d\n", cplus_add(40, 2));
printf("cplus_scale(7, 6) = %d\n", cplus_scale(7, 6));
return 0;
}
$ cc main.c mathx.o -o demo
$ ./demo
cplus_add(40, 2) = 42
cplus_scale(7, 6) = 42
That is the whole story: cpc produced a .o that clang linked with no special flags. Anything a C build system can link, it can link C+. In a real project that means dropping the object into your existing Make or CMake build and replacing functions one symbol at a time, while the program keeps producing the same output, until enough is ported that C+ drives the build on its own.
Raw pointers, unsafe, and restrict
At the systems layer you sometimes hold a raw pointer *T. Every operation on one (load, store, arithmetic, calling through it) requires unsafe, because the borrow checker does not track a raw pointer's lifetime. That is the deal: you get C-level control exactly where you ask for it, marked so a reader sees it.
The payoff marker is restrict. It tells the optimizer that a pointer parameter does not alias any other pointer in the function, which is the fact a vectorizer needs to avoid a defensive scalar fallback:
extern fn malloc(n: usize) -> *u8;
extern fn free(p: *u8);
fn axpy(n: usize, restrict x: *i32, restrict y: *i32) {
let mut i: usize = 0 as usize;
while i < n {
unsafe { y[i] = x[i] +% y[i]; }
i = i +% (1 as usize);
}
return;
}
fn main() -> i32 {
let xb: *u8 = unsafe { malloc(8 as usize) };
let yb: *u8 = unsafe { malloc(8 as usize) };
let x: *i32 = unsafe { xb as *i32 };
let y: *i32 = unsafe { yb as *i32 };
unsafe { x[0] = 10; x[1] = 20; y[0] = 1; y[1] = 2; }
axpy(2 as usize, x, y);
let r: i32 = unsafe { y[0] +% y[1] };
println(r); // 33
unsafe { free(xb); free(yb); }
return 0;
}
It prints 33: y becomes [11, 22], and 11 + 22 = 33. restrict does not change what the program computes; it removes a guarantee the optimizer would otherwise have to prove or work around. It is a contract about the body, so it needs no unsafe of its own. The unsafe you do see sits on the pointer operations, where the real obligation is.
Deterministic by contract
A hot path (an audio callback, a control loop, a frame step) cannot afford a surprise allocation or a lock. Mark the function #[realtime] and the compiler proves the absence of both across the whole call graph. This compiles:
#[realtime]
fn scale(n: usize, restrict y: *i32, by: i32) {
let mut i: usize = 0 as usize;
while i < n {
unsafe { y[i] = y[i] *% by; }
i = i +% (1 as usize);
}
return;
}
Now add a heap allocation to a function with the same contract, and it does not:
extern fn malloc(n: usize) -> *u8;
#[realtime]
fn hot() {
let p: *u8 = unsafe { malloc(16 as usize) };
return;
}
rt_bad.cplus:4:27: error[E0901]: function `hot` is marked `#[no_alloc]` but calls allocating function `malloc`
| let p: *u8 = unsafe { malloc(16 as usize) };
rt_bad.cplus:4:27: error[E0907]: function `hot` is marked `#[no_block]` but calls extern `malloc` which is not in the known-nonblocking leaf set
| let p: *u8 = unsafe { malloc(16 as usize) };
#[realtime] bundles the smaller contracts: #[no_alloc] (no path reaches an allocator) and #[no_block] (no locks, waits, or blocking I/O), plus bounded recursion and stack. They compose transitively: a #[realtime] function may only call functions that are themselves proven safe. This is not a lint you can ignore. It is a compile error, which means you cannot ship a real-time function that quietly allocates, and neither can a model writing one for you.
Performance idioms worth knowing
When a hot loop matters, a few habits pay off, and they happen to keep the code legible too:
- Return through an out-pointer for large results.
fn hit(..., restrict out: *Hit) -> boolwrites straight into the caller's slot instead of returning a big struct by value. It is the same shape C uses withT *out, and it avoids the return-value shuffle. - Prefer free functions for small numeric helpers. A free
fn add(a: V3, b: V3) -> V3and an equivalent method lower the same way, and the free function is one less call shape to reason about. - Keep scratch on the stack. A small fixed array like
let mut tmp: [u8; 64] = [0u8; 64];lives on the stack;mallocin a tight loop does not. The zero-fill case lowers to a singlememset. - Put
restricton every heap pointer the call site can promise doesn't alias. It is the cheapest way to unblock vectorization.
None of these are tricks the compiler hides from you. They are visible in the source, which is the rule the whole language follows.
What you can build now
You have the full systems surface: call C, be called by C, hold raw pointers under unsafe, hand the optimizer aliasing facts with restrict, and let the compiler prove a hot path is allocation-free and lock-free. Combined with the structs, ownership, and error handling from the earlier guides, that is enough to write a library others link from C, a real-time audio or control loop, or a numeric kernel, and to know, before anything runs, that it does what the signatures say.
Where to go next
- Read the language reference for the full list of attributes, intrinsics, and standard-library modules.
- Look at the
vendor/rtpackage for lock-free SPSC rings and fixed pools built to the same#[realtime]contract. - Port one function. Take a small piece of an existing C project, rewrite it in C+, link the object back in, and confirm the output is unchanged. That is the loop that scales.
‹ Back to all guides