Files
bun-src/docs/project/bindgen.mdx
T
2026-08-27 21:09:14 +00:00

286 lines
7.9 KiB
Plaintext

---
title: Bindgen
description: Bindgen for Bun
---
<Note>This document is for maintainers and contributors to Bun, and describes internal implementation details.</Note>
The bindings generator scans for `*.bind.ts` files to find function and class
definitions, and generates glue code to interop between JavaScript and native
code.
There are other code generators and systems that achieve similar purposes;
the following will all eventually be phased out in favor of this one:
- "Classes generator", converting `*.classes.ts` for custom classes.
- "JS2Native", allowing ad-hoc calls from `src/js` to native code.
## Creating JS Functions in Rust
Given a file implementing a function, such as `add`:
```rust src/jsc/bindgen_test.rs
use crate::{JSGlobalObject, JsResult};
use crate::r#gen::bindgen_test as generated;
pub fn add(global: &JSGlobalObject, a: i32, b: i32) -> JsResult<i32> {
match a.checked_add(b) {
Some(v) => Ok(v),
None => {
// Binding functions can propagate out-of-memory and JS exceptions
// directly; other failures (like this integer overflow) must be
// converted into a thrown error. Remember to be descriptive.
Err(global.throw(format_args!("Integer overflow while adding")))
}
}
}
```
Then describe the API schema using a `.bind.ts` file. The binding file goes
next to the Rust file.
```ts src/jsc/bindgen_test.bind.ts icon="/icons/typescript.svg"
import { fn, t } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(-1),
},
ret: t.i32,
});
```
This function declaration is equivalent to:
```ts
/**
* Throws if zero arguments are provided.
* Wraps out of range numbers using modulo.
*/
declare function add(a: number, b: number = -1): number;
```
The code generator emits a C++ thunk that validates and coerces the JS
arguments, then calls the Rust implementation. On the Rust side bindgen emits
nothing; both the dispatch shim the thunk calls
(`bindgen_Bindgen_test_dispatchAdd1` in `src/runtime/hw_exports.rs`, which
calls `add`) and the `create_*_callback` module in
`src/jsc/bindings/GeneratedBindings.rs` are hand-written. The module is
reachable as `crate::r#gen::<basename>` (for `bindgen_test.bind.ts`, that's
`crate::r#gen::bindgen_test`). To construct a `JSFunction` wrapping the
native implementation, use `generated::create_add_callback(global)`:
```rust
use crate::r#gen::bindgen_test as generated;
let js_fn: JSValue = generated::create_add_callback(global);
```
In JS files in `src/js/`, `$bindgenFn("bindgen_test.bind.ts", "add")` returns
a handle to the implementation, through a hand-written
`js2native_bindgen_<basename>_<fn>` export in `src/runtime/hw_exports.rs`.
Exported bindgen functions are snake_cased on the Rust side
(`requiredAndOptionalArg` → `required_and_optional_arg`). The hand-written
callback constructor follows the same convention
(`create_required_and_optional_arg_callback`).
## Strings
To receive a string, use [`t.DOMString`](https://webidl.spec.whatwg.org/#idl-DOMString), [`t.ByteString`](https://webidl.spec.whatwg.org/#idl-ByteString), or [`t.USVString`](https://webidl.spec.whatwg.org/#idl-USVString). These map directly to their WebIDL counterparts and have slightly different conversion logic. Bindgen passes `bun_core::String` to native code in all cases.
When in doubt, use DOMString.
`t.UTF8String` works in place of `t.DOMString`, but eagerly converts to UTF-8.
The native callback receives a `&[u8]` slice (WTF-8 data) that is
freed after the function returns.
TLDRs from the WebIDL spec:
- ByteString can only contain valid latin1 characters. It is not safe to assume `bun_core::String` is already in 8-bit format, but it is extremely likely.
- USVString does not contain invalid surrogate pairs, so its text can be represented correctly in UTF-8.
- DOMString is the loosest but also the most recommended strategy.
## Function Variants
The `variants` key declares multiple variants (also known as overloads) of a function.
```ts
import { fn, t } from "bindgen";
export const action = fn({
variants: [
{
args: {
a: t.i32,
},
ret: t.i32,
},
{
args: {
a: t.DOMString,
},
ret: t.DOMString,
},
],
});
```
Each variant gets a numbered Rust function:
```rust
pub fn action1(a: i32) -> i32 {
a
}
pub fn action2(a: bun_core::String) -> bun_core::String {
a
}
```
## `t.dictionary`
A `dictionary` describes a JavaScript object, typically a function input. For function outputs, prefer a class type so you can add methods and support destructuring.
## Enumerations
`t.stringEnum` creates a [WebIDL enumeration](https://webidl.spec.whatwg.org/#idl-enums) and generates a new enum type for it.
An example of `stringEnum` from `fmt_jsc.bind.ts` / `bun:internal-for-testing`:
```ts
export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell");
export const fmtString = fn({
args: {
global: t.globalObject,
code: t.UTF8String,
formatter: Formatter,
},
ret: t.DOMString,
});
```
On the Rust side, the enum is mirrored as a `#[repr(u8)]` enum. Bindgen
**sorts `t.stringEnum` values alphabetically** before emitting the C++
`enum class`, so discriminants must match the generated header's order, not
the `.bind.ts` declaration order:
```rust
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum Formatter {
EscapePowershell = 0,
HighlightJavascript = 1,
HighlightJavascriptRedacted = 2,
}
pub fn fmt_string(
global: &JSGlobalObject,
code: &[u8],
formatter_id: Formatter,
) -> JsResult<bun_core::String> {
// ...
}
```
WebIDL strongly encourages kebab-case for enumeration values, to be consistent with existing Web APIs.
## `t.oneOf`
A `oneOf` is a union of two or more types. It is represented as a Rust
`enum` with one variant per member type.
## Attributes
You can chain attributes onto `t.*` types. On all types:
- `.required`, in dictionary parameters only
- `.optional`, in function arguments only
- `.default(T)`
When a value is `.optional`, it is lowered to a Rust `Option<T>`:
```ts
export const requiredAndOptionalArg = fn({
args: {
a: t.boolean,
b: t.usize.optional,
c: t.i32.enforceRange(0, 100).default(42),
d: t.u8.optional,
},
ret: t.i32,
});
```
```rust
pub fn required_and_optional_arg(a: bool, b: Option<usize>, c: i32, d: Option<u8>) -> i32 {
// ...
}
```
Depending on the type, more attributes are available. See the type definitions
in auto-complete for details. You can apply only one of these three attributes,
and you must apply it last.
### Integer Attributes
Integer types take `clamp` or `enforceRange` to customize overflow behavior:
```ts
import { fn, t } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// enforce in i32 range
a: t.i32.enforceRange(),
// clamp to u16 range
b: t.u16,
// enforce in arbitrary range, with a default if not provided
c: t.i32.enforceRange(0, 1000).default(5),
// clamp to arbitrary range, or None
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});
```
Node.js validator functions such as `validateInteger` and `validateNumber`
are also available. Use these when implementing Node.js APIs so the error
messages match Node exactly.
Unlike `enforceRange`, which is taken from WebIDL, the `validate*` functions
are much stricter about the input they accept. For example, Node's numerical
validator checks `typeof value === 'number'`, while WebIDL uses `ToNumber` for
lossy conversion.
```ts
import { fn, t } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// throw if not given a number
a: t.f64.validateNumber(),
// valid in i32 range
b: t.i32.validateInt32(),
// f64 within safe integer range
c: t.f64.validateInteger(),
// f64 in given range
d: t.f64.validateNumber(-10000, 10000),
},
ret: t.i32,
});
```
## Callbacks
TODO
## Classes
TODO