mirror of
https://github.com/fluffos/fluffos
synced 2026-08-12 18:26:06 -04:00
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>
85 lines
3.3 KiB
Python
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())
|