skills/bun-runtime-ffi/SKILL.md
Use Bun's FFI module to efficiently call native libraries from JavaScript
npx skillsauth add jarle/bun-skills Bun FFIInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
<Warning> `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 module](/runtime/node-api). </Warning>Use Bun's FFI module to efficiently call native libraries from JavaScript
Use the built-in bun:ffi module to efficiently call native libraries from JavaScript. It works with languages that support the C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).
bun:ffi)To print the version number of sqlite3:
import { dlopen, FFIType, suffix } from "bun:ffi";
// `suffix` is either "dylib", "so", or "dll" depending on the platform
// you don't have to use "suffix", it's just there for convenience
const path = `libsqlite3.${suffix}`;
const {
symbols: {
sqlite3_libversion, // the function to call
},
} = dlopen(
path, // a library name or file path
{
sqlite3_libversion: {
// no arguments, returns a string
args: [],
returns: FFIType.cstring,
},
},
);
console.log(`SQLite 3 version: ${sqlite3_libversion()}`);
According to our benchmark, bun:ffi is roughly 2-6x faster than Node.js FFI via Node-API.
Bun generates & just-in-time compiles C bindings that efficiently convert values between JavaScript types and native types. To compile C, Bun embeds TinyCC, a small and fast C compiler.
pub export fn add(a: i32, b: i32) i32 {
return a + b;
}
To compile:
zig build-lib add.zig -dynamic -OReleaseFast
Pass a path to the shared library and a map of symbols to import into dlopen:
import { dlopen, FFIType, suffix } from "bun:ffi";
const { i32 } = FFIType;
const path = `libadd.${suffix}`;
const lib = dlopen(path, {
add: {
args: [i32, i32],
returns: i32,
},
});
console.log(lib.symbols.add(1, 2));
// add.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
To compile:
rustc --crate-type cdylib add.rs
#include <cstdint>
extern "C" int32_t add(int32_t a, int32_t b) {
return a + b;
}
To compile:
zig build-lib add.cpp -dynamic -lc -lc++
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 | |
Note: buffer arguments must be a TypedArray or DataView.
JavaScript strings and C-like strings are different, and that complicates using strings with native libraries.
<Accordion title="How are JavaScript strings and C strings different?"> JavaScript strings:length stored separatelyC strings:
\0 it findsTo solve this, bun:ffi exports CString which extends JavaScript's built-in String to support null-terminated strings and add a few extras:
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;
}
To convert from a null-terminated string pointer to a JavaScript string:
const myString = new CString(ptr);
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.
my_library_free(myString.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.
<Note>Async functions are not yet supported</Note>
To call a function pointer from JavaScript, use CFunction. This is useful if using Node-API (napi) with Bun, and you've already loaded some symbols.
import { CFunction } from "bun:ffi";
let myNativeLibraryGetVersion = /* somehow, you got this pointer */
const getVersion = new CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
});
getVersion();
If you have multiple function pointers, you can define them all at once with linkSymbols:
import { linkSymbols } from "bun:ffi";
// getVersionPtrs defined elsewhere
const [majorPtr, minorPtr, patchPtr] = getVersionPtrs();
const lib = linkSymbols({
// Unlike with dlopen(), the names here can be whatever you want
getMajor: {
returns: "cstring",
args: [],
// Since this doesn't use dlsym(), you have to provide a valid ptr
// That ptr could be a number or a bigint
// An invalid pointer will crash your program.
ptr: majorPtr,
},
getMinor: {
returns: "cstring",
args: [],
ptr: minorPtr,
},
getPatch: {
returns: "cstring",
args: [],
ptr: patchPtr,
},
});
const [major, minor, patch] = [lib.symbols.getMajor(), lib.symbols.getMinor(), lib.symbols.getPatch()];
Use JSCallback to create JavaScript callback functions that can be passed to C/FFI functions. The C/FFI function can call into the JavaScript/TypeScript code. This is useful for asynchronous code or whenever you want to call into JavaScript code from C.
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("wwutwutwutwutwutwutwutwutwutwutut\0", "utf8");
if (search(ptr(str), searchIterator)) {
// found a match!
}
// Sometime later:
setTimeout(() => {
searchIterator.close();
close();
}, 5000);
When you're done with a JSCallback, you should call close() to free the memory.
JSCallback has experimental support for thread-safe callbacks. This will be needed if you pass a callback function into a different thread from its instantiation context. You can enable it with the optional threadsafe parameter.
Currently, thread-safe callbacks work best when run from another thread that is running JavaScript code, i.e. a Worker. 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).
const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), {
returns: "bool",
args: ["ptr", "usize"],
threadsafe: true, // Optional. Defaults to `false`
});
<Note>
**⚡️ Performance tip** — For a slight performance boost, directly pass `JSCallback.prototype.ptr` instead of the `JSCallback` object:
const onResolve = new JSCallback(arg => arg === 42, {
returns: "bool",
args: ["i32"],
});
const setOnResolve = new CFunction({
returns: "bool",
args: ["function"],
ptr: myNativeLibrarySetOnResolve,
});
// This code runs slightly faster:
setOnResolve(onResolve.ptr);
// Compared to this:
setOnResolve(onResolve);
</Note>
Bun represents pointers as a number in JavaScript.
Why not BigInt? BigInt is slower. JavaScript engines allocate a separate BigInt which means they can't fit into a regular JavaScript value. If you pass a BigInt to a function, it will be converted to a number
Windows Note: The Windows API type HANDLE does not represent a virtual address, and using ptr for it will not work as expected. Use u64 to safely represent HANDLE values.
</Accordion>
To convert from a TypedArray to a pointer:
import { ptr } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
To convert from a pointer to an ArrayBuffer:
import { ptr, toArrayBuffer } from "bun:ffi";
let myTypedArray = new Uint8Array(32);
const myPtr = ptr(myTypedArray);
// toArrayBuffer accepts a `byteOffset` and `byteLength`
// if `byteLength` is not provided, it is assumed to be a null-terminated pointer
myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);
To read data from a pointer, you have two options. For long-lived pointers, use a DataView:
import { toArrayBuffer } from "bun:ffi";
let myDataView = new DataView(toArrayBuffer(myPtr, 0, 32));
console.log(
myDataView.getUint8(0, true),
myDataView.getUint8(1, true),
myDataView.getUint8(2, true),
myDataView.getUint8(3, true),
);
For short-lived pointers, use read:
import { read } from "bun:ffi";
console.log(
// ptr, byteOffset
read.u8(myPtr, 0),
read.u8(myPtr, 1),
read.u8(myPtr, 2),
read.u8(myPtr, 3),
);
The read function behaves similarly to DataView, but it's usually faster because it doesn't need to create a DataView or ArrayBuffer.
| FFIType | read 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 |
bun:ffi does not manage memory for you. You must free the memory when you're done with it.
If you want to track when a TypedArray is no longer in use from JavaScript, you can use a FinalizationRegistry.
If you want to track when a TypedArray is no longer in use from C or FFI, you can pass a callback and an optional context pointer to toArrayBuffer or toBuffer. This function is called at some point later, once the garbage collector frees the underlying ArrayBuffer JavaScript object.
The expected signature is the same as in JavaScriptCore's C API:
typedef void (*JSTypedArrayBytesDeallocator)(void *bytes, void *deallocatorContext);
import { toArrayBuffer } from "bun:ffi";
// with a deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// this is an optional pointer to a callback
deallocatorContext,
// this is a pointer to a function
jsTypedArrayBytesDeallocator,
);
// without a deallocatorContext:
toArrayBuffer(
bytes,
byteOffset,
byteLength,
// this is a pointer to a function
jsTypedArrayBytesDeallocator,
);
Using raw pointers outside of FFI is extremely not recommended. A future version of Bun may add a CLI flag to disable bun:ffi.
If an API expects a pointer sized to something other than char or u8, make sure the TypedArray is also that size. A u64* is not exactly the same as [8]u8* due to alignment.
Where FFI functions expect a pointer, pass a TypedArray of equivalent size:
import { dlopen, FFIType } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType's can be specified as strings too
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
pixels.subarray(0, 32 * 32 * 2).fill(0);
const out = encode_png(
// pixels will be passed as a pointer
pixels,
128,
128,
);
The auto-generated wrapper converts the pointer to a TypedArray.
import { dlopen, FFIType, ptr } from "bun:ffi";
const {
symbols: { encode_png },
} = dlopen(myLibraryPath, {
encode_png: {
// FFIType's can be specified as strings too
args: ["ptr", "u32", "u32"],
returns: FFIType.ptr,
},
});
const pixels = new Uint8ClampedArray(128 * 128 * 4);
pixels.fill(254);
// this returns a number! not a BigInt!
const myPtr = ptr(pixels);
const out = encode_png(
myPtr,
// dimensions:
128,
128,
);
</Accordion>
const out = encode_png(
// pixels will be passed as a pointer
pixels,
// dimensions:
128,
128,
);
// assuming it is 0-terminated, it can be read like this:
let png = new Uint8Array(toArrayBuffer(out));
// save it to disk:
await Bun.write("out.png", png);
development
Using TypeScript with Bun, including type definitions and compiler options
development
Learn how to write tests using Bun's Jest-compatible API with support for async tests, timeouts, and various test modifiers
testing
Learn how to use snapshot testing in Bun to save and compare output between test runs
testing
Learn about Bun test's runtime integration, environment variables, timeouts, and error handling