# Native boundary contract The private ABI is declared in `platform/include/radio_bridge.h` and imported only by `esp32-radio/src/platform/ffi.rs`. Its functions pass fixed-width integers, C strings, opaque handles, raw buffers and one private `#[repr(C)]` HMAC scatter/gather pair. They never pass Rust-layout structs, trait objects, owned Rust containers, enums or references. `size_t` corresponds to Rust `usize`; both are 32-bit on this target. All callers must provide valid pointer/length pairs, initialized inputs and writable output storage of the declared capacity. Each Rust wrapper constructs raw arguments from live slices or strings and checks returned lengths. Most C outputs are bounded writes into caller-owned storage. HTTP and DSP expose handle-tied buffer borrows; analysis history is a C allocation released through its matching free function. The C header is private, not a general-purpose public FFI library that accepts arbitrary hostile pointers. ## Ownership and calls | Resource | Owner and lifetime | Synchronization | | --------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Board drivers | One initialized board; C retains driver handles | Rust moves exclusive LED access to the radio task through a wrapper that is not `Sync` | | Crypto adapter keys and hash snapshots | Rust-owned bytes; adapter key and secret buffers use zeroizing storage | Synchronous native operations; shared ESP-IDF peripheral locks; no retained Rust pointers | | DTLS certificate and private key | C generates with mbedTLS; Rust and str0m retain bounded DER buffers for the peer lifetime | Synchronous generation after Wi-Fi/time initialization; native contexts freed before return | | Music partition and cache | IDF owns the immutable partition descriptor; Rust owns a 32 KiB cache and a bounded offset index | Synchronous C reads borrow writable Rust storage only for the call; the radio task owns the cache | | HTTPS client/configuration/response | C allocates; Rust owns the opaque handle and calls C cleanup in `Drop` | Client is neither `Send` nor `Sync`; blocking operations stay on signaling | | HTTPS POST body | Borrowed for one blocking call | Native POST field is replaced with a static empty body before return; callbacks write C-owned response storage | | Decoder and aligned FFT buffer | C owns config, PCM, packet and FFT storage; Rust holds a task-affine handle | Synchronous methods; no callbacks; one global ESP-DSP table owner | | Analysis history | C allocates initialized PSRAM; Rust exclusively borrows it for the window/ring | No C aliases; C frees after the Rust borrow ends | | Task creation configuration | Rust saves the creator's settings for new threads while configuring a task | A non-Send guard restores those settings on the creating task after the spawn attempt | | Metrics context and temperature sensor | C owns a task-affine handle; Rust closes it on the same core-1 sampler task | Synchronous output copies into initialized Rust arrays; no callbacks or retained Rust pointers | | Recovery count in RTC memory | C retains across software resets | A C critical section protects updates; a Rust atomic selects the one task that initiates recovery | The HTTPS response is capped at 24,576 bytes, HTTP request bodies at 20,000 bytes, SDP at 16,000 bytes, outbound data at 2,048 bytes, and individual Opus packets at 1,275 bytes. Redirects are disabled. Time synchronization precedes verified TLS, using the ESP-IDF certificate bundle. Credentials are compiled into the private C configuration and never formatted into Rust errors. ## WebRTC ownership WebRTC has no C peer handle or callbacks. `radio-webrtc` owns str0m, its UDP socket and protocol queues entirely in Rust. The radio task passes UDP datagrams and expired timers to str0m, writes audio and application data, and sends the UDP packets that str0m returns. Certificate generation writes DER and used lengths into caller-owned buffers, then frees its mbedTLS contexts before returning. The host does not provision the live peer's certificate or key. Keys and SDP must never enter logs. ## Crypto operations AES and signature calls create native contexts locally, complete synchronously, and free them before returning. The hardware APIs serialize peripheral access with the same ESP-IDF locks used by HTTPS. Rust cipher, signing and key-exchange instances contain only owned, zeroizing private bytes and public metadata; they need no native pointer or unsafe `Send`/`Sync` implementation. For in-place GCM, Rust derives both native buffer arguments from one exclusive pointer instead of creating overlapping shared and mutable borrows. Authentication failure clears the plaintext range before returning an error. HMAC passes at most eight private C-layout pointer/length pairs, borrowed for one call. Its SHA-1 interface and SHA-256 fingerprints can fall back to RustCrypto on native failure. DTLS HMAC preserves the requested hash algorithm. The 256-byte SHA snapshot stores a copy of the native context's value fields in private opaque storage; it is not a public serialization format. For each operation, C copies those bytes into an aligned local context. The pinned ESP32-S3 SHA-256/SHA-512 contexts release their hardware locks between calls and contain no heap pointer or persistent peripheral ownership. C initializes the snapshot's unused bytes, checks its capacity at compile time, and finalizes a copy so the stored state remains unchanged. Review the native context fields and lock lifetime when upgrading ESP-IDF or porting to another chip. A failed incremental hash aborts: its upstream trait cannot return an error, and continuing would produce an invalid transcript. EC verification parses only the supplied certificate's public key and checks the signature. It is not CA-chain validation. The Rust adapter validates the signature/hash/curve combination; str0m verifies the authenticated SDP fingerprint. P-256 exchange exports a 32-byte private scalar and 65-byte uncompressed public point to Rust-owned buffers. Completion validates the private scalar and remote point before computing the secret, with RNG-backed blinding. Signing parses DER into a temporary native context and returns a bounded DER signature. Wi-Fi starts before these calls so ESP-IDF hardware entropy is available. No native EC context survives a call. The adapter's signing-key copy, ECDH scalar and derived secret use zeroizing storage; the DTLS layer releases its owned certificate and key buffers with the peer. ## Failure and teardown Release builds use `panic = "abort"`. Rust panics cannot unwind through C. Expected input and transport failures use results/status codes; malformed commands are rejected and counted without restarting the device. A terminal recovery is a board restart after a task encounters a fatal runtime error. A Rust atomic allows only the first failing task to initiate recovery; other failing tasks wait for the restart. The initiator updates the persistent recovery count and waits for a bounded backoff before restarting the board. Dropping a str0m peer releases its Rust allocations and socket. For normal protocol closure, call `close`, then continue processing str0m input and output until it reports completion or the caller's deadline expires. No transport callback can outlive the peer. HTTP owns one 24 KiB response buffer in PSRAM. Its blocking post completes its response callbacks before returning a read-only slice tied to `&mut Http`. That borrow prevents another post or cleanup while the slice is used. Only initialized response bytes are exposed. Native transport failures return a negative status and an empty slice. Deserialization produces owned setup state before reuse. C cleanup closes the client before freeing its configuration and response buffer. Analysis APIs exchange `float`/Rust `f32` and `int16_t`/Rust `i16` buffers. The C build asserts 32-bit floats. PCM is exactly 960 stereo samples per packet; FFT storage is 4,096 interleaved real/imaginary values for 2,048 points. Rust buffer borrows exclude native decode/transform/cleanup calls. The decoder and FFT have no asynchronous callbacks, so their `Drop` can close resources on the owning task. ESP-DSP tables are global: the adapter permits only one owner and no reinitialization after it is dropped. Keep FFT work out of interrupt context. Metrics sampling writes three u64 microsecond counters (time since boot and idle time on cores 0 and 1), six u32 memory values (free/minimum/largest for internal RAM, then PSRAM), and two i32 sensor values (milli-Celsius and RSSI). `INT32_MIN` marks an unavailable sensor. The wrapper supplies exactly those capacities. The sampler's core affinity is checked before native temperature conversion; its handle is neither `Send` nor `Sync`. Task creation uses ESP-IDF HAL's safe pthread configuration API. A non-`Send` Rust guard saves the creator's settings for new threads and restores them on the same task after spawning, including when spawning fails. Missing settings or HAL's zero-priority error result fall back to SDK defaults. Task names come from static Rust C-string literals; configuration does not cross the private C ABI.