Skip to content

Bun FFI (Foreign Function Interface)

Bun provides FFI (Foreign Function Interface) functionality that allows direct calling of native libraries written in C/C++/Rust. This chapter introduces Bun FFI usage.

FFI Introduction

FFI allows JavaScript to call native code directly without writing Node.js native plugins or WebAssembly.

Advantages

  • High Performance: Direct native code calls, no IPC overhead
  • Simple: No need to compile binding code
  • Flexible: Can call any C ABI compatible library

Limitations

  • Need to understand the target library's C API
  • Memory management requires special attention
  • Different platforms may require different library files

Basic Usage

Loading Dynamic Libraries

typescript
import { dlopen, FFIType, suffix } from "bun:ffi";

// Automatically handle platform differences
// suffix: macOS -> "dylib", Linux -> "so", Windows -> "dll"
const libPath = `./libmath.${suffix}`;

const lib = dlopen(libPath, {
  // Declare function signatures
  add: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
});

// Call function
const result = lib.symbols.add(2, 3);
console.log(result); // 5

System Libraries

typescript
import { dlopen, FFIType } from "bun:ffi";

// Load C standard library
const libc = dlopen("libc.so.6", {
  getpid: {
    args: [],
    returns: FFIType.i32,
  },
  getenv: {
    args: [FFIType.cstring],
    returns: FFIType.cstring,
  },
});

console.log("PID:", libc.symbols.getpid());
console.log("HOME:", libc.symbols.getenv("HOME"));

Data Types

FFIType Types

FFITypeC TypeDescription
boolboolBoolean
i8int8_t8-bit signed integer
u8uint8_t8-bit unsigned integer
i16int16_t16-bit signed integer
u16uint16_t16-bit unsigned integer
i32int32_t32-bit signed integer
u32uint32_t32-bit unsigned integer
i64int64_t64-bit signed integer
u64uint64_t64-bit unsigned integer
f32float32-bit float
f64double64-bit double
ptrvoid*Pointer
cstringchar*C string

Type Examples

typescript
import { dlopen, FFIType, ptr, toArrayBuffer, toBuffer } from "bun:ffi";

const lib = dlopen("./mylib.so", {
  // Integer types
  processInt: {
    args: [FFIType.i32],
    returns: FFIType.i32,
  },
  
  // Float types
  calculateFloat: {
    args: [FFIType.f64, FFIType.f64],
    returns: FFIType.f64,
  },
  
  // Boolean type
  checkCondition: {
    args: [FFIType.bool],
    returns: FFIType.bool,
  },
  
  // String
  processString: {
    args: [FFIType.cstring],
    returns: FFIType.cstring,
  },
  
  // Pointer
  allocateBuffer: {
    args: [FFIType.u32],
    returns: FFIType.ptr,
  },
});

Pointer Operations

Creating Pointers

typescript
import { ptr, toArrayBuffer, toBuffer } from "bun:ffi";

// Get pointer from TypedArray
const buffer = new Uint8Array([1, 2, 3, 4]);
const pointer = ptr(buffer);

console.log("Pointer address:", pointer);

Reading Pointer Data

typescript
import { toArrayBuffer, toBuffer } from "bun:ffi";

// Assume lib.symbols.getData returns a pointer
const dataPtr = lib.symbols.getData();

// Convert to ArrayBuffer (need to know length)
const arrayBuffer = toArrayBuffer(dataPtr, 0, 100); // 100 bytes

// Convert to Buffer
const buffer = toBuffer(dataPtr, 0, 100);

Passing Buffers

typescript
import { ptr } from "bun:ffi";

const lib = dlopen("./mylib.so", {
  processData: {
    args: [FFIType.ptr, FFIType.u32],
    returns: FFIType.i32,
  },
});

// Create buffer
const data = new Uint8Array([1, 2, 3, 4, 5]);

// Pass pointer and length
const result = lib.symbols.processData(ptr(data), data.length);

String Handling

C Strings

typescript
import { dlopen, FFIType, CString } from "bun:ffi";

const lib = dlopen("./strlib.so", {
  getString: {
    args: [],
    returns: FFIType.cstring,
  },
  setString: {
    args: [FFIType.cstring],
    returns: FFIType.void,
  },
});

// Get C string
const cstr = lib.symbols.getString();
console.log(cstr); // Automatically converted to JS string

// Pass string
lib.symbols.setString("Hello from JavaScript");

Encoders

typescript
// Convert JS string to C string
const encoder = new TextEncoder();
const bytes = encoder.encode("Hello\0"); // Include null terminator

const lib = dlopen("./strlib.so", {
  processBytes: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
});

lib.symbols.processBytes(ptr(bytes));

Structs

