bun:ffi is experimental, with known bugs and limitations, and should not be relied on in production. The most stable way to interact with native code from Bun is to write a Node-API (NAPI) module.
Bun’s built-in bun:ffi module lets you call native shared libraries directly from JavaScript. It supports any language that exposes a C ABI, including C, C++, Rust, Zig, C#, Nim, and Kotlin.
How it works
When you call dlopen, Bun generates and JIT-compiles C bindings that efficiently convert values between JavaScript types and native types. To compile these bindings, Bun embeds TinyCC, a small and fast C compiler. This approach makes bun:ffi roughly 2–6x faster than Node.js FFI via Node-API.
Basic usage
Use dlopen to load a shared library and declare the symbols you want to call.
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` resolves to the platform-appropriate extension:
// "dylib" on macOS, "so" on Linux, "dll" on Windows
const { symbols } = dlopen(`libmylib.${suffix}`, {
add: {
args: [FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
});
console.log(symbols.add(1, 2)); // 3
Calling SQLite
A complete example: print the SQLite version number.
import { dlopen, FFIType, suffix } from "bun:ffi";
const {
symbols: { sqlite3_libversion },
} = dlopen(`libsqlite3.${suffix}`, {
sqlite3_libversion: {
args: [],
returns: FFIType.cstring,
},
});
console.log(`SQLite version: ${sqlite3_libversion()}`);
Compiling native libraries
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}
zig build-lib add.zig -dynamic -OReleaseFast
import { dlopen, FFIType, suffix } from "bun:ffi";
const lib = dlopen(`libadd.${suffix}`, {
add: {
args: [FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
});
console.log(lib.symbols.add(1, 2)); // 3
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
rustc --crate-type cdylib add.rs
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}
zig build-lib add.cpp -dynamic -lc -lc++
FFI types
Use FFIType to declare the native type for each argument and return value.
FFIType | C type | Aliases |
|---|
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 | |
ptr | void* | pointer, void*, char* |
cstring | char* | |
buffer | char* | (must be TypedArray or DataView) |
function | (void*)(*)() | fn, callback |
napi_env | napi_env | |
napi_value | napi_value | |
Strings
JavaScript strings are UTF-16. C strings are null-terminated UTF-8. Bun provides CString to bridge the two.
CString extends JavaScript’s built-in String and automatically reads until the null terminator, transcoding from UTF-8 to UTF-16 as needed.
import { CString } from "bun:ffi";
// Convert a null-terminated pointer to a JavaScript string
const str = new CString(ptr);
// Or specify a byte offset and length
const str2 = new CString(ptr, 0, byteLength);
CString clones the string content, so it’s safe to use after the original pointer has been freed.
my_library_free(str.ptr);
// Safe — the string content was cloned on construction
console.log(str);
When FFIType.cstring is used as a return type, Bun automatically coerces the pointer to a JavaScript string. When used as an argument type, it behaves like ptr.
Pointers
Bun represents native pointers as JavaScript number values.
64-bit processors support up to 52 bits of addressable space. JavaScript numbers support 53 bits of usable space, so all valid pointer addresses fit in a JavaScript number without precision loss.
Getting a pointer to a TypedArray
import { ptr } from "bun:ffi";
const buffer = new Uint8Array(32);
const myPtr = ptr(buffer);
Converting a pointer to an ArrayBuffer
import { toArrayBuffer } from "bun:ffi";
// byteLength must be provided, or the pointer is treated as null-terminated
const arrayBuffer = toArrayBuffer(myPtr, 0, 32);
const view = new Uint8Array(arrayBuffer);
Reading from a pointer
For long-lived pointers, use DataView:
import { toArrayBuffer } from "bun:ffi";
const view = new DataView(toArrayBuffer(myPtr, 0, 32));
console.log(view.getUint8(0), view.getUint8(1));
For short-lived reads, use the read helper — it’s faster because it skips ArrayBuffer allocation:
import { read } from "bun:ffi";
console.log(read.u8(myPtr, 0));
console.log(read.u32(myPtr, 4));
console.log(read.f64(myPtr, 8));
read functions by type:
| Type | Function |
|---|
ptr | read.ptr |
i8 | read.i8 |
i16 | read.i16 |
i32 | read.i32 |
i64 | read.i64 |
u8 | read.u8 |
u16 | read.u16 |
u32 | read.u32 |
u64 | read.u64 |
f32 | read.f32 |
f64 | read.f64 |
Passing a pointer as an argument
Where a native function expects a pointer, pass a TypedArray directly. Bun converts it automatically.
import { dlopen, FFIType } from "bun:ffi";
const { symbols: { encode_png } } = dlopen(myLibraryPath, {
encode_png: {
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
const out = encode_png(pixels, 128, 128);
Memory management
bun:ffi does not manage memory. You are responsible for freeing any memory allocated by native code.
To track when a TypedArray is garbage collected from JavaScript, use a FinalizationRegistry.
To receive a callback when a TypedArray is freed from native code, pass a deallocator pointer to toArrayBuffer:
import { toArrayBuffer } from "bun:ffi";
toArrayBuffer(
bytes,
byteOffset,
byteLength,
deallocatorContext, // optional context pointer
jsTypedArrayBytesDeallocator, // pointer to cleanup callback
);
The expected signature matches JavaScriptCore’s JSTypedArrayBytesDeallocator:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
Callbacks
Use JSCallback to pass a JavaScript function to native code. The native library can then call back into JavaScript.
Async functions are not yet supported as JSCallback targets.
import { dlopen, JSCallback, ptr, CString } from "bun:ffi";
const { symbols: { search }, close } = dlopen("libmylib", {
search: {
returns: "usize",
args: ["cstring", "callback"],
},
});
const searchIterator = new JSCallback(
(ptr, length) => /hello/.test(new CString(ptr, length)),
{
returns: "bool",
args: ["ptr", "usize"],
},
);
const str = Buffer.from("find hello here\0", "utf8");
if (search(ptr(str), searchIterator)) {
console.log("found a match");
}
// Clean up when done
setTimeout(() => {
searchIterator.close();
close();
}, 5000);
Always call close() on a JSCallback when you’re done with it to free memory.
Thread-safe callbacks
JSCallback has experimental support for thread-safe callbacks. Enable it with the threadsafe option:
const cb = new JSCallback(
(ptr, length) => /hello/.test(new CString(ptr, length)),
{
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true,
},
);
Thread-safe callbacks currently work best when called from a thread that is already running JavaScript, such as a Worker. Future versions of Bun will support calling them from any thread, including threads spawned by native libraries.
For a slight performance boost, pass JSCallback.prototype.ptr directly instead of the JSCallback object itself:setOnResolve(onResolve.ptr); // faster
setOnResolve(onResolve); // slightly slower
Function pointers
Use CFunction to call a function pointer you already have a reference to — for example, one obtained from a Node-API module.
import { CFunction } from "bun:ffi";
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
console.log(getVersion());
To define multiple function pointers at once, use linkSymbols:
import { linkSymbols } from "bun:ffi";
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
getMajor: { returns: "cstring", args: [], ptr: majorPtr },
getMinor: { returns: "cstring", args: [], ptr: minorPtr },
getPatch: { returns: "cstring", args: [], ptr: patchPtr },
});
const version = `${lib.symbols.getMajor()}.${lib.symbols.getMinor()}.${lib.symbols.getPatch()}`;
The suffix export resolves to the correct shared library extension for the current platform.
| Platform | suffix |
|---|
| macOS | dylib |
| Linux | so |
| Windows | dll |
import { suffix } from "bun:ffi";
const lib = dlopen(`libcrypto.${suffix}`, { /* ... */ });
On Windows, the HANDLE type does not represent a virtual address. Do not use ptr for Windows HANDLE values — use u64 instead.