Skip to main content
Bun’s Foreign Function Interface (FFI) lets you call native libraries directly from JavaScript without writing bindings. It supports C ABI-compatible functions from any language.

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

  1. Check library existence
    const path = `./lib.${suffix}`;
    if (!existsSync(path)) {
      throw new Error("Library not found");
    }
    
  2. Type safety
    type LibSymbols = {
      add(a: number, b: number): number;
      getName(): string;
    };
    
    const lib = dlopen<LibSymbols>(path, {/* ... */});
    
  3. Resource cleanup
    const lib = dlopen(path, symbols);
    try {
      // Use library
    } finally {
      lib.close();
    }
    
  4. 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
Benchmark:
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

Build docs developers (and LLMs) love