Real Hardware (ESP32-C3 / Cortex-M4)
Links: GitHub · mailang.h · test_c.c · IoT Deployment
Embed mailang-ffi on a real MCU, wire gpio_* / delay_ms / adc_read to a real HAL via host functions, and ship precompiled .mailangbc.
Prerequisite: SimulatedHal is host-only
Built-ins gpio_write / gpio_read / delay_ms / adc_read use mailang_stdlib::hal::SimulatedHal (crates/mailang-stdlib/src/hal.rs) and are desktop/simulation only:
- 32 virtual GPIO pins, 8 virtual ADC channels
delay_msonly advances a simulated clock (no real sleep)- no real peripherals
On a real MCU you must register host functions with mailang_register_host_fn and bind them to the vendor HAL. SimulatedHal will not blink an LED.
1. Build the static library
cargo build -p mailang-ffi --release
# target/release/libmailang_ffi.a
# header: crates/mailang-ffi/include/mailang.h
rustup target add riscv32imc-unknown-none-elf
cargo build -p mailang-ffi --release --target riscv32imc-unknown-none-elf # ESP32-C3
rustup target add thumbv7em-none-eabihf
cargo build -p mailang-ffi --release --target thumbv7em-none-eabihf # Cortex-M4The C FFI layer expects std. For bare-metal Cortex-M4 prefer mailang-vm + mailang-bytecode (no_std + alloc) — see IoT Deployment.
Key C API (full list in mailang.h):
MailangInterpreter *mailang_create(void);
void mailang_destroy(MailangInterpreter *interp);
int mailang_eval(MailangInterpreter *interp, const char *code, MailangResult *result);
int mailang_eval_file(MailangInterpreter *interp, const char *path, MailangResult *result);
int mailang_register_host_fn(MailangInterpreter *interp, const char *name,
MailangHostFn callback, void *user_data);
void mailang_free_string(char *ptr);2. Bind real GPIO / Delay / ADC
Follow the host_add pattern in ffi/tests/test_c.c:
#include "mailang.h"
extern void board_gpio_write(int pin, int level);
extern int board_gpio_read(int pin);
extern void board_delay_ms(uint32_t ms);
extern int board_adc_read(int channel);
static int host_gpio_write(void *ud, int32_t argc, const MailangValue *argv, MailangValue *out) {
(void)ud;
if (argc != 2 || argv[0].tag != 2) return 1;
board_gpio_write((int)argv[0].i, argv[1].i ? 1 : 0);
out->tag = 0; out->i = 0; out->f = 0; out->s = NULL;
return 0;
}
static int host_gpio_read(void *ud, int32_t argc, const MailangValue *argv, MailangValue *out) {
(void)ud;
if (argc != 1 || argv[0].tag != 2) return 1;
out->tag = 1; out->i = board_gpio_read((int)argv[0].i) ? 1 : 0;
out->f = 0; out->s = NULL;
return 0;
}
static int host_delay_ms(void *ud, int32_t argc, const MailangValue *argv, MailangValue *out) {
(void)ud;
if (argc != 1 || argv[0].tag != 2 || argv[0].i < 0) return 1;
board_delay_ms((uint32_t)argv[0].i);
out->tag = 0; out->i = 0; out->f = 0; out->s = NULL;
return 0;
}
static int host_adc_read(void *ud, int32_t argc, const MailangValue *argv, MailangValue *out) {
(void)ud;
if (argc != 1 || argv[0].tag != 2) return 1;
out->tag = 2; out->i = board_adc_read((int)argv[0].i);
out->f = 0; out->s = NULL;
return 0;
}
void mailang_bind_hal(MailangInterpreter *interp) {
mailang_register_host_fn(interp, "gpio_write", host_gpio_write, NULL);
mailang_register_host_fn(interp, "gpio_read", host_gpio_read, NULL);
mailang_register_host_fn(interp, "delay_ms", host_delay_ms, NULL);
mailang_register_host_fn(interp, "adc_read", host_adc_read, NULL);
}Scripts keep calling gpio_write(2, true) / delay_ms(50); the host registration takes over.
3. Load source or .mailangbc
Source (dev)
MailangInterpreter *interp = mailang_create();
mailang_bind_hal(interp);
MailangResult r;
if (mailang_eval(interp, "gpio_write(2, true)\ndelay_ms(100)", &r) == MAILANG_OK)
mailang_free_string(r.output);
mailang_destroy(interp);Precompiled bytecode (production)
cargo run -p mailang-cli -- build app.mai -o app.mailangbcEmbed the blob (xxd -i app.mailangbc > app_bc.h) and run it through the C bytecode API:
#include "mailang.h"
extern const unsigned char app_bc[];
extern const unsigned int app_bc_len;
char *out = NULL;
MailangStatus st = mailang_eval_bytecode(app_bc, app_bc_len, &out);
if (st == MAILANG_OK) {
/* out holds program output */
} else {
/* out holds an error message */
}
if (out) mailang_free_string(out);
/* Or load from a filesystem / flash-backed path */
st = mailang_load_bytecode_file("app.mailangbc", &out);
if (out) mailang_free_string(out);mailang_eval_bytecode(data, len, &out)— decode an in-memory.mailangbcblob and run it in a fresh interpreter.mailang_load_bytecode_file(path, &out)— read file → decode → run.- Failure codes:
MAILANG_ERR_DECODE(bad container),MAILANG_ERR_IO(read failed),MAILANG_ERR_EVAL(runtime).
These bytecode entry points run in a fresh interpreter and do not inherit host functions. For HAL callbacks, keep using mailang_eval on source, or a Rust entry (run_bytecode + register_host_fn).
Checklist
- Host:
mailang build→.mailangbc - Cross-build
mailang-ffistaticlib (ormailang-vmbare-metal) mailang_bind_hal()→ real GPIO/delay/ADCmailang_create()→ bind HAL →mailang_eval; ormailang_eval_bytecode/mailang_load_bytecode_filefor precompiled bytecode- Free strings with
mailang_free_string; checkmailang_last_error
References
- End-to-end blink: ESP32 Blink (
examples/iot_blink.mai+ IDF-style C host) - Footprint: Footprint
- Header:
crates/mailang-ffi/include/mailang.h - C smoke test:
ffi/tests/test_c.c - SimulatedHal:
crates/mailang-stdlib/src/hal.rs - Bytecode:
crates/mailang-bytecode(.mailangbc) - Demo scripts:
examples/hal_sim.mai,examples/iot_blink.mai