# 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 (stays in the project)
├── fastcode.py         ← simulator fallback (stays in the editor)
└── fastcode.mpy        ← compiled ESP32-S3 code (this goes to the badge)
```

```python
import fastcode

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

## 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. Two files appear:
   - `native/fastcode.c` — a working template using MicroPython's dynamic
     runtime API. Edit this.
   - `fastcode.py` — a plain-Python **simulator fallback** with the same
     functions. The browser preview imports this one, because a browser
     cannot execute ESP32-S3 machine code. The editor **checks the two
     stay in sync at the API level**: if the C exports a name the fallback
     lacks (or vice versa), a console warning spells out exactly what the
     badge will have that the preview won't. What the functions *compute*
     isn't verified yet — that needs running your C in the preview, which
     is on the roadmap (the toolchain already carries the wasm backend for
     it). Until then: when you edit one file, touch the other; the
     fallback doubles as readable documentation of what the C does.
4. Press **build for badge** in the bar under the tabs when you want the
   real thing compiled. Building needs a compiler (see below); the preview
   never does.

## 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

## The simulator and your C code

The editor's badge preview runs the real firmware under Python-in-WASM —
an x86/ARM browser cannot run Xtensa machine code, and we won't pretend it
can. So:

- if `fastcode.py` (the fallback) exists, the preview imports it and your
  app runs normally — clearly labelled in the bar under the tabs;
- if you delete the fallback, `import fastcode` in the preview raises an
  ImportError explaining the situation, and the last working version keeps
  running on screen.

The fallback is **never installed on the badge**. MicroPython prefers
`foo.py` over `foo.mpy` when both exist, so shipping both would silently
shadow your native module with the slow Python copy. The editor ships only
`fastcode.mpy` to the badge, and only the sources in your downloads.

## Building

Compiling C for the ESP32-S3 needs a 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). The simulator fallback plus small, buffer-oriented C functions
  is the happy path.
- 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.
