Skip to content

Engine-native FFI (bun:ffi) under USE(BUN_JSC_ADDITIONS) - #319

Merged
Jarred-Sumner merged 48 commits into
mainfrom
jarred/jsc-native-ffi
Jul 28, 2026
Merged

Engine-native FFI (bun:ffi) under USE(BUN_JSC_ADDITIONS)#319
Jarred-Sumner merged 48 commits into
mainfrom
jarred/jsc-native-ffi

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Adds an engine-native FFI to JavaScriptCore under USE(BUN_JSC_ADDITIONS) — the machinery Bun's bun:ffi now runs on (oven-sh/bun#35246), replacing the TinyCC-JIT'd trampolines that dlopen/linkSymbols/CFunction/JSCallback used to compile per symbol.

What it is

A JSFFIFunction is a callable JS object bound to a native target and a Signature (argument and return types). Calling it converts the JS arguments to their native representations, invokes the target with the platform C calling convention, and boxes the result. There is no per-symbol generated C.

  • Cold path: a shared C++ host function, or a per-signature IC stub installed as the executable's call code.
  • Hot path: the DFG rewrites the call site into a CallFFI node with unboxed, type-speculated arguments and no CallFrame; the FTL then emits a direct call to the native target (SysV64 / AAPCS64 / Darwin arm64; Win64 keeps the thunk).
  • Callbacks (C→JS): JSFFICallback is a JIT'd trampoline giving native code a real function pointer. One implementation covers synchronous and thread-safe callbacks: a foreign thread copies the raw C argument slots and hands them to an embedder-registered dispatch; the JS thread converts and invokes. Thunks are specialized at generation for thread-safety, so the sync path carries no runtime branch.

Types

char i8 u8 i16 u16 i32 u32 i64 u64 i64_fast u64_fast f32 f64 bool ptr function jsvalue buffer buffer_length cstring void.

  • jsvalue (tag 19) is a raw EncodedJSValue pass-through in both directions; "napi_value" still parses as its alias. Tag 18 is permanently reserved and rejected.
  • buffer accepts a TypedArray/DataView and passes its data pointer. buffer_length passes the byte length of the same view as a uint64_t, read off the same object at call time — an atomic pointer+length snapshot; argument-only.
  • cstring arguments accept a JS string (transcoded to NUL-terminated UTF-8 in the call arena), a pointer, or a TypedArray. cstring returns are decoded to a JS string primitive inside the call (NULLnull).
  • .ptr and .native are real read-only own properties whose slots are added to the shared Structure once at structure-creation time (statically known offsets) and written with putDirectOffset when the cell is created — every instance is born with the final Structure, no instance ever transitions, and the ordinary object machinery supplies correct put / delete / defineOwnProperty / ownKeys semantics with no overrides.
  • Pointers box exactly: a number when the address is ≤ 2⁵³−1, otherwise an exact BigInt.

Strings and the call arena

Each JSGlobalObject owns a FFIContext with a call-scoped string arena: a bracket opens for the call, JS-string arguments bump-allocate into it, and it reclaims at the outermost scope exit. Storage is a parked chunk reused across calls (no allocator traffic on the warm path); a string over the 64 KB retention cap is freed when its call ends, and FFIContext is a HeapObserver that drops the parked chunk after any GC when no call is in flight, so an idle process retains nothing. A cstring return is materialized before its arena reclaims — an echoed argument round-trips as a value. A callback's cstring return outlives the call, so it lives in a buffer owned by the callback (valid until that callback's next invocation), never the arena.

Callback lifetime

Live callbacks are rooted from the global object under its cell lock. A thread-safe callback keeps a packed closed+pending-count word: close() refuses new foreign-thread calls but every invocation already accepted is a commitment and is still delivered; the pending count keeps the cell — and through its barrier, the JS callable — rooted until the last queued invocation drains, so a callback closed and collected mid-flight cannot use-after-free and cannot drop work.

Performance (release, this branch, --useDollarVM=1 fixtures on macOS arm64)

operation ns/call
noop() 1.2
echo_i32(i) 1.0
echo_f64(x) feeding arithmetic 6.0
strlen(36-char JS string) — string argument 30
cstring return → decoded JS string 23

End-to-end numbers through Bun (including the cross-runtime comparison) are in oven-sh/bun#35246.

Scope and requirements

  • 64-bit x86-64 and arm64 (SysV64, Win64, AAPCS64, Darwin arm64). 32-bit and other CPUs throw at creation rather than crash.
  • Requires the JIT. With useJIT=0, creation throws TypeError (the marshalling is generated code; there is no interpreter marshaller).
  • RESERVED_WasNapiEnv (tag 18) and buffer_length-as-return are rejected by the shared signature parser at every entry point.

Testing

  • testFFI (C++, Source/JavaScriptCore/ffi/tests/): 9,519 checks — a differential harness that runs every generated invoke thunk against clang-compiled reference calls across the type × arity × ABI matrix (including Darwin sub-word stack packing and >8-register spills), plus conversion, signature, and callback tables.
  • JSTests/stress/ffi-*.js: 32 files covering tier transitions and OSR, exceptions unwinding through native frames, thread-safe callbacks from real foreign threads (close-while-queued, GC races, delivery order), the jsvalue pass-through matrix, and view/buffer arguments. All pass in default and eager-tier configurations.

Comment thread Source/JavaScriptCore/dfg/DFGMayExit.cpp Outdated
Comment thread JSTests/stress/ffi-raw-read.js Outdated
Comment thread Source/JavaScriptCore/ffi/FFIDFG.cpp Outdated
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

FFI integration

