---
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 reads every `*.bind.ts` file under `src/` and generates
the glue between JavaScript and Rust: the C++ that checks and converts
arguments with JavaScriptCore's own API, the Rust that receives them already
converted, and the TypeScript types `src/js` sees.

A binding's Rust code never sees a `CallFrame`, never converts a value by hand,
and cannot keep a pointer into JavaScript-owned memory for longer than it is
valid: the borrow checker refuses.

The generator lives in `src/codegen/bindgen*.ts`, the Rust it generates against in
`src/jsc/bindgen.rs`, and the C++ in `src/jsc/bindings/Bindgen/` and
`src/jsc/bindings/BunIDL*.h`.

The body of a binding is always Rust. These do overlapping work:

- "Classes generator", converting `*.classes.ts` for custom classes.
- "JS2Native", allowing ad-hoc calls from `src/js` to native code (`$rust`, `$cpp`).
- `#[bun_jsc::host_fn]` functions, which take a `CallFrame`.

## Functions

Describe the function in a `.bind.ts` file next to the Rust file:

```ts src/jsc/bindgen_test.bind.ts icon="/icons/typescript.svg"
import * as b from "../codegen/bindgen-lib.ts";

export default b.module({ rustPath: "crate::bindgen_test" });

export const add = b.fn({
  args: {
    a: b.i32.wrap,
    b: b.i32.wrap.nullable.default(-1),
  },
  returns: b.i32,
  capability: "heap",
  throws: true,
});
```

and write it:

```rust src/jsc/bindgen_test.rs
use crate::bindgen::{self, JsHeap};

pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> {
    a.checked_add(b)
        .ok_or_else(|| heap.error(format_args!("Integer overflow while adding")))
}
```

`b.module` says where the file's functions are; an export named
`requiredAndOptionalArg` is `required_and_optional_arg` there. A `rustPath` key on
a function gives its full path instead. `length` sets the function's `.length`,
which is otherwise the number of arguments up to the last that is not written
`.optional`, `.nullable`, `.falsyAsNull` or `.default()`. The ones after that are
the ones TypeScript lets a caller leave out.

Nothing else is written by hand, except once per crate: `src/<crate>/generated_bindgen.rs`
includes the generated file, the crate root declares `mod generated_bindgen`, and a
`build.rs` like `src/semver_jsc/build.rs` says where the generated file is, for a `cargo check`
outside of `bun bd`. If the Rust signature does not match the schema, the generated code does
not compile.

To reach the function:

- from `src/js`: `$bindgen("bindgen_test.bind.ts", "add")`, which is typed from the schema;
- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`.

### Namespaces

`b.namespace` is a plain object that holds functions, like `Bun.semver`. Each is written as
the argument of `b.fn` is:

```ts src/semver_jsc/SemverObject.bind.ts icon="/icons/typescript.svg"
export const semver = b.namespace({
  satisfies: {
    args: { version: b.string.loose.utf8, range: b.string.loose.utf8 },
    returns: b.bool,
    throws: true,
  },
  order: { .. },
});
```

`Bun::Bindgen::Generated::SemverObject::createSemver` makes the object. It has the signature of
a `PropertyCallback` of a static property table, so the object is made when the property is
first read.

A namespace that can itself be called, like `Bun.hash`, names the function that a call runs:
`b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a
function named after the namespace.

### What a function may do: `capability`

What a function can do to the JS heap is a parameter it is given, so it is
visible in the schema, in the signature and to the compiler.

| `capability`       | First parameter    | The function can                                                     |
| ------------------ | ------------------ | -------------------------------------------------------------------- |
| `"none"` (default) | none               | compute                                                              |
| `"heap"`           | `&JsHeap<'a>`      | also allocate JS values and describe an error to throw               |
| `"js"`             | `&mut JsScope<'a>` | also run JavaScript, or anything that can call back into native code |

Ask for the least that works. The less a function can do, the more it is handed
directly: a function that cannot run JavaScript gets `&[u8]` for a typed array
and `&mut self` for its object, because nothing can invalidate either while it
runs.

The compiler holds a body to its capability as far as this API goes. The older API has
other ways to a `&JSGlobalObject` (`VirtualMachine::get().global()`), and a body that
takes one of those to run JavaScript has stepped around the check.

