C+
Systems · View as Markdown

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.