Layer / File(s) Summary
FFI contracts and runtime state
Source/JavaScriptCore/ffi/*, Source/JavaScriptCore/runtime/*, Source/JavaScriptCore/heap/*
Adds FFI type and signature contracts, ABI metadata, per-global context state, JavaScript FFI objects, VM options, heap integration, and build wiring.
ABI layouts and native thunks
Source/JavaScriptCore/ffi/FFICallingConvention.*, Source/JavaScriptCore/ffi/FFIInvokeThunk.*, Source/JavaScriptCore/ffi/FFICallbackThunk.*
Adds native calling-convention layout calculation, invoke and callback thunks, canonical slot marshaling, return normalization, and ABI preservation handling.
Value conversion and host calls
Source/JavaScriptCore/ffi/FFIConversions.*, Source/JavaScriptCore/ffi/FFICallHost.*, Source/JavaScriptCore/ffi/FFIRawMemory.*
Implements JavaScript/native conversion, pointer and string handling, arena storage, host-call fallback, and raw-memory readers.
JIT lowering
Source/JavaScriptCore/dfg/*, Source/JavaScriptCore/ftl/*, Source/JavaScriptCore/ffi/FFIICStub.*, Source/JavaScriptCore/ffi/FFIDFG*
Adds CallFFI and FFIRawRead nodes, DFG/FTL lowering, strength reduction, raw-read inlining, and FFI inline-cache generation.
Native validation
Source/JavaScriptCore/ffi/tests/*, Source/JavaScriptCore/shell/CMakeLists.txt
Adds native ABI, layout, conversion, thunk, callback, fixture, and end-to-end tests plus the testFFI executable.
JavaScript stress coverage
JSTests/stress/ffi-*.js
Adds stress tests for arity, signatures, conversions, callbacks, pointers, N-API values, raw reads, tiering, exceptions, alignment, storage modes, and no-JIT behavior.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it does not follow the required commit-message template and is missing the Bugzilla link, Reviewed by line, and file list. Reformat it to the repository template: bug title, Bugzilla URL, Reviewed by NOBODY line, explanation, and a brief changed-files list.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding engine-native bun:ffi support under USE(BUN_JSC_ADDITIONS).

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JSTests/stress/ffi-arity.js`:
- Around line 75-76: Update the comment immediately above the addF32 check to
describe the actual missing-argument behavior as undefined/NaN, matching the
asserted NaN result and the corresponding addF64 case; leave the assertion
unchanged.

In `@JSTests/stress/ffi-raw-read.js`:
- Around line 1-12: Add the top-level $vm.useJIT() guard used by
ffi-typedarray-storage-modes.js before the ffiFunction setup in this test, so
the entire test is skipped when JIT is disabled while retaining the existing
behavior when JIT is available.

In `@Source/JavaScriptCore/dfg/DFGMayExit.cpp`:
- Around line 225-230: Move the USE(BUN_JSC_ADDITIONS)-guarded FFIRawRead case
out of the preceding fall-through chain and place it after the
ExitsForExceptions group has terminated. Preserve FFIRawRead’s result = Exits
behavior while ensuring the empty cases before it continue reaching result =
ExitsForExceptions.

In `@Source/JavaScriptCore/dfg/DFGSafeToExecute.h`:
- Around line 349-351: Remove FFIRawRead from the safe-to-hoist switch cases in
safeToExecute() and route it through the return-false path. Preserve safe
handling for the other node types, ensuring FFIRawRead cannot be code-motion
optimized without proven pointer validity and memory ordering.

In `@Source/JavaScriptCore/ffi/FFIDFG.cpp`:
- Around line 219-224: Update the Type::Pointer, Type::CString, Type::Function,
and Type::Buffer branch in the CallFFI return-type speculation logic to include
SpecHeapBigInt alongside SpecBytecodeNumber and SpecOther. Preserve the existing
null-to-jsNull and numeric representation behavior while ensuring the abstract
interpreter accounts for exact JSBigInt results.

In `@Source/JavaScriptCore/ffi/FFIRawMemory.h`:
- Around line 49-55: Update the documentation above createReadObject to state
that raw read address inputs are limited to numeric values and BigInt, and that
views, ArrayBuffers, null, and undefined are not accepted; callers must convert
supported pointer-like inputs first. Remove the inaccurate claim that raw
readers accept every ptr argument type while preserving the existing
no-bounds-checking and reader behavior descriptions.

In `@Source/JavaScriptCore/tools/JSDollarVM.cpp`:
- Around line 4545-4569: Update dollarVMParseFFIType to delegate FFI type
conversion to the exported FFI::typeFromJS helper instead of duplicating numeric
and string parsing. Preserve the existing exception-scope handling and confirm
the $vm API does not require its current TypeError wording before removing the
local validation and parse logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b0b2b2d-f911-490f-a8a9-ef9439a7274a

📥 Commits

Reviewing files that changed from the base of the PR and between c9296e3 and 415b185.

📒 Files selected for processing (80)
  • JSTests/stress/ffi-align.js
  • JSTests/stress/ffi-arity-ladders.js
  • JSTests/stress/ffi-arity.js
  • JSTests/stress/ffi-callbacks.js
  • JSTests/stress/ffi-callffi-was-compiled.js
  • JSTests/stress/ffi-canary.js
  • JSTests/stress/ffi-conversion-errors-host.js
  • JSTests/stress/ffi-conversion-errors.js
  • JSTests/stress/ffi-fuzz-signatures.js
  • JSTests/stress/ffi-host-path.js
  • JSTests/stress/ffi-napi.js
  • JSTests/stress/ffi-no-jit.js
  • JSTests/stress/ffi-osr-and-exceptions.js
  • JSTests/stress/ffi-pointers-and-buffers.js
  • JSTests/stress/ffi-raw-read.js
  • JSTests/stress/ffi-signature-errors.js
  • JSTests/stress/ffi-subword-and-returns.js
  • JSTests/stress/ffi-tier-differential.js
  • JSTests/stress/ffi-typedarray-storage-modes.js
  • JSTests/stress/ffi-types-echo.js
  • Source/JavaScriptCore/CMakeLists.txt
  • Source/JavaScriptCore/Sources.txt
  • Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
  • Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp
  • Source/JavaScriptCore/dfg/DFGClobberize.h
  • Source/JavaScriptCore/dfg/DFGDataViewData.h
  • Source/JavaScriptCore/dfg/DFGDoesGC.cpp
  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGMayExit.cpp
  • Source/JavaScriptCore/dfg/DFGNode.cpp
  • Source/JavaScriptCore/dfg/DFGNode.h
  • Source/JavaScriptCore/dfg/DFGNodeType.h
  • Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSafeToExecute.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
  • Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp
  • Source/JavaScriptCore/ffi/BunFFI.cpp
  • Source/JavaScriptCore/ffi/BunFFI.h
  • Source/JavaScriptCore/ffi/FFICallHost.cpp
  • Source/JavaScriptCore/ffi/FFICallHost.h
  • Source/JavaScriptCore/ffi/FFICallbackThunk.cpp
  • Source/JavaScriptCore/ffi/FFICallbackThunk.h
  • Source/JavaScriptCore/ffi/FFICallingConvention.cpp
  • Source/JavaScriptCore/ffi/FFICallingConvention.h
  • Source/JavaScriptCore/ffi/FFIContext.cpp
  • Source/JavaScriptCore/ffi/FFIContext.h
  • Source/JavaScriptCore/ffi/FFIConversions.cpp
  • Source/JavaScriptCore/ffi/FFIConversions.h
  • Source/JavaScriptCore/ffi/FFIDFG.cpp
  • Source/JavaScriptCore/ffi/FFIDFG.h
  • Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp
  • Source/JavaScriptCore/ffi/FFIICStub.cpp
  • Source/JavaScriptCore/ffi/FFIICStub.h
  • Source/JavaScriptCore/ffi/FFIInvokeThunk.cpp
  • Source/JavaScriptCore/ffi/FFIInvokeThunk.h
  • Source/JavaScriptCore/ffi/FFIRawMemory.cpp
  • Source/JavaScriptCore/ffi/FFIRawMemory.h
  • Source/JavaScriptCore/ffi/FFISignature.cpp
  • Source/JavaScriptCore/ffi/FFISignature.h
  • Source/JavaScriptCore/ffi/FFIType.h
  • Source/JavaScriptCore/ffi/JSFFICallback.cpp
  • Source/JavaScriptCore/ffi/JSFFICallback.h
  • Source/JavaScriptCore/ffi/JSFFIFunction.cpp
  • Source/JavaScriptCore/ffi/JSFFIFunction.h
  • Source/JavaScriptCore/ffi/tests/FFITestFixtures.cpp
  • Source/JavaScriptCore/ffi/tests/FFITestFixtures.h
  • Source/JavaScriptCore/ffi/tests/testFFI.cpp
  • Source/JavaScriptCore/ftl/FTLCapabilities.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/heap/Heap.cpp
  • Source/JavaScriptCore/heap/Heap.h
  • Source/JavaScriptCore/runtime/Intrinsic.h
  • Source/JavaScriptCore/runtime/JSGlobalObject.cpp
  • Source/JavaScriptCore/runtime/JSGlobalObject.h
  • Source/JavaScriptCore/runtime/OptionsList.h
  • Source/JavaScriptCore/runtime/VM.h
  • Source/JavaScriptCore/shell/CMakeLists.txt
  • Source/JavaScriptCore/tools/JSDollarVM.cpp

Comment thread JSTests/stress/ffi-arity.js Outdated
Comment thread JSTests/stress/ffi-raw-read.js Outdated
Comment thread Source/JavaScriptCore/dfg/DFGMayExit.cpp Outdated
Comment thread Source/JavaScriptCore/dfg/DFGSafeToExecute.h Outdated
Comment thread Source/JavaScriptCore/ffi/FFIDFG.cpp Outdated
Comment thread Source/JavaScriptCore/ffi/FFIRawMemory.h Outdated
Comment thread Source/JavaScriptCore/tools/JSDollarVM.cpp
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Comment thread Source/JavaScriptCore/dfg/DFGFixupPhase.cpp Outdated
Comment thread Source/JavaScriptCore/dfg/DFGSafeToExecute.h Outdated
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
e553887f autobuild-preview-pr-319-e553887f 2026-07-28 11:37:19 UTC
131d8a8e autobuild-preview-pr-319-131d8a8e 2026-07-28 10:17:39 UTC
a554d317 autobuild-preview-pr-319-a554d317 2026-07-28 02:56:01 UTC
5adc9e3b autobuild-preview-pr-319-5adc9e3b 2026-07-28 00:26:15 UTC
d67e864c autobuild-preview-pr-319-d67e864c 2026-07-27 23:07:23 UTC
de5b6c5d autobuild-preview-pr-319-de5b6c5d 2026-07-27 22:15:48 UTC
1bb31913 autobuild-preview-pr-319-1bb31913 2026-07-27 14:07:12 UTC
a862d951 autobuild-preview-pr-319-a862d951 2026-07-27 11:01:04 UTC
bbf3d80f autobuild-preview-pr-319-bbf3d80f 2026-07-27 09:49:48 UTC
b1c78f8a autobuild-preview-pr-319-b1c78f8a 2026-07-27 08:16:04 UTC
54f9cc8c autobuild-preview-pr-319-54f9cc8c 2026-07-27 07:03:02 UTC
8ae95332 autobuild-preview-pr-319-8ae95332 2026-07-26 18:29:15 UTC
d15626ae autobuild-preview-pr-319-d15626ae 2026-07-26 12:38:41 UTC
aa47e4c0 autobuild-preview-pr-319-aa47e4c0 2026-07-26 11:51:02 UTC
2007dd87 autobuild-preview-pr-319-2007dd87 2026-07-26 06:23:19 UTC
7bd52e08 autobuild-preview-pr-319-7bd52e08 2026-07-26 04:21:02 UTC
ac08a0aa autobuild-preview-pr-319-ac08a0aa 2026-07-26 02:44:38 UTC
87658cf2 autobuild-preview-pr-319-87658cf2 2026-07-26 00:41:49 UTC
feea5e10 autobuild-preview-pr-319-feea5e10 2026-07-25 23:31:07 UTC
6d2fe0b9 autobuild-preview-pr-319-6d2fe0b9 2026-07-25 04:37:26 UTC
b2487492 autobuild-preview-pr-319-b2487492 2026-07-24 09:59:20 UTC
6d6505bf autobuild-preview-pr-319-6d6505bf 2026-07-24 07:42:30 UTC
21975506 autobuild-preview-pr-319-21975506 2026-07-24 05:30:51 UTC
3e6b9a9d autobuild-preview-pr-319-3e6b9a9d 2026-07-24 04:21:46 UTC
dd034da9 autobuild-preview-pr-319-dd034da9 2026-07-24 03:01:42 UTC
596f4fc4 autobuild-preview-pr-319-596f4fc4 2026-07-24 02:01:25 UTC
48250cdb autobuild-preview-pr-319-48250cdb 2026-07-24 00:49:44 UTC
ca52299c autobuild-preview-pr-319-ca52299c 2026-07-23 23:36:29 UTC
a3a2455f autobuild-preview-pr-319-a3a2455f 2026-07-23 22:35:46 UTC
737093a0 autobuild-preview-pr-319-737093a0 2026-07-23 09:18:02 UTC

Implements the machinery bun:ffi needs directly in JavaScriptCore, so Bun
can create FFI functions and callbacks without generating and JIT-compiling
a C trampoline per symbol with TinyCC.

Core (Source/JavaScriptCore/ffi/):
- FFI::Type / FFI::Signature: interned signatures, wire-compatible with
  Bun's existing FFIType tags.
- FFICallingConvention: SysV x86-64, AAPCS64 (incl. Apple sub-word stack
  packing) and Win64 argument/return classification.
- FFIInvokeThunk: one JIT'd, signature-pure invoke thunk per signature; the
  only code that emits the native callee ABI. Every tier funnels through a
  canonical uint64_t slot buffer.
- JSFFIFunction: a JSFunction subclass. Calls run through a C++ host path or,
  when useFFIICStub is on, a per-function JIT'd entry stub installed as the
  executable's call code, with a shared C++ slow path for coercion misses.
- JSFFICallback + FFICallbackThunk: C-callable JIT'd trampolines that box
  native arguments and call back into JS, leaving exceptions pending for the
  outer FFI call site. FFI::CallbackEntryScope suspends the exception-check
  verifier's outstanding obligation across the foreign-frame re-entry.
- FFIConversions: the JS<->native conversion rules, shared by every tier.
  Numeric params accept numbers, booleans, null/undefined and BigInts and
  wrap to width (never clamp); strings/Symbols throw. Pointers above 2^53
  are surfaced as exact BigInts and BigInt addresses are accepted.
- FFIRawMemory: the bun:ffi `read` singleton (u8..f64/ptr/intptr) with
  unaligned-safe host paths, plus a shared FFIRawReadIntrinsic.

JIT integration:
- CallFFI DFG/FTL node: created by strength reduction from a Call whose
  callee is a constant JSFFIFunction (mirroring CallWasm). Arguments are
  speculated to their declared types and written unboxed into a stack slot
  buffer; no CallFrame is built for the native call.
- FFIRawRead DFG/FTL node: the DataView typed-load machinery with the base
  taken from the (Int52 / Int32 / truncated-double) address argument; no
  bounds check by design. DataViewData is split out into DFGDataViewData.h.

Tests: an ffi/tests C++ harness (testFFI: calling-convention goldens, the
conversion matrix, and a differential of the invoke thunk against clang
calling the same fixtures natively) plus JSTests/stress/ffi-*.js covering
types, arity, sub-word/return normalization, pointers/buffers/typed-array
storage modes, callbacks (marshaling, exceptions, GC, re-entrancy),
callee-saved canaries, stack alignment, OSR exits, tier differentials, the
raw-memory readers, and a seeded conversion fuzzer.
Address defects found in code review of the engine-native FFI change:

- DFGMayExit.cpp: the FFIRawRead case had been spliced into a fall-through
  case group, so under USE(BUN_JSC_ADDITIONS) ~30 node types (Call/New*/
  RegExpExec*/CallWasm/...) fell into `result = Exits` and never reached
  their intended ExitsForExceptions. Move the case after the group's body.
