Skip to main content
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

add.zig
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

FFI types

Use FFIType to declare the native type for each argument and return value.
FFITypeC typeAliases
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t, int
i64int64_tint64_t
i64_fastint64_t
u8uint8_tuint8_t
u16uint16_tuint16_t
u32uint32_tuint32_t
u64uint64_tuint64_t
u64_fastuint64_t
f32floatfloat
f64doubledouble
boolbool
charchar
ptrvoid*pointer, void*, char*
cstringchar*
bufferchar*(must be TypedArray or DataView)
function(void*)(*)()fn, callback
napi_envnapi_env
napi_valuenapi_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:
TypeFunction
ptrread.ptr
i8read.i8
i16read.i16
i32read.i32
i64read.i64
u8read.u8
u16read.u16
u32read.u32
u64read.u64
f32read.f32
f64read.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()}`;

Platform suffixes

The suffix export resolves to the correct shared library extension for the current platform.
Platformsuffix
macOSdylib
Linuxso
Windowsdll
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.

Build docs developers (and LLMs) love