# C modules in badge apps

Write part of your badge app in C. The editor compiles it into a MicroPython
**dynamic native module** — a `.mpy` file that ships *inside your app* and
loads when the app imports it. Nothing about the badge's firmware changes:
no rebuild, no reflash, no soldering iron. Install the app, run it, done.

```text
my_app/
├── app.py              ← your app, plain MicroPython
├── native/
│   └── fastcode.c      ← your C code (this is all you write)
└── fastcode.mpy        ← compiled ESP32-S3 code (this goes to the badge)
```

```python
import fastcode

print(fastcode.add(20, 22))   # 42, at C speed
```

**One implementation.** You write only the C. The live preview runs that
*same* C — the editor compiles it to WebAssembly and imports it — so what
you see as you type is your real code, not a Python stand-in you'd have to
keep in sync. Integers are 32-bit in the preview just like on the badge, so
overflow and the whole API behave identically in both places.

## Why no reflashing?

MicroPython can load machine code at runtime from a `.mpy` file, the same
format it uses for compiled bytecode. The badge firmware (MicroPython
v1.28.0 on the ESP32-S3) ships with that loader enabled. A native `.mpy`
carries a header naming its ABI — format 6.3, architecture `xtensawin` —
and the firmware checks it at import time: a matching module is relocated
into internal RAM and runs; a mismatched one raises a clean `ValueError`
instead of crashing.

## Creating a C module

1. Open the file tab bar (above the code) and press **＋ → C module**.
2. Name it — lowercase letters, digits, `_`. The name **is** the import:
   `native/fastcode.c` compiles to `fastcode.mpy`, imported as
   `import fastcode`.
3. One file appears: `native/fastcode.c`, a working template using
   MicroPython's dynamic runtime API. Edit it, and the preview updates as
   you type — it compiles your C to WebAssembly and runs it, the same way
   `import fastcode` runs the compiled `.mpy` on the badge. A compile error
   marks the offending line, and the last working version keeps running on
   screen (just like a Python syntax error).
4. Press **build for badge** in the bar under the tabs to cross-compile the
   ESP32-S3 `.mpy` for flashing. (The preview compile is separate and
   automatic; it targets WebAssembly, the badge build targets Xtensa.)

## What you can use in C

Native modules are written against `py/dynruntime.h` — a small, stable
subset of the MicroPython C API. In scope:

- integers, booleans, floats (single precision is hardware-fast; doubles
  are software emulated)
- strings and bytes, in and out
- buffers via `mp_get_buffer` — pass a `bytearray` from Python and crunch
  it in C, zero copies
- lists/tuples/dicts through the generic object API
- raising and catching Python exceptions
- defining functions and (with care) classes

Out of scope — the badge will refuse the import or the build will fail:

- arbitrary ESP-IDF / FreeRTOS calls (no `esp_wifi_*`, no direct
  peripheral registers)
- most of libc — there is no libc in a natmod; a few compiler builtins
  (`memcpy`-style) are provided, everything else must be self-contained
- C++, threads, global constructors
- static data with initializers works, but keep it small: native code and
  data live in the badge's internal RAM (the 8 MB PSRAM cannot execute
  code), so a module should stay in the tens-of-KB range

## How the preview runs your C

The badge preview runs the real firmware under Python-in-WASM (Pyodide,
which is CPython compiled to WebAssembly). A browser can't execute Xtensa
machine code, so the editor compiles your `native/fastcode.c` to a
*different* target — a WebAssembly extension module — against a shim that
reimplements MicroPython's `py/dynruntime.h` on top of CPython's C API.
Then `import fastcode` in the preview loads your real, compiled C.

Because a native module can only ever touch the `dynruntime.h` API, and
the shim implements that API faithfully — including 32-bit integers, so
overflow wraps exactly as on hardware — a module can't tell the preview
from a badge. The two things it can't reproduce (defining brand-new
MicroPython object *types* in C, or poking at raw object memory layout) are
outside the supported API and the editor rejects them.

If the preview compiler isn't available yet (a fresh deployment still
enabling it), a C module simply doesn't run in the preview and says so; the
badge build is unaffected. Nothing is ever silently faked.

## Building for the badge

The preview compile (above) is automatic and targets WebAssembly. Getting
the module onto a badge needs the Xtensa cross-toolchain, pinned to match
the firmware exactly (MicroPython v1.28.0, `.mpy` 6.3, `xtensawin`). The
**build for badge** button looks for, in order:

1. an artifact already built for exactly these sources (content-addressed
   cache — any change to any source rebuilds);
2. a prebuilt artifact shipped with the site (the bundled demos);
3. **the in-browser compiler**: clang with the Xtensa backend, compiled to
   WebAssembly, paired with MicroPython's own linker running under Python-
   in-WASM. Your C never leaves the page. The first build downloads the
   toolchain (~15 MB, cached by the browser); after that it's seconds,
   and it works offline;
4. a compile service, as a fallback: run the editor locally
   (`web/serve.py`) with docker available and builds use the exact GCC
   container that builds the firmware itself. See
   `livecoding-web/native-build/README.md`.

Both compilers produce interchangeable, firmware-accepted modules; the
docker path is the byte-for-byte reference implementation.

## Compile errors

Errors land in three places at once: the offending line is marked in the
editor (switch to the `fastcode.c` tab if you're elsewhere — its tab shows
a red dot), a one-line summary appears in the bar, and the full compiler
output is in the console panel at the bottom right.

## Flashing and installing

Once built, ⚡ **Flash to badge** writes the `.mpy` next to your `app.py`
on the hexpansion (mind the ~6.5 KB EEPROM budget — a small module is
~150 bytes, but they add up), and **save to badge** installs it into
`/apps/<name>/` over USB. Both verify every byte they write.

For `import fastcode` to work on the badge from `/apps/<name>/`, have your
`app.py` add its own directory to the import path first:

```python
import sys
_dir = __file__.rsplit("/", 1)[0]
if _dir not in sys.path:
    sys.path.insert(0, _dir)

import fastcode
```

(The editor's C-module template app does this for you.)

## Compatibility and safety notes

- A native `.mpy` is tied to the firmware's `.mpy` sub-version. Firmware
  v1.12+ ships MicroPython v1.28 (`.mpy` 6.3); after a *major* firmware
  update, rebuild and reinstall — until then the badge refuses the stale
  module with a clear error rather than misbehaving.
- Native code is native code: it can crash the badge (and then the badge
  reboots — apps can't brick it, the firmware and your files live in
  flash). Small, buffer-oriented C functions are the happy path — and the
  preview runs the same C, so most mistakes surface there first.
- Compilation treats your C as untrusted input: server-side builds run in
  a no-network, resource-limited, read-only container, and nothing that
  gets compiled is ever executed on the build host.
