Basic Usage
import { dlopen, FFIType, suffix } from "bun:ffi";
// Open a dynamic library
const lib = dlopen(
`libmylib.${suffix}`, // .dylib, .so, or .dll
{
// Define function signatures
add: {
args: [FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
get_name: {
args: [],
returns: FFIType.cstring,
},
},
);
// Call the functions
const result = lib.symbols.add(5, 3);
console.log(result); // 8
const name = lib.symbols.get_name();
console.log(name); // "MyLibrary"
Opening Libraries
Platform-Specific Extensions
import { suffix } from "bun:ffi";
// suffix is "dylib" on macOS, "so" on Linux, "dll" on Windows
const path = `./native/lib.${suffix}`;
Absolute and Relative Paths
import { dlopen } from "bun:ffi";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const path = join(__dirname, "native", `lib.${suffix}`);
const lib = dlopen(path, {});
System Libraries
// On macOS and Linux, dlopen searches system paths
const lib = dlopen("libsqlite3.so", {
sqlite3_libversion: {
args: [],
returns: FFIType.cstring,
},
});
FFI Types
Primitive Types
import { FFIType } from "bun:ffi";
// Integers
FFIType.i8 // int8_t
FFIType.i16 // int16_t
FFIType.i32 // int32_t
FFIType.i64 // int64_t (as BigInt)
FFIType.i64_fast // int64_t (as number, loses precision)
// Unsigned integers
FFIType.u8 // uint8_t
FFIType.u16 // uint16_t
FFIType.u32 // uint32_t
FFIType.u64 // uint64_t (as BigInt)
FFIType.u64_fast // uint64_t (as number)
// Floating point
FFIType.f32 // float
FFIType.f64 // double
// Other
FFIType.bool // bool (_Bool)
FFIType.char // char
FFIType.ptr // void*
FFIType.void // void (return type only)
Strings
// C-style null-terminated string
FFIType.cstring // const char*
// Example
const lib = dlopen("lib.so", {
greet: {
args: [FFIType.cstring],
returns: FFIType.void,
},
});
lib.symbols.greet("Hello from JavaScript!");
Pointers
// Generic pointer
FFIType.ptr // void*
// Example: function that takes a buffer
const lib = dlopen("lib.so", {
process_data: {
args: [FFIType.ptr, FFIType.i32], // data pointer, length
returns: FFIType.i32,
},
});
const buffer = new Uint8Array([1, 2, 3, 4]);
lib.symbols.process_data(buffer, buffer.length);
Function Pointers
import { CFunction } from "bun:ffi";
// Create a callback that C can call
const callback = new CFunction(
{
args: [FFIType.i32],
returns: FFIType.i32,
},
(value) => {
console.log("C called JavaScript with:", value);
return value * 2;
},
);
const lib = dlopen("lib.so", {
register_callback: {
args: [FFIType.ptr],
returns: FFIType.void,
},
});
lib.symbols.register_callback(callback.ptr);
// Important: Keep callback in scope!
// It will be garbage collected if dereferenced
Working with Pointers
Reading Memory
import { ptr, read } from "bun:ffi";
// Read from a pointer
const value = read.i32(somePointer, offset);
const str = read.cstring(stringPointer);
const buf = read.u8(bufferPointer, length);
Writing Memory
import { ptr, write } from "bun:ffi";
// Allocate memory
const buffer = new ArrayBuffer(1024);
// Write to memory
write.i32(buffer, 42, offset);
write.f64(buffer, 3.14, offset);
Pointer Casting
import { ptr } from "bun:ffi";
// Convert TypedArray to pointer
const data = new Uint8Array(100);
const dataPtr = ptr(data);
// Pass to native function
lib.symbols.process(dataPtr, data.length);
C Compiler (TinyCC)
Bun includes TinyCC for compiling C code at runtime:import { cc } from "bun:ffi";
const lib = cc({
source: `
int add(int a, int b) {
return a + b;
}
const char* get_version() {
return "1.0.0";
}
`,
symbols: {
add: {
args: [FFIType.i32, FFIType.i32],
returns: FFIType.i32,
},
get_version: {
args: [],
returns: FFIType.cstring,
},
},
});
console.log(lib.symbols.add(5, 3)); // 8
console.log(lib.symbols.get_version()); // "1.0.0"
Including Headers
const lib = cc({
source: `
#include <stdio.h>
#include <math.h>
double calculate(double x) {
printf("Calculating...\\n");
return sqrt(x);
}
`,
symbols: {
calculate: {
args: [FFIType.f64],
returns: FFIType.f64,
},
},
});
Linking Libraries
const lib = cc({
source: `
#include <sqlite3.h>
const char* get_sqlite_version() {
return sqlite3_libversion();
}
`,
symbols: {
get_sqlite_version: {
args: [],
returns: FFIType.cstring,
},
},
library: ["sqlite3"], // Link against libsqlite3
});
Multiple Source Files
const lib = cc({
source: [
"helpers.c",
"main.c",
],
symbols: { /* ... */ },
});
Performance Optimization
Use Native Types
Prefer native sizes for best performance:// Slower - requires conversion
args: [FFIType.i64]
// Faster - native size
args: [FFIType.i32]
Batch Operations
Minimize FFI calls by batching:// Slow - many FFI calls
for (let i = 0; i < 1000; i++) {
lib.symbols.process_item(i);
}
// Fast - single FFI call
const items = new Int32Array(1000);
for (let i = 0; i < 1000; i++) items[i] = i;
lib.symbols.process_batch(items, 1000);
Reuse Buffers
// Allocate once
const buffer = new Uint8Array(1024);
// Reuse for multiple calls
for (const item of items) {
buffer[0] = item;
lib.symbols.process(buffer);
}
Real-World Examples
SQLite
import { dlopen, FFIType, suffix } from "bun:ffi";
const sqlite = dlopen(`libsqlite3.${suffix}`, {
sqlite3_open: {
args: [FFIType.cstring, FFIType.ptr],
returns: FFIType.i32,
},
sqlite3_close: {
args: [FFIType.ptr],
returns: FFIType.i32,
},
sqlite3_exec: {
args: [
FFIType.ptr,
FFIType.cstring,
FFIType.ptr,
FFIType.ptr,
FFIType.ptr,
],
returns: FFIType.i32,
},
});
// Open database
const db = new BigUint64Array(1);
sqlite.symbols.sqlite3_open(":memory:", ptr(db));
// Execute SQL
const sql = "CREATE TABLE users (id INTEGER, name TEXT)";
sqlite.symbols.sqlite3_exec(db[0], sql, null, null, null);
// Close
sqlite.symbols.sqlite3_close(db[0]);
Image Processing (stb_image)
import { cc, FFIType, ptr } from "bun:ffi";
const stb = cc({
source: `
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
unsigned char* load_image(const char* path, int* w, int* h, int* c) {
return stbi_load(path, w, h, c, 0);
}
void free_image(unsigned char* data) {
stbi_image_free(data);
}
`,
symbols: {
load_image: {
args: [FFIType.cstring, FFIType.ptr, FFIType.ptr, FFIType.ptr],
returns: FFIType.ptr,
},
free_image: {
args: [FFIType.ptr],
returns: FFIType.void,
},
},
include: ["./vendor/stb"],
});
const width = new Int32Array(1);
const height = new Int32Array(1);
const channels = new Int32Array(1);
const data = stb.symbols.load_image(
"image.png",
ptr(width),
ptr(height),
ptr(channels),
);
console.log(`Image: ${width[0]}x${height[0]}, ${channels[0]} channels`);
stb.symbols.free_image(data);
Rust Interop
// lib.rs
#[no_mangle]
pub extern "C" fn fibonacci(n: i32) -> i64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[no_mangle]
pub extern "C" fn process_array(data: *const i32, len: usize) -> i32 {
let slice = unsafe { std::slice::from_raw_parts(data, len) };
slice.iter().sum()
}
// Build: cargo build --release
import { dlopen, FFIType } from "bun:ffi";
const lib = dlopen("./target/release/libmylib.dylib", {
fibonacci: {
args: [FFIType.i32],
returns: FFIType.i64,
},
process_array: {
args: [FFIType.ptr, FFIType.usize],
returns: FFIType.i32,
},
});
console.log(lib.symbols.fibonacci(10)); // 55
const arr = new Int32Array([1, 2, 3, 4, 5]);
console.log(lib.symbols.process_array(arr, 5)); // 15
Memory Management
Manual Allocation
import { ptr } from "bun:ffi";
// JavaScript owns this memory
const buffer = new Uint8Array(1024);
// Pass to C (C should not free this)
lib.symbols.process(ptr(buffer), buffer.length);
C Allocation
// C allocates memory
const dataPtr = lib.symbols.allocate_buffer(1024);
// JavaScript reads it
const data = new Uint8Array(
new ArrayBuffer(1024),
dataPtr,
);
// Important: Let C free it
lib.symbols.free_buffer(dataPtr);
Memory Leaks
// Bad - memory leak
function process() {
const callback = new CFunction({/* ... */}, () => {});
lib.symbols.register(callback.ptr);
// callback goes out of scope but C still has pointer!
}
// Good - keep callback alive
const callbacks = [];
function process() {
const callback = new CFunction({/* ... */}, () => {});
callbacks.push(callback); // Prevent GC
lib.symbols.register(callback.ptr);
}
Error Handling
Check Return Values
const result = lib.symbols.do_something();
if (result !== 0) {
throw new Error(`Operation failed with code ${result}`);
}
Try-Catch
try {
lib.symbols.risky_operation();
} catch (err) {
console.error("FFI call failed:", err);
}
errno
import { read } from "bun:ffi";
// Some C functions set errno
const result = lib.symbols.open_file("/path");
if (result === null) {
// Check errno in JavaScript
console.error("Failed to open file");
}
Platform Differences
Windows
// Windows uses different calling conventions
const lib = dlopen("library.dll", {
WinApiFunction: {
args: [FFIType.i32],
returns: FFIType.i32,
abi: "win64", // Use Windows x64 ABI
},
});
macOS Code Signing
Dynamic libraries must be signed:codesign -s - libmylib.dylib
Best Practices
-
Check library existence
const path = `./lib.${suffix}`; if (!existsSync(path)) { throw new Error("Library not found"); } -
Type safety
type LibSymbols = { add(a: number, b: number): number; getName(): string; }; const lib = dlopen<LibSymbols>(path, {/* ... */}); -
Resource cleanup
const lib = dlopen(path, symbols); try { // Use library } finally { lib.close(); } -
Document ABIs
// Document the C signature // C: int add(int a, int b) add: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32, }
Debugging
// Enable FFI debug logging
process.env.BUN_DEBUG_FFI = "1";
// Check if library loaded
const lib = dlopen(path, symbols);
console.log("Symbols:", Object.keys(lib.symbols));
// Validate pointers
const ptr = lib.symbols.get_data();
console.log("Pointer:", ptr.toString(16));
Performance
FFI is extremely fast in Bun:- Overhead: ~5-10ns per call
- Zero-copy for buffers
- JIT-compiled trampolines
- SIMD optimization for bulk transfers
const start = performance.now();
for (let i = 0; i < 1_000_000; i++) {
lib.symbols.add(i, i);
}
console.log(`1M calls: ${performance.now() - start}ms`);
// ~15ms on M1 Mac