Much of the older API takes a `&JSGlobalObject` only to allocate or to throw. A `"heap"`
function calls it with `heap.global()`, which is `unsafe`: nothing the global object is passed
to may run JavaScript, and the compiler cannot see that. Read what is called all the way down
before writing the `SAFETY` comment. `ToString` of an object, a property that is looked up or
set and not defined, and a call to a constructor found on the global object all run JavaScript.
Throwing tells the debugger, which can run code, so after a throw the body returns without
reading what it borrowed.

```rust
// SAFETY: parsing runs no JavaScript. Nor does making the result: its keys are primitives, and its properties are
// defined and not set.
let global = unsafe { heap.global() };
```

`throws: true` makes the return type a `Result`. The error is a
`bindgen::Error<'a>`, made by `heap.error(..)`, `heap.type_error(..)`,
`heap.range_error(..)`, `heap.error_with_code(ErrorCode::OUT_OF_RANGE, ..)`, `heap.invalid_arguments(..)` or
`bindgen::Error::from_value(value)` (`?` makes one of a `JsError` or an `AllocError`), or any type that
implements `bindgen::IntoError` (`bun_sys::Error`, `SystemError`, `JsError` and a `Box` of
one do), which lets a `capability: "none"` function fail too.

`cx.global()` is the `&JSGlobalObject`, for code that takes one. It borrows all of
`cx`. Such a function can return `JsResult<T>`, and a plain `JSValue` under
`returns: b.any`.

### Overloads

`b.overloads(a, b, ..)` is one JS function with several signatures. Each names its
own Rust function, which gets exactly the types of that signature:

```ts
export const read = b.overloads(
  { args: { path: b.string.utf8, options: ReadOptions.optional }, rustPath: "crate::read_path" },
  { args: { fd: b.i32, buffer: b.bytes({ from: ["buffer"] }), offset: b.u32.default(0) }, rustPath: "crate::read_fd" },
);
```

Use it when the type of one argument says what the others are. A union for each
argument would accept combinations that mean nothing, and leave the body to sort them out.

The generated function starts with every signature as a candidate and looks at the
arguments from left to right:

