Calling a C function from Bun via bun:ffi
Published March 19, 2027
import { dlopen, FFIType, suffix } from "bun:ffi";
const path = `libsqlite3.${suffix}`;
const { symbols } = dlopen(path, {
sqlite3_libversion: { args: [], returns: FFIType.cstring },
sqlite3_libversion_number: { args: [], returns: FFIType.i32 },
});
console.log(symbols.sqlite3_libversion()); // e.g. "3.45.1"
console.log(symbols.sqlite3_libversion_number()); // e.g. 3045001What
dlopen(path, symbols) opens a shared library (.so/.dylib/.dll) and returns typed
JavaScript functions for the C symbols you describe — argument and return types included — with
no separate binding layer to write or compile.
Why it matters
Calling native code from Node normally means writing a Node-API (N-API) addon: C++ glue code,
a native build step (node-gyp or similar), and a .node file that has to be rebuilt per
platform and per Node ABI version. bun:ffi skips the glue code entirely — Bun embeds a small C
compiler (TinyCC) and JIT-compiles the JS-to-native conversion for each symbol you declare, so
calling into an existing shared library needs only a type description, not a build step. Bun's
own benchmark puts this roughly 2-6x faster than the equivalent Node-API round-trip, on top of
the development-time savings.
How it works
suffixresolves to"dylib","so", or"dll"depending on platform, so`libsqlite3.${suffix}`finds the right file without an if/else onprocess.platform.dlopentakes the library path and a map of symbol names to{ args, returns }type descriptions — this example calls into the system's own SQLite library, which every Bun-supported OS already ships, so there's nothing extra to install or compile to try this.FFIType.cstringonreturnstells Bun to convert the nativechar*result into a JS string automatically;FFIType.i32for the version number returns a plain number, no conversion needed either way.symbols.sqlite3_libversion()andsymbols.sqlite3_libversion_number()are now ordinary JS functions — calling them runs the JIT-compiled native call underneath.
Gotchas
bun:ffiis explicitly documented as experimental, with known limitations — Bun's own docs recommend a real Node-API module for anything going to production rather than depending on it long-term.- Pointers are represented as plain JS
numbers, notBigInt— memory bugs (freeing something still referenced, reading past a buffer) are as possible here as in C itself, and Bun does not manage that memory for you. - Passing the wrong
FFITypefor an argument or return value doesn't throw a friendly JS error — it can silently misinterpret bytes or crash the process, since this is a direct native call, not a safety-checked JS API.
Related
- Node-API (
node:napivia a compiled addon) is the more stable path for native interop that needs to ship to production long-term rather than being called occasionally from a script. WebAssemblyis a safer, sandboxed alternative when the native code can be compiled to.wasminstead of linked as a platform-specific shared library.