- All FFIRawRead `case` labels are now unconditional with a guarded body
  (DFG_CRASH under !USE(BUN_JSC_ADDITIONS)), matching the CallFFI convention;
  the guarded labels broke -Wswitch in every non-Bun port.
- DFG CallFFI: add the missing exceptionCheck() after operationFFIBoxSlot
  (which allocates BigInts and can throw OOM); the FTL and IC stub twins
  already checked.
- read.ptr / read.intptr now surface a plain double at every tier (Bun's
  reader contract), rather than the host path reusing the FFI ptr-return
  rule (null / BigInt) and diverging from the DFG/FTL lowering.
- SafeToExecute: FFIRawRead returns false. A raw dereference must never be
  speculatively hoisted (e.g. by LICM) above its guarding null check.
- The CallFFI pointer-family AI type now includes the BigInt speculations
  (an address above 2^53 boxes to a HeapBigInt), fixing an unsound type.
- JSFFICallback::create eagerly materializes the FFIContext on the mutator,
  as JSFFIFunction::create already did.
- Signature::invokeThunk(): lock-free published pointer for the per-call fast
  path; the lock is now taken only for one-time generation.
- Reader-table signedness metadata and an Intrinsic.h comment corrected.
@Jarred-Sumner
Jarred-Sumner force-pushed the jarred/jsc-native-ffi branch from 737093a to a3a2455 Compare July 23, 2026 22:01
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 23, 2026
Points WEBKIT_VERSION at the autobuild-preview-pr-319-737093a0 prebuilt
so CI can build and test this branch without a local WebKit. Repin to
the merged commit sha once the WebKit PR lands.
Comment thread JSTests/stress/ffi-arity.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/ffi/FFIDFG.cpp`:
- Around line 109-120: Update the narrow integer cases in the type switch around
operationFFIWriteSlot to guard Int32Use insertion with the argument’s
shouldSpeculateInt32() result; when speculation is not appropriate, use
UntypedUse so runtime conversion handles boxed or non-Int32 values. First verify
that operationFFIWriteSlot supports UntypedUse for Char, Int8, Uint8, Int16,
Uint16, Int32, and Uint32, preserving the existing fast path when speculation
succeeds.

In `@Source/JavaScriptCore/ffi/JSFFICallback.cpp`:
- Line 148: Update the callback pointer assignment in the JSFFICallback
construction path to preserve exact nativeEntrypoint() values: use a numeric
result for safely representable pointers and a BigInt result when the pointer
exceeds Number.MAX_SAFE_INTEGER, matching the existing FFI pointer conversion
contract.

In `@Source/JavaScriptCore/ftl/FTLCapabilities.cpp`:
- Around line 214-215: Guard all Bun-specific FFI opcode handling with
USE(BUN_JSC_ADDITIONS), matching Intrinsic.h: wrap the FFIRawRead and CallFFI
cases in FTLCapabilities.cpp, the unsupported-backend FFI cases in
DFGSpeculativeJIT32_64.cpp, and the CallFFI heap-prediction and cell-operand
cases in DFGNode.h at the specified ranges. Ensure these cases are excluded when
Bun additions are disabled.

In `@Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp`:
- Around line 1575-1586: Guard the entire CallFFI switch case in the FTL
lowering dispatch with USE(BUN_JSC_ADDITIONS), matching the existing FFIRawRead
guard. Keep compileCallFFI() and its case-specific handling within that
conditional so non-Bun builds do not reference the unavailable CallFFI enum.

In `@Source/JavaScriptCore/heap/Heap.cpp`:
- Around line 120-121: Wrap the JSFFICallback.h and JSFFIFunction.h includes in
`#if` USE(BUN_JSC_ADDITIONS) / `#endif` guards, matching the existing conditional
boundary used by the related initializer below.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1507f51c-dc1a-4ffb-840a-0b47507f15ac

📥 Commits

Reviewing files that changed from the base of the PR and between 737093a and a3a2455.