1. It keeps the candidates whose type at that position takes the [kind](#unions-and-kinds)
   of the argument.
2. An argument that was not passed is `undefined`. So `f(x)` and `f(x, undefined)` are the
   same call.
3. A candidate with no argument at that position stays.
4. If no candidate takes the argument, all of them stay, and the next argument decides.
5. When the arguments run out, the candidate written first wins. Its conversion reports
   whatever is wrong. So of two signatures that start the same, the longer one goes first.

Choosing converts nothing and runs no JavaScript. A signature that no call can reach is
an error when the schema is loaded.

## Types

Conversions are strict unless the schema says otherwise: `b.i32` takes a number
that is an integer in range, and nothing else. A failed strict or
`.validateInteger` conversion, or a `.loose` one of an integer, throws
`ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary
member; `.wrap`, `.enforceRange` and the other `.loose` conversions throw what WebIDL does. An argument that was not passed is `undefined` and is converted like
any other: nothing looks at how many there are.

| Schema                                           | Rust, as an argument             | Takes                                                         |
| ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- |
| `b.bool`                                         | `bool`                           | a boolean; `.loose` is `ToBoolean`                            |
| `b.u8` `b.u16` `b.u32` `b.u64` `b.usize` `b.i32` | the same                         | a number that is an integer in range; see below               |
| `b.f64`                                          | `f64`                            | a number; `.loose` is `ToNumber`; see below                   |
| `b.string`                                       | `bun_core::String`               | a string; `.loose` is `ToString`                              |
| `b.string.utf8`                                  | `&'a [u8]`                       | the same, and a `String` object                               |
| `b.string.utf8.owned`                            | `bun_core::Utf8Bytes<'static>`   | as `b.string.utf8`                                            |
| `b.bufferSource`                                 | `JsLocal<'a, BufferSource>`      | a typed array, `DataView` or `ArrayBuffer` that is not shared |
| `b.bufferSource.arrayBuffer`                     | `ExternalShared<JSCArrayBuffer>` | the same; the bytes of a view are copied                      |
| `b.bytes(options)`                               | `&'a [u8]`, and others           | see [Bytes](#bytes)                                           |
| `b.blob`                                         | `ExternalShared<Blob>`           | a `Blob`                                                      |
| `b.any`                                          | `JsLocal<'a>`                    | anything                                                      |
| `b.null`                                         | `()`                             | `null` or `undefined`; only in a union                        |
| `b.array(T)`                                     | `Vec<T>`                         | an array; `T` holds no JS value, which the GC would not see   |
| `b.enumeration(values)`                          | a generated `enum`               | one of the strings                                            |
| `b.dictionary(members)`                          | a generated `struct`             | an object that is not an array                                |
| `b.union(alternatives)`                          | a generated `enum`               | see [Unions and kinds](#unions-and-kinds)                     |
| `b.class(options)`                               | `JsWrapper<'a, T>`               | an instance                                                   |

One JS type has one name; a modifier picks the Rust form. The last four are named
types: each is called what it is exported as, and has to be exported. It is used by
schemas of the crate it is generated into. A member or an alternative also names a C++
field, so one that is a C++ keyword is refused: a member takes a `rustName`.

These come last, in this order (`b.any` already takes `undefined` and `null`, so none applies to it):

- `.optional`: `undefined` means absent; `Option<T>`.
- `.nullable`: `null` or `undefined` means absent; `Option<T>`.
- `.falsyAsNull`: every falsy value means absent; `Option<T>`.
- `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`.

As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that
is `IntoIterator`. The body of a `b.fn` or of a namespace's function whose bytes do not outlive
it returns the string itself, from `heap.create_utf8_string(bytes)`. `b.u64.asBigInt` is a
result only: a `u64` that is always returned as a BigInt. `b.bufferSource` as a result is what
`heap.create_uninitialized_uint8_array(length)`, `heap.create_uint8_array_from_box(bytes)` or
`heap.create_buffer_from_box(bytes)` made. The last two take over the allocation.

### Numbers

A BigInt is refused unless the schema says `.allowBigInt`, which is for an argument whose
type is `number | bigint`. `b.f64.loose.allowBigInt` is `ToNumeric`, and a BigInt becomes the
double nearest to it. On an integer:

- `.allowBigInt`: a BigInt in the same range too. It is how a `b.u64` above 2<sup>53</sup> gets in. It goes with the strict conversion and with `.loose`; the others refuse a BigInt.
- `.loose`: `ToNumber` first, or `ToNumeric` with `.allowBigInt`, then as strict as the default.
- `.wrap`: the WebIDL conversion, `ToNumber` then wrap modulo the width.
- `.enforceRange(min, max)`: WebIDL `[EnforceRange]`.
- `.validateInteger(min, max)`: Node's `validateInteger`, with its exact errors. Use it when implementing Node.js APIs.

### Strings

`b.string.utf8` is the one to reach for. When the JS string is 8-bit and ASCII the
slice points at the string's own bytes; otherwise it is transcoded into a buffer
in the generated function's frame, which holds 128 bytes and is sized for the worst
case up front: beyond 64 Latin-1 characters or 42 UTF-16 code units it allocates.
Nothing needs freeing. An unpaired surrogate becomes U+FFFD. `.loose` comes before
`.utf8`. A `String` object, which `b.string` refuses, is converted with `ToString`, so
its `toString` runs.

`b.string.utf8.owned` is for a body that stores the string: a
`bun_core::Utf8Bytes<'static>`, which shares the JS string's buffer when it is 8-bit
and ASCII and is a transcoded copy otherwise.

`b.string` shares the JS string's storage and can be stored. It belongs to the
JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending
one anywhere.

### Dictionaries

```ts
export const BracesOptions = b.dictionary({
  tokenize: b.bool.loose.default(false),
  parse: b.bool.loose.default(false),
});
```

Each member is read, then converted, in declaration order, once. A member
whose type does not accept `undefined` is required. A member can be written
`{ type, rustName, altNames }` to rename its field or to accept other spellings.
A dictionary whose members can all be absent implements `Default`. The thunk hands the struct
over by value, so the body of one over 64 bytes needs
`#[allow(clippy::large_types_passed_by_value)]`.

A member is looked up on the object and its prototype chain, **stopping before
the calling realm's `Object.prototype`**, so a polluted `Object.prototype` cannot
configure a Bun API. For a plain object, where the own properties are therefore
the whole answer, the member is read straight out of the object's property table. The two paths are indistinguishable to JavaScript;
`test/internal/bindgen.test.ts` holds them to that.

Returning a dictionary makes an object with room for its members inline and puts
each one on it. An `.optional` member that is `None` is left out.

The second argument of `b.dictionary` takes:

| Key                | What it does                                                                                                                                                                                                      |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `allowArray`       | an array is read like any other object, not refused                                                                                                                                                               |
| `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default |
| `fromJS`           | also generates `Name::from_js(global, value)`, for a `#[host_fn]` body; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)`                                           |

### Enumerations

```ts
export const Formatter = b.enumeration(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]);
```

generates `enum Formatter { HighlightJavascript, .. }`. An entry can be a list of
spellings; the first is the one returned to JavaScript. Returning an enumeration
allocates nothing after the first time.

### Unions and kinds

Where there is a choice, between the alternatives of a union or between overloads,
it is made on the _kind_ of the value, with the cheapest test that tells the kinds
in play apart:

| Looking at                         | Kinds                                                                            |
| ---------------------------------- | -------------------------------------------------------------------------------- |
| the bits of the value              | `undefined`, `null`, boolean, number                                             |
| the type byte in the cell's header | string, BigInt, array, `ArrayBuffer`, typed array or `DataView`, `String` object |
| the cell's `ClassInfo`             | an instance of a class                                                           |

A strict type takes the kinds its row under [Types](#types) says. A type that coerces or
reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`,
`.validateInteger`, `b.any`, `b.bytes`, a dictionary.

**A value goes to the first alternative, in written order, that takes its kind.** That
alternative then converts it, and reports what is wrong with it: `1.5` given to
`b.union({ integer: b.i32, text: b.string })` "must be an integer", not "must be of type integer or string".
The buffer types and `b.blob` refuse some values of their kinds (a shared buffer, a
detached `Blob`); those go on to the next alternative that takes the kind and does not
refuse the same. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all.

So the order only matters between alternatives that overlap, and one that takes every
kind goes last. An alternative that can never be chosen is an error when the schema is
loaded. Choosing tests whether the value is a cell once, loads the type byte once and
repeats no test. Strings as UTF-8, dictionaries and unions are then converted in the
place they end up in.

### Buffers

`b.bufferSource` is the buffer itself. JavaScript can detach, resize or transfer it, so the
body looks the bytes up when it needs them:

```rust
let before = buffer.with_bytes(&cx.heap, sum);
callback.call(cx, JsLocal::UNDEFINED, &[])?;
let after = buffer.with_bytes(&cx.heap, sum);
```

Calling `callback.call(cx, ..)` inside the closure does not compile. A detached or
out-of-bounds view is empty. A shared buffer is refused: [`b.bytes`](#bytes) takes one.

### Bytes

Many functions take "a string or a buffer" and want its bytes. They differ in which values they
take, in what becomes of anything else, and in how long they need the bytes. `b.bytes` takes
those as named options. The combinations that are build errors are listed at the end.

```ts
export const gzipSync = b.fn({
  args: { data: b.bytes({ from: ["string", "buffer"] }), options: ZlibCompressionOptions.default({}) },
  returns: b.bufferSource,
  capability: "heap",
  throws: true,
});
```

```rust
fn gzip_sync<'a>(heap: &JsHeap<'a>, data: &[u8], options: ZlibCompressionOptions<'a>) -> bindgen::Result<'a, JsLocal<'a, BufferSource>>
```

| Option      | Values                                               | Meaning                                                                |
| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------- |
| `from`      | `"string"`, `"buffer"`, `"blob"`                     | what is taken without `ToString`                                       |
| `otherwise` | **`"throw"`**, `"toString"`                          | anything else                                                          |
| `refuse`    | `"undefined"`, `"null"`                              | with `"toString"`: these throw, and are not `"undefined"` and `"null"` |
| `string`    | **`"utf8"`**, `"asIs"`                               | a string is its UTF-8, or stays a `bun_core::String`                   |
| `shared`    | **`"borrowed"`**, `"copied"`                         | the bytes of a shared buffer                                           |
| `held`      | **`"borrowed"`**, `"handle"`, `"copied"`, `"pinned"` | what the body gets                                                     |

`"string"` is a string or a `String` object. `"buffer"` is a typed array, `DataView`,
`ArrayBuffer` or `SharedArrayBuffer`. `"blob"` is a `Blob` whose bytes are in memory: a file,
here or in S3, throws.

| `held`       | `string` | Rust                  |
| ------------ | -------- | --------------------- |
| `"borrowed"` | `"utf8"` | `&'a [u8]`            |
| `"handle"`   | `"utf8"` | `JsBytes<'a>`         |
| `"handle"`   | `"asIs"` | `StringOrJsBytes<'a>` |
| `"copied"`   | `"utf8"` | `Vec<u8>`             |
| `"pinned"`   | `"utf8"` | `PinnedBytes`         |

**Borrowed** bytes are where they are. A string is converted into the frame of the host function,
unless it is stored as ASCII already. JavaScript can detach, resize or free a buffer, so this is
for a function that cannot run it.

**A handle** is for a function that can. `JsBytes::get` borrows the bytes from the `JsHeap`, and the
compiler refuses whatever takes the `&mut JsScope` until the slice is gone:

```rust
fn parse<'a>(cx: &mut JsScope<'a>, input: JsBytes<'a>) -> JsResult<JSValue> {
    let tree = parse_tree(input.get(&cx.heap))?; // no JavaScript here
    let global = cx.global();                    // `tree` owns what it kept, or this does not compile
```

Each `get` reads the pointer and the length of a buffer again. With `string: "asIs"` the body
gets either `String`, in Latin-1 or UTF-16, or `Bytes`, the handle. That also tells a string
from a buffer.

**Copied** bytes are the body's. **Pinned** bytes are for a job on another thread to read after
the call returns: a string is thread-isolated, a buffer is a GC-rooted `PinnedArrayBuffer`, and a
resizable one is copied. A `PinnedBytes` is `Send`, derefs to `[u8]`, and is dropped on the JS
thread.

**A shared buffer** is borrowed like any other, as JSC's own `toBase64` and `indexOf` read one, and
another thread that writes it during the call changes what the body reads. `shared: "copied"` is
for a body that reads a byte again and counts on what it was, such as a parser that scans ahead
and comes back. JSC's `sort` copies for the same reason.

The kind of the value is checked, and a string converted, in the order of the arguments. The
bytes of a buffer are read, copied or pinned after every argument is converted. The argument is
always there: it is not `.optional`, and not a member of a dictionary, a union or an array.

An option that cannot apply is a build error: `refuse` without `"toString"`, `string` where no
string is taken, `shared` where no buffer is taken or `held: "copied"` copies it anyway. So is
what no function needs yet, which is not written: `"asIs"` without `"handle"` or that copies a
shared buffer, `"pinned"` that takes a `Blob` or copies a shared buffer, and a `"pinned"` argument
next to another or next to a `"borrowed"` one.

## Values and lifetimes

A JS value in Rust is a `JsLocal<'a>`: a `JSValue` tied to the host call it belongs
to. It is `Copy`, one register, and not rooted, because JavaScriptCore scans the
stack. The lifetime is what stops it from being stored in a struct, captured by a
task or sent to a thread. `JsLocal<'a, T>` is one known to be a `JSString`,
`JSObject`, `Callable` or `BufferSource`. `value.to_js()` gives the `JSValue`;
`JsLocal::new(heap, value)` goes the other way.

What outlives a call:

- one of a class's **`internalFields`** (below), which the GC visits;
- a `Strong`, made with `value.to_strong(heap)` and read with `JsLocal::new(heap, strong.get())`. It is made, read and dropped on the JS thread.

A list of values goes in a `MarkedArgumentBuffer`, never a `Vec` or a `Box`: the GC does
not look inside heap allocations. The compiler does not check this one.

## Classes

The keys are mostly those of `.classes.ts`, with types. `klass` is `static` and `values` is `internalFields`.

```ts
export const Glob = b.class({
  rustPath: "crate::api::glob::Glob",
  construct: {
    args: { pattern: b.string.utf8.owned },
  },
  proto: {
    match: {
      args: { str: b.string.utf8 },
      returns: b.bool,
    },
  },
});
```

generates a trait, which the struct implements:

```rust
pub(crate) struct Glob {
    pattern: bun_core::Utf8Bytes<'static>,
}

impl GlobClass for Glob {
    fn constructor(pattern: bun_core::Utf8Bytes<'static>) -> Self {
        Glob { pattern }
    }

    fn r#match<'a>(&self, str: &'a [u8]) -> bool {
        bun_glob::r#match(self.pattern.slice(), str).matches()
    }
}
```

The struct, the _payload_, is stored **inside** the JS object, the _wrapper_, so the
two have one lifetime and one allocation. It is dropped when the object is
collected. `Drop` gets no capability, so it cannot touch the JS heap, and no Rust
runs on the collector's threads. The collector is not told of memory the payload owns
outside the object: there is no `estimatedSize` yet.

| Key              | What it declares                                                                               |
| ---------------- | ---------------------------------------------------------------------------------------------- |
| `rustPath`       | the struct                                                                                     |
| `construct`      | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.into_js(heap)`. |
| `proto`          | properties of the prototype                                                                    |
| `static`         | functions on the constructor                                                                   |
| `internalFields` | JS values the object holds                                                                     |
| `configurable`   | `false` makes the native properties of the prototype non-configurable                          |

A property of `proto` is one of:

| Property                                    | What it is                                                                   |
| ------------------------------------------- | ---------------------------------------------------------------------------- |
| `{ args, returns, capability, throws, .. }` | a native function; `privateSymbol` keys it by a private name of `src/js`     |
| `{ getter: T, capability, throws }`         | an accessor; with `setter: true` it can be assigned                          |
| `{ getter: T, cache: true, .. }`            | an accessor whose value is computed once per object; it cannot have a setter |
| `{ builtin: true, length }`                 | the export of that name in `src/js/builtins/<class name>.ts`                 |

If any member has `throws: true`, the trait has a
`type Error<'a>: bindgen::IntoError<'a>` that they all fail with. `b.self` is the
class itself as an argument, a result or a getter of its own members. `Glob::get_constructor(heap)` is the
constructor function.

### The receiver

| The method is                    | It receives                 |
| -------------------------------- | --------------------------- |
| `capability: "none"` or `"heap"` | `&self`                     |
| …with `mutates: true`            | `&mut self`                 |
| `capability: "js"`               | `this: JsWrapper<'a, Self>` |

A method that can run JavaScript can be re-entered, by a callback that calls
another method of the same object. A `&mut self` held across that would be
undefined behaviour, so it gets the wrapper instead and borrows the payload for as
long as nothing else can run:

```rust
fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsScope<'a>) -> bindgen::Result<'a, i32> {
    if let Some(listener) = this.listener_field(&cx.heap).as_callable() {
        listener.call(cx, this.as_value(), &[])?;
    }
    Ok(this.with(&cx.payloads, |counter| counter.count))
}
```

`this.with(&cx.payloads, ..)` and `this.with_mut(&mut cx.payloads, ..)` borrow one
field of `cx`; running JavaScript needs all of it. `cx.heap` is another field, so
allocating or viewing a buffer inside the closure is fine.

### `internalFields`

`internalFields: ["listener"]` generates `this.listener_field(heap)` and
`this.set_listener_field(heap, value)` on `JsWrapper<'a, T>`. A `&self` or
`&mut self` method that needs them asks for the wrapper too, with `passThis: true`.
The wrapper reaches the internal fields and not the payload.

### Staying alive

To keep an object alive while native work is pending, hold a
`JsWrapperStrong<T>` (`this.to_strong(heap)`) with the work and drop it on the JS
thread when the work completes.

## Tests

- `test/internal/bindgen.test.ts`: conversions with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. A schema named `*_test.bind.ts` is only part of a build with assertions (a debug build, CI's ASAN lane), so nothing it binds ships; `$bindgen` of one is `undefined` elsewhere, and its Rust module is `#[cfg(debug_assertions)]`. What that schema does not use is covered by the suites of the APIs that do.
