Native words

Add Frothy words backed by C through a library manifest.


Native words are Frothy words whose implementation is compiled from C. They are part of a library. There is no separate foreign-call syntax.

lib.toml adds [extension] for C sources and one [[natives]] block per C word:

name    = "neopixel"
version = "0.1.0"
boards  = ["esp32_devkit_v1"]

[extension]
sources = ["native/neopixel.c"]

[[natives]]
name       = "neopixel.show"
arity      = 1
c_function = "fr_lib_neopixel_show"
  • [extension].sources lists .c files, with paths relative to the library root.
  • Each [[natives]] maps a Frothy word name and arity to a C function.
  • Native word names usually look like <lib>.<word>. They may contain letters, digits, ., _, and -, but may not start with - or a digit.

C ABI

A native C function has exactly this signature:

#include "runtime.h"
#include "tagged.h"
#include "types.h"

fr_err_t fr_lib_neopixel_show(fr_runtime_t *runtime, const fr_tagged_t *args,
                              uint8_t arg_count, fr_tagged_t *out) {
  fr_int_t count = 0;
  FR_TRY(fr_tagged_decode_int(args[0], &count));
  /* ... drive the hardware ... */
  return fr_tagged_encode_int(count, out);
}

The return type is fr_err_t. The parameters are fixed: (fr_runtime_t *runtime, const fr_tagged_t *args, uint8_t arg_count, fr_tagged_t *out).

Values cross the boundary as tagged values. Decode integer arguments with fr_tagged_decode_int(args[i], &n), where n is fr_int_t. Test values with fr_tagged_is_int, fr_tagged_is_nil, fr_tagged_is_bool, or fr_tagged_is_falsy. Produce results by writing *out with fr_tagged_encode_int(v, out) or fr_tagged_encode_bool(b, out).

FR_TRY(expr) propagates a non-OK fr_err_t.

Generated Build Files

The C compiles into the firmware. frothy build runs a generator that writes derived files under .frothy/build/<board>/:

  • lib_natives.c, a table binding each native word name to its C function
  • libs.cmake, the extension sources and includes for the firmware build

Do not edit generated files by hand.

Persistence

Values that cross the boundary are Int, Bool, Nil, and, where a word supports it, Text, Bytes, or Handle.

A Handle is a tagged Frothy value that names an entry in the runtime handle table. The entry records a resource kind, generation, and platform resource index. Do not expose a raw pointer or invent an integer resource ID.

The handle kinds and platform close path are core contracts. A manifest-only library cannot invent a safe new Handle kind; adding a handle-bearing capability requires corresponding core and platform support.

Bytes and Handles are volatile. Pack Bytes into Text for persistence. A Handle may live in a top-level slot during a session, but save rejects the overlay while it remains there. Neither belongs in cells or record fields. Close open resources and rebind top-level Handle names before save; reopen them from boot after restore.

The Concepts guide gives the complete reusable pattern in Persist the Recipe, Not the Handle .

Native words live in the firmware base image. A saved overlay references them by name. At boot, the native implementation is rebuilt from firmware, so saved images stay pointer-safe.