📒 Files selected for processing (80)
  • JSTests/stress/ffi-align.js
  • JSTests/stress/ffi-arity-ladders.js
  • JSTests/stress/ffi-arity.js
  • JSTests/stress/ffi-callbacks.js
  • JSTests/stress/ffi-callffi-was-compiled.js
  • JSTests/stress/ffi-canary.js
  • JSTests/stress/ffi-conversion-errors-host.js
  • JSTests/stress/ffi-conversion-errors.js
  • JSTests/stress/ffi-fuzz-signatures.js
  • JSTests/stress/ffi-host-path.js
  • JSTests/stress/ffi-napi.js
  • JSTests/stress/ffi-no-jit.js
  • JSTests/stress/ffi-osr-and-exceptions.js
  • JSTests/stress/ffi-pointers-and-buffers.js
  • JSTests/stress/ffi-raw-read.js
  • JSTests/stress/ffi-signature-errors.js
  • JSTests/stress/ffi-subword-and-returns.js
  • JSTests/stress/ffi-tier-differential.js
  • JSTests/stress/ffi-typedarray-storage-modes.js
  • JSTests/stress/ffi-types-echo.js
  • Source/JavaScriptCore/CMakeLists.txt
  • Source/JavaScriptCore/Sources.txt
  • Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
  • Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp
  • Source/JavaScriptCore/dfg/DFGClobberize.h
  • Source/JavaScriptCore/dfg/DFGDataViewData.h
  • Source/JavaScriptCore/dfg/DFGDoesGC.cpp
  • Source/JavaScriptCore/dfg/DFGFixupPhase.cpp
  • Source/JavaScriptCore/dfg/DFGMayExit.cpp
  • Source/JavaScriptCore/dfg/DFGNode.cpp
  • Source/JavaScriptCore/dfg/DFGNode.h
  • Source/JavaScriptCore/dfg/DFGNodeType.h
  • Source/JavaScriptCore/dfg/DFGPredictionPropagationPhase.cpp
  • Source/JavaScriptCore/dfg/DFGSafeToExecute.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT32_64.cpp
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
  • Source/JavaScriptCore/dfg/DFGStrengthReductionPhase.cpp
  • Source/JavaScriptCore/ffi/BunFFI.cpp
  • Source/JavaScriptCore/ffi/BunFFI.h
  • Source/JavaScriptCore/ffi/FFICallHost.cpp
  • Source/JavaScriptCore/ffi/FFICallHost.h
  • Source/JavaScriptCore/ffi/FFICallbackThunk.cpp
  • Source/JavaScriptCore/ffi/FFICallbackThunk.h
  • Source/JavaScriptCore/ffi/FFICallingConvention.cpp
  • Source/JavaScriptCore/ffi/FFICallingConvention.h
  • Source/JavaScriptCore/ffi/FFIContext.cpp
  • Source/JavaScriptCore/ffi/FFIContext.h
  • Source/JavaScriptCore/ffi/FFIConversions.cpp
  • Source/JavaScriptCore/ffi/FFIConversions.h
  • Source/JavaScriptCore/ffi/FFIDFG.cpp
  • Source/JavaScriptCore/ffi/FFIDFG.h
  • Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp
  • Source/JavaScriptCore/ffi/FFIICStub.cpp
  • Source/JavaScriptCore/ffi/FFIICStub.h
  • Source/JavaScriptCore/ffi/FFIInvokeThunk.cpp
  • Source/JavaScriptCore/ffi/FFIInvokeThunk.h
  • Source/JavaScriptCore/ffi/FFIRawMemory.cpp
  • Source/JavaScriptCore/ffi/FFIRawMemory.h
  • Source/JavaScriptCore/ffi/FFISignature.cpp
  • Source/JavaScriptCore/ffi/FFISignature.h
  • Source/JavaScriptCore/ffi/FFIType.h
  • Source/JavaScriptCore/ffi/JSFFICallback.cpp
  • Source/JavaScriptCore/ffi/JSFFICallback.h
  • Source/JavaScriptCore/ffi/JSFFIFunction.cpp
  • Source/JavaScriptCore/ffi/JSFFIFunction.h
  • Source/JavaScriptCore/ffi/tests/FFITestFixtures.cpp
  • Source/JavaScriptCore/ffi/tests/FFITestFixtures.h
  • Source/JavaScriptCore/ffi/tests/testFFI.cpp
  • Source/JavaScriptCore/ftl/FTLCapabilities.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/heap/Heap.cpp
  • Source/JavaScriptCore/heap/Heap.h
  • Source/JavaScriptCore/runtime/Intrinsic.h
  • Source/JavaScriptCore/runtime/JSGlobalObject.cpp
  • Source/JavaScriptCore/runtime/JSGlobalObject.h
  • Source/JavaScriptCore/runtime/OptionsList.h
  • Source/JavaScriptCore/runtime/VM.h
  • Source/JavaScriptCore/shell/CMakeLists.txt
  • Source/JavaScriptCore/tools/JSDollarVM.cpp

Comment thread Source/JavaScriptCore/ffi/FFIDFG.cpp
Comment thread Source/JavaScriptCore/ffi/JSFFICallback.cpp Outdated
Comment thread Source/JavaScriptCore/ftl/FTLCapabilities.cpp Outdated
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp Outdated
Comment thread Source/JavaScriptCore/heap/Heap.cpp
…r args

The DFG's CallFFI already resolved a typed-array / DataView view passed to a
pointer-family argument straight to its data pointer inline, but the FTL sent
every UntypedUse argument -- including plain int32/double pointers -- through
operationFFIWriteSlot, so the hottest tier paid an out-of-line C++ conversion
per call. Give the FTL the same inline paths (numbers, and views), so passing
a view directly costs a type check plus a caged vector load and no host call.

Two guards keep the view fast path sound in both tiers: a detached view has a
null vector, and a resizable / growable-shared view carries the
isResizableOrGrowableShared mode bits; both punt to the C++ conversion, whose
semantics stay authoritative. The DFG helper gains the same mode-bit check so
the tiers reject those cases identically.

Adds ffi-view-args.js: a per-call tier-differential (a noDFG-pinned oracle vs
the FTL-hot twin) over all twelve view types, storage-mode transitions,
cstring-from-view, the detached / resizable / shared guards, buffer-param
rejection, and the throwing-second-argument exception path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp (1)

482-493: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep the arena alive until return boxing completes.

A CString argument can point into FFIContext::arena(), and native code may legally return that same pointer. Exiting the arena at Lines 490-492 before operationFFIBoxSlot boxes the return value at Line 609 can therefore read stale or overwritten memory, causing corrupted strings or a crash.

Exit immediately on native-call exceptions, but keep the arena active through normal return boxing; if boxing throws, exit before propagating and preserve the boxed result across the exit call.

Also applies to: 593-614

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp` around lines 482 - 493, Revise
the needsArenaBracket flow around the native call and operationFFIBoxSlot so the
arena remains active through normal return boxing. Exit it immediately on
native-call exceptions, and when boxing throws, exit before propagating while
preserving the boxed result across the exit call; remove the unconditional
operationFFIArenaExit immediately after the native call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@JSTests/stress/ffi-view-args.js`:
- Around line 121-124: Wrap the hotIdentity and refIdentity calls in the
numberArgs regression loop with the existing tryHot and tryCall helpers. Keep
the loop and agreement labels unchanged so null and undefined conversion
behavior is compared as normalized thrown results rather than escaping as
uncaught exceptions.

---

Outside diff comments:
In `@Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp`:
- Around line 482-493: Revise the needsArenaBracket flow around the native call
and operationFFIBoxSlot so the arena remains active through normal return
boxing. Exit it immediately on native-call exceptions, and when boxing throws,
exit before propagating while preserving the boxed result across the exit call;
remove the unconditional operationFFIArenaExit immediately after the native
call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d818419f-e085-4b09-bfd5-eef64c29b898

📥 Commits

Reviewing files that changed from the base of the PR and between a3a2455 and ca52299.

📒 Files selected for processing (3)
  • JSTests/stress/ffi-view-args.js
  • Source/JavaScriptCore/ffi/FFIDFGCodegen.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

Comment thread JSTests/stress/ffi-view-args.js
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 23, 2026
…path

Repins to autobuild-preview-pr-319-ca52299c, which adds the inline number
and typed-array-view conversion for CallFFI pointer arguments in the FTL.
Serve "ptr" (the resolved native target as a double-encoded pointer) from
the m_target field via getOwnPropertySlot instead of relying on embedders to
add it with putDirect. An own-property addition transitions the function's
Structure, and a JSFFIFunction with a non-canonical structure loses the
callee fast paths on polymorphic (non-devirtualized) call sites -- measured
~2.5x slower per call through a shared, polymorphic runner. Keeping the
structure canonical keeps every call site fast, not only the ones the DFG
turns into CallFFI. The property stays read-only, non-enumerable and
non-deletable, matching the previous surface.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/ffi/JSFFIFunction.cpp`:
- Around line 143-154: Add a JSFFIFunction::getOwnPropertyNames() override
alongside getOwnPropertySlot() that appends the non-enumerable "ptr" property to
own-key results while preserving base-class keys and attributes. Add a
reflection test covering Object.getOwnPropertyNames() and Reflect.ownKeys() to
verify ptr is included.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d65fe97c-83bd-43f3-bac7-2041292a28d0

📥 Commits

Reviewing files that changed from the base of the PR and between ca52299 and 48250cd.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/ffi/JSFFIFunction.cpp
  • Source/JavaScriptCore/ffi/JSFFIFunction.h

Comment thread Source/JavaScriptCore/ffi/JSFFIFunction.cpp Outdated
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
Repins to autobuild-preview-pr-319-48250cdb, which serves JSFFIFunction's
.ptr as an intrinsic property instead of a per-instance putDirect, fixing
the ~2.5x slowdown on polymorphic FFI call sites.
…n ESM

The parser's constant-callee feed for JSFFIFunction was gated on the call
opcode being Call. In strict / ES-module code an FFI call in tail position
-- notably every `() => sym(...)` arrow body -- is a bytecode TailCall, so
the feed never ran there and the call fell to the generic path (into the
C++ host marshaller), slower than the interpreter. That is the shape of
essentially every real call site: mitata's bench and any dlopen'd symbol
called inside a closure in an ES module.

Accept TailCall in the feed and emit the resulting FFI call as a plain
Call. Dropping tail-call frame reuse is a legal optimization loss (proper
tail calls are off by default) and keeps CallFFI a non-terminal node, so the
existing strength-reduction fold applies unchanged. Converting a TailCall
terminal to CallFFI in place would break block terminality, so strength
reduction still (correctly) only ever sees op() == Call.

Adds ffi-tailcall.js: values, exception propagation and a deep chain of
tail-position FFI calls agree with the interpreter across tiers.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
Repins to autobuild-preview-pr-319-596f4fc4: FFI calls in tail position
(every closure-wrapped call in an ES module) now take the CallFFI fast
path instead of the C++ host marshaller.
Bun's public API exposes symbol.native (the raw callable, which for an
engine-native function is the function itself). Serve it as a second
intrinsic property from getOwnPropertySlot, next to "ptr", so the glue never
has to assign it: an own-property write transitions the cell's Structure
and slows every polymorphic call site, exactly like the earlier "ptr" case.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
Wires bun:ffi onto JavaScriptCore's engine-native FFI (JSC::JSFFIFunction /
JSC::JSFFICallback from oven-sh/WebKit#319) instead of JIT-compiling a
TinyCC trampoline per symbol.

dlopen()/linkSymbols()/CFunction() symbols take the JSC path when the
signature has no napi_env/napi_value; JSCallback when it isn't threadsafe.
cc(), napi signatures and threadsafe callbacks are unchanged and stay on
TinyCC. BUN_FEATURE_FLAG_DISABLE_JSC_FFI=1 restores TinyCC everywhere.

Engine-native symbols get no per-argument JS coercion shim: conversions,
arity and result boxing happen in the engine, and hot call sites compile
into DFG/FTL CallFFI nodes. dlopen returns a per-symbol jscSymbols map so
the glue knows which symbols to leave unwrapped; the symbol's .ptr/.native
are intrinsic engine properties (writing them as own properties transitioned
the cell's Structure and slowed polymorphic call sites).

Coercion: numeric params accept numbers, booleans, null/undefined and
BigInts and wrap to width; strings/Symbols throw. Fixes u32 >= 2^31
(#7007); pointers > 2^53 round-trip as exact BigInts and BigInt addresses
are accepted (#28068, #22751).

Pins WEBKIT_VERSION to the WebKit#319 preview build; repin to the merged
sha once it lands.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
The FTL CallFFI lowering used to store every argument into the slot
buffer and call the per-signature invoke thunk, which reloaded the slots
into registers and made the real native call: two call/ret pairs, an
extra frame, and a store-then-reload of every argument. The target is a
compile-time constant in the FTL, so call it straight from B3 as a
CCallValue (tagged CFunctionPtrTag, exactly as the thunk calls it):

  - KnownInt32 / KnownBoolean / DoubleRep arguments are already single
    typed SSA values and become register operands directly.
  - UntypedUse and synthetic arguments keep their existing conversion code
    (typed-array-view fast path, i64/u64 BigInt wrap-mod-2^64, the C++ slow
    path), all of which finish by leaving the canonical 64-bit value in the
    slot; the direct call reloads that slot as its operand.
  - The return value is normalized back into the return slot with the same
    encoding as the thunk's return normalization, so the boxing code is
    shared and unchanged.

A bare noop() call goes from 1.49 ns to 0.73 ns.

One ABI corner B3 cannot express: Darwin/arm64 packs sub-8-byte STACK
arguments at their natural size, and B3 has no 8/16-bit value type, so a
char/i8/u8/i16/u16 argument that spills past the 8 argument GPRs would be
laid out at Int32 stride. Detect that case and keep the thunk (which
implements the packing by hand) for exactly those signatures; every other
signature uses the direct call. Guarded by --useFFIDirectCall.
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp Outdated
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp Outdated
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp Outdated
Split compileCallFFI into a thin dispatcher (which evaluates
Options::useFFIDirectCall() and the Darwin sub-word-spill eligibility
once) and template<bool DirectCall> compileCallFFIImpl(), turning every
runtime `if (directCall)` in the argument/call/return lowering into
`if constexpr`. The direct-call and invoke-thunk paths are now separate
instantiations sharing the conversion table, arena bracketing, keep-alive
and boxing code, and each dead path folds away at compile time. No
behavior change: both instantiations pass testFFI and the ffi stress
suite (the thunk one forced via --useFFIDirectCall=false under
eager/validate); noop stays 0.69ns direct / 1.41ns thunk.

Also record, next to the code, why the IC stub is kept: in the unoptimized
tiers it is worth ~1.5x on noop and ~2.4x on argument-carrying calls
versus the generic C++ host marshaller.
Comment thread Source/JavaScriptCore/ffi/FFIRawMemory.h Outdated
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
Repins to autobuild-preview-pr-319-21975506: the FTL calls the native FFI
target directly (no invoke thunk; noop 1.4ns -> 0.7ns in the engine), plus
the intrinsic .ptr/.native and the templated CallFFI lowering. Repin to the
merged sha once #319 lands.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 24, 2026
Wires bun:ffi onto JavaScriptCore's engine-native FFI (JSC::JSFFIFunction /
JSC::JSFFICallback from oven-sh/WebKit#319) instead of JIT-compiling a
TinyCC trampoline per symbol.

dlopen()/linkSymbols()/CFunction() symbols take the JSC path when the
signature has no napi_env/napi_value; JSCallback when it isn't threadsafe.
cc(), napi signatures and threadsafe callbacks are unchanged and stay on
TinyCC. BUN_FEATURE_FLAG_DISABLE_JSC_FFI=1 restores TinyCC everywhere.

Engine-native symbols get no per-argument JS coercion shim: conversions,
arity and result boxing happen in the engine, and hot call sites compile
into DFG/FTL CallFFI nodes (the FTL calling the target directly). dlopen
returns a per-symbol jscSymbols map so the glue knows which symbols to
leave unwrapped; .ptr/.native are intrinsic engine properties (writing
them as own properties transitioned the cell's Structure and slowed
polymorphic call sites).

Coercion: numeric params accept numbers, booleans, null/undefined and
BigInts and wrap to width; strings/Symbols throw. Fixes u32 >= 2^31
(#7007); pointers > 2^53 round-trip as exact BigInts and BigInt addresses
are accepted (#28068, #22751).

Pins WEBKIT_VERSION to the WebKit#319 preview build; repin to the merged
sha once #319 lands.
Comment thread Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp Outdated
Comment thread Source/JavaScriptCore/ffi/FFIConversions.cpp Outdated
Unbrace the single-statement clauses in compileCallFFIImpl (the
if-constexpr directCall pair, the numbersInline branch pair, and the
operand-marshalling else-if chain), lay out the return-type case labels
one per line, drop the trailing alignment comments in
FFICallingConvention.h, and remove a stale trailing comment in
FFIConversions.cpp (strings and Symbols are rejected before that branch).

testFFI 9519/9519; all ffi stress files pass; build clean.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Reimplement bun:ffi on the engine-native FFI in JavaScriptCore
(oven-sh/WebKit#319). For dlopen(), linkSymbols(), CFunction() and
JSCallback the engine generates the marshalling itself, promotes hot calls
to a direct native call from JIT'd code, and owns callback lifetime;
TinyCC remains only as cc()'s C compiler.

- CString is a constructor returning a plain string primitive; 'cstring'
  returns and callback cstring parameters are decoded strings (NULL -> null).
- New buffer_length argument type; .ptr/.native are real read-only own
  properties; N-API types are cc()-only; the JIT is required.
- BigInt pointers accepted consistently across CString, read.*, toBuffer,
  toArrayBuffer, CFunction / linkSymbols, with negative and out-of-range
  BigInts rejected instead of wrapping.
- Fix the shared isBigIntInInt64Range / isBigIntInUInt64Range bindings,
  which were effectively always-true (broken disjunction plus swapped
  (max, min) at the Rust boundary); MySQL BigInt binding and the integer
  range validator now behave as written.
- test/js/bun/ffi builds its C fixture with the host compiler at test time
  and includes an ABI conformance suite; cc() bundles N-API headers under
  <bun-cc>/node.

The branch history is rebuilt onto current main to contain only these
files; a prior rebase had linearized a main merge and replayed unrelated
main commits onto the branch.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new findings after a554d31. This is a large, security-sensitive change (new FFI subsystem with JIT-generated thunks, a new DFG/FTL node type, cross-ABI calling-convention codegen, and thread-safe callback GC rooting), so it warrants human review.

What was reviewed this round:

  • a554d31's fixes in compileCallFFIImpl (unbraced single-statement clauses, one-per-line returnLType cases) — verified complete, no mixed-style leftover in the switch.
  • The removed stale comment in writeFloatingPointSlot() and the removed alignment comments in FFICallingConvention.h.
  • check-webkit-style [safercpp/memcpy] hits in the JSDollarVM FFI additions — examined and ruled out (test-fixture glue, bounded copies).
Extended reasoning...

Overview

This PR adds an engine-native FFI subsystem to JavaScriptCore under USE(BUN_JSC_ADDITIONS): 88 files (~30 new source files under ffi/, 32 new stress tests, and integration hooks across dfg/, ftl/, heap/, runtime/, and tools/). It introduces two new JS cell types (JSFFIFunction, JSFFICallback), a new DFG/FTL node type (CallFFI), per-signature JIT-generated invoke/callback thunks for four ABIs (SysV64, Win64, AAPCS64, Darwin arm64), a per-global string arena tied to GC via HeapObserver, and a thread-safe callback rooting protocol with a packed closed+pending-count word.

Security risks

FFI is inherently security-sensitive: it marshals raw pointers between JS and native code, JIT-generates executable trampolines, reads TypedArray vectors directly (with Gigacage uncaging), and lets foreign OS threads enqueue work that later runs on the JS thread. Correctness of the calling-convention codegen (register assignment, stack alignment, sub-word packing), OSR-exit / exception-unwind interaction with the arena bracket, and the thread-safe callback lifetime (close-while-queued, GC-while-pending) all have memory-safety implications. The differential test harness (9.5k checks) and 32 stress tests give substantial coverage, and many earlier review rounds caught real issues in these areas (arena depth leaks on exception, pointer-return SpeculatedType unsoundness, fall-through breakage in DFGMayExit), all now resolved.

Level of scrutiny

High. This is production-critical JIT-compiler and runtime code that will back Bun's bun:ffi API. It contains hand-written codegen across four ABIs and touches every DFG/FTL phase. It is far outside the scope of changes that can be approved without human review.

Other factors

The review history spans ~60 resolved threads over five days; the last several rounds converged on style/whitespace cleanup and stale-comment removal, and this is the first round with zero findings. The two candidate issues raised this run (mixed-style leftover in the a554d31 reformat, and safercpp/memcpy lint hits in the JSDollarVM test glue) were both examined and ruled out. All previously-raised inline comments are marked resolved by the author with commit references.

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Picks up the remaining check-webkit-style cleanups and the orphaned-comment
removals from oven-sh/WebKit#319; no behavioral change since the last
pin.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Bring the engine's bun:ffi coverage into Bun's CI:

- test/js/bun/jsc-stress/fixtures/ffi/: the 32 ffi-*.js stress files from
  oven-sh/WebKit#319 (tier transitions, OSR, exception unwinding through
  native frames, threadsafe callbacks, arity/ABI ladders, arena depth). They
  drive the engine through the $vm test hooks and the FFITestFixtures
  library compiled into JavaScriptCore, so the runner sets
  BUN_JSC_useDollarVM=1 and gates the block on a runtime probe -- builds
  that compile $vm out (LTO / shipped release) skip cleanly, debug / ASAN /
  plain release run them.
- The directive parser learns //@ requireOptions(...) and //@ runNoJIT, and
  preload.js maps the jsc-shell globals the fixtures use (gc, fullGC,
  edenGC, numberOfDFGCompiles, noDFG) onto bun:jsc / Bun.gc.
- ffi-callbacks.js: one assertion (the throwing callback's frame name in
  caught.stack) is intermittent under Bun -- the frame is occasionally
  reported as <anonymous> -- so assert the stack exists and file the
  frame-name intermittency separately; every substantive check stays.
- testFFI.test.ts runs the C++/ABI test binary that the WebKit artifact now
  ships in bin/, matching its 'OK: N checks passed, 0 failed' summary, and
  skips when the pinned bundle predates the binary.

jsc-stress 115/0 locally (all 32 ffi fixtures pass); ffi dir 211/0;
source lints 59/0.
testFFI (the FFI C++/ABI test binary, 9519 checks including the
differential harness that runs every generated invoke thunk against
clang-compiled reference calls) was never built because every build path
built only --target jsc. Build it alongside jsc on all of them so it lands
in the artifact's bin/ next to jsc: the Linux glibc/musl, FreeBSD, Android,
and macOS/Windows cross Dockerfiles, mac-release.bash, windows-release.ps1
and build.ts. FreeBSD and Android previously packaged no bin/ at all, so
add the copy there; every other path already copies bin/ wholesale.

testFFI links the same libraries as jsc (CMAKE_DL_LIBS, plus Winmm on
Windows) rather than only the vendored mimalloc objects -- it has only ever
been link-tested on macOS, and the other platforms now build it. Bun's
CI runs the shipped binary via test/js/bun/jsc-stress/testFFI.test.ts
(oven-sh/bun#35246).
Comment thread Source/JavaScriptCore/shell/CMakeLists.txt Outdated
Bun's CI test lanes run the built bun binary and never have the WebKit
deps tree, so a testFFI shipped only inside the 400 MB bun-webkit tarball
is unreachable there. After the tarballs are gathered into ./out/, extract
each platform's bin/testFFI (testFFI.exe on Windows) into
./out/testFFI-<platform> and attach those to the release, so a test only
downloads the small standalone binary for its own platform.
Bun's CI now carries testFFI to its test lanes inside the existing
build-bun profile zip (the same artifact channel the bun binary already
uses), so the tests find it beside bunExe and never download from GitHub
releases. The per-platform release-asset upload is unused; remove it.
build.ts and the release scripts build testFFI alongside jsc now, so the
'(it only configures + builds --target jsc)' clause contradicted the
sentence after it.
Comment thread Source/JavaScriptCore/ffi/tests/testFFI.cpp
@Jarred-Sumner
Jarred-Sumner merged commit e6063b0 into main Jul 28, 2026
43 checks passed
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Reimplement bun:ffi on the engine-native FFI in JavaScriptCore
(oven-sh/WebKit#319). For dlopen(), linkSymbols(), CFunction() and
JSCallback the engine generates the marshalling itself, promotes hot calls
to a direct native call from JIT'd code, and owns callback lifetime;
TinyCC remains only as cc()'s C compiler.

- CString is a constructor returning a plain string primitive; 'cstring'
  returns and callback cstring parameters are decoded strings (NULL -> null).
- New buffer_length argument type; .ptr/.native are real read-only own
  properties; N-API types are cc()-only; the JIT is required.
- BigInt pointers accepted consistently across CString, read.*, toBuffer,
  toArrayBuffer, CFunction / linkSymbols, with negative and out-of-range
  BigInts rejected instead of wrapping.
- Fix the shared isBigIntInInt64Range / isBigIntInUInt64Range bindings,
  which were effectively always-true (broken disjunction plus swapped
  (max, min) at the Rust boundary); MySQL BigInt binding and the integer
  range validator now behave as written.
- test/js/bun/ffi builds its C fixture with the host compiler at test time
  and includes an ABI conformance suite; cc() bundles N-API headers under
  <bun-cc>/node.

The branch history is rebuilt onto current main to contain only these
files; a prior rebase had linearized a main merge and replayed unrelated
main commits onto the branch.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Picks up the remaining check-webkit-style cleanups and the orphaned-comment
removals from oven-sh/WebKit#319; no behavioral change since the last
pin.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Bring the engine's bun:ffi coverage into Bun's CI:

- test/js/bun/jsc-stress/fixtures/ffi/: the 32 ffi-*.js stress files from
  oven-sh/WebKit#319 (tier transitions, OSR, exception unwinding through
  native frames, threadsafe callbacks, arity/ABI ladders, arena depth). They
  drive the engine through the $vm test hooks and the FFITestFixtures
  library compiled into JavaScriptCore, so the runner sets
  BUN_JSC_useDollarVM=1 and gates the block on a runtime probe -- builds
  that compile $vm out (LTO / shipped release) skip cleanly, debug / ASAN /
  plain release run them.
- The directive parser learns //@ requireOptions(...) and //@ runNoJIT, and
  preload.js maps the jsc-shell globals the fixtures use (gc, fullGC,
  edenGC, numberOfDFGCompiles, noDFG) onto bun:jsc / Bun.gc.
- ffi-callbacks.js: one assertion (the throwing callback's frame name in
  caught.stack) is intermittent under Bun -- the frame is occasionally
  reported as <anonymous> -- so assert the stack exists and file the
  frame-name intermittency separately; every substantive check stays.
- testFFI.test.ts runs the C++/ABI test binary that the WebKit artifact now
  ships in bin/, matching its 'OK: N checks passed, 0 failed' summary, and
  skips when the pinned bundle predates the binary.

jsc-stress 115/0 locally (all 32 ffi fixtures pass); ffi dir 211/0;
source lints 59/0.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
oven-sh/WebKit#319 merged as e6063b004fa4; move WEBKIT_VERSION off the
preview-pr-319 pre-releases onto the main autobuild for the merge commit.
No behavioral difference from the last preview pin.
robobun pushed a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Reimplement bun:ffi on the engine-native FFI in JavaScriptCore
(oven-sh/WebKit#319). For dlopen(), linkSymbols(), CFunction() and
JSCallback the engine generates the marshalling itself, promotes hot calls
to a direct native call from JIT'd code, and owns callback lifetime;
TinyCC remains only as cc()'s C compiler.

- CString is a constructor returning a plain string primitive; 'cstring'
  returns and callback cstring parameters are decoded strings (NULL -> null).
- New buffer_length argument type; .ptr/.native are real read-only own
  properties; N-API types are cc()-only; the JIT is required.
- BigInt pointers accepted consistently across CString, read.*, toBuffer,
  toArrayBuffer, CFunction / linkSymbols, with negative and out-of-range
  BigInts rejected instead of wrapping.
- Fix the shared isBigIntInInt64Range / isBigIntInUInt64Range bindings,
  which were effectively always-true (broken disjunction plus swapped
  (max, min) at the Rust boundary); MySQL BigInt binding and the integer
  range validator now behave as written.
- test/js/bun/ffi builds its C fixture with the host compiler at test time
  and includes an ABI conformance suite; cc() bundles N-API headers under
  <bun-cc>/node.

The branch history is rebuilt onto current main to contain only these
files; a prior rebase had linearized a main merge and replayed unrelated
main commits onto the branch.
robobun pushed a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Picks up the remaining check-webkit-style cleanups and the orphaned-comment
removals from oven-sh/WebKit#319; no behavioral change since the last
pin.
robobun pushed a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
Bring the engine's bun:ffi coverage into Bun's CI:

- test/js/bun/jsc-stress/fixtures/ffi/: the 32 ffi-*.js stress files from
  oven-sh/WebKit#319 (tier transitions, OSR, exception unwinding through
  native frames, threadsafe callbacks, arity/ABI ladders, arena depth). They
  drive the engine through the $vm test hooks and the FFITestFixtures
  library compiled into JavaScriptCore, so the runner sets
  BUN_JSC_useDollarVM=1 and gates the block on a runtime probe -- builds
  that compile $vm out (LTO / shipped release) skip cleanly, debug / ASAN /
  plain release run them.
- The directive parser learns //@ requireOptions(...) and //@ runNoJIT, and
  preload.js maps the jsc-shell globals the fixtures use (gc, fullGC,
  edenGC, numberOfDFGCompiles, noDFG) onto bun:jsc / Bun.gc.
- ffi-callbacks.js: one assertion (the throwing callback's frame name in
  caught.stack) is intermittent under Bun -- the frame is occasionally
  reported as <anonymous> -- so assert the stack exists and file the
  frame-name intermittency separately; every substantive check stays.
- testFFI.test.ts runs the C++/ABI test binary that the WebKit artifact now
  ships in bin/, matching its 'OK: N checks passed, 0 failed' summary, and
  skips when the pinned bundle predates the binary.

jsc-stress 115/0 locally (all 32 ffi fixtures pass); ffi dir 211/0;
source lints 59/0.
robobun pushed a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
oven-sh/WebKit#319 merged as e6063b004fa4; move WEBKIT_VERSION off the
preview-pr-319 pre-releases onto the main autobuild for the merge commit.
No behavioral difference from the last preview pin.
@heimskr

heimskr commented Jul 28, 2026

Copy link
Copy Markdown

neat stuff!

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
Reimplement bun:ffi on the engine-native FFI in JavaScriptCore
(oven-sh/WebKit#319). For dlopen(), linkSymbols(), CFunction() and
JSCallback the engine generates the marshalling itself, promotes hot calls
to a direct native call from JIT'd code, and owns callback lifetime;
TinyCC remains only as cc()'s C compiler.

- CString is a constructor returning a plain string primitive; 'cstring'
  returns and callback cstring parameters are decoded strings (NULL -> null).
- New buffer_length argument type; .ptr/.native are real read-only own
  properties; N-API types are cc()-only; the JIT is required.
- BigInt pointers accepted consistently across CString, read.*, toBuffer,
  toArrayBuffer, CFunction / linkSymbols, with negative and out-of-range
  BigInts rejected instead of wrapping.
- Fix the shared isBigIntInInt64Range / isBigIntInUInt64Range bindings,
  which were effectively always-true (broken disjunction plus swapped
  (max, min) at the Rust boundary); MySQL BigInt binding and the integer
  range validator now behave as written.
- test/js/bun/ffi builds its C fixture with the host compiler at test time
  and includes an ABI conformance suite; cc() bundles N-API headers under
  <bun-cc>/node.

The branch history is rebuilt onto current main to contain only these
files; a prior rebase had linearized a main merge and replayed unrelated
main commits onto the branch.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
Picks up the remaining check-webkit-style cleanups and the orphaned-comment
removals from oven-sh/WebKit#319; no behavioral change since the last
pin.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
Bring the engine's bun:ffi coverage into Bun's CI:

- test/js/bun/jsc-stress/fixtures/ffi/: the 32 ffi-*.js stress files from
  oven-sh/WebKit#319 (tier transitions, OSR, exception unwinding through
  native frames, threadsafe callbacks, arity/ABI ladders, arena depth). They
  drive the engine through the $vm test hooks and the FFITestFixtures
  library compiled into JavaScriptCore, so the runner sets
  BUN_JSC_useDollarVM=1 and gates the block on a runtime probe -- builds
  that compile $vm out (LTO / shipped release) skip cleanly, debug / ASAN /
  plain release run them.
- The directive parser learns //@ requireOptions(...) and //@ runNoJIT, and
  preload.js maps the jsc-shell globals the fixtures use (gc, fullGC,
  edenGC, numberOfDFGCompiles, noDFG) onto bun:jsc / Bun.gc.
- ffi-callbacks.js: one assertion (the throwing callback's frame name in
  caught.stack) is intermittent under Bun -- the frame is occasionally
  reported as <anonymous> -- so assert the stack exists and file the
  frame-name intermittency separately; every substantive check stays.
- testFFI.test.ts runs the C++/ABI test binary that the WebKit artifact now
  ships in bin/, matching its 'OK: N checks passed, 0 failed' summary, and
  skips when the pinned bundle predates the binary.

jsc-stress 115/0 locally (all 32 ffi fixtures pass); ffi dir 211/0;
source lints 59/0.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
oven-sh/WebKit#319 merged as e6063b004fa4; move WEBKIT_VERSION off the
preview-pr-319 pre-releases onto the main autobuild for the merge commit.
No behavioral difference from the last preview pin.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
Reimplements `bun:ffi` on top of a new **engine-native FFI in
JavaScriptCore** (oven-sh/WebKit#319). For `dlopen()`, `linkSymbols()`,
`CFunction()`, and `JSCallback`, the engine generates the marshalling
itself, promotes hot calls to a direct native call from JIT'd code, and
owns callback lifetime. TinyCC is used only as `cc()`'s C compiler.

## Performance

Engine-native vs TinyCC-based `bun:ffi` (macOS arm64, release):

| operation | TinyCC | engine-native | |
|---|---|---|---|
| noop call | 2.13 ns | **0.70 ns** | 3.0× |
| `new CString(ptr)` (46-char string) | 92.5 ns | **24.1 ns** | 3.8× |

Against Deno on the same native library:

| operation | Bun | Deno | |
|---|---|---|---|
| noop call | **0.69 ns** | 1.51 ns | 2.2× |
| hash (`ptr` + `u32`) | 39 ns | 36 ns | parity — dominated by the C
function's own work |
| C string return | **22 ns** (`returns: "cstring"`) | 34 ns | 1.5× |

String arguments, same string ("550e8400-…", 36 chars):

| how the string is passed | ns/call |
|---|---|
| raw pointer (`ptr(buf)`) | 16 |
| TypedArray | 17 |
| JS string (encoded into the call arena) | 27 |
| result of `CString(ptr)` | 27 |

Passing a JS string re-encodes it on every call; for a hot loop over the
same string, pass a pointer or TypedArray.

### opentui

Three of opentui's own benchmark suites (`packages/core`), engine-native
vs TinyCC-based `bun:ffi`, same machine. opentui binds its Zig core --
including its native Yoga port (222 symbols) -- through one `bun:ffi`
`dlopen`, so its Yoga layout passes are FFI-call-dense.

**`render-traversal`** (Yoga reads + scrollbox culling) -- geomean
**1.30x**, scaling with call count as fixed per-scenario cost amortizes:

| scenario | speedup |
|---|---|
| `yoga_layout_reads_1000` | **2.08x** |
| `yoga_layout_reads_10000` | 2.02x |
| `yoga_layout_reads_100` | 1.38x |
| `scrollbox_culling_scaling_10000` | **1.43x** |
| `scrollbox_culling_scaling_5000` | 1.32x |
| `scrollbar_stack` / `layout_only_opencode_wrappers` | 1.00x |

**`layout-benchmark`** (16 dirty-and-relayout scenarios through the Yoga
FFI) -- geomean **1.19x**, all scenarios faster (1.04x-1.38x);
OpenCode-shaped full-render passes 1.30x-1.38x, pure `calculate_only`
layout 1.07x-1.19x.

**`native-span-feed`** (`default` suite, a memcpy-bound span stream) --
geomean **+4.9%** throughput, worst scenario -1.7%, best +22.9% on
`commit_4k` write; gains concentrate at high call rates and vanish at 32
MB spans.

opentui is unaffected by the `CString` change (it uses neither `CString`
nor `cstring` returns).

## Behavior changes

- **`cstring` returns are strings.** A symbol declared `returns:
"cstring"` yields a JS **string primitive** (`typeof "string"`, `===`
works) decoded from the callee's `char*`; a `NULL` return is `null`.
There is no wrapper object and no address is exposed. Callback
parameters typed `cstring` arrive as strings the same way.
- **`CString` is a constructor that returns a string.** `new
CString(ptr, byteOffset?, byteLength?)` and `CString(ptr, ...)`
transcode the bytes at `ptr` and return a plain string; `CString` has no
accessor surface.
- **`buffer_length`** — new argument type: pass the same
TypedArray/DataView you passed for a `buffer` argument and the callee
receives that view's byte length as a `uint64_t`, read off the same
object at call time so pointer and length always agree. Argument-only;
not available inside `cc()`.
- **`.ptr` / `.native`** on FFI functions are real read-only own
properties.
- **N-API types are `cc()`-only.** `napi_env` / `napi_value` in
`dlopen`, `linkSymbols`, `CFunction`, or `JSCallback` throw a
`TypeError`. Inside `cc()`, a `napi_env` parameter is filled in by the
compiled trampoline and its JS argument is a consumed-but-ignored
placeholder.
- **`cc()`** performs C-side conversions in its TinyCC-compiled
trampoline (integer arguments wrap), and bundles N-API headers under
`<bun-cc>/node/` so `#include <node/node_api.h>` resolves without a `-I`
flag.
- **Thread-safe callbacks** can be invoked from any thread and are
delivered on the JS thread with arguments converted there (64-bit
integers and large pointers arrive as exact BigInts). `close()` refuses
new foreign-thread calls but every already-queued invocation is still
delivered.
- **The JIT is required.** With the JIT disabled, `dlopen()` and friends
throw a `TypeError`.

## What is removed

- The TinyCC compile path for
`dlopen`/`linkSymbols`/`CFunction`/`JSCallback` symbols, `viewSource` of
callbacks (there is no generated C to show), and the per-symbol wrapper
objects — every symbol is the engine function itself.
- `CString`'s object surface (see above) and `toArrayBuffer`-backed
`arrayBuffer` on it.

## Testing

- `test/js/bun/ffi/` builds its C fixture with the host compiler at test
time, so the suite runs on every CI platform: **203 tests, 0 fail**.
Includes an ABI conformance suite whose fixture returns
position-weighted combinations of its arguments, so any
calling-convention error changes the observable result — verified to
detect a deliberately mis-declared signature.
- Source lints, the napi FFI file, and the ffi bench all green; opentui
audited as unaffected by the `CString` change (it uses neither `CString`
nor `cstring` returns).

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
springmin pushed a commit to springmin/bun that referenced this pull request Jul 30, 2026
…e FFI)

vendor/WebKit updated from c9296e35 to 8172ab94 which merges oven-sh:main
and includes the engine-native FFI changes (oven-sh/WebKit#319).

Revert JSCFFIBridge.cpp to the upstream version without the __has_include
stub, since the updated WebKit provides the required FFI headers.
liooil pushed a commit to liooil/poly that referenced this pull request Aug 7, 2026
Reimplements `bun:ffi` on top of a new **engine-native FFI in
JavaScriptCore** (oven-sh/WebKit#319). For `dlopen()`, `linkSymbols()`,
`CFunction()`, and `JSCallback`, the engine generates the marshalling
itself, promotes hot calls to a direct native call from JIT'd code, and
owns callback lifetime. TinyCC is used only as `cc()`'s C compiler.

## Performance

Engine-native vs TinyCC-based `bun:ffi` (macOS arm64, release):

| operation | TinyCC | engine-native | |
|---|---|---|---|
| noop call | 2.13 ns | **0.70 ns** | 3.0× |
| `new CString(ptr)` (46-char string) | 92.5 ns | **24.1 ns** | 3.8× |

Against Deno on the same native library:

| operation | Bun | Deno | |
|---|---|---|---|
| noop call | **0.69 ns** | 1.51 ns | 2.2× |
| hash (`ptr` + `u32`) | 39 ns | 36 ns | parity — dominated by the C
function's own work |
| C string return | **22 ns** (`returns: "cstring"`) | 34 ns | 1.5× |

String arguments, same string ("550e8400-…", 36 chars):

| how the string is passed | ns/call |
|---|---|
| raw pointer (`ptr(buf)`) | 16 |
| TypedArray | 17 |
| JS string (encoded into the call arena) | 27 |
| result of `CString(ptr)` | 27 |

Passing a JS string re-encodes it on every call; for a hot loop over the
same string, pass a pointer or TypedArray.

### opentui

Three of opentui's own benchmark suites (`packages/core`), engine-native
vs TinyCC-based `bun:ffi`, same machine. opentui binds its Zig core --
including its native Yoga port (222 symbols) -- through one `bun:ffi`
`dlopen`, so its Yoga layout passes are FFI-call-dense.

**`render-traversal`** (Yoga reads + scrollbox culling) -- geomean
**1.30x**, scaling with call count as fixed per-scenario cost amortizes:

| scenario | speedup |
|---|---|
| `yoga_layout_reads_1000` | **2.08x** |
| `yoga_layout_reads_10000` | 2.02x |
| `yoga_layout_reads_100` | 1.38x |
| `scrollbox_culling_scaling_10000` | **1.43x** |
| `scrollbox_culling_scaling_5000` | 1.32x |
| `scrollbar_stack` / `layout_only_opencode_wrappers` | 1.00x |

**`layout-benchmark`** (16 dirty-and-relayout scenarios through the Yoga
FFI) -- geomean **1.19x**, all scenarios faster (1.04x-1.38x);
OpenCode-shaped full-render passes 1.30x-1.38x, pure `calculate_only`
layout 1.07x-1.19x.

**`native-span-feed`** (`default` suite, a memcpy-bound span stream) --
geomean **+4.9%** throughput, worst scenario -1.7%, best +22.9% on
`commit_4k` write; gains concentrate at high call rates and vanish at 32
MB spans.

opentui is unaffected by the `CString` change (it uses neither `CString`
nor `cstring` returns).

## Behavior changes

- **`cstring` returns are strings.** A symbol declared `returns:
"cstring"` yields a JS **string primitive** (`typeof "string"`, `===`
works) decoded from the callee's `char*`; a `NULL` return is `null`.
There is no wrapper object and no address is exposed. Callback
parameters typed `cstring` arrive as strings the same way.
- **`CString` is a constructor that returns a string.** `new
CString(ptr, byteOffset?, byteLength?)` and `CString(ptr, ...)`
transcode the bytes at `ptr` and return a plain string; `CString` has no
accessor surface.
- **`buffer_length`** — new argument type: pass the same
TypedArray/DataView you passed for a `buffer` argument and the callee
receives that view's byte length as a `uint64_t`, read off the same
object at call time so pointer and length always agree. Argument-only;
not available inside `cc()`.
- **`.ptr` / `.native`** on FFI functions are real read-only own
properties.
- **N-API types are `cc()`-only.** `napi_env` / `napi_value` in
`dlopen`, `linkSymbols`, `CFunction`, or `JSCallback` throw a
`TypeError`. Inside `cc()`, a `napi_env` parameter is filled in by the
compiled trampoline and its JS argument is a consumed-but-ignored
placeholder.
- **`cc()`** performs C-side conversions in its TinyCC-compiled
trampoline (integer arguments wrap), and bundles N-API headers under
`<bun-cc>/node/` so `#include <node/node_api.h>` resolves without a `-I`
flag.
- **Thread-safe callbacks** can be invoked from any thread and are
delivered on the JS thread with arguments converted there (64-bit
integers and large pointers arrive as exact BigInts). `close()` refuses
new foreign-thread calls but every already-queued invocation is still
delivered.
- **The JIT is required.** With the JIT disabled, `dlopen()` and friends
throw a `TypeError`.

## What is removed

- The TinyCC compile path for
`dlopen`/`linkSymbols`/`CFunction`/`JSCallback` symbols, `viewSource` of
callbacks (there is no generated C to show), and the per-symbol wrapper
objects — every symbol is the engine function itself.
- `CString`'s object surface (see above) and `toArrayBuffer`-backed
`arrayBuffer` on it.

## Testing

- `test/js/bun/ffi/` builds its C fixture with the host compiler at test
time, so the suite runs on every CI platform: **203 tests, 0 fail**.
Includes an ABI conformance suite whose fixture returns
position-weighted combinations of its arguments, so any
calling-convention error changes the observable result — verified to
detect a deliberately mis-declared signature.
- Source lints, the napi FFI file, and the ffi bench all green; opentui
audited as unaffected by the `CString` change (it uses neither `CString`
nor `cstring` returns).

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants