Embedded & ESP32
C+ cross-compiles to microcontrollers. The first 32-bit target is
esp32-xtensa (Espressif's ESP32), and the same language you write for a
desktop runs on a chip with a few hundred kilobytes of RAM, with the
real-time contracts proven by the compiler before the firmware
ever flashes.
cpc build --target esp32-xtensa
On this target usize, isize, and pointers are 4 bytes, and #size_of /
#align_of and layout all follow the target's pointer width, so the same source
is correct on 32-bit and 64-bit (64-bit output is byte-identical to before). The
Xtensa C ABI is pinned against a real esp-clang probe, so aggregates cross the
FFI boundary the way ESP-IDF expects.
Heap types work on the chip
Text, Vec, Box, and the rest of the heap modules run on the ESP32's newlib
heap: fat pointers, string and collection lengths, and the libc size_t surface
(malloc / memcpy / snprintf) all follow the target pointer width. A Text
and a Vec[i32] built on-device print correctly over the UART. You are not
restricted to a no-heap subset.
The embedded package profile
A microcontroller has no threads, sockets, or filesystem, so a target can
exclude the standard-library modules whose mechanism it lacks. On
esp32-xtensa, importing the POSIX half of stdlib (thread, mutex,
channel, env, net, netsys, reactor, executor, time, fs) fails at
resolve time with E0866, naming the target and pointing you at
espidf — a clear diagnostic instead of a verifier error
after codegen. async fn is rejected at check time with E0867, because the
coroutine runtime is 64-bit only. The heap modules stay available, and the host
profile is unchanged.
#[realtime], proven and then run
The point of #[realtime] is that the compiler proves a hot
path never allocates or blocks. On a microcontroller that guarantee is
load-bearing, and it holds end to end: a fixed-point PID controller marked
#[realtime] (so #[no_alloc] + #[no_block] + bounded recursion) builds as an
esp32-xtensa staticlib, links into an ESP-IDF firmware, and runs closed-loop on
an ESP32 at roughly 1.84 µs per step (about 442 cycles). The same contract
rejects an allocating variant with E0901 at cpc check, before any hardware
is involved.
Talking to the hardware: espidf
The espidf package binds the ESP-IDF drivers a control
loop needs: GPIO, the esp_timer microsecond clock, task sleep, and UART
console output. Its gpio and timer externs are #[no_alloc] + #[no_block]
leaves, so a #[realtime] loop can drive pins and read the clock without
breaking its contract.
The app exports a cplus_app_main entry point, and ESP-IDF's main component
keeps a two-line C shim:
extern void cplus_app_main(void);
void app_main(void) { cplus_app_main(); }
That shim is the only C in an otherwise all-C+ firmware (GPIO blink, a
#[realtime] PID, and telemetry have all run on hardware with nothing else).
Toolchain
Building for esp32-xtensa needs esp-clang (LLVM 19+). cpc finds it from
$CPC_ESP_CLANG, then $IDF_TOOLS_PATH, then ~/.espressif (newest
tools/esp-clang/); a missing install gets the idf_tools.py install esp-clang
hint.