How to Use bun:ffi to Call Native C Functions from JavaScript in Bun

Bun's FFI (Foreign Function Interface) allows JavaScript to dynamically load shared libraries and invoke C functions with near-native performance by generating optimized call stubs at runtime.

The bun:ffi module provides a zero-dependency mechanism for calling native code from the Bun JavaScript runtime. As implemented in the oven-sh/bun repository, this API eliminates the need for Node-API or C++ wrapper code by dynamically generating JavaScript-to-native bridges at load time.

Core Architecture of bun:ffi

The implementation spans three distinct layers. The high-level JavaScript façade resides in src/js/bun/ffi.ts, which manages type conversions and function generation. This interfaces with Zig primitives defined in src/bun.js/api/ffi.zig and C++ bindings in src/bun.js/bindings/ffi.cpp that connect directly to the JavaScriptCore engine.

FFIType and Type Mapping

At the foundation of the type system is the FFIType definition found in src/js/bun/ffi.ts (lines 1-61). This mapping translates human-readable type names—such as int, double, cstring, function, and ptr—into internal numeric IDs consumed by the builder. When you declare a function signature, these types determine how JavaScript values are marshalled into C-compatible binary representations.

Dynamic Stub Generation with FFIBuilder

Unlike generic FFI systems that rely on slow reflection, Bun uses the FFIBuilder class (lines 40-66 in src/js/bun/ffi.ts) to generate optimized call stubs. The builder concatenates JavaScript snippets from the ffiWrappers object (lines 59-124), which contain per-type conversion logic. It then uses new Function() to create an inline stub that converts arguments, calls the raw native pointer (functionToCall), and wraps return values.

Loading Native Libraries with dlopen

The primary entry point for library interaction is Bun.FFI.dlopen(), implemented in src/js/bun/ffi.ts (lines 46-77). This function loads a shared library (.so, .dylib, or .dll) and replaces specified symbols with generated wrappers.

Basic Function Signatures

For each symbol, provide an object containing args (an array of FFI types) and returns (a single FFI type). If no signature is provided, the raw native pointer is exposed without type conversion.

const lib = Bun.FFI.dlopen("./libcalc.so", {
  add: { args: ["int", "int"], returns: "int" },
  mul: { args: ["int", "int"], returns: "int" },
  greet: { args: ["cstring"], returns: "cstring" }
});

console.log(lib.add(3, 4)); // 7

Working with Pointers

The ptr helper (lines 68-70 in src/js/bun/ffi.ts) creates raw pointers from ArrayBuffer instances or numeric addresses. Use this when passing binary data to functions expecting void* or typed pointers.

const buffer = new ArrayBuffer(1024);
const ptr = Bun.FFI.ptr(buffer);
lib.process_buffer(ptr, buffer.byteLength);

Handling C Strings and Memory Management

The CString class (lines 178-234 in src/js/bun/ffi.ts) wraps null-terminated C strings returned from native code. It stores the native pointer and lazily creates an ArrayBuffer view when you call toString() or toArrayBuffer().

Critical: CString does not copy memory or manage allocation lifetime. The underlying C memory must remain valid while JavaScript holds the reference, or you must manually free it via an exposed C function.

const msg = lib.greet("Bun");
console.log(msg.toString()); // Accesses the C string
lib.free(msg.ptr); // Manual cleanup if required by the C API

Creating Thread-Safe JavaScript Callbacks

When native code must call back into JavaScript, use the JSCallback class (lines 82-115 in src/js/bun/ffi.ts). This wraps a JavaScript function in a native function pointer that C code can store and invoke.

Thread Safety Configuration

Pass { threadsafe: true } as the first argument to enable safe invocation from any thread. Bun automatically marshals these calls back to the JavaScript event loop.

const callback = new Bun.FFI.JSCallback(
  { threadsafe: true },
  (a, b) => a * b
);

lib.register_callback(callback);
// Later:
callback.close(); // Releases native resources

Runtime C Compilation with cc

For scenarios requiring ad-hoc native code, Bun.FFI.cc() compiles C source at runtime via the Zig driver in src/bun.js/api/ffi.zig. This bypasses the need for a separate build step when prototyping or generating small native shims.

Advanced Linking and CFunction

Use Bun.FFI.linkSymbols() (lines 27-42 in src/js/bun/ffi.ts) to add symbols to an already-loaded library without reopening the file. For standalone function pointers not tied to a shared object, the CFunction helper creates single-function wrappers and registers a FinalizationRegistry to close handles automatically when the JavaScript object is garbage collected.

Summary

  • bun:ffi lives in src/js/bun/ffi.ts and provides dlopen(), cc(), and callback APIs backed by Zig and C++ bindings.
  • The FFIBuilder generates optimized JavaScript stubs at runtime using new Function() and ffiWrappers for minimal call overhead.
  • CString wraps returned C strings without copying; you must manage the underlying native memory lifetime manually.
  • JSCallback with { threadsafe: true } allows C code to safely invoke JavaScript from any thread.
  • Use ptr to pass ArrayBuffer data as native pointers, and linkSymbols to augment loaded libraries without reopening them.

Frequently Asked Questions

What types does bun:ffi support?

The FFIType enum in src/js/bun/ffi.ts defines support for int, uint32, int64, double, float, bool, cstring, ptr, and function. Complex structs are not directly supported; you must pass them as ptr to raw memory and manually manage offsets.

How do I pass structs or complex types to C functions?

Pass structs as ptr values pointing to ArrayBuffer memory. Calculate field offsets manually or use a C helper function to return packed data. The ptr helper in src/js/bun/ffi.ts accepts ArrayBuffer instances or numeric addresses for this purpose.

Is bun:ffi thread-safe for callbacks?

Yes, when you create a JSCallback with the { threadsafe: true } option, the resulting native function pointer can be called from any thread. Bun automatically queues the invocation onto the JavaScript event loop. Without this flag, callbacks are restricted to the main thread.

How do I free memory allocated by the C library?

The CString class and pointer helpers do not manage memory. You must expose a C function (such as free()) through the same library handle and call it from JavaScript, passing the pointer value obtained from cstring.ptr or the original pointer returned by the function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →