Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1b0955b
bun:ffi: engine-native FFI in JavaScriptCore
Jarred-Sumner Jul 28, 2026
22d2b2d
bench/ffi: revert incidental bun.lock churn
Jarred-Sumner Jul 28, 2026
8393145
bun:ffi review: fix threadsafe-invocation leak on worker teardown, cl…
Jarred-Sumner Jul 28, 2026
579ed2c
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
69e73e1
bun:ffi: negative byteOffset must not abort; drop duplicate declaration
Jarred-Sumner Jul 28, 2026
7ca4026
bun:ffi: correct threadsafe-invocation ref accounting; precise test e…
Jarred-Sumner Jul 28, 2026
f6ef554
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
c59e943
bun:ffi: check the zero address unconditionally; use one-arg postTaskTo
Jarred-Sumner Jul 28, 2026
fd512d1
test(ffi): cover the engine-native cstring path; fold FFI cases into …
Jarred-Sumner Jul 28, 2026
f534f7d
webkit: repin to autobuild-preview-pr-319-a554d317
Jarred-Sumner Jul 28, 2026
155a023
bun:ffi: fix remaining negative-offset aborts; make the N-API include…
Jarred-Sumner Jul 28, 2026
da6956b
jsc-stress: run the engine FFI stress corpus and the shipped testFFI
Jarred-Sumner Jul 28, 2026
5c61940
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
5731fed
testFFI.test.ts: download the standalone testFFI-<platform> asset
Jarred-Sumner Jul 28, 2026
c10253b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
b6aaa65
Ship testFFI to the test lanes inside the profile zip
Jarred-Sumner Jul 28, 2026
4d6c350
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
2a952d2
webkit: repin to autobuild-preview-pr-319-131d8a8e
Jarred-Sumner Jul 28, 2026
73799ad
jsc-stress preload: map noDFG to jsc.noFTL, not noInline
Jarred-Sumner Jul 28, 2026
5b7c273
test(ffi): worker teardown with queued threadsafe invocations
Jarred-Sumner Jul 28, 2026
a520d58
ci: forward the WebKit testFFI to the test lanes
Jarred-Sumner Jul 28, 2026
0098afe
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
2bfb578
ci: set testFFI executable bit at package time
Jarred-Sumner Jul 28, 2026
3da49bf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 28, 2026
8c7aa95
webkit: pin to the merged engine-native FFI on main
Jarred-Sumner Jul 28, 2026
c7a440b
testFFI.test.ts: disable LSan leak detection for the spawn
Jarred-Sumner Jul 28, 2026
b130272
webkit: repin to the testFFI NaN-comparison fix (oven-sh/WebKit#367)
Jarred-Sumner Jul 28, 2026
ad25e4b
webkit: pin to oven-sh/WebKit main (6886cd28f89d)
Jarred-Sumner Jul 28, 2026
df4e6dd
Stop tracking declared/lexical module variables; JSC derives them now
Jarred-Sumner Jul 28, 2026
f1b65f9
Adapt to two JSC API signature changes in the upgrade
Jarred-Sumner Jul 28, 2026
13895bc
Drop leftover empty conditional and stale ModuleInfo comments
Jarred-Sumner Jul 28, 2026
1328d36
webkit: pin to main 34c01d13391e (Intl default-timezone fix)
Jarred-Sumner Jul 29, 2026
232e925
Delete the DeclCollector pipeline and TopLevel threading made dead by…
Jarred-Sumner Jul 29, 2026
787c740
ci: only pack testFFI into the profile zip when it exists
Jarred-Sumner Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bench/ffi/bun.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,31 @@ const {
ffi_noop: { native: ffi_noop },
ffi_hash: { native: ffi_hash },
ffi_string: { native: ffi_string },
ffi_strlen: { native: ffi_strlen },
},
} = dlopen(import.meta.dir + "/src/ffi_napi_bench.node", {
ffi_noop: { args: [], returns: "void" },
ffi_string: { args: [], returns: "ptr" },
ffi_hash: { args: ["ptr", "u32"], returns: "u32" },
ffi_strlen: { args: ["cstring"], returns: "u32" },
});

const bytes = new Uint8Array(64);
const str36 = "550e8400-e29b-41d4-a716-446655440000";
const strBuf = Buffer.from(str36 + "\0", "utf8");
const strPtr = ptr(strBuf);
const cachedCString = new CString(strPtr);

