bun:ffi
is experimental, with known bugs and limitations, and should not be relied on in
production. The most stable way to interact with native code from Bun is to write a Node-API
module.bun:ffi
module to efficiently call native libraries from JavaScript. It works with languages that support the C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).
dlopen usage (bun:ffi
)
To print the version number of sqlite3
:
Performance
According to our benchmark,bun:ffi
is roughly 2-6x faster than Node.js FFI via Node-API
.
Bun generates & just-in-time compiles C bindings that efficiently convert values between JavaScript types and native types. To compile C, Bun embeds TinyCC, a small and fast C compiler.
Usage
Zig
add.zig
terminal
dlopen
:
Rust
C++
FFI types
The followingFFIType
values are supported.
FFIType | C Type | Aliases |
---|---|---|
buffer | char* | |
cstring | char* | |
function | (void*)(*)() | fn , callback |
ptr | void* | pointer , void* , char* |
i8 | int8_t | int8_t |
i16 | int16_t | int16_t |
i32 | int32_t | int32_t , int |
i64 | int64_t | int64_t |
i64_fast | int64_t | |
u8 | uint8_t | uint8_t |
u16 | uint16_t | uint16_t |
u32 | uint32_t | uint32_t |
u64 | uint64_t | uint64_t |
u64_fast | uint64_t | |
f32 | float | float |
f64 | double | double |
bool | bool | |
char | char | |
napi_env | napi_env | |
napi_value | napi_value |
buffer
arguments must be a TypedArray
or DataView
.
Strings
JavaScript strings and C-like strings are different, and that complicates using strings with native libraries.How are JavaScript strings and C strings different?
How are JavaScript strings and C strings different?
JavaScript strings:
- UTF16 (2 bytes per letter) or potentially latin1, depending on the JavaScript engine & what characters are used
length
stored separately- Immutable
- UTF8 (1 byte per letter), usually
- The length is not stored. Instead, the string is null-terminated which means the length is the index of the first
\0
it finds - Mutable
bun:ffi
exports CString
which extends JavaScript’s built-in String
to support null-terminated strings and add a few extras:
new CString()
constructor clones the C string, so it is safe to continue using myString
after ptr
has been freed.
returns
, FFIType.cstring
coerces the pointer to a JavaScript string
. When used in args
, FFIType.cstring
is identical to ptr
.
Function pointers
Async functions are not yet supported
CFunction
. This is useful if using Node-API (napi) with Bun, and you’ve already loaded some symbols.
linkSymbols
:
Callbacks
UseJSCallback
to create JavaScript callback functions that can be passed to C/FFI functions. The C/FFI function can call into the JavaScript/TypeScript code. This is useful for asynchronous code or whenever you want to call into JavaScript code from C.
close()
to free the memory.
Experimental thread-safe callbacks
JSCallback
has experimental support for thread-safe callbacks. This will be needed if you pass a callback function into a different thread from its instantiation context. You can enable it with the optional threadsafe
parameter.
Currently, thread-safe callbacks work best when run from another thread that is running JavaScript code, i.e. a Worker
. A future version of Bun will enable them to be called from any thread (such as new threads spawned by your native library that Bun is not aware of).
⚡️ Performance tip — For a slight performance boost, directly pass
JSCallback.prototype.ptr
instead of the JSCallback
object:Pointers
Bun represents pointers as anumber
in JavaScript.
How does a 64 bit pointer fit in a JavaScript number?
How does a 64 bit pointer fit in a JavaScript number?
64-bit processors support up to 52 bits of addressable space. JavaScript numbers support 53 bits of usable space, so that leaves us with about 11 bits of extra space.Why not
BigInt
? BigInt
is slower. JavaScript engines allocate a separate BigInt
which means they can’t fit into a regular JavaScript value. If you pass a BigInt
to a function, it will be converted to a number
TypedArray
to a pointer:
ArrayBuffer
:
DataView
:
read
:
read
function behaves similarly to DataView
, but it’s usually faster because it doesn’t need to create a DataView
or ArrayBuffer
.
FFIType | read function |
---|---|
ptr | read.ptr |
i8 | read.i8 |
i16 | read.i16 |
i32 | read.i32 |
i64 | read.i64 |
u8 | read.u8 |
u16 | read.u16 |
u32 | read.u32 |
u64 | read.u64 |
f32 | read.f32 |
f64 | read.f64 |
Memory management
bun:ffi
does not manage memory for you. You must free the memory when you’re done with it.
From JavaScript
If you want to track when aTypedArray
is no longer in use from JavaScript, you can use a FinalizationRegistry.
From C, Rust, Zig, etc
If you want to track when aTypedArray
is no longer in use from C or FFI, you can pass a callback and an optional context pointer to toArrayBuffer
or toBuffer
. This function is called at some point later, once the garbage collector frees the underlying ArrayBuffer
JavaScript object.
The expected signature is the same as in JavaScriptCore’s C API:
Memory safety
Using raw pointers outside of FFI is extremely not recommended. A future version of Bun may add a CLI flag to disablebun:ffi
.
Pointer alignment
If an API expects a pointer sized to something other thanchar
or u8
, make sure the TypedArray
is also that size. A u64*
is not exactly the same as [8]u8*
due to alignment.
Passing a pointer
Where FFI functions expect a pointer, pass aTypedArray
of equivalent size:
TypedArray
.
Hardmode
Hardmode
If you don’t want the automatic conversion or you want a pointer to a specific byte offset within the
TypedArray
, you can also directly get the pointer to the TypedArray
: