How FFI Works
A C binding becomes an ordinary Froth word by providing:
- a native callback
- a metadata record with name, parameters, result type, stack effect, and help
- an entry in a null-terminated project or board binding table
At boot, Froth installs those tables into the same top-level slot space used by language-defined words. After registration, there is no separate foreign-call syntax.
The Core Type
Project code includes the public header:
#include "froth_ffi.h"
The metadata record is:
typedef struct {
const char *name;
const froth_ffi_param_t *params;
uint8_t param_count;
uint8_t arity;
froth_ffi_value_type_t result_type;
const char *help;
uint32_t flags;
froth_native_fn_t callback;
const void *context;
const char *stack_effect;
} froth_ffi_entry_t;
The important fields are:
name: the Froth word nameparams: typed argument metadataarity: how many arguments the word acceptsresult_type: the single result class, orFROTH_FFI_VALUE_VOIDcallback: the C function that implements the wordstack_effect: the inspectable effect line shown by tooling
Value Classes
The public boundary is defined around:
FROTH_FFI_VALUE_INTFROTH_FFI_VALUE_BOOLFROTH_FFI_VALUE_NILFROTH_FFI_VALUE_TEXTFROTH_FFI_VALUE_CELLSFROTH_FFI_VALUE_VOID
Raw pointers are not ordinary Froth values. Native state behind an FFI word is not saved into the live image.
A Minimal Binding
#include "froth_ffi.h"
static const froth_ffi_param_t limit_params[] = {
FROTH_FFI_PARAM_INT("x"),
FROTH_FFI_PARAM_INT("lo"),
FROTH_FFI_PARAM_INT("hi"),
};
static froth_error_t limit_word(froth_runtime_t *runtime,
const void *context,
const froth_value_t *args,
size_t arg_count,
froth_value_t *out) {
int32_t x = 0;
int32_t lo = 0;
int32_t hi = 0;
(void)runtime;
(void)context;
(void)arg_count;
FROTH_TRY(froth_ffi_expect_int(args, 0, &x));
FROTH_TRY(froth_ffi_expect_int(args, 1, &lo));
FROTH_TRY(froth_ffi_expect_int(args, 2, &hi));
if (x < lo) x = lo;
if (x > hi) x = hi;
return froth_ffi_return_int(x, out);
}
From Froth, that word is just another call:
limit: 42, 0, 10
Registration Tables
Project bindings are exported from a null-terminated table named
froth_project_bindings:
const froth_ffi_entry_t froth_project_bindings[] = {
{
.name = "limit",
.params = limit_params,
.param_count = FROTH_FFI_PARAM_COUNT(limit_params),
.arity = 3,
.result_type = FROTH_FFI_VALUE_INT,
.help = "Clamp a value into an inclusive range.",
.flags = FROTH_FFI_FLAG_NONE,
.callback = limit_word,
.stack_effect = "( x lo hi -- y )",
},
{0},
};
Boot installs project bindings after board bindings and before restore and
boot. That order means a saved overlay can refer to stable board and project
words, while native runtime state itself remains outside the saved image.