fluffos/tools/ffi/test.py
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

85 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Dependency-free tests for the FFI bindings generator.
python3 tools/ffi/test.py
Generates bindings from test_sample.h and asserts the emitted LPC/struct
output and the parsed JSON contract.
"""
import json
import os
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
GEN = os.path.join(HERE, "generate.py")
SAMPLE = os.path.join(HERE, "test_sample.h")
failures = 0
def check(name, cond, detail=""):
global failures
if cond:
print(" OK " + name)
else:
print("FAIL " + name + ((" -- " + detail) if detail else ""))
failures += 1
def main():
with tempfile.TemporaryDirectory() as d:
out = os.path.join(d, "sample")
res = subprocess.run(
[sys.executable, GEN, SAMPLE, "--lib", "libm.so.6", "--out", out,
"--string-convenience", "--emit-json"],
capture_output=True, text=True)
check("generator exits 0", res.returncode == 0, res.stderr)
lpc = open(out + ".lpc").read()
structs = open(out + "_structs.h").read()
data = json.load(open(out + ".json"))
names = [f["name"] for f in data["functions"]]
check("supported functions parsed",
set(["sqrt", "pow", "abs", "strlen", "qsort"]).issubset(set(names)), str(names))
check("varargs skipped", "printf" not in names)
check("function-pointer param skipped", "set_handler" not in names)
# sqrt(double)->double becomes float sqrt(float ...).
check("scalar wrapper shape", "float sqrt(float a0)" in lpc, lpc)
check("double return code", '"sqrt", FFI_DOUBLE, ({ FFI_DOUBLE })' in lpc)
check("pow two-arg codes", "FFI_DOUBLE, FFI_DOUBLE" in lpc)
check("int abs mapped", "int abs(int a0)" in lpc)
# char* becomes a buffer parameter (never string).
check("char* param is buffer", "int strlen(buffer a0)" in lpc, lpc)
check("strlen return code UINT64", '"strlen", FFI_UINT64' in lpc)
# ...with a string-convenience overload that encodes UTF-8.
check("string-convenience overload emitted", "strlen_s(string a0)" in lpc)
check("convenience encodes utf-8", 'string_encode(a0, "utf-8")' in lpc)
# void return: no `return`.
check("void wrapper has no return", "void qsort(" in lpc and "return ffi_call(_h_qsort" not in lpc)
# Struct layout: field-type array + symbolic offsets.
check("Point struct types", "#define STRUCT_POINT_TYPES ({ FFI_INT32, FFI_INT32 })" in structs)
check("Point field indices", "#define STRUCT_POINT_x 0" in structs and
"#define STRUCT_POINT_y 1" in structs)
check("Mixed struct types",
"#define STRUCT_MIXED_TYPES ({ FFI_INT8, FFI_DOUBLE, FFI_INT16 })" in structs)
# JSON contract mirrors the emitted bindings.
sqrt_fn = next(f for f in data["functions"] if f["name"] == "sqrt")
check("json ret code", sqrt_fn["ret"][0] == "FFI_DOUBLE")
check("json struct fields",
any(s["name"] == "Point" and [x["name"] for x in s["fields"]] == ["x", "y"]
for s in data["structs"]))
print("\nAll ffi-gen tests passed." if failures == 0 else "\n%d FAILURES" % failures)
return 0 if failures == 0 else 1
if __name__ == "__main__":
sys.exit(main())