group("bun:ffi", () => {
bench("noop", () => ffi_noop());
bench("hash", () => ffi_hash(ptr(bytes), bytes.byteLength));

bench("c string", () => new CString(ffi_string()));

bench("string arg: JS string", () => ffi_strlen(str36));
bench("string arg: cached CString", () => ffi_strlen(cachedCString));
bench("string arg: raw pointer", () => ffi_strlen(strPtr));
bench("string arg: TypedArray", () => ffi_strlen(strBuf));
});

if (process.env.SHOW_NAPI)
Expand Down
13 changes: 7 additions & 6 deletions bench/ffi/src/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ fn hash(buf: &[u8]) -> u32 {
return hash;
}



#[cfg(feature="enable-napi")]
#[napi] pub fn napi_noop() {
// do nothing
Expand All @@ -26,19 +24,22 @@ fn hash(buf: &[u8]) -> u32 {
// do nothing
}



#[cfg(feature="enable-napi")]
#[napi] pub fn napi_string() -> &'static str {
return &STRING[0..(STRING.len() - 1)];
}

#[no_mangle] unsafe extern "C" fn ffi_strlen(p: *const u8) -> u32 {
if p.is_null() { return 0; }
let mut n = 0u32;
while *p.add(n as usize) != 0 { n += 1; }
n
}

#[no_mangle] unsafe extern "C" fn ffi_string() -> *const u8 {
return STRING.as_ptr();
}



