fluffos/tools/ffi
Yucong Sun 9be761aa00 package_ffi: foreign function interface for LPC (libffi), with callbacks
Fully implements the docs/driver/ffi-plan.md design. LPC can now load
native shared libraries, call C functions whose signatures are described
at runtime, manage native memory, pass in/out parameters, and expose LPC
function pointers to C as callbacks.

Package (src/packages/ffi, option PACKAGE_FFI ON, libffi via pkg-config):
- ffi_load/unload/symbol; ffi_prepare/ffi_call (ffi_prep_cif + ffi_call);
  ffi_alloc/free/sizeof/peek/address; ffi_read/write; ffi_struct_layout;
  ffi_callback/ffi_callback_addr/ffi_callback_free (libffi closures that
  re-enter the VM via safe_call_function_pointer); ffi_error/ffi_status.
- Buffers are the currency for all pointer/byte data; raw pointer VALUES
  (returned pointers, buffer/callback addresses) are ints. LPC strings
  are UTF-8-native and never implicitly marshalled -- a char* is a
  buffer the caller encoded (pinned by ffi_string.lpc).
- Native allocations are LPC buffers (GC-tracked); handle tables freed at
  shutdown (ffi_cleanup) and marked for DEBUGMALLOC (mark_ffi).

Security: master apply valid_ffi(op, arg, caller) gates every
load/symbol/prepare/callback (VALID_FFI added to the applies table); a
missing apply denies by default. Optional "ffi allowed libraries" config
allow-list (rc.cc + runtime_config.h + regenerated config.md, new
Security category). __PACKAGE_FFI__ predefine added.

tools/ffi/generate.py: turns a C header into LPC bindings (buffer params
for char*, optional --string-convenience UTF-8 overloads) plus a struct
layout include; reports+skips unsupported forms; --emit-json contract.
Dependency-free test.py.

Tests: 20 testsuite/single/tests/efuns/ffi_*.lpc (every efun, the qsort
callback round trip, the generated-bindings end-to-end path), guarded by
__PACKAGE_FFI__ with a libc-reachability probe. The efuns are VM-stack-
based, so the LPC testsuite is the surface -- libffi's call/closure paths
run there under ASan/UBSan and the per-file check_memory leak gate.

The clang RelWithDebInfo sanitizer caught an error()-unwind leak: both
ffi_prepare and ffi_callback allocated before a code_to_type() that can
error() -- now unique_ptr/custom-deleter owned (AGENTS.md section 4).

Verified: testsuite x3 (ASan Debug) + ctest 297, RelWithDebInfo suite x3
+ ctest 298, clang RelWithDebInfo sanitizer (leak-clean), tools/ffi
test.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:48:48 -04:00
..
generate.py package_ffi: foreign function interface for LPC (libffi), with callbacks 2026-07-09 20:48:48 -04:00
README.md package_ffi: foreign function interface for LPC (libffi), with callbacks 2026-07-09 20:48:48 -04:00
test.py package_ffi: foreign function interface for LPC (libffi), with callbacks 2026-07-09 20:48:48 -04:00
test_sample.h package_ffi: foreign function interface for LPC (libffi), with callbacks 2026-07-09 20:48:48 -04:00

tools/ffi — LPC bindings generator for package_ffi

Generates ready-to-use LPC bindings for a native library from its C header, targeting the FFI package (src/packages/ffi, efuns declared in <ffi.h>). See the full design in docs/driver/ffi-plan.md.

Usage

tools/ffi/generate.py <header.h> --lib <libpath> --out <basename> \
    [--string-convenience] [--emit-json]

Emits next to <basename>:

File Contents
<basename>.lpc One LPC wrapper per exported C function. Handles are prepared lazily and cached; the library is ffi_loaded on first use.
<basename>_structs.h ffi_struct_layout field-type arrays + symbolic field-offset #defines for each struct in the header.

Example:

tools/ffi/generate.py /usr/include/math.h --lib libm.so.6 --out mudlib/std/libm

The buffer / byte boundary

A C char* (or any data pointer) is emitted as an LPC buffer parameter, never string — LPC strings are UTF-8-native and the byte encoding must be explicit (string_encode/string_decode). With --string-convenience the generator also emits a <name>_s(string ...) overload that string_encodes to a NUL-terminated UTF-8 buffer for the common case, clearly named so the encoding is never hidden by default.

Supported C subset

Scalar types (charlong long, the intN_t/uintN_t family, size_t, float, double), any pointer (→ buffer), void returns, and plain struct { ... } bodies of those field types. Anything else — function-pointer parameters, unions, bitfields, varargs — is reported to stderr and skipped, never mis-bound. --emit-json dumps the parsed signature table (the shared contract for the bindings and tests).

Tests

python3 tools/ffi/test.py

Dependency-free; generates from test_sample.h and checks the emitted LPC/struct output and the JSON contract. The end-to-end path (generated bindings compiled and calling real native functions) is pinned by the driver testsuite: testsuite/single/tests/efuns/ffi_generated.lpc.