Repository navigation
runtime: C-ABI FFI layer (bun:ffi shape) — dlopen, typed calls, pinned buffers, toArrayBuffer, JSCallback #6562
Description
The single biggest platform feature for compiling Bun-specific apps (driver: opencode, but this is what the Bun-native ecosystem standardizes on): a C-ABI FFI layer with the bun:ffi API shape (and optionally Node's experimental node:ffi shape — bun-ffi-structs already bridges both, so one implementation can serve either import).
Evidence from opencode
Everything native in the TUI product funnels through plain C-ABI dylibs — no N-API anywhere (verified: libopentui.dylib from @opentui/core-darwin-arm64@0.4.5 exports 1,775 C symbols, zero napi_* entries; the "node backend" is the same dylib driven via experimental node:ffi):
| consumer | lib | FFI decl size | notes |
|---|---|---|---|
@opentui/core (the TUI renderer) |
libopentui.dylib per platform |
~348 typed symbol decls | renderer + buffers + embedded yoga (YG*) + embedded tree-sitter with bundled grammars (js/ts/markdown/zig) |
bun-pty |
librust_pty.{dylib,so,dll} (bundled in tarball) |
17 symbols | pty spawn/read/write/resize/kill |
@ff-labs/fff-bun |
@ff-labs/fff-bin-* platform pkgs |
dlopen in src/ffi.ts |
fuzzy finder; opencode's node condition stubs it gracefully — deferrable |
Primitives required (from actual usage, not the full spec)
dlopen(path, symbolTable)with typed args/returns —FFIType.{void, ptr, pointer, cstring, bool, i8..i64, u8..u64, usize, f32, f64, function, callback}(observed set in@opentui/coredist).ptr(arrayBufferOrView)— hand JS buffer memory to native. GC design constraint: perry's GC relocates objects (cf. the forwarding-stub/array-growth work, Default GC: array from a direct function call, stored in a variable, is lost when its backing store grows past 32MiB; object arrays get corrupted length #6228/fix(gc): minor sweep reclaimed array-growth forwarding stubs still referenced from old-gen parents #6471) — FFI-exposed buffers must be pinned or allocated in non-moving/off-heap space for their native lifetime.toArrayBuffer(ptr, byteOffset, byteLength)— 28 uses in opentui — zero-copy JS views over native-owned memory; needs external backing-store support in perry's ArrayBuffer (no GC ownership, explicit lifetime).CString(read NUL-terminated, encode).JSCallback/FFIType.function— native→JS callbacks (opentui registers them). Hardest piece: trampoline into perry's JS execution, closure rooting, and a decision on threading (reject cross-thread invocation initially unless opentui proves to need it — verify which callbacks it actually registers before building).bun-ffi-structsis pure JS over these primitives — free once they exist.
For a native-code runtime, this is natural territory: direct dlopen + call-stub generation (libffi or generated thunks), no JIT trampolines needed.
Suggested bring-up
- dlopen + typed calls +
ptron pinned buffers +CString, proven against bun-pty's 17-symbol dylib (spawn a shell, echo round-trip) — small, observable, exercises strings/ints/pointers/read-loop. toArrayBufferexternal backing stores.JSCallbacktrampolines.- opentui's 348-symbol table + renderer bring-up (the opencode target tracker owns the app-level milestone).
Acceptance per stage; stage 1 acceptance: a perry-compiled script drives bun-pty end-to-end on macOS-arm64 + linux-x64.
Activity
runtime: C-ABI FFI stage 1 — dlopen, typed calls, ptr/CString on pinn…
bun:ffi stage 1: address CodeRabbit review (PerryTS#6580)
runtime: C-ABI FFI stage 1 — dlopen, typed calls, ptr/CString on pinn…
runtime: add zero-copy bun:ffi memory views (#6562) (#8329)
proggeramlug commentedon Aug 21, 2026
The new full-source OpenCode target is tracked by #8506. This remains its P0 native boundary: complete callbacks/closure rooting, external memory views, and node:ffi compatibility so the unmodified OpenTUI/Yoga and FFF wrappers can compile and use their upstream dylibs.
feat(runtime): add bun:ffi JSCallback support (PerryTS#6562)
feat(runtime): add bun:ffi JSCallback support (#6562) (#8522)
fix(runtime): complete C-ABI FFI layer (PerryTS#6562)
proggeramlug commentedon Aug 24, 2026
Closed by #8704, landed on
mainvia #8721 (squash3f655301e).That PR was landed through a batch wrapper PR rather than merged directly, so GitHub's close keyword never fired at merge time — its
mergedAtis null even though the work is onmain. Verified before closing:crates/perry-runtime/src/bun_ffi/(call, callback, dlopen, memory, read) is present onmain,changelog.d/8704-complete-c-abi-ffi.mdlanded, and its 29bun_ffitests pass.docs: regenerate the API reference (#8744)