#[cfg(feature="enable-napi")]
#[napi] pub fn napi_hash(buffer: Buffer) -> u32 {
return hash(&buffer);
Expand Down
114 changes: 69 additions & 45 deletions docs/runtime/ffi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ According to [our benchmark](https://github.com/oven-sh/bun/tree/main/bench/ffi)

<Image src="/images/ffi.png" height="400" />

Bun generates and just-in-time compiles C bindings that efficiently convert values between JavaScript types and native types. To compile C, Bun embeds [TinyCC](https://github.com/TinyCC/tinycc), a small and fast C compiler.
`dlopen`, `linkSymbols`, `CFunction`, and `JSCallback` are implemented natively by Bun's JavaScript engine (JavaScriptCore): argument conversion, arity handling, and result boxing happen in-engine, and hot call sites compile down through the DFG/FTL JIT tiers into direct native calls with no per-argument JavaScript shim. [TinyCC](https://github.com/TinyCC/tinycc), a small and fast C compiler, is embedded only for [`cc()`](/runtime/c-compiler), which compiles C source you provide at runtime.
Comment thread
Jarred-Sumner marked this conversation as resolved.

---

Expand Down Expand Up @@ -129,31 +129,57 @@ clang++ -dynamiclib add.cpp -o libadd.dylib

The following `FFIType` values are supported.

| `FFIType` | C Type | Aliases |
| ---------- | -------------- | --------------------------- |
| buffer | `char*` | |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| napi_env | `napi_env` | |
| napi_value | `napi_value` | |
| `FFIType` | C Type | Aliases |
| ------------- | --------------------- | ------------------------------- |
| buffer | `char*` | |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| napi_env | `napi_env` | `cc()` only |
| napi_value | `napi_value` | `cc()` only |
| buffer_length | `uint64_t` / `size_t` | engine-native only (not `cc()`) |

`buffer` arguments must be a `TypedArray` or `DataView`.

`buffer_length` is `buffer`'s length twin: pass the **same** `TypedArray`/`DataView` you passed
for the `buffer` parameter, and the callee receives that view's **byte length** as an unsigned
64-bit integer. The engine reads the pointer and the length off the same object at the moment of
the call, so the two always agree — an atomic snapshot you can't get by passing
`view.byteLength` yourself (a length read in JavaScript beforehand can go stale against a
resizable, growable, or transferred buffer). It's argument-only and, like the napi types, not
available inside `cc()`.

```ts
const {
symbols: { write_all },
} = dlopen(path, {
// C: size_t write_all(int fd, const void *buf, size_t len)
write_all: { args: ["i32", "buffer", "buffer_length"], returns: "u64" },
});
const chunk = new TextEncoder().encode("hello");
write_all(1, chunk, chunk); // buf and len both come from `chunk`
```

`napi_env` and `napi_value` are only valid in [`cc()`](/runtime/c-compiler) source, where a
`napi_env` parameter is filled in with the module's environment by the compiled trampoline (the
JavaScript argument passed at that position is a placeholder and is ignored) and `napi_value`
passes the JavaScript value through unchanged. Using either type in a `dlopen`, `linkSymbols`, `JSCallback`,
or `CFunction` descriptor throws a `TypeError`.
Comment thread
claude[bot] marked this conversation as resolved.

Comment thread
Jarred-Sumner marked this conversation as resolved.
---

## Strings
Expand All @@ -175,26 +201,10 @@ C strings:

</Accordion>

To solve this, `bun:ffi` exports `CString` which extends JavaScript's built-in `String` to support null-terminated strings and add a few extras:
To solve this, `bun:ffi` exports `CString`, which reads a UTF-8 C string at a pointer and returns a plain JavaScript string:

```ts
class CString extends String {
/**
* Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary.
*/
constructor(ptr: number, byteOffset?: number, byteLength?: number): string;

/**
* The ptr to the C string
*
* This `CString` instance is a clone of the string, so it
* is safe to continue using this instance after the `ptr` has been
* freed.
*/
ptr: number;
byteOffset?: number;
byteLength?: number;
}
CString(ptr: number, byteOffset?: number, byteLength?: number): string;
```

To convert from a null-terminated string pointer to a JavaScript string:
Expand All @@ -209,16 +219,30 @@ To convert from a pointer with a known length to a JavaScript string:
const myString = new CString(ptr, 0, byteLength);
```

The `new CString()` constructor clones the C string, so it is safe to continue using `myString` after `ptr` has been freed.
`new CString()` returns a normal string (`typeof myString === "string"`, `myString === "hello"` works) that is a clone of the C string, so it is safe to continue using it after `ptr` has been freed.

```ts
my_library_free(myString.ptr);
const myString = new CString(ptr);
my_library_free(ptr);

// this is safe because myString is a clone
console.log(myString);
```

When used in `returns`, `FFIType.cstring` coerces the pointer to a JavaScript `string`. When used in `args`, `FFIType.cstring` is identical to `ptr`.
When used in `returns`, `FFIType.cstring` coerces the pointer to a JavaScript `string`. When used in `args`, `FFIType.cstring` accepts everything `ptr` does **and** additionally accepts a JavaScript string directly — the engine transcodes it to a null-terminated UTF-8 buffer that lives for the duration of the call, so you don't need to encode it into a `Buffer` yourself:

```ts
symbols.puts("Hello, world!"); // args: ["cstring"] — pass the string directly
```
Comment thread
Jarred-Sumner marked this conversation as resolved.

**Lifetime of a `cstring` return.** The pointer is whatever the C function returned — memory
owned by the native side (a static, a buffer it manages, or heap it allocated); the engine copies
nothing on return, and the JavaScript string is cloned out of it. The one aliasing case is a C
function that hands back a pointer _derived from a `cstring` argument you passed as a JavaScript
string_: that argument was transcoded into the engine's call-scoped buffer, so treat such a
returned pointer as valid only until your next FFI call reuses that buffer (the usual C rule for
functions that return their input). Clone it (via the returned string, or `new CString`) rather
than holding the raw address.

---

Expand Down Expand Up @@ -317,7 +341,7 @@ When you're done with a `JSCallback`, call `close()` to free the memory.

`JSCallback` has experimental support for thread-safe callbacks. You need this if you pass a callback function into a different thread from the one that created it. Enable it with the optional `threadsafe` parameter.

Thread-safe callbacks work best when run from another thread that is running JavaScript code, that is, a [`Worker`](/runtime/workers). A future version of Bun will enable them to be called from any thread, such as new threads spawned by your native library that Bun is not aware of.
Thread-safe callbacks can be invoked from **any thread** — including threads spawned by your native library that Bun is not otherwise aware of. The engine copies the C arguments on the calling thread and marshals the invocation onto the JavaScript thread, where the arguments are converted (64-bit integers and pointers arrive as exact BigInts) and your function runs. Because the invocation is asynchronous from C's point of view, the value returned to the C caller is unspecified: you may declare a non-`void` `returns` (the example below uses `"bool"`), but the C side must treat a thread-safe callback as returning `void` and ignore its return value.

```ts
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
Expand Down
Loading
Loading