Passing Structs

typescript
import { ptr } from "bun:ffi";

// C struct:
// struct Point {
//   float x;
//   float y;
// };

// Create struct data
const point = new Float32Array([3.14, 2.71]);

const lib = dlopen("./geometry.so", {
  printPoint: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
});

lib.symbols.printPoint(ptr(point));

Struct Arrays

typescript
// struct Point { float x; float y; };
// Each Point is 8 bytes

// Create array of 3 points
const points = new Float32Array([
  1.0, 2.0,  // Point 1
  3.0, 4.0,  // Point 2
  5.0, 6.0,  // Point 3
]);

const lib = dlopen("./geometry.so", {
  processPoints: {
    args: [FFIType.ptr, FFIType.u32],
    returns: FFIType.void,
  },
});

lib.symbols.processPoints(ptr(points), 3);

Callback Functions

JavaScript Callbacks

typescript
import { callback, FFIType } from "bun:ffi";

// Create callback function
const cb = callback({
  args: [FFIType.i32, FFIType.i32],
  returns: FFIType.i32,
}, (a, b) => {
  console.log(`Callback called: ${a} + ${b}`);
  return a + b;
});

const lib = dlopen("./mylib.so", {
  registerCallback: {
    args: [FFIType.ptr],
    returns: FFIType.void,
  },
  triggerCallback: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
});

// Register callback
lib.symbols.registerCallback(cb);

// Trigger callback
const result = lib.symbols.triggerCallback(10, 20);
console.log("Result:", result); // 30

Practical Examples

Calling SQLite (Without Built-in)

typescript
import { dlopen, FFIType, ptr, CString } from "bun:ffi";

const sqlite = dlopen("libsqlite3.so", {
  sqlite3_open: {
    args: [FFIType.cstring, FFIType.ptr],
    returns: FFIType.i32,
  },
  sqlite3_exec: {
    args: [FFIType.ptr, FFIType.cstring, FFIType.ptr, FFIType.ptr, FFIType.ptr],
    returns: FFIType.i32,
  },
  sqlite3_close: {
    args: [FFIType.ptr],
    returns: FFIType.i32,
  },
});

// Using SQLite
// ...

Calling System APIs

typescript
import { dlopen, FFIType } from "bun:ffi";

// macOS example
const foundation = dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", {
  // ...
});

// Linux example: get system info
const libc = dlopen("libc.so.6", {
  uname: {
    args: [FFIType.ptr],
    returns: FFIType.i32,
  },
});

Calling Rust Libraries

rust
// src/lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn greet(name: *const std::os::raw::c_char) -> *const std::os::raw::c_char {
    let c_str = unsafe { std::ffi::CStr::from_ptr(name) };
    let name = c_str.to_str().unwrap();
    let greeting = format!("Hello, {}!", name);
    std::ffi::CString::new(greeting).unwrap().into_raw()
}
bash
# Compile Rust library
cargo build --release
typescript
// Call Rust library
import { dlopen, FFIType, suffix } from "bun:ffi";

const lib = dlopen(`./target/release/libmylib.${suffix}`, {
  add: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
  greet: {
    args: [FFIType.cstring],
    returns: FFIType.cstring,
  },
});

console.log(lib.symbols.add(5, 3)); // 8
console.log(lib.symbols.greet("Bun")); // "Hello, Bun!"

Error Handling

Checking Return Values

typescript
const lib = dlopen("./mylib.so", {
  riskyOperation: {
    args: [FFIType.ptr],
    returns: FFIType.i32, // 0 = success, -1 = failure
  },
});

const result = lib.symbols.riskyOperation(null);

if (result !== 0) {
  throw new Error(`Operation failed, error code: ${result}`);
}

Closing Libraries

typescript
const lib = dlopen("./mylib.so", {
  // ...
});

// Close when done
lib.close();

Performance Considerations

Avoiding Frequent Calls

typescript
// ❌ Bad: frequent FFI calls
for (let i = 0; i < 1000000; i++) {
  lib.symbols.add(i, 1);
}

// ✅ Good: batch processing
const data = new Int32Array(1000000);
lib.symbols.processArray(ptr(data), data.length);

Caching Function References

typescript
// Cache symbol references
const { add, multiply, divide } = lib.symbols;

// Direct call
const sum = add(1, 2);

Summary

This chapter covered:

  • ✅ FFI basic concepts and usage
  • ✅ Data type mapping
  • ✅ Pointer and buffer operations
  • ✅ String handling
  • ✅ Struct passing
  • ✅ Callback functions
  • ✅ Calling Rust library examples

Next Steps

Continue reading Performance Optimization to learn about Bun's performance tuning techniques.

Content is for learning and research only.