From 5088d17ede1255a6100fa77971addf41bcba179d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 02:43:45 +0000 Subject: [PATCH 01/44] Replace bindgen v1 and v2 with one generator that also does classes --- .prettierrc | 6 - docs/project/bindgen.mdx | 459 ++-- scripts/build/bun.ts | 1 - scripts/build/codegen.ts | 142 +- scripts/glob-sources.ts | 12 +- src/codegen/bindgen-emit.ts | 1229 ++++++++++ src/codegen/bindgen-lib-internal.ts | 2081 +++++++++-------- src/codegen/bindgen-lib.ts | 401 +--- src/codegen/bindgen.ts | 1365 +---------- src/codegen/bindgenv2/internal/any.ts | 42 - src/codegen/bindgenv2/internal/array.ts | 32 - src/codegen/bindgenv2/internal/base.ts | 134 -- src/codegen/bindgenv2/internal/dictionary.ts | 373 --- src/codegen/bindgenv2/internal/enumeration.ts | 177 -- src/codegen/bindgenv2/internal/interfaces.ts | 40 - src/codegen/bindgenv2/internal/optional.ts | 119 - src/codegen/bindgenv2/internal/primitives.ts | 229 -- src/codegen/bindgenv2/internal/string.ts | 44 - src/codegen/bindgenv2/internal/union.ts | 136 -- src/codegen/bindgenv2/lib.ts | 10 - src/codegen/bindgenv2/script.ts | 156 -- src/codegen/bindgenv2/tsconfig.json | 12 - src/codegen/generate-js2native.ts | 2 - src/js/builtins.d.ts | 1 + src/js/internal-for-testing.ts | 22 +- src/js/node/os.ts | 17 +- src/js/private.d.ts | 9 +- src/jsc/NodeModuleModule.bind.ts | 9 +- src/jsc/Strong.rs | 10 - src/jsc/bindgen.rs | 853 +++++-- src/jsc/bindgen/extern_types.rs | 131 ++ src/jsc/bindgen_test.bind.ts | 113 +- src/jsc/bindgen_test.rs | 251 +- .../bindings/Bindgen/DictionaryMemberReader.h | 68 + src/jsc/bindings/Bindgen/ExternTraits.h | 33 +- src/jsc/bindings/Bindgen/IDLConvert.h | 121 +- src/jsc/bindings/Bindgen/IDLTypes.h | 55 +- src/jsc/bindings/Bindgen/Identifiers.h | 52 + src/jsc/bindings/Bindgen/JSBindgenObject.h | 166 ++ src/jsc/bindings/Bindgen/JSStringUTF8View.h | 68 + src/jsc/bindings/BindgenIdentifiers.cpp | 132 ++ src/jsc/bindings/BunClientData.cpp | 2 + src/jsc/bindings/BunClientData.h | 2 + src/jsc/bindings/BunObject.cpp | 4 +- src/jsc/bindings/GeneratedBindings.rs | 47 - src/jsc/bindings/IDLTypes.h | 2 +- src/jsc/bindings/JSBindgenObject.cpp | 90 + src/jsc/bindings/ZigGlobalObject.cpp | 18 + src/jsc/bindings/ZigGlobalObject.h | 13 + .../bindings/webcore/DOMClientIsoSubspaces.h | 2 + src/jsc/bindings/webcore/DOMIsoSubspaces.h | 2 + src/jsc/fmt_jsc.bind.ts | 19 +- src/jsc/fmt_jsc.rs | 31 +- src/jsc/generated.rs | 707 +----- src/jsc/lib.rs | 18 +- src/jsc/modules/NodeModuleModule.cpp | 2 +- src/runtime/api/BunObject.bind.ts | 30 +- src/runtime/api/BunObject.rs | 28 +- src/runtime/api/Glob.bind.ts | 55 + src/runtime/api/Glob.classes.ts | 39 - src/runtime/api/glob.rs | 497 +--- src/runtime/bake/DevServer.bind.ts | 10 +- src/runtime/bake/tsconfig.json | 3 +- src/runtime/dispatch_js2native.rs | 1 - src/runtime/hw_exports.rs | 378 --- src/runtime/lib.rs | 15 + src/runtime/node.rs | 2 - src/runtime/node/node_os.bind.ts | 134 +- src/runtime/node/node_os.rs | 175 +- src/runtime/socket/Handlers.rs | 44 +- ...{SSLConfig.bindv2.ts => SSLConfig.bind.ts} | 40 +- src/runtime/socket/SSLConfig.rs | 66 +- src/runtime/socket/SocketConfig.bind.ts | 92 + src/runtime/socket/SocketConfig.bindv2.ts | 92 - .../timers/FakeTimersConfig.bindv2.ts | 15 - src/tsconfig.json | 5 +- test/internal/bindgen-compile-fail.test.ts | 96 + .../fail/bytes_across_js.rs | 7 + .../bindgen-compile-fail/fail/bytes_escape.rs | 6 + .../fail/error_escapes_call.rs | 5 + .../fail/error_to_thread.rs | 4 + .../fail/forge_capability.rs | 4 + .../bindgen-compile-fail/fail/forge_error.rs | 4 + .../bindgen-compile-fail/fail/forge_value.rs | 4 + .../fail/global_across_payload.rs | 7 + .../fail/heap_only_runs_js.rs | 4 + .../fail/internal_field_without_unsafe.rs | 4 + .../fail/pass_this_reaches_payload.rs | 6 + .../fail/payload_mut_across_js.rs | 7 + .../fail/payload_ref_across_js.rs | 7 + .../fail/payload_ref_and_mut.rs | 6 + .../fail/payload_ref_escapes.rs | 6 + .../fail/payload_without_unsafe.rs | 12 + .../fail/shared_call_runs_js.rs | 4 + .../fail/string_arg_escapes.rs | 9 + .../fail/strong_to_thread.rs | 4 + .../fail/two_payload_muts.rs | 6 + .../fail/value_between_calls.rs | 9 + .../fail/value_escapes_call.rs | 5 + .../fail/value_in_payload.rs | 14 + .../fail/value_in_static_task.rs | 5 + .../fail/value_to_thread.rs | 4 + .../fail/with_value_converted_stored.rs | 9 + .../pass/keep_past_the_call.rs | 16 + .../pass/lists_of_values.rs | 12 + .../pass/methods_that_cannot_run_js.rs | 24 + .../pass/reentrant_method.rs | 23 + .../bindgen-compile-fail/pass/with_value.rs | 18 + test/internal/bindgen-compile-fail/prelude.rs | 32 + test/internal/bindgen.test.ts | 603 +++++ .../build-codegen-declared-outputs.test.ts | 142 +- .../source-lints/host-export-callers.test.ts | 10 +- 112 files changed, 6386 insertions(+), 6745 deletions(-) create mode 100644 src/codegen/bindgen-emit.ts delete mode 100644 src/codegen/bindgenv2/internal/any.ts delete mode 100644 src/codegen/bindgenv2/internal/array.ts delete mode 100644 src/codegen/bindgenv2/internal/base.ts delete mode 100644 src/codegen/bindgenv2/internal/dictionary.ts delete mode 100644 src/codegen/bindgenv2/internal/enumeration.ts delete mode 100644 src/codegen/bindgenv2/internal/interfaces.ts delete mode 100644 src/codegen/bindgenv2/internal/optional.ts delete mode 100644 src/codegen/bindgenv2/internal/primitives.ts delete mode 100644 src/codegen/bindgenv2/internal/string.ts delete mode 100644 src/codegen/bindgenv2/internal/union.ts delete mode 100644 src/codegen/bindgenv2/lib.ts delete mode 100755 src/codegen/bindgenv2/script.ts delete mode 100644 src/codegen/bindgenv2/tsconfig.json create mode 100644 src/jsc/bindgen/extern_types.rs create mode 100644 src/jsc/bindings/Bindgen/DictionaryMemberReader.h create mode 100644 src/jsc/bindings/Bindgen/Identifiers.h create mode 100644 src/jsc/bindings/Bindgen/JSBindgenObject.h create mode 100644 src/jsc/bindings/Bindgen/JSStringUTF8View.h create mode 100644 src/jsc/bindings/BindgenIdentifiers.cpp delete mode 100644 src/jsc/bindings/GeneratedBindings.rs create mode 100644 src/jsc/bindings/JSBindgenObject.cpp create mode 100644 src/runtime/api/Glob.bind.ts delete mode 100644 src/runtime/api/Glob.classes.ts rename src/runtime/socket/{SSLConfig.bindv2.ts => SSLConfig.bind.ts} (77%) create mode 100644 src/runtime/socket/SocketConfig.bind.ts delete mode 100644 src/runtime/socket/SocketConfig.bindv2.ts delete mode 100644 src/runtime/test_runner/timers/FakeTimersConfig.bindv2.ts create mode 100644 test/internal/bindgen-compile-fail.test.ts create mode 100644 test/internal/bindgen-compile-fail/fail/bytes_across_js.rs create mode 100644 test/internal/bindgen-compile-fail/fail/bytes_escape.rs create mode 100644 test/internal/bindgen-compile-fail/fail/error_escapes_call.rs create mode 100644 test/internal/bindgen-compile-fail/fail/error_to_thread.rs create mode 100644 test/internal/bindgen-compile-fail/fail/forge_capability.rs create mode 100644 test/internal/bindgen-compile-fail/fail/forge_error.rs create mode 100644 test/internal/bindgen-compile-fail/fail/forge_value.rs create mode 100644 test/internal/bindgen-compile-fail/fail/global_across_payload.rs create mode 100644 test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs create mode 100644 test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs create mode 100644 test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs create mode 100644 test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs create mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs create mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs create mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs create mode 100644 test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs create mode 100644 test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs create mode 100644 test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs create mode 100644 test/internal/bindgen-compile-fail/fail/strong_to_thread.rs create mode 100644 test/internal/bindgen-compile-fail/fail/two_payload_muts.rs create mode 100644 test/internal/bindgen-compile-fail/fail/value_between_calls.rs create mode 100644 test/internal/bindgen-compile-fail/fail/value_escapes_call.rs create mode 100644 test/internal/bindgen-compile-fail/fail/value_in_payload.rs create mode 100644 test/internal/bindgen-compile-fail/fail/value_in_static_task.rs create mode 100644 test/internal/bindgen-compile-fail/fail/value_to_thread.rs create mode 100644 test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs create mode 100644 test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs create mode 100644 test/internal/bindgen-compile-fail/pass/lists_of_values.rs create mode 100644 test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs create mode 100644 test/internal/bindgen-compile-fail/pass/reentrant_method.rs create mode 100644 test/internal/bindgen-compile-fail/pass/with_value.rs create mode 100644 test/internal/bindgen-compile-fail/prelude.rs diff --git a/.prettierrc b/.prettierrc index 14285ca70431..c9da1bd439c6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,12 +19,6 @@ "options": { "printWidth": 80 } - }, - { - "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], - "options": { - "printWidth": 100 - } } ] } diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 7593fe0f2ab4..d679fad66116 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -5,281 +5,342 @@ description: Bindgen for Bun This document is for maintainers and contributors to Bun, and describes internal implementation details. -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. +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. -There are other code generators and systems that achieve similar purposes; -the following will all eventually be phased out in favor of this one: +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`. + +These other generators do overlapping work and are to be replaced by this one: - "Classes generator", converting `*.classes.ts` for custom classes. - "JS2Native", allowing ad-hoc calls from `src/js` to native code. +- `#[bun_jsc::host_fn]` functions, which take a `CallFrame`. -## Creating JS Functions in Rust +## Functions -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 { - 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. +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 { fn, t } from "bindgen"; +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::bindgen_test" }); -export const add = fn({ +export const add = b.fn({ args: { - global: t.globalObject, - a: t.i32, - b: t.i32.default(-1), + a: b.i32.wrap, + b: b.i32.wrap.nullable.default(-1), }, - ret: t.i32, + returns: b.i32, + capability: "heap", + throws: true, }); ``` -This function declaration is equivalent to: +and write it: -```ts -/** - * Throws if zero arguments are provided. - * Wraps out of range numbers using modulo. - */ -declare function add(a: number, b: number = -1): number; +```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.throw(format_args!("Integer overflow while adding"))) +} ``` -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::` (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)`: +`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. -```rust -use crate::r#gen::bindgen_test as generated; +Nothing else is written by hand. If the Rust signature does not match the +schema, the generated code does not compile. -let js_fn: JSValue = generated::create_add_callback(global); -``` +To reach the function: + +- from `src/js`: `$bindgenFn("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`. + +### 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. -In JS files in `src/js/`, `$bindgenFn("bindgen_test.bind.ts", "add")` returns -a handle to the implementation, through a hand-written -`js2native_bindgen__` export in `src/runtime/hw_exports.rs`. +| `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 JsCall<'a>` | also run JavaScript, or anything that can call back into native code | -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`). +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. -## Strings +`throws: true` makes the return type a `Result`. The error is a +`bindgen::Error<'a>`, made by `heap.throw(..)`, `heap.throw_type_error(..)`, +`heap.throw_range_error(..)` or `heap.throw_value(value)`, or any type that +implements `bindgen::IntoError` (`bun_sys::Error` does), which lets a +`capability: "none"` function fail too. -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. +`cx.with_global(|global| ..)` and `cx.with_global_value(|global| ..)` run code +that takes a `&JSGlobalObject`. -When in doubt, use DOMString. +### Overloads -`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. +`b.overloads(a, b, ..)` is one JS function with several signatures, told apart by +how many arguments were passed. Each names its own Rust function. -TLDRs from the WebIDL spec: +## Types -- 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. +Conversions are strict unless the schema says otherwise: `b.i32` takes a number +that is an integer in range, and nothing else. A failed conversion throws +`ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the +dictionary member. -## Function Variants +| Schema | Rust, as an argument | Accepts | +| ----------------------------- | -------------------------- | ------------------------------------------------------------------------------------------ | +| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | +| `b.i8` … `b.u64`, `b.usize` | `i8` … `u64`, `usize` | an integer in range; see below | +| `b.f64` | `f64` | a number; `.finite` rejects NaN and infinities, `.loose` is `ToNumber` | +| `b.utf8` | `&'a [u8]` | a string or `String` object; `.loose` is `ToString` | +| `b.string` | `bun_core::String` | a string; `.loose` is `ToString` | +| `b.bytes` | `&'a [u8]` | a typed array, `DataView` or `ArrayBuffer` that is not shared; not with `capability: "js"` | +| `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | the same | +| `b.arrayBuffer`, `b.blob` | `ExternalShared<..>` | a reference-counted handle on the native object | +| `b.any` | `JsValue<'a>` | anything | +| `b.array(T)` | `Vec` | an array | +| `b.enumeration(name, values)` | a generated `enum` | one of the strings | +| `b.dictionary(name, members)` | a generated `struct` | an object that is not an array | +| `b.union(name, alternatives)` | a generated `enum` | the first alternative that fits | +| `b.class(name, ..)` | `JsWrapper<'a, T>` | an instance | -The `variants` key declares multiple variants (also known as overloads) of a function. +Any type takes one of these, last: + +- `.optional`: `undefined` means "not given"; `Option`. +- `.nullable`: `null` or `undefined` means "not given"; `Option`. +- `.default(value)`: what to use when not given; `T`. `.nullable.default(value)` also applies to `null`. +- `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). + +As results, `b.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything +that is `IntoIterator`. + +### Integers + +- `.loose`: `ToNumeric` first, 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.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. Either way nothing is allocated for a short +string and nothing needs freeing. An unpaired surrogate becomes U+FFFD. + +`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 -import { fn, t } from "bindgen"; - -export const action = fn({ - variants: [ - { - args: { - a: t.i32, - }, - ret: t.i32, - }, - { - args: { - a: t.DOMString, - }, - ret: t.DOMString, - }, - ], +export const BracesOptions = b.dictionary("BracesOptions", { + tokenize: b.bool.loose.default(false), + parse: b.bool.loose.default(false), }); ``` -Each variant gets a numbered Rust function: +Members are read and converted in declaration order, each once. A member +whose type does not accept `undefined` is required. A member can be written +`{ type, internalName, altNames }` to rename its field or to accept other spellings. +A dictionary whose members can all be left out implements `Default`. -```rust -pub fn action1(a: i32) -> i32 { - a -} +A member is looked up on the object and its prototype chain, **stopping before +`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 generated code reads them straight out of the object's property +table. The two paths are indistinguishable to JavaScript; +`test/internal/bindgen.test.ts` holds them to that. -pub fn action2(a: bun_core::String) -> bun_core::String { - a -} -``` +Returning a dictionary makes an object with a prebuilt `Structure`: one +allocation and one store per member. An `.optional` member that is `None` is +left out. -## `t.dictionary` +`b.dictionary({ name, fromJS: true }, ..)` also generates +`Name::from_js(cx, value)`, for code that is not a generated binding yet. -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 -## Enumerations +```ts +export const Formatter = b.enumeration("Formatter", [ + "highlight-javascript", + "highlight-javascript-redacted", + "escape-powershell", +]); +``` -`t.stringEnum` creates a [WebIDL enumeration](https://webidl.spec.whatwg.org/#idl-enums) and generates a new enum type for it. +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. -An example of `stringEnum` from `fmt_jsc.bind.ts` / `bun:internal-for-testing`: +### Buffers -```ts -export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell"); +`b.bytes` is only for functions that cannot run JavaScript, because JavaScript +can detach, resize or transfer a buffer. A function that can takes `b.bufferSource` +and looks the bytes up when it needs them: -export const fmtString = fn({ - args: { - global: t.globalObject, - code: t.UTF8String, - formatter: Formatter, - }, - ret: t.DOMString, -}); +```rust +let before = buffer.with_bytes(&cx.heap, sum); +callback.call(cx, JsValue::UNDEFINED, &[])?; +let after = buffer.with_bytes(&cx.heap, sum); ``` -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: +Calling `callback.call(cx, ..)` inside the closure does not compile. -```rust -#[repr(u8)] -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum Formatter { - EscapePowershell = 0, - HighlightJavascript = 1, - HighlightJavascriptRedacted = 2, -} +### Keeping the original value + +A converted argument is for use now. To hand the value to something that holds +it longer (one of a class's `values`, a `Strong`, another JS call, the result), ask +for both: + +```ts +args: { name: b.utf8.withValue }, +``` -pub fn fmt_string( - global: &JSGlobalObject, - code: &[u8], - formatter_id: Formatter, -) -> JsResult { - // ... +```rust +fn set_name<'a>(&mut self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) { + this.name_set_cached(heap, name.value); // the JS string itself, not a copy + self.name_length = name.converted.len(); } ``` -WebIDL strongly encourages kebab-case for enumeration values, to be consistent with existing Web APIs. +## Values and lifetimes -## `t.oneOf` +A JS value in Rust is a `JsValue<'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. `JsValue<'a, T>` is one known to be a `JSString`, +`JSObject`, `JSFunction` or `ArrayBuffer`. `value.to_js()` gives the `JSValue`; +`JsValue::new(heap, value)` goes the other way. -A `oneOf` is a union of two or more types. It is represented as a Rust -`enum` with one variant per member type. +What outlives a call: -## Attributes +- one of a class's **`values`** (below), which the GC visits; +- a `Strong`, made with `value.to_strong(heap)` and read with `JsValue::from_strong(heap, &strong)`. It is made, read and dropped on the JS thread. -You can chain attributes onto `t.*` types. On all types: +A list of values goes in a `MarkedArgumentBuffer`, never a `Vec`: the GC does not +look inside heap allocations. -- `.required`, in dictionary parameters only -- `.optional`, in function arguments only -- `.default(T)` +## Classes -When a value is `.optional`, it is lowered to a Rust `Option`: +The keys are those of `.classes.ts`, with types. ```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, +export const Glob = b.class("Glob", { + rustPath: "crate::api::glob::Glob", + construct: { + args: { pattern: b.utf8 }, + }, + proto: { + match: { + args: { str: b.utf8 }, + returns: b.bool, + }, }, - ret: t.i32, }); ``` +generates a trait, which the struct implements: + ```rust -pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { - // ... +pub(crate) struct Glob { + pattern: Box<[u8]>, +} + +impl GlobClass for Glob { + fn constructor<'a>(pattern: &'a [u8]) -> Self { + Glob { pattern: Box::from(pattern) } + } + + fn r#match<'a>(&self, str: &'a [u8]) -> bool { + bun_glob::r#match(&self.pattern, str).matches() + } } ``` -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. +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. -### Integer Attributes +| Key | What it declares | +| ----------- | -------------------------------------------------------------------------------------------- | +| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.to_js(heap)`. | +| `proto` | properties of the prototype | +| `klass` | functions on the constructor | +| `values` | JS values the object holds | -Integer types take `clamp` or `enforceRange` to customize overflow behavior: +A property of `proto` is one of: -```ts -import { fn, t } from "bindgen"; +| Property | What it is | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `{ args, returns, capability, throws, .. }` | a native function; `fn` renames the trait method, `privateSymbol` keys it by a private name | +| `{ getter: T, setter: true, cache: true, capability, .. }` | an accessor; `cache` computes the value once per object | +| `{ builtin, length }` | a function written in JavaScript in `src/js/builtins` | -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, -}); -``` +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 in its own signatures. `Glob::get_constructor(heap)` is the +constructor function and `Glob::from_js(value)` the wrapper, if `value` is one. -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. +### The receiver -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. +| The method is | It receives | +| -------------------------------- | --------------------------- | +| `capability: "none"` or `"heap"` | `&self` | +| …with `mutates: true` | `&mut self` | +| `capability: "js"` | `this: JsWrapper<'a, Self>` | -```ts -import { fn, t } from "bindgen"; +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: -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, -}); +```rust +fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { + if let Some(listener) = this.listener_get_cached(&cx.heap).as_function() { + listener.call(cx, this.as_value(), &[])?; + } + Ok(this.with(&cx.payloads, |counter| counter.count)) +} ``` -## Callbacks +`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. -TODO +### `values` -## Classes +`values: ["listener"]` generates `this.listener_get_cached(heap)` and +`this.listener_set_cached(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 values and not the payload. + +### Staying alive + +To keep an object alive while native work is pending, hold a +`JsWrapperStrong` (`this.to_strong(heap)`) with the work and drop it on the JS +thread when the work completes. + +## Tests -TODO +- `test/internal/bindgen.test.ts`: every conversion, with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. +- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index 94b2d70c888e..e55cc310e4aa 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -316,7 +316,6 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput { // Codegen .cpp files — compiled like regular sources. cxxSources.push(...codegen.cppSources); - cxxSources.push(...codegen.bindgenV2Cpp); // All deps must be ready (headers extracted, libs built) before compile. // diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index 24881088be36..96e58011db70 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -10,10 +10,6 @@ * configure time via `globAllSources()`. The expanded * paths are baked into build.ninja; adding a file picks up on next configure. * - * bindgenv2 is special: its output set is dynamic (depends on which types - * the .bindv2.ts files export). We invoke it with `--command=list-outputs` - * at CONFIGURE time to get the actual output paths. - * * ## Undeclared outputs * * Every generated file that is compiled or #included must be a declared @@ -22,8 +18,8 @@ * depfile entry does that within the same run only if it names a declared * output, which ninja re-stats after the step ran. A file no edge declares * is stat'd once at startup, so a compile that includes it picks up a rerun - * of the step one build late. That is why emitBindgen/emitBindgenV2 declare - * the per-file Generated*.h headers and not just the .cpp that gets compiled. + * of the step one build late. That is why emitBindgen declares the per-file + * Generated*.h headers and not just the .cpp files that get compiled. * * Files nothing compiles may stay undeclared (bundle-modules' eval/ dir, * JSSink.lut.txt consumed within its own step). The .d.ts files src/js is @@ -32,8 +28,7 @@ * reached through the PCH. */ -import { spawnSync } from "node:child_process"; -import { mkdirSync, readFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { basename, dirname, relative, resolve } from "node:path"; import type { Sources } from "../glob-sources.ts"; import { generateBuildOptionsRs } from "./buildOptionsRs.ts"; @@ -245,8 +240,8 @@ export interface CodegenOutputs { cppHeaders: string[]; /** - * ALL cpp-relevant codegen outputs — the union of cppHeaders, cppSources, - * bindgenV2Cpp. cxx compilation order-depends on THIS (not `all`): cxx + * ALL cpp-relevant codegen outputs — the union of cppHeaders and + * cppSources. cxx compilation order-depends on THIS (not `all`): cxx * doesn't need bake.*.js, runtime.out.js, or any other rust-side embedded * outputs: those are inputs of the Rust crates, not of any C++ object. * @@ -256,9 +251,6 @@ export interface CodegenOutputs { */ cppAll: string[]; - /** The bindgenv2 .cpp outputs (compiled separately from handwritten C++). */ - bindgenV2Cpp: string[]; - /** * The InternalModuleRegistryConstants.S path — compiled via cc() in bun.ts * and linked alongside the C++ objects. Carries the concatenated JS module @@ -299,7 +291,6 @@ export function emitCodegen(n: Ninja, cfg: CodegenFields, sources: Sources): Cod cppSources: [], cppHeaders: [], cppAll: [], - bindgenV2Cpp: [], internalModulesAsm: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.S"), internalModulesBin: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.bin"), rootInstall, @@ -335,7 +326,6 @@ export function emitCodegen(n: Ninja, cfg: CodegenFields, sources: Sources): Cod emitCppBind(ctx); emitJsModules(ctx); emitBakeCodegen(ctx); - emitBindgenV2(ctx); emitBindgen(ctx); emitJsSink(ctx); emitObjectLuts(ctx); @@ -346,7 +336,7 @@ export function emitCodegen(n: Ninja, cfg: CodegenFields, sources: Sources): Cod n.blank(); // Assemble cppAll — the cxx-relevant subset. See field docstring. - o.cppAll = [...new Set([...o.cppHeaders, ...o.cppSources, ...o.bindgenV2Cpp])]; + o.cppAll = [...new Set([...o.cppHeaders, ...o.cppSources])]; return o; } @@ -882,99 +872,65 @@ function emitBakeCodegen({ n, cfg, sources, o, dirStamp }: Ctx): void { o.rustInputs.push(...outputs); } -/** Exported (with emitBindgen) for test/internal/build-codegen-declared-outputs.test.ts. */ -export function emitBindgenV2({ n, cfg, sources, o, dirStamp }: Ctx): void { - const script = resolve(cfg.cwd, "src", "codegen", "bindgenv2", "script.ts"); - - // The script's output set depends on which NamedTypes the .bindv2.ts files - // export. We run `--command=list-outputs` SYNCHRONOUSLY at configure time - // to get the real list. This is a configure-time dependency on the - // sources — same tradeoff CMake makes with execute_process(). - // - // It runs with cfg.jsRuntime, the runtime that runs configure. It is a - // child process, not an import: in this process, node would print - // MODULE_TYPELESS_PACKAGE_JSON for the .ts files under src/. cfg.jsRuntime - // is a shell command prefix, so the command goes through the host shell. - // - // If list-outputs fails (e.g. syntax error in a .bindv2.ts file), we fail - // configure immediately with a clear error. Better to catch that here than - // get a cryptic "multiple rules generate " from ninja. - const sourcesArg = sources.bindgenV2.join(","); - const listArgs = [script, "--command=list-outputs", `--sources=${sourcesArg}`, `--codegen-path=${cfg.codegenDir}`]; - const listResult = spawnSync(`${cfg.jsRuntime} ${shJoin(cfg, listArgs)}`, { - cwd: cfg.cwd, - encoding: "utf8", - shell: true, - }); - if (listResult.status !== 0) { - throw new BuildError(`bindgenv2 list-outputs failed (exit ${listResult.status})`, { - file: script, - hint: listResult.stderr?.trim(), - }); - } - // Output is semicolon-separated (CMake list format). - const allOutputs = listResult.stdout - .trim() - .split(";") - .filter(p => p.length > 0); - - assert(allOutputs.length > 0, "bindgenv2 list-outputs returned no files"); - - const cppOutputs = allOutputs.filter(p => p.endsWith(".cpp")); - const headerOutputs = allOutputs.filter(p => p.endsWith(".h")); - const other = allOutputs.filter(p => !p.endsWith(".cpp") && !p.endsWith(".h")); - assert(other.length === 0, `bindgenv2 emitted unexpected output type: ${other.join(", ")}`); - - n.build({ - outputs: allOutputs, - rule: "codegen", - inputs: [script, ...sources.bindgenV2, ...sources.bindgenV2Internal], - orderOnlyInputs: [dirStamp], - vars: { - cwd: cfg.cwd, - desc: "bindgenv2", - args: shJoin(cfg, [script, "--command=generate", `--codegen-path=${cfg.codegenDir}`, `--sources=${sourcesArg}`]), - }, - }); - - o.all.push(...allOutputs); - o.bindgenV2Cpp.push(...cppOutputs); - o.cppHeaders.push(...headerOutputs); -} - +/** Exported for test/internal/build-codegen-declared-outputs.test.ts. */ export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); - const cppOut = resolve(cfg.codegenDir, "GeneratedBindings.cpp"); - // Plus one header per .bind.ts (node_os.bind.ts → GeneratedNodeOs.h), which - // hand-written .cpp files include; see "Undeclared outputs" above. - const headers = sources.bindgen.map(src => - resolve(cfg.codegenDir, `Generated${pascalCase(basename(src, ".bind.ts"))}.h`), + // What bindgen.ts writes follows from the names and places of its inputs alone. + const stems = sources.bindgen.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); + const cpp = stems.map(stem => resolve(cfg.codegenDir, `${stem}.cpp`)); + const headers = [ + ...stems.map(stem => resolve(cfg.codegenDir, `${stem}.h`)), + resolve(cfg.codegenDir, "GeneratedBindings+Tables.h"), + resolve(cfg.codegenDir, "GeneratedBindings+Counts.h"), + ]; + const rust = [...new Set(sources.bindgen.map(crateOf))].map(crate => + resolve(cfg.codegenDir, `generated_bindgen_${crate}.rs`), ); + const types = resolve(cfg.typesDir, "Bindgen.d.ts"); - // bindgen.ts scans src/ for .bind.ts files itself; this list only tells - // ninja the inputs and which headers come out. New .bind.ts files need a - // reconfigure to be picked up (next glob gets them). n.build({ - outputs: [cppOut, ...headers], - rule: "codegen_bun", - inputs: [script, ...sources.bindgen], + outputs: [...cpp, ...headers, ...rust, types], + rule: "codegen", + inputs: [script, ...sources.bindgen, ...sources.bindgenInternal], orderOnlyInputs: [dirStamp], vars: { cwd: cfg.cwd, - desc: ".bind.ts → GeneratedBindings.cpp", - args: shJoin(cfg, ["run", script, debugFlag(cfg), `--codegen-root=${cfg.codegenDir}`]), + desc: ".bind.ts → Generated*.{cpp,h}, generated_bindgen_*.rs", + args: shJoin(cfg, [ + script, + `--sources=${sources.bindgen.join(",")}`, + `--codegen-path=${cfg.codegenDir}`, + `--types-path=${cfg.typesDir}`, + ]), }, }); - o.all.push(cppOut, ...headers); - o.cppSources.push(cppOut); + o.all.push(...cpp, ...headers, ...rust, types); + o.generatedTypes.push(types); + o.rustInputs.push(...rust); + o.cppSources.push(...cpp); o.cppHeaders.push(...headers); } -/** Same transform as `pascal()` in src/codegen/bindgen-lib-internal.ts: `node_os` → `NodeOs`. */ +/** Same transform as `pascalCase()` in src/codegen/bindgen-lib-internal.ts: `node_os` → `NodeOs`. */ function pascalCase(s: string): string { - return s[0]!.toUpperCase() + s.slice(1).replace(/[_-](\w)?/g, (_, c?: string) => c?.toUpperCase() ?? ""); + return s + .split(/[^A-Za-z0-9]+/) + .filter(part => part.length > 0) + .map(part => part[0]!.toUpperCase() + part.slice(1)) + .join(""); +} + +/** Same lookup as `crateOf()` in src/codegen/bindgen.ts: the Cargo package whose directory contains `file`. */ +function crateOf(file: string): string { + for (let dir = dirname(file); dir !== dirname(dir); dir = dirname(dir)) { + const manifest = resolve(dir, "Cargo.toml"); + if (!existsSync(manifest)) continue; + const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(readFileSync(manifest, "utf8")); + if (name) return name[1]!; + } + throw new BuildError(`${file} is not inside a Cargo package`); } function emitJsSink({ n, cfg, o, dirStamp }: Ctx): void { diff --git a/scripts/glob-sources.ts b/scripts/glob-sources.ts index 19878c368d76..31b751d569bf 100644 --- a/scripts/glob-sources.ts +++ b/scripts/glob-sources.ts @@ -63,17 +63,13 @@ const patterns = { paths: ["src/runtime/bake/*.ts", "src/runtime/bake/*/*.{ts,css}", "src/runtime/bake/dev_server/mod.rs"], exclude: ["src/runtime/bake/generated.ts"], }, - /** legacy bindgen input */ + /** bindgen input */ bindgen: { paths: ["src/**/*.bind.ts"], }, - /** v2 bindgen input */ - bindgenV2: { - paths: ["src/**/*.bindv2.ts"], - }, - /** bindgen v2 generator code */ - bindgenV2Internal: { - paths: ["src/codegen/bindgenv2/**/*.ts"], + /** bindgen generator code */ + bindgenInternal: { + paths: ["src/codegen/bindgen*.ts", "src/codegen/helpers.ts"], }, /** * all `*.rs` + workspace manifests — implicit inputs to the cargo step. diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts new file mode 100644 index 000000000000..1a29777e1e12 --- /dev/null +++ b/src/codegen/bindgen-emit.ts @@ -0,0 +1,1229 @@ +// Turns the loaded schema into text: one C++ header and source per `.bind.ts` file, one table header +// for the whole program, one Rust file per crate, one `.d.ts`. + +import assert from "node:assert"; +import { + asciiLiteral, + ClassType, + DefaultType, + DictionaryType, + EnumType, + type FunctionDefinition, + type Getter, + type Method, + type ModuleDefinition, + NamedType, + OptionalType, + type Origin, + rustIdent, + type RustScope, + type Signature, + snakeCase, + type Type, + UnionType, + UnitType, + WithValueType, +} from "./bindgen-lib-internal.ts"; + +export interface SchemaFile { + origin: Origin; + moduleDefinition: ModuleDefinition | null; + /** In dependency order. */ + types: NamedType[]; + functions: FunctionDefinition[]; +} + +export interface Tables { + /** Made into `Identifier`s per VM on first use. */ + memberNames: string[]; + /** Made into `JSString`s per VM on first use. */ + enumerationValues: string[]; + /** Returned dictionaries: a run of `memberNames`, made into a `Structure` per global object on first use. */ + objectStructures: { firstMemberNameIndex: number; memberCount: number }[]; + classes: ClassType[]; +} + +const banner = (origin: Origin | null) => + `// Generated by src/codegen/bindgen.ts${origin ? ` from ${origin.stem}.bind.ts` : ""}. Do not edit.\n`; + +function indent(text: string, by: number): string { + const pad = " ".repeat(by); + return text + .split("\n") + .map(line => (line.length > 0 ? pad + line : line)) + .join("\n"); +} + +// ───────────────────────────── analysis ───────────────────────────── + +function walk(type: Type, visit: (type: Type) => void, seen = new Set()): void { + if (seen.has(type)) return; + seen.add(type); + visit(type); + for (const dependency of type.dependencies) walk(dependency, visit, seen); +} + +/** Marks which direction each dictionary travels and gives every table entry its index. */ +export function analyze(files: readonly SchemaFile[]): Tables { + const tables: Tables = { memberNames: [], enumerationValues: [], objectStructures: [], classes: [] }; + const markAccepted = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.accepted = true))); + const markReturned = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.returned = true))); + for (const file of files) { + for (const type of file.types) { + if (type instanceof DictionaryType && type.fromJS) markAccepted(type); + if (!(type instanceof ClassType)) continue; + type.classIndex = tables.classes.push(type) - 1; + for (const arg of type.construct?.args ?? []) markAccepted(arg.type); + for (const method of [...type.methods, ...type.staticMethods]) { + for (const arg of method.args) markAccepted(arg.type); + markReturned(method.returns); + } + for (const getter of type.getters) { + markReturned(getter.type); + if (getter.hasSetter) markAccepted(getter.type); + } + } + for (const fn of file.functions) { + for (const signature of fn.signatures) { + for (const arg of signature.args) markAccepted(arg.type); + markReturned(signature.returns); + } + } + assert( + file.functions.length === 0 || + !files.some(other => other.types.some(type => type.name === file.origin.namespace)), + `${file.origin.stem}.bind.ts: a type is named like the namespace of this file's functions`, + ); + } + for (const file of files) { + for (const type of file.types) { + if (type instanceof EnumType) { + type.firstEnumerationValueIndex = tables.enumerationValues.length; + tables.enumerationValues.push(...type.spellings.map(([canonical]) => canonical)); + } else if (type instanceof DictionaryType) { + if (type.accepted) { + type.firstMemberNameIndex = tables.memberNames.length; + tables.memberNames.push(...type.names); + } + if (type.returned) { + type.structureIndex = tables.objectStructures.length; + tables.objectStructures.push({ + firstMemberNameIndex: tables.memberNames.length, + memberCount: type.members.length, + }); + tables.memberNames.push(...type.members.map(member => member.name)); + const scope = type.origin!; + for (const member of type.members) { + assert( + !type.accepted || member.type.rustType(scope) === member.type.rustResultFieldType(scope), + `${type.name}.${member.name}: ${member.type.tsType} has different Rust types as an argument and as a result; split the dictionary`, + ); + } + } + } + } + } + const symbols = files.flatMap(file => [ + ...file.functions.flatMap(fn => fn.signatures.map(signature => thunkSymbol(fn, signature))), + ...file.types.flatMap(type => (type instanceof ClassType ? classSymbols(type) : [])), + ]); + assert(new Set(symbols).size === symbols.length, "two bindings make the same symbol"); + return tables; +} + +// ───────────────────────────── C++ ───────────────────────────── + +const implementationType = (type: Type) => `${type.idlType}::ImplementationType`; +const externType = (type: Type): string => + type instanceof WithValueType + ? `::Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` + : `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::ExternType`; + +function layoutAssert(cppType: string, type: Type): string { + const { size, align } = type.layout(); + return `static_assert(sizeof(${cppType}) == ${size} && alignof(${cppType}) == ${align});`; +} + +function humanReadableEnum(type: EnumType): string { + const quoted = type.spellings.map(([canonical]) => `\\"${canonical}\\"`); + if (quoted.length <= 2) return quoted.join(" or "); + return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; +} + +function cppHeaderForType(type: NamedType): string { + const { name } = type; + if (type instanceof ClassType) return cppHeaderForClass(type); + if (type instanceof EnumType) { + return ` +namespace Bun { +namespace Bindgen { +namespace Generated { +enum class ${name} : ::std::uint32_t { +${type.variants.map(variant => ` ${variant},`).join("\n")} +}; +} +template<> struct ExternTraits : TrivialExtern {}; +} +template<> struct IDLHumanReadableName<${type.idlType}> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = ::std::to_array("${humanReadableEnum(type)}"); +}; +} +template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String&); +template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue); +`; + } + if (type instanceof UnionType) { + return ` +namespace Bun::Bindgen::Generated { +using ${name} = ${implementationType(type)}; +${layoutAssert(`ExternTraits<${name}>::ExternType`, type)} +} +`; + } + assert(type instanceof DictionaryType); + if (!type.accepted) return ""; + return ` +namespace Bun { +namespace Bindgen { +namespace Generated { +struct ${name} { +${type.members.map(member => ` ${implementationType(member.type)} ${member.internalName};`).join("\n")} +}; +struct Extern${name} { +${type.members.map(member => ` ${externType(member.type)} ${member.internalName};`).join("\n")} +}; +${layoutAssert(`Extern${name}`, type)} +} +template<> struct ExternTraits { + using ExternType = Generated::Extern${name}; + static ExternType convertToExtern(Generated::${name}&& value) + { + return ExternType { +${type.members + .map( + member => + ` .${member.internalName} = ExternTraits<${implementationType(member.type)}>::convertToExtern(::std::move(value.${member.internalName})),`, + ) + .join("\n")} + }; + } +}; +} +template<> struct IDLHumanReadableName<::WebCore::IDLDictionary> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = ::std::to_array("${type.userFacingName}"); +}; +} +${ + type.holdsJSValue || type.borrowsFrame + ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : ::Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};\n` + : "" +}template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue); +`; +} + +export function cppHeader(file: SchemaFile): string { + const headers = new Set(); + for (const type of file.types) for (const dependency of type.dependencies) dependency.getHeaders(headers); + if (file.types.some(type => type instanceof ClassType)) headers.add("Bindgen/JSBindgenObject.h"); + headers.delete(`Generated${file.origin.namespace}.h`); + return `${banner(file.origin)}#pragma once +#include "root.h" +#include "Bindgen.h" +#include "Bindgen/IDLConvert.h" +#include "JSDOMConvertDictionary.h" +#include "JSDOMConvertEnumeration.h" +${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types.map(cppHeaderForType).join("")}${ + file.functions.length > 0 + ? ` +namespace Bun::Bindgen::Generated::${file.origin.namespace} { +${file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`).join("\n")} +} +` + : "" + }`; +} + +const hostFunctionName = (fn: FunctionDefinition) => + `js${fn.name[0] === "_" ? fn.name : fn.name[0].toUpperCase() + fn.name.slice(1)}`; + +function cppSourceForEnum(type: EnumType): string { + const pairs = type.spellings + .flatMap((spellings, index) => spellings.map(spelling => [spelling, type.variants[index]] as const)) + .sort(([a], [b]) => (a < b ? -1 : 1)); + const pair = `::std::pair<::WTF::ComparableASCIILiteral, ${type.cpp}>`; + return ` +template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) +{ + static constexpr ::WTF::SortedArrayMap mapping { ::std::to_array<${pair}>({ +${pairs.map(([spelling, variant]) => ` ${pair} { ${asciiLiteral(spelling)}, ${type.cpp}::${variant} },`).join("\n")} + }) }; + if (auto* value = mapping.tryGet(string)) [[likely]] + return *value; + return std::nullopt; +} + +template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) +{ + return parseEnumerationFromString<${type.cpp}>(value.toWTFString(&globalObject)); +} +`; +} + +function cppSourceForDictionary(type: DictionaryType): string { + if (!type.accepted) return ""; + let nameIndex = 0; + const members = type.members.map((member, i) => { + const reads = member.names.map((_, alt) => { + const read = `value${i} = members.get(${nameIndex++});\n RETURN_IF_EXCEPTION(scope, {});`; + return alt === 0 + ? read + : `if (value${i}.isUndefined()) {\n ${read.replace("\n", "\n ")}\n }`; + }); + const qualified = `${type.userFacingName}.${member.name}`; + const convert = `result.${member.internalName} = ${member.type.cppConvert(`value${i}`, `ctx${i}`, qualified)};\n RETURN_IF_EXCEPTION(scope, {});`; + let body: string; + if (member.type instanceof DefaultType) { + const absent = member.type.onNull ? "isUndefinedOrNull" : "isUndefined"; + body = `if (value${i}.${absent}()) { + result.${member.internalName} = ${member.type.toCpp(member.type.value)}; + } else { + ${convert.replace("\n", "\n ")} + }`; + } else if (member.type.permitsUndefined) { + body = convert; + } else { + body = `if (value${i}.isUndefined()) [[unlikely]] { + ctx${i}.throwRequired(lexicalGlobalObject, scope); + return {}; + } + ${convert}`; + } + return ` { + ::JSC::JSValue value${i}; + [[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} }; + ${reads.join("\n ")} + ${body} + }`; + }); + const count = type.names.length; + return ` +template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) +{ + auto* globalObject = &lexicalGlobalObject; + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; + auto* object = value.getObject(); + if (!object) [[unlikely]] { + ctx.throwNotObject(lexicalGlobalObject, scope); + return {}; + } + bool isArray = JSC::isArray(globalObject, value); + RETURN_IF_EXCEPTION(scope, {}); + if (isArray) [[unlikely]] { + ctx.throwNotObject(lexicalGlobalObject, scope); + return {}; + } + ::Bun::Bindgen::DictionaryMemberReader<${count}> members(vm, globalObject, object, ${type.firstMemberNameIndex}); + ${type.cpp} result; +${members.join("\n")} + return result; +} +${ + type.fromJS + ? ` +extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value, ::Bun::Bindgen::Generated::Extern${type.name}* result) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto converted = WebCore::convertDictionary<${type.cpp}>(*globalObject, JSC::JSValue::decode(value)); + RETURN_IF_EXCEPTION(scope, false); + *result = ::Bun::Bindgen::ExternTraits<${type.cpp}>::convertToExtern(::std::move(converted)); + return true; +} +` + : "" +}`; +} + +/** Told apart by argument count, so named by it. */ +const overloadSuffix = (fn: FunctionDefinition, signature: Signature) => + fn.signatures.length > 1 ? `With${signature.args.length}Arguments` : ""; + +const thunkSymbol = (fn: FunctionDefinition, signature: Signature) => + `Bun__Bindgen__${fn.origin!.namespace}__${fn.name.replace(/^_+/, "")}${overloadSuffix(fn, signature)}`; + +/** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */ +function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string { + const lines: string[] = []; + if (signature.requiredCount > 0) { + lines.push(`if (callFrame->argumentCount() < ${signature.requiredCount}) [[unlikely]] + return JSC::throwVMError(globalObject, scope, JSC::createNotEnoughArgumentsError(globalObject));`); + } + signature.args.forEach(({ name, type }, i) => { + const argument = i < signature.requiredCount ? "uncheckedArgument" : "argument"; + lines.push(`::JSC::EnsureStillAliveScope argument${i} = callFrame->${argument}(${i});`); + lines.push(`[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); + const convert = type.cppConvert(`argument${i}.value()`, `ctx${i}`, name); + if (type instanceof DefaultType) { + const absent = type.onNull ? "isUndefinedOrNull" : "isUndefined"; + lines.push( + `${implementationType(type)} value${i} = argument${i}.value().${absent}() ? ${type.toCpp(type.value)} : ${convert};`, + ); + } else { + lines.push(`${implementationType(type)} value${i} = ${convert};`); + } + lines.push(`RETURN_IF_EXCEPTION(scope, {});`); + }); + const args = signature.args.map(({ type }, i) => { + const converted = `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::convertToExtern(::std::move(value${i}))`; + return type instanceof WithValueType + ? `${externType(type)} { ${converted}, JSC::JSValue::encode(argument${i}.value()) }` + : converted; + }); + lines.push(`RELEASE_AND_RETURN(scope, ${symbol}(${["globalObject", ...leading, ...args].join(", ")}));`); + return lines.map(line => ` ${line}`).join("\n"); +} + +const cppPrologue = ` auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm);`; + +const cppExtern = (symbol: string, signature: Signature, leading: readonly string[] = []) => + `extern "C" JSC::EncodedJSValue ${symbol}(${["JSC::JSGlobalObject*", ...leading, ...signature.args.map(arg => externType(arg.type))].join(", ")});`; + +// ───────────────────────────── C++: classes ───────────────────────────── + +const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1); + +/** As generate-classes.ts spells them: the role is part of the type segment. */ +const symbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}__${member}`; +const classSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}Class__${member}`; +const protoSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}Prototype__${member}`; +const methodSymbolName = (type: ClassType, method: Method) => + (method.receiver === "static" ? classSymbolName : protoSymbolName)(type, method.name.replace(/^_+/, "")); +const getterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `get${capitalize(getter.name)}`); +const setterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `set${capitalize(getter.name)}`); + +const classSymbols = (type: ClassType) => [ + ...[...type.methods, ...type.staticMethods].map(method => methodSymbolName(type, method)), + ...type.getters.flatMap(getter => [getterSymbolName(type, getter), setterSymbolName(type, getter)]), + classSymbolName(type, "construct"), + classSymbolName(type, "finalize"), +]; + +const methodHostFunctionName = (type: ClassType, method: Method) => + `js${type.name}${method.receiver === "static" ? "Constructor" : "Prototype"}Function_${method.name.replace(/^_+/, "")}`; +const getterHostFunctionName = (type: ClassType, getter: Getter) => `js${type.name}PrototypeGetter_${getter.name}`; +const setterHostFunctionName = (type: ClassType, getter: Getter) => `js${type.name}PrototypeSetter_${getter.name}`; + +function cppHeaderForClass(type: ClassType): string { + const { name } = type; + return ` +extern "C" const size_t ${symbolName(type, "payloadSize")}; +extern "C" void ${classSymbolName(type, "finalize")}(void*); +namespace Bun::Bindgen::Generated { +struct ${name}Traits { + static constexpr auto className = ${asciiLiteral(name)}; + static constexpr unsigned classIndex = ${type.classIndex}; + static constexpr unsigned numberOfInternalFields = ${type.internalFields.length}; + static constexpr unsigned numberOfValues = ${type.values.length}; + static size_t payloadSize() { return ${symbolName(type, "payloadSize")}; } + static void finalize(void* payload) { ${classSymbolName(type, "finalize")}(payload); } +}; +using JS${name} = JSBindgenObject<${name}Traits>; +extern const ClassDefinition JS${name}ClassDefinition; +} +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { + static constexpr auto humanReadableName = ::std::to_array("${name}"); +}; +`; +} + +function cppMethod(type: ClassType, method: Method): string { + const host = methodHostFunctionName(type, method); + const symbol = methodSymbolName(type, method); + const isStatic = method.receiver === "static"; + return ` +${cppExtern(symbol, method, isStatic ? [] : ["JSC::JSCell*"])} +static JSC_DECLARE_HOST_FUNCTION(${host}); +JSC_DEFINE_HOST_FUNCTION(${host}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +${cppPrologue} +${ + isStatic + ? "" + : ` auto* thisObject = dynamicDowncast<${type.cpp}>(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + RELEASE_AND_RETURN(scope, ::Bun::throwInvalidThisCallError(globalObject, callFrame, ${asciiLiteral(type.name)})); +` +}${cppCallBody(method, symbol, isStatic ? [] : ["thisObject"])} +} +`; +} + +function cppGetter(type: ClassType, getter: Getter): string { + const get = getterHostFunctionName(type, getter); + const set = setterHostFunctionName(type, getter); + const getSymbol = getterSymbolName(type, getter); + const setSymbol = setterSymbolName(type, getter); + let text = ` +extern "C" JSC::EncodedJSValue ${getSymbol}(JSC::JSGlobalObject*, JSC::JSCell*); +static JSC_DECLARE_CUSTOM_GETTER(${get}); +JSC_DEFINE_CUSTOM_GETTER(${get}, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ +${cppPrologue} + // DOMAttribute: JSC has checked the receiver's ClassInfo. + auto* thisObject = uncheckedDowncast<${type.cpp}>(JSC::JSValue::decode(thisValue)); + ::JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; +${ + getter.internalFieldIndex < 0 + ? ` RELEASE_AND_RETURN(scope, ${getSymbol}(globalObject, thisObject));` + : ` auto& cachedValue = thisObject->internalField(${getter.internalFieldIndex}); + if (JSC::JSValue cached = cachedValue.get()) + return JSC::JSValue::encode(cached); + JSC::JSValue result = JSC::JSValue::decode(${getSymbol}(globalObject, thisObject)); + RETURN_IF_EXCEPTION(scope, {}); + cachedValue.set(vm, thisObject, result); + return JSC::JSValue::encode(result);` +} +} +`; + if (getter.hasSetter) { + text += ` +extern "C" bool ${setSymbol}(JSC::JSGlobalObject*, JSC::JSCell*, ${externType(getter.type)}); +static JSC_DECLARE_CUSTOM_SETTER(${set}); +JSC_DEFINE_CUSTOM_SETTER(${set}, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName attributeName)) +{ +${cppPrologue} + auto* thisObject = dynamicDowncast<${type.cpp}>(JSC::JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] { + JSC::throwDOMAttributeSetterTypeError(globalObject, scope, ${type.cpp}::info(), attributeName); + return false; + } + ::JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; + ::JSC::EnsureStillAliveScope ensureValueStillAlive { JSC::JSValue::decode(encodedValue) }; + [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }; + ${implementationType(getter.type)} value = ${getter.type.cppConvert("ensureValueStillAlive.value()", "ctx", getter.name)}; + RETURN_IF_EXCEPTION(scope, false); + RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, thisObject, ::Bun::Bindgen::ExternTraits<${implementationType(getter.type)}>::convertToExtern(::std::move(value)))); +} +`; + } + return text; +} + +function cppSourceForClass(type: ClassType): string { + const { name } = type; + const dontDelete = type.configurable ? "" : " | JSC::PropertyAttribute::DontDelete"; + const row = (key: string, attributes: string, value: string) => + ` { ${asciiLiteral(key)}, static_cast(${attributes}), JSC::NoIntrinsic, { ${value} } },`; + const methodRow = (method: Method) => + row( + method.name, + `JSC::PropertyAttribute::Function${dontDelete}`, + `JSC::HashTableValue::NativeFunctionType, ${methodHostFunctionName(type, method)}, ${method.length ?? method.requiredCount}`, + ); + const prototypeRows = [ + ...type.methods.filter(method => !method.privateSymbol).map(methodRow), + ...type.getters.map(getter => + row( + getter.name, + `${getter.hasSetter ? "" : "JSC::PropertyAttribute::ReadOnly | "}JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${dontDelete}`, + `JSC::HashTableValue::GetterSetterType, ${getterHostFunctionName(type, getter)}, ${getter.hasSetter ? setterHostFunctionName(type, getter) : "0"}`, + ), + ), + ...type.builtins.map(builtin => + row( + builtin.name, + "JSC::PropertyAttribute::Builtin", + `JSC::HashTableValue::BuiltinGeneratorType, ::WebCore::${builtin.builtin}, ${builtin.length}`, + ), + ), + ]; + const constructorRows = type.staticMethods.map(methodRow); + const table = (id: string, rows: string[]) => + rows.length > 0 ? `static const JSC::HashTableValue ${id}[] = {\n${rows.join("\n")}\n};\n` : ""; + const span = (id: string, rows: string[]) => (rows.length > 0 ? id : "{}"); + const privateMethods = type.methods.filter(method => method.privateSymbol); + const construct = type.construct; + return ` +${[...type.methods, ...type.staticMethods].map(method => cppMethod(type, method)).join("")}${type.getters.map(getter => cppGetter(type, getter)).join("")} +${ + construct + ? `${cppExtern(classSymbolName(type, "construct"), construct, ["JSC::Structure*"])} +static JSC_DECLARE_HOST_FUNCTION(construct${name}); +JSC_DEFINE_HOST_FUNCTION(construct${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +${cppPrologue} + JSC::Structure* structure = ::Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); + RETURN_IF_EXCEPTION(scope, {}); +${cppCallBody(construct, classSymbolName(type, "construct"), ["structure"])} +} +` + : "" +} +${table(`JS${name}PrototypeTableValues`, prototypeRows)}${table(`JS${name}ConstructorTableValues`, constructorRows)}${ + privateMethods.length > 0 + ? `static void finish${name}PrototypeCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* prototype) +{ + auto& names = ::WebCore::builtinNames(vm); +${privateMethods + .map( + method => + ` prototype->putDirect(vm, names.${method.privateSymbol}PrivateName(), JSC::JSFunction::create(vm, globalObject, ${method.length ?? method.requiredCount}, ${asciiLiteral(method.name)}, ${methodHostFunctionName(type, method)}, JSC::ImplementationVisibility::Public), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | 0);`, + ) + .join("\n")} +} +` + : "" + } +const ::Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { + .className = ${asciiLiteral(name)}, + .info = ${type.cpp}::info(), + .createStructure = ${type.cpp}::createStructure, + .prototypeTableValues = ${span(`JS${name}PrototypeTableValues`, prototypeRows)}, + .constructorTableValues = ${span(`JS${name}ConstructorTableValues`, constructorRows)}, + .finishPrototypeCreation = ${privateMethods.length > 0 ? `finish${name}PrototypeCreation` : "nullptr"}, + .construct = ${construct ? `construct${name}` : "nullptr"}, + .constructorLength = ${construct ? (construct.length ?? construct.requiredCount) : 0}, +}; + +extern "C" JSC::JSCell* ${symbolName(type, "create")}(JSC::JSGlobalObject* globalObject, JSC::Structure* structure) +{ + static_assert(${type.cpp}::offsetOfInternalFields() == ${offsetOfInternalFields} && ${type.cpp}::offsetOfPayload() == ${offsetOfPayload(type)}); + return ${type.cpp}::create(JSC::getVM(globalObject), structure ? structure : ::Bun::Bindgen::structureForClass(globalObject, ${type.classIndex})); +} + +extern "C" JSC::JSCell* ${symbolName(type, "fromJS")}(JSC::EncodedJSValue value) +{ + return dynamicDowncast<${type.cpp}>(JSC::JSValue::decode(value)); +} +${ + construct + ? ` +extern "C" JSC::EncodedJSValue ${symbolName(type, "getConstructor")}(JSC::JSGlobalObject* globalObject) +{ + return JSC::JSValue::encode(::Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); +} +extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${type.origin!.stem}_${name}(Zig::GlobalObject* globalObject) +{ + return JSC::JSValue::encode(::Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); +} +` + : "" +}`; +} + +/** sizeof(JSC::JSDestructibleObject) */ +const offsetOfInternalFields = 24; +const offsetOfPayload = (type: ClassType) => offsetOfInternalFields + 8 * type.internalFields.length; + +function cppSourceForFunction(fn: FunctionDefinition): string { + const externs = fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature)).join("\n"); + const name = hostFunctionName(fn); + let definition: string; + if (fn.signatures.length === 1) { + definition = `JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +${cppPrologue} +${cppCallBody(fn.signatures[0], thunkSymbol(fn, fn.signatures[0]))} +}`; + } else { + const overloads = fn.signatures.map( + signature => `static JSC::EncodedJSValue ${name}${overloadSuffix(fn, signature)}(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) +{ +${cppPrologue} +${cppCallBody(signature, thunkSymbol(fn, signature))} +}`, + ); + const dispatch = fn.signatures.map((signature, index) => + index === fn.signatures.length - 1 + ? ` RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, signature)}(globalObject, callFrame));` + : ` if (callFrame->argumentCount() >= ${signature.requiredCount}) + RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, signature)}(globalObject, callFrame));`, + ); + definition = `${overloads.join("\n")} +JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +${cppPrologue} +${dispatch.join("\n")} +}`; + } + return ` +${externs} +namespace Bun::Bindgen::Generated::${fn.origin!.namespace} { +${definition} +} +extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject) +{ + return JSC::JSValue::encode(JSC::JSFunction::create(globalObject->vm(), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}, JSC::ImplementationVisibility::Public)); +} +`; +} + +export function cppSource(file: SchemaFile): string { + const headers = new Set(); + for (const fn of file.functions) { + for (const signature of fn.signatures) for (const arg of signature.args) arg.type.getHeaders(headers); + } + for (const type of file.types) { + if (!(type instanceof ClassType)) continue; + for (const used of type.signatureTypes) used.getHeaders(headers); + if (type.builtins.length > 0) headers.add("WebCoreJSBuiltins.h"); + } + headers.delete(`Generated${file.origin.namespace}.h`); + return `${banner(file.origin)}#include "root.h" +#include "Generated${file.origin.namespace}.h" +#include "Bindgen/DictionaryMemberReader.h" +#include "ZigGlobalObject.h" +#include +#include +#include +${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types + .map(type => + type instanceof EnumType + ? cppSourceForEnum(type) + : type instanceof DictionaryType + ? cppSourceForDictionary(type) + : type instanceof ClassType + ? cppSourceForClass(type) + : "", + ) + .join("")}${file.functions.map(cppSourceForFunction).join("")}`; +} + +/** Included by headers most of the C++ includes, so it holds only what sizes their members. */ +export function cppCounts(tables: Tables): string { + return `${banner(null)}#pragma once + +namespace Bun::Bindgen::Counts { +inline constexpr unsigned numberOfClassSubspaces = ${Math.max(tables.classes.length, 1)}; +} +`; +} + +export function cppTables(tables: Tables): string { + const list = (items: readonly string[]) => (items.length > 0 ? items.join(",\n ") : "{}"); + const length = (items: readonly unknown[]) => Math.max(items.length, 1); + return `${banner(null)}#pragma once +#include "Bindgen/JSBindgenObject.h" +#include + +namespace Bun::Bindgen::Generated { +${tables.classes.map(type => `extern const ClassDefinition JS${type.name}ClassDefinition;`).join("\n")} +} + +namespace Bun::Bindgen::Tables { + +inline constexpr unsigned memberNameCount = ${tables.memberNames.length}; +inline constexpr WTF::ASCIILiteral memberNames[${length(tables.memberNames)}] = { + ${list(tables.memberNames.map(asciiLiteral))} +}; + +inline constexpr unsigned enumerationValueCount = ${tables.enumerationValues.length}; +inline constexpr WTF::ASCIILiteral enumerationValues[${length(tables.enumerationValues)}] = { + ${list(tables.enumerationValues.map(asciiLiteral))} +}; + +struct ObjectStructure { + unsigned firstMemberNameIndex; + unsigned memberCount; +}; +inline constexpr unsigned objectStructureCount = ${tables.objectStructures.length}; +inline constexpr ObjectStructure objectStructures[${length(tables.objectStructures)}] = { + ${list(tables.objectStructures.map(structure => `{ ${structure.firstMemberNameIndex}, ${structure.memberCount} }`))} +}; + +inline constexpr unsigned classCount = ${tables.classes.length}; +inline constexpr const ClassDefinition* classes[${length(tables.classes)}] = { + ${list(tables.classes.map(type => `&Generated::JS${type.name}ClassDefinition`))} +}; + +} +`; +} + +// ───────────────────────────── Rust ───────────────────────────── + +const jsValueResult = "bindgen::Result<'a, bindgen::JsValue<'a>>"; + +function rustLayoutAssert(rustType: string, type: Type): string { + const { size, align } = type.layout(); + return `const _: () = assert!(size_of::<${rustType}>() == ${size} && align_of::<${rustType}>() == ${align});`; +} + +/** Names the generated thunks use themselves. */ +const reservedArgumentNames = new Set(["global", "this", "structure", "cx", "heap", "result"]); + +function rustArgumentName(arg: { name: string }): string { + const name = snakeCase(arg.name); + assert(!reservedArgumentNames.has(name), `an argument cannot be named ${arg.name}`); + return rustIdent(name); +} + +const rustParameters = (signature: Signature, scope: RustScope) => + signature.args.map(arg => `${rustArgumentName(arg)}: ${arg.type.rustType(scope)}`); +const rustExternParameters = (signature: Signature, scope: RustScope) => + signature.args.map(arg => `${rustArgumentName(arg)}: ${arg.type.rustExternType(scope)}`); +const rustArguments = (signature: Signature, scope: RustScope) => + signature.args.map(arg => arg.type.rustFromExtern(rustArgumentName(arg), scope)); + +const capabilityParameter = { + none: [], + heap: ["heap: &bindgen::JsHeap<'a>"], + js: ["cx: &mut bindgen::JsCall<'a>"], +}; +const capabilityArgument = { none: [], heap: ["&cx.heap"], js: ["cx"] }; + +const infallible = (call: string, throws: boolean) => (throws ? call : `Ok::<_, core::convert::Infallible>(${call})`); + +const rustThunk = (symbol: string, parameters: string[], returns: string, body: string) => ` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn ${symbol}(${["global: *mut JSGlobalObject", ...parameters].join(", ")}) -> ${returns} { + // SAFETY: called by generated C++, whose frame holds \`this\` and the arguments until this returns. + unsafe { + bindgen::JsCall::with(global, |cx| { +${indent(body, 12)} + }) + } +} +`; + +function rustForEnum(type: EnumType): string { + return ` +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ${type.name} { +${type.variants.map((variant, i) => ` /// \`${JSON.stringify(type.spellings[i][0])}\`\n ${variant} = ${i},`).join("\n")} +} +impl ${type.name} { + #[inline] + pub fn to_js<'a>(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + bindgen::to_js::enumeration_value(heap, ${type.firstEnumerationValueIndex} + self as u32) + } +} +`; +} + +function rustForUnion(type: UnionType, scope: RustScope): string { + const lifetime = type.needsLifetime ? "<'a>" : ""; + return ` +#[repr(C)] +#[derive(Clone, Copy)] +pub struct Extern${type.name} { + data: Extern${type.name}Data, + tag: u8, +} +#[repr(C)] +#[derive(Clone, Copy)] +union Extern${type.name}Data { +${type.alternatives.map(a => ` ${rustIdent(snakeCase(a.name))}: ${a.type.rustExternType(scope)},`).join("\n")} +} +${rustLayoutAssert(`Extern${type.name}`, type)} +pub enum ${type.name}${lifetime} { +${type.alternatives.map(a => (a.type instanceof UnitType ? ` ${a.variant},` : ` ${a.variant}(${a.type.rustType(scope)}),`)).join("\n")} +} +impl${lifetime} ${type.name}${lifetime} { + /// # Safety + /// \`value\` was just filled in by generated C++ and is read once. + #[doc(hidden)] + pub unsafe fn from_extern(value: Extern${type.name}) -> Self { + // SAFETY: C++ initialized the arm \`tag\` names. + unsafe { + match value.tag { +${type.alternatives + .map((a, i) => + a.type instanceof UnitType + ? ` ${i} => Self::${a.variant},` + : ` ${i} => Self::${a.variant}(${a.type.rustFromExtern(`value.data.${rustIdent(snakeCase(a.name))}`, scope)}),`, + ) + .join("\n")} + _ => core::hint::unreachable_unchecked(), + } + } + } +} +`; +} + +function rustForDictionary(type: DictionaryType, scope: RustScope): string { + const lifetime = type.needsLifetime ? "<'a>" : ""; + const fieldType = (t: Type) => (type.accepted ? t.rustType(scope) : t.rustResultFieldType(scope)); + let text = ` +pub struct ${type.name}${lifetime} { +${type.members.map(member => ` pub ${member.rustName}: ${fieldType(member.type)},`).join("\n")} +} +`; + if (type.accepted && type.hasDefault) { + const value = (t: Type) => t.toRust(t instanceof DefaultType ? t.value : undefined, scope); + text += `impl${lifetime} Default for ${type.name}${lifetime} { + fn default() -> Self { + Self { +${type.members.map(member => ` ${member.rustName}: ${value(member.type)},`).join("\n")} + } + } +} +`; + } + if (type.accepted) { + text += `#[repr(C)] +#[derive(Clone, Copy)] +pub struct Extern${type.name} { +${type.members.map(member => ` ${member.rustName}: ${member.type.rustExternType(scope)},`).join("\n")} +} +${rustLayoutAssert(`Extern${type.name}`, type)} +impl${lifetime} ${type.name}${lifetime} { + /// # Safety + /// \`value\` was just filled in by generated C++ and is read once. + #[doc(hidden)] + pub unsafe fn from_extern(value: Extern${type.name}) -> Self { + // SAFETY: as above, member by member. + unsafe { + Self { +${type.members.map(member => ` ${member.rustName}: ${member.type.rustFromExtern(`value.${member.rustName}`, scope)},`).join("\n")} + } + } + } +} +`; + } + if (type.fromJS) { + text += `unsafe extern "C" { + fn Bun__Bindgen__${type.name}__fromJS(global: *mut JSGlobalObject, value: JSValue, result: *mut Extern${type.name}) -> bool; +} +impl<'a> ${type.name}${lifetime} { + /// Reads getters and proxies, so it can run JavaScript. + pub fn from_js(cx: &mut bindgen::JsCall<'a>, value: bindgen::JsValue<'a, T>) -> bindgen::Result<'a, Self> { + let mut result = core::mem::MaybeUninit::::uninit(); + // SAFETY: C++ fills \`result\` exactly when it returns true. + unsafe { + bindgen::extern_types::from_js(cx, |global| Bun__Bindgen__${type.name}__fromJS(global, value.to_js(), result.as_mut_ptr()))?; + Ok(Self::from_extern(result.assume_init())) + } + } +} +`; + } + if (type.returned) { + const value = (member: (typeof type.members)[number]) => { + const field = `self.${member.rustName}`; + if (member.type instanceof OptionalType && member.type.kind === "optional") { + return `match ${field} { Some(value) => ${member.type.payload.rustToJS("value", scope)}?.to_js(), None => JSValue::ZERO }`; + } + return `${member.type.rustToJS(field, scope)}?.to_js()`; + }; + text += `impl<'a> ${type.name}${lifetime} { + pub fn to_js(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + let values = [ +${type.members.map(member => ` ${value(member)},`).join("\n")} + ]; + bindgen::to_js::object_with_structure(heap, ${type.structureIndex}, &values) + } +} +`; + } + return text; +} + +function rustForClass(type: ClassType, scope: RustScope): string { + const { name, rustPath } = type; + const trait = `${name}Class`; + const result = (value: string, throws: boolean) => (throws ? `Result<${value}, Self::Error<'a>>` : value); + const anyThrows = + !!type.construct?.throws || + [...type.methods, ...type.staticMethods].some(method => method.throws) || + type.getters.some(getter => getter.throws); + const receiverParameter = { + ref: ["&self"], + mut: ["&mut self"], + wrapper: ["this: bindgen::JsWrapper<'a, Self>"], + static: [], + }; + const receiverArgument = { ref: ["this.payload_ref()"], mut: ["this.payload_mut()"], wrapper: ["this"], static: [] }; + const takeThis = `let this = bindgen::JsWrapper::<${rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(this));`; + const declare = (fn: string, parameters: string[], returns: string) => { + const signature = `(${parameters.join(", ")}) -> ${returns};`; + return `fn ${fn}${signature.includes("'a") ? "<'a>" : ""}${signature}`; + }; + + const declarations: string[] = []; + if (anyThrows) + declarations.push("/// What the members declared `throws` fail with.\n type Error<'a>: bindgen::IntoError<'a>;"); + let thunks = ""; + + if (type.construct) { + const signature = type.construct; + declarations.push( + declare( + "constructor", + [...capabilityParameter[signature.capability], ...rustParameters(signature, scope)], + result("Self", signature.throws), + ), + ); + const call = `<${rustPath} as ${trait}>::constructor(${[...capabilityArgument[signature.capability], ...rustArguments(signature, scope)].join(", ")})`; + thunks += rustThunk( + classSymbolName(type, "construct"), + ["structure: *mut core::ffi::c_void", ...rustExternParameters(signature, scope)], + "JSValue", + `let result = ${infallible(call, signature.throws)}; +bindgen::host_fn_return(cx, result, |heap, value| bindgen::to_js::any(heap, bindgen::JsWrapper::create_with_structure(heap, structure, value).as_value()))`, + ); + declarations.push(`/// The constructor function, \`${name}\` in JavaScript. + fn get_constructor<'a>(heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { + // SAFETY: cannot throw; makes the class's objects on first use. + unsafe { bindgen::JsValue::from_extern(${symbolName(type, "getConstructor")}(heap.global_ptr())) } + }`); + } + + for (const method of [...type.methods, ...type.staticMethods]) { + const isStatic = method.receiver === "static"; + declarations.push( + declare( + method.rustName, + [ + ...receiverParameter[method.receiver], + ...(method.passThis ? ["this: bindgen::JsWrapper<'a, Self>"] : []), + ...capabilityParameter[method.capability], + ...rustParameters(method, scope), + ], + result(method.returns.rustReturnType(scope), method.throws), + ), + ); + const call = `<${rustPath} as ${trait}>::${method.rustName}(${[ + ...receiverArgument[method.receiver], + ...(method.passThis ? ["this"] : []), + ...capabilityArgument[method.capability], + ...rustArguments(method, scope), + ].join(", ")})`; + thunks += rustThunk( + methodSymbolName(type, method), + [...(isStatic ? [] : ["this: *mut u8"]), ...rustExternParameters(method, scope)], + "JSValue", + `${isStatic ? "" : `${takeThis}\n`}let result = ${infallible(call, method.throws)}; +bindgen::host_fn_return(cx, result, |heap, value| ${method.returns.rustToJS("value", scope)})`, + ); + } + + for (const getter of type.getters) { + const receiver = getter.capability === "js" ? "wrapper" : "ref"; + declarations.push( + declare( + getter.rustName, + [...receiverParameter[receiver], ...capabilityParameter[getter.capability]], + result(getter.type.rustReturnType(scope), getter.throws), + ), + ); + thunks += rustThunk( + getterSymbolName(type, getter), + ["this: *mut u8"], + "JSValue", + `${takeThis} +let result = ${infallible(`<${rustPath} as ${trait}>::${getter.rustName}(${[...receiverArgument[receiver], ...capabilityArgument[getter.capability]].join(", ")})`, getter.throws)}; +bindgen::host_fn_return(cx, result, |heap, value| ${getter.type.rustToJS("value", scope)})`, + ); + if (!getter.hasSetter) continue; + const setterReceiver = getter.capability === "js" ? "wrapper" : "mut"; + const setterName = `set_${snakeCase(getter.name)}`; + declarations.push( + declare( + setterName, + [ + ...receiverParameter[setterReceiver], + ...capabilityParameter[getter.capability], + `value: ${getter.type.rustType(scope)}`, + ], + result("()", getter.throws), + ), + ); + thunks += rustThunk( + setterSymbolName(type, getter), + ["this: *mut u8", `value: ${getter.type.rustExternType(scope)}`], + "bool", + `${takeThis} +let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiverArgument[setterReceiver], ...capabilityArgument[getter.capability], getter.type.rustFromExtern("value", scope)].join(", ")})`, getter.throws)}; +!bindgen::host_fn_return(cx, result, bindgen::to_js::undefined).is_empty()`, + ); + } + + const valueDeclarations = type.values + .map(value => { + const id = snakeCase(value); + return ` fn ${id}_get_cached(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a>; + fn ${id}_set_cached(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>);`; + }) + .join("\n"); + const valueDefinitions = type.values + .map((value, index) => { + const id = snakeCase(value); + return ` #[inline(always)] + fn ${id}_get_cached(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { + // SAFETY: internal field ${index} of ${type.internalFields.length}. + unsafe { self.internal_field(heap, ${index}) } + } + #[inline(always)] + fn ${id}_set_cached(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>) { + // SAFETY: internal field ${index} of ${type.internalFields.length}. + unsafe { self.set_internal_field(heap, ${index}, value) } + }`; + }) + .join("\n"); + + return ` +/// What \`${rustPath}\` implements to be the JS class \`${name}\`. +pub trait ${trait}: bindgen::JsPayload { +${declarations.map(declaration => ` ${declaration}`).join("\n")} +} + +unsafe extern "C" { + fn ${symbolName(type, "create")}(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull; + fn ${symbolName(type, "fromJS")}(value: JSValue) -> *mut u8;${ + type.construct + ? ` + fn ${symbolName(type, "getConstructor")}(global: *mut JSGlobalObject) -> JSValue;` + : "" + } +} +// SAFETY: the offsets are \`static_assert\`ed against the C++ cell. +unsafe impl bindgen::JsPayload for ${rustPath} { + const INTERNAL_FIELDS_OFFSET: usize = ${offsetOfInternalFields}; + const PAYLOAD_OFFSET: usize = ${offsetOfPayload(type)}; + #[inline(always)] + unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull { + // SAFETY: forwarded. + unsafe { ${symbolName(type, "create")}(global, structure) } + } + #[inline(always)] + fn cell_from_js(value: JSValue) -> *mut u8 { + // SAFETY: a type check; reads the value's header. + unsafe { ${symbolName(type, "fromJS")}(value) } + } +} +// Cells are 16-byte aligned and the payload starts at a multiple of 8. A cell fits in half a MarkedBlock. +const _: () = assert!(align_of::<${rustPath}>() <= 8 && size_of::<${rustPath}>() <= 8000); +#[unsafe(no_mangle)] +pub static ${symbolName(type, "payloadSize")}: usize = size_of::<${rustPath}>(); +#[unsafe(no_mangle)] +pub unsafe extern "C" fn ${classSymbolName(type, "finalize")}(payload: *mut ${rustPath}) { + // SAFETY: the sweeper calls this once, on an initialized payload nothing refers to. + unsafe { core::ptr::drop_in_place(payload) } +} +${ + type.values.length > 0 + ? `/// The JS values a \`${name}\` holds. +pub trait ${name}Values<'a>: Copy { +${valueDeclarations} +} +impl<'a> ${name}Values<'a> for bindgen::JsWrapper<'a, ${rustPath}> { +${valueDefinitions} +} +` + : "" +}${thunks}`; +} + +function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string { + const scope = file.origin; + return fn.signatures + .map(signature => { + let path = signature.rustPath; + if (!path) { + assert( + file.moduleDefinition, + `${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`, + ); + path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`; + } + const call = `${path}(${[...capabilityArgument[signature.capability], ...rustArguments(signature, scope)].join(", ")})`; + return rustThunk( + thunkSymbol(fn, signature), + rustExternParameters(signature, scope), + "JSValue", + `let result = ${infallible(call, signature.throws)}; +bindgen::host_fn_return(cx, result, |heap, value| ${signature.returns.rustToJS("value", scope)})`, + ); + }) + .join(""); +} + +export function rustForCrate(files: readonly SchemaFile[]): string { + return ( + banner(null) + + files + .map( + file => ` +/// \`${file.origin.stem}.bind.ts\` +pub mod ${file.origin.rustModule} { + use bun_jsc::{JSGlobalObject, JSValue, bindgen}; +${indent( + file.types + .map(type => + type instanceof EnumType + ? rustForEnum(type) + : type instanceof UnionType + ? rustForUnion(type, file.origin) + : type instanceof ClassType + ? rustForClass(type, file.origin) + : rustForDictionary(type as DictionaryType, file.origin), + ) + .join("") + file.functions.map(fn => rustForFunction(fn, file)).join(""), + 4, +)}} +`, + ) + .join("") + ); +} + +// ───────────────────────────── TypeScript ───────────────────────────── + +export function dts(files: readonly SchemaFile[]): string { + const parameters = (signature: Signature) => + signature.args.map(arg => `${arg.name}${arg.type.permitsUndefined ? "?" : ""}: ${arg.type.tsType}`).join(", "); + const signatureType = (signature: Signature) => `(${parameters(signature)}) => ${signature.returns.tsType}`; + const member = (name: string, optional: boolean, type: string) => + ` ${JSON.stringify(name)}${optional ? "?" : ""}: ${type};`; + const declarations = files.flatMap(file => + file.types.map(type => { + if (type instanceof DictionaryType) { + return ` interface ${type.name} {\n${type.members.map(m => member(m.name, m.type.permitsUndefined, m.type.tsType)).join("\n")}\n }`; + } + if (!(type instanceof ClassType)) return ""; + return ` interface ${type.name} { +${[ + ...type.methods + .filter(method => !method.privateSymbol) + .map(method => member(method.name, false, signatureType(method))), + ...type.getters.map( + getter => ` ${getter.hasSetter ? "" : "readonly "}${JSON.stringify(getter.name)}: ${getter.type.tsType};`, + ), +].join("\n")} + } + interface ${type.name}Constructor { +${[ + ...(type.construct ? [` new (${parameters(type.construct)}): ${type.name};`] : []), + ` readonly prototype: ${type.name};`, + ...type.staticMethods.map(method => member(method.name, false, signatureType(method))), +].join("\n")} + }`; + }), + ); + const exported = (file: SchemaFile) => [ + ...file.functions.map( + fn => ` ${JSON.stringify(fn.name)}: ${fn.signatures.map(s => `(${signatureType(s)})`).join(" & ")};`, + ), + ...file.types + .filter(type => type instanceof ClassType && type.construct) + .map(type => ` ${JSON.stringify(type.name)}: ${type.name}Constructor;`), + ]; + return `${banner(null)}declare namespace $Bindgen { +${declarations.filter(text => text.length > 0).join("\n")} + /** What \`$bindgenFn(file, name)\` returns. */ + interface Exports { +${files + .filter(file => exported(file).length > 0) + .map(file => ` ${JSON.stringify(`${file.origin.stem}.bind.ts`)}: {\n${exported(file).join("\n")}\n };`) + .join("\n")} + } +} +`; +} diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 99677b16d47b..d6281d1a0e0d 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1,1061 +1,1296 @@ -// While working on this file, it is important to have very rigorous errors -// and checking on input data. The goal is to allow people not aware of -// various footguns in JavaScript, C++, and the bindings generator to -// always produce correct code, or bail with an error. -import { expect } from "bun:test"; -import assert from "node:assert"; -import * as path from "node:path"; -import type { FuncOptions, t } from "./bindgen-lib"; - -export const src = path.join(import.meta.dirname, "../"); - -export type TypeKind = keyof typeof t; +// The type model behind `bindgen-lib.ts`. Each type knows four things: +// +// idlType the C++ tag type whose `WebCore::Converter` turns a JSValue into a C++ value +// (src/jsc/bindings/BunIDL*.h, Bindgen/IDL*.h, webcore/JSDOMConvert*.h) +// rustExternType the Rust mirror of what `Bun::Bindgen::ExternTraits` lowers that C++ value to +// (src/jsc/bindings/Bindgen/Extern*.h, src/jsc/bindgen/extern_types.rs), with its C layout +// rustType the type a Rust body sees, and `rustFromExtern`, the expression that gets there +// rustToJS for types that can be returned: the expression that converts back -export let files = new Map(); -/** A reachable type is one that is required for code generation */ -export let typeHashToReachableType = new Map(); -export let typeHashToStruct = new Map(); -export let typeHashToNamespace = new Map(); -export let structHashToSelf = new Map(); +import assert from "node:assert"; +import util from "node:util"; -/** String literal */ -export const str = (v: any) => JSON.stringify(v); -/** Capitalize */ -export const cap = (s: string) => s[0].toUpperCase() + s.slice(1); -/** Camel Case */ -export const camel = (s: string) => - s[0].toLowerCase() + s.slice(1).replace(/[_-](\w)?/g, (_, letter) => letter?.toUpperCase() ?? ""); -/** Pascal Case */ -export const pascal = (s: string) => cap(camel(s)); +export interface Layout { + size: number; + align: number; +} -// Return symbol names of extern values (must be equivalent on both sides of the FFI boundary) +/** Where a named type or function was declared. */ +export interface Origin { + /** Absolute path of the `.bind.ts` file. */ + file: string; + /** `node_os` for `node_os.bind.ts`. */ + stem: string; + /** `NodeOs`: the C++ namespace of the file's functions, and `Generated.h`. */ + namespace: string; + /** Rust module the file's items are generated into. */ + rustModule: string; + /** Cargo package that owns the file. */ + crate: string; +} -/** The JS Host function, aka fn (*JSC.JSGlobalObject, *JSC.CallFrame) JSValue.MaybeException */ -export const extJsFunction = (namespaceVar: string, fnLabel: string) => - `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}`; -/** Each variant gets a dispatcher function. */ -export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) => - `bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`; -export const extInternalDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: string | number) => - `bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}_v${variantNumber}`; +/** Where Rust code is being emitted, so paths to named types can be spelled from there. */ +export type RustScope = Pick; -interface TypeDataDefs { - /** The name */ - ref: string; +export const roundUp = (n: number, align: number) => Math.ceil(n / align) * align; - sequence: { - element: TypeImpl; - repr: "slice"; - }; - record: { - value: TypeImpl; - repr: "kv-slices"; - }; - stringEnum: string[]; - oneOf: TypeImpl[]; - dictionary: DictionaryField[]; +export function structLayout(fields: readonly Layout[]): Layout { + let size = 0; + let align = 1; + for (const field of fields) { + size = roundUp(size, field.align) + field.size; + align = Math.max(align, field.align); + } + return { size: roundUp(size, align), align }; } -type TypeData = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any; -export const enum NodeValidator { - validateInteger = "validateInteger", +/** `ExternVariant<...>`: `{ union data; uint8_t tag; }`. */ +export function variantLayout(alternatives: readonly Layout[]): Layout { + const align = Math.max(...alternatives.map(a => a.align)); + const union = { size: roundUp(Math.max(...alternatives.map(a => a.size)), align), align }; + return structLayout([union, { size: 1, align: 1 }]); } -interface Flags { - nodeValidator?: NodeValidator; - optional?: boolean; - required?: boolean; - nonNull?: boolean; - default?: any; - range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"]; - finite?: boolean; +export function snakeCase(name: string): string { + return name + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") + .replace(/[^A-Za-z0-9]+/g, "_") + .toLowerCase(); } -export interface DictionaryField { - key: string; - type: TypeImpl; +export function pascalCase(name: string): string { + return name + .split(/[^A-Za-z0-9]+/) + .filter(part => part.length > 0) + .map(part => part[0].toUpperCase() + part.slice(1)) + .join(""); } -export declare const isType: unique symbol; - -const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]); - -/** - * Implementation of the Type interface. All types are immutable and hashable. - * Hashes de-duplicate structure and union definitions. Flags do not account for - * the hash, so `oneOf(A, B)` and `oneOf(A, B).optional` will point to the same - * generated struct type, the purpose of the flags are to inform receivers like - * `t.dictionary` and `fn` to mark uses as optional or provide default values. - */ -export class TypeImpl { - kind: K; - data: TypeData; - flags: Flags; - /** Access via .name(). */ - nameDeduplicated: string | null | undefined = undefined; - /** Access via .hash() */ - #hash: string | undefined = undefined; - ownerFile: string; - - declare [isType]: true; +const rustKeywords = new Set( + "as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn gen".split( + " ", + ), +); +export const rustIdent = (name: string) => (rustKeywords.has(name) ? `r#${name}` : name); - constructor(kind: K, data: TypeData, flags: Flags = {}) { - this.kind = kind; - this.data = data; - this.flags = flags; - this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts"); - } +export function asciiLiteral(value: string): string { + assert(!/[^\x20-\x7e]/.test(value), `must be printable ASCII: ${util.inspect(value)}`); + return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"_s`; +} - isVirtualArgument() { - return this.kind === "globalObject" || this.kind === "virtualMachine"; +export abstract class Type { + /** `undefined` means "not provided". */ + get optional(): OptionalType { + return new OptionalType(this, "optional"); } - - hash() { - if (this.#hash) { - return this.#hash; - } - let h = `${this.kind}:`; - switch (this.kind) { - case "ref": - throw new Error("TODO"); - case "sequence": - h += this.data.element.hash(); - break; - case "record": - h += this.data.value.hash(); - break; - case "stringEnum": - h += this.data.join(","); - break; - case "oneOf": - h += this.data.map(t => t.hash()).join(","); - break; - case "dictionary": - h += this.data.map(({ key, required, type }) => `${key}:${required}:${type.hash()}`).join(","); - break; - } - let hash = String(Bun.hash(h)); - this.#hash = hash; - return hash; + /** `null` or `undefined` means "not provided". */ + get nullable(): OptionalType { + return new OptionalType(this, "nullable"); } - /** - * If this type lowers to a named type (struct, union, enum) + * Both forms: converted for use now, and the original value to hand to something that holds it + * longer (one of a class's `values`, a `Strong`, a JS call, the result). Only for arguments. */ - lowersToNamedType() { - switch (this.kind) { - case "ref": - throw new Error("TODO"); - case "sequence": - case "record": - case "oneOf": - case "dictionary": - case "stringEnum": - return true; - default: - return false; - } + get withValue(): WithValueType { + return new WithValueType(this); + } + /** Value to use when not provided. Only for arguments and dictionary members. */ + default(value: unknown): DefaultType { + return new DefaultType(this, value); } - canDirectlyMapToCAbi(): CAbiType | null { - let kind = this.kind; - switch (kind) { - case "ref": - throw new Error("TODO"); - case "any": - return "JSValue"; - case "ByteString": - case "DOMString": - case "USVString": - case "UTF8String": - return "bun.String"; - case "boolean": - return "bool"; - case "strictBoolean": - return "bool"; - case "f64": - case "i8": - case "i16": - case "i32": - case "i64": - case "u8": - case "u16": - case "u32": - case "u64": - case "usize": - return kind; - case "globalObject": - case "virtualMachine": - return "*JSGlobalObject"; - case "stringEnum": - return cAbiTypeForEnum(this.data.length); - case "undefined": - return "u0"; - case "oneOf": // `union(enum)` - case "record": // undecided how to lower records - case "sequence": // []const T - return null; - case "externalClass": - throw new Error("TODO"); - case "dictionary": { - let existing = typeHashToStruct.get(this.hash()); - if (existing) return existing; - existing = new Struct(); - for (const { key, type } of this.data as DictionaryField[]) { - if (type.flags.optional && !("default" in type.flags)) { - return null; // ?T - } - const repr = type.canDirectlyMapToCAbi(); - if (!repr) return null; + abstract readonly idlType: string; + /** TypeScript spelling. */ + abstract readonly tsType: string; - existing.add(key, repr); - } - existing.reorderForSmallestSize(); - if (!structHashToSelf.has(existing.hash())) { - structHashToSelf.set(existing.hash(), existing); - } - existing.assignName(this.name()); - typeHashToStruct.set(this.hash(), existing); - return existing; - } - default: { - throw new Error("unexpected: " + (kind satisfies never)); - } - } + /** How `IDLNullable::ImplementationType` says "null". */ + get nullRepresentation(): "optional" | "pointer" | "undefined" { + return "optional"; } - - name() { - if (this.nameDeduplicated) { - return this.nameDeduplicated; - } - const hash = this.hash(); - const existing = typeHashToReachableType.get(hash); - if (existing) return (this.nameDeduplicated = existing.nameDeduplicated ??= this.#generateName()); - return (this.nameDeduplicated = `anon_${this.kind}_${hash}`); + /** Converting `undefined` succeeds, so a dictionary member of this type is not required. */ + get permitsUndefined(): boolean { + return false; } - - cppInternalName() { - const name = this.name(); - const cAbiType = this.canDirectlyMapToCAbi(); - const namespace = typeHashToNamespace.get(this.hash()); - if (cAbiType) { - if (typeof cAbiType === "string") { - return cAbiType; - } - } - return namespace ? `${namespace}${name}` : name; + /** Holds a JS value: the Rust type is branded with the call. */ + get holdsJSValue(): boolean { + return this.dependencies.some(type => type.holdsJSValue); } - - cppClassName() { - assert(this.lowersToNamedType(), `Does not lower to named type: ${inspect(this)}`); - const name = this.name(); - const namespace = typeHashToNamespace.get(this.hash()); - return namespace ? `${namespace}::${cap(name)}` : name; + /** Points into memory JavaScript can detach, resize or free: not for a function that can run it. */ + get borrowsHeap(): boolean { + return this.dependencies.some(type => type.borrowsHeap); } - - cppName() { - const name = this.name(); - const cAbiType = this.canDirectlyMapToCAbi(); - const namespace = typeHashToNamespace.get(this.hash()); - if (cAbiType && typeof cAbiType === "string" && this.kind !== "stringEnum") { - return cAbiTypeName(cAbiType); - } - return namespace ? `${namespace}::${cap(name)}` : name; + /** Points into the C++ thunk's frame: only usable for the duration of a host call. */ + get borrowsFrame(): boolean { + return this.dependencies.some(type => type.borrowsFrame); } - - #generateName() { - return `bindgen_${this.ownerFile}_${this.hash()}`; + get needsLifetime(): boolean { + return this.holdsJSValue || this.borrowsFrame || this.borrowsHeap; + } + get dependencies(): readonly Type[] { + return []; + } + getHeaders(out: Set): void { + for (const type of this.dependencies) type.getHeaders(out); } - /** - * Name assignment is done to give readable names. - * The first name to a unique hash wins. - */ - assignName(name: string) { - if (this.nameDeduplicated) return; - const hash = this.hash(); - const existing = typeHashToReachableType.get(hash); - if (existing) { - this.nameDeduplicated = existing.nameDeduplicated ??= name; - return; - } - this.nameDeduplicated = name; + /** A C++ expression of `idlType::ImplementationType` for a default value written in the schema. */ + abstract toCpp(value: unknown): string; + /** The same value as a Rust expression of `rustType`. */ + toRust(value: unknown, scope: RustScope): string { + throw new TypeError(`${this.tsType} cannot have a default`); + } + /** A C++ expression that converts `value`. `ctx` is a conversion context, `name` what to call the value. */ + cppConvert(value: string, ctx: string, name: string): string { + return `::Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; } - markReachable() { - if (!this.lowersToNamedType()) return; - const hash = this.hash(); - const existing = typeHashToReachableType.get(hash); - this.nameDeduplicated ??= existing?.name() ?? `anon_${this.kind}_${hash}`; - if (!existing) typeHashToReachableType.set(hash, this); + abstract layout(): Layout; + abstract rustExternType(scope: RustScope): string; + abstract rustType(scope: RustScope): string; + /** A Rust expression (inside `unsafe`) that consumes `expr`, a `rustExternType`. */ + rustFromExtern(expr: string, scope: RustScope): string { + return expr; + } - switch (this.kind) { - case "ref": - throw new Error("TODO"); - case "sequence": - this.data.element.markReachable(); - break; - case "record": - this.data.value.markReachable(); - break; - case "oneOf": - for (const type of this.data as TypeImpl[]) { - type.markReachable(); - } - break; - case "dictionary": - for (const { type } of this.data as DictionaryField[]) { - type.markReachable(); - } - break; - } + /** The type of a field of a returned dictionary. */ + rustResultFieldType(scope: RustScope): string { + return this.rustType(scope); } + /** The return type of a trait method. */ + rustReturnType(scope: RustScope): string { + return this.rustResultFieldType(scope); + } + /** A Rust expression of type `bindgen::Result<'a, bindgen::JsValue<'a>>` that consumes `value`; `heap` is in scope. */ + rustToJS(value: string, scope: RustScope): string { + throw new TypeError(`${this.tsType} cannot be returned to JavaScript`); + } +} - #rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") { - if (this.flags.range) { - throw new Error("This type already has a range modifier set"); +// ───────────────────────────── primitives ───────────────────────────── + +class PrimitiveType extends Type { + readonly idlType: string; + readonly tsType: string; + readonly rustName: string; + readonly size: number; + constructor(idlType: string, tsType: string, rustName: string, size: number) { + super(); + this.idlType = idlType; + this.tsType = tsType; + this.rustName = rustName; + this.size = size; + } + toCpp(value: unknown): string { + if (this.rustName === "bool") { + assert(typeof value === "boolean"); + return String(value); } + assert(typeof value === "number"); + if (this.rustName !== "f64") assert(Number.isSafeInteger(value)); + return `${this.idlType}::ImplementationType { ${value} }`; + } + toRust(value: unknown): string { + return this.rustName === "f64" && Number.isInteger(value) ? `${value}.0` : String(value); + } + layout(): Layout { + return { size: this.size, align: this.size }; + } + rustExternType(): string { + return this.rustName; + } + rustType(): string { + return this.rustName; + } + rustToJS(value: string): string { + return `bindgen::to_js::${this.rustName}(heap, ${value})`; + } +} - // cAbiIntegerLimits throws on non-integer types - const range = cAbiIntegerLimits(this.kind as CAbiType); - const abiMin = BigInt(range[0]); - const abiMax = BigInt(range[1]); - if (min === undefined) { - min = abiMin; - max = abiMax; - } else { - if (max === undefined) { - throw new Error("Expected min and max to be both set or both unset"); - } - min = BigInt(min); - max = BigInt(max); +export class BoolType extends PrimitiveType { + constructor(idlType = "::Bun::IDLStrictBoolean") { + super(idlType, "boolean", "bool", 1); + } + /** `ToBoolean`: any value is accepted. */ + get loose(): BoolType { + return new BoolType("::WebCore::IDLBoolean"); + } +} - if (min < abiMin || min > abiMax) { - throw new Error(`Expected integer in range ${range}, got ${inspect(min)}`); - } - if (max < abiMin || max > abiMax) { - throw new Error(`Expected integer in range ${range}, got ${inspect(max)}`); - } - if (min > max) { - throw new Error(`Expected min <= max, got ${inspect(min)} > ${inspect(max)}`); - } - } +interface IntegerInfo { + rust: string; + cpp: string; + size: number; + /** The WebIDL integer type of this width. */ + webidl: string; + min: number | bigint; + max: number | bigint; +} - return new TypeImpl(this.kind, this.data, { - ...this.flags, - range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max], - }); +export class IntegerType extends PrimitiveType { + readonly info: IntegerInfo; + constructor(info: IntegerInfo, idlType = `::Bun::IDLStrictInteger<${info.cpp}>`) { + super(idlType, "number", info.rust, info.size); + this.info = info; } - - assertDefaultIsValid(value: unknown) { - switch (this.kind) { - case "DOMString": - case "ByteString": - case "USVString": - case "UTF8String": - if (typeof value !== "string") { - throw new Error(`Expected string, got ${inspect(value)}`); - } - break; - case "boolean": - if (typeof value !== "boolean") { - throw new Error(`Expected boolean, got ${inspect(value)}`); - } - break; - case "f64": - if (typeof value !== "number") { - throw new Error(`Expected number, got ${inspect(value)}`); - } - break; - case "usize": - case "u8": - case "u16": - case "u32": - case "u64": - case "i8": - case "i16": - case "i32": - case "i64": - const range = this.flags.range?.slice(1) ?? cAbiIntegerLimits(this.kind); - if (typeof value === "number") { - if (value % 1 !== 0) { - throw new Error(`Expected integer, got ${inspect(value)}`); - } - if (value >= Number.MAX_SAFE_INTEGER || value <= Number.MIN_SAFE_INTEGER) { - throw new Error( - `Specify default ${this.kind} outside of max safe integer range as a BigInt to avoid precision loss`, - ); - } - if (value < Number(range[0]) || value > Number(range[1])) { - throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); - } - } else if (typeof value === "bigint") { - if (value < BigInt(range[0]) || value > BigInt(range[1])) { - throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`); - } - } else { - throw new Error(`Expected integer, got ${inspect(value)}`); - } - break; - case "dictionary": - if (typeof value !== "object" || value === null) { - throw new Error(`Expected object, got ${inspect(value)}`); - } - for (const { key, type } of this.data as DictionaryField[]) { - if (key in value) { - type.assertDefaultIsValid(value[key]); - } else if (type.flags.required) { - throw new Error(`Missing key ${key} in dictionary`); - } - } - break; - case "undefined": - assert(value === undefined, `Expected undefined, got ${inspect(value)}`); - break; - default: - throw new Error(`TODO: set default value on type ${this.kind}`); - } + /** `ToNumeric` first, then as strict as the default. */ + get loose(): IntegerType { + return new IntegerType(this.info, `::Bun::IDLLooseInteger<${this.info.cpp}>`); } - - emitCppDefaultValue(w: CodeWriter) { - const value = this.flags.default; - switch (this.kind) { - case "boolean": - w.add(value ? "true" : "false"); - break; - case "f64": - w.add(String(value)); - break; - case "usize": - case "u8": - case "u16": - case "u32": - case "u64": - case "i8": - case "i16": - case "i32": - case "i64": - w.add(String(value)); - break; - case "dictionary": - const struct = this.structType(); - w.line(`${this.cppName()} {`); - w.indent(); - for (const { name } of struct.fields) { - w.add(`.${name} = `); - const type = this.data.find(f => f.key === name)!.type; - type.emitCppDefaultValue(w); - w.line(","); - } - w.dedent(); - w.add(`}`); - break; - case "DOMString": - case "ByteString": - case "USVString": - case "UTF8String": - if (typeof value === "string") { - w.add("Bun::BunStringEmpty"); - } else { - throw new Error(`TODO: non-empty string default`); - } - break; - case "undefined": - throw new Error("Zero-sized type"); - default: - throw new Error(`TODO: set default value on type ${this.kind}`); - } + /** WebIDL conversion: `ToNumber`, then wrap modulo the width. */ + get wrap(): IntegerType { + return new IntegerType(this.info, `::WebCore::${this.info.webidl}`); } - - structType() { - const direct = this.canDirectlyMapToCAbi(); - assert(typeof direct !== "string"); - if (direct) return direct; - throw new Error("TODO: generate non-extern struct for representing this data type"); + /** WebIDL `[EnforceRange]` over `[min, max]`. */ + enforceRange(min = Number(this.info.min), max = Number(this.info.max)): RangeType { + return new RangeType(this.info, min, max, "Web"); } - - isIgnoredUndefinedType() { - return this.kind === "undefined"; + /** Node's `validateInteger` / `validateInt32` / `validateUint32`: `ERR_INVALID_ARG_TYPE`, `ERR_OUT_OF_RANGE`. */ + validateInteger(min = Number(this.info.min), max = Number(this.info.max)): RangeType { + return new RangeType(this.info, min, max, "Node"); } +} - isStringType() { - return ( - this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String" +export class RangeType extends IntegerType { + readonly kind: "Web" | "Node"; + constructor(info: IntegerInfo, min: number, max: number, kind: "Web" | "Node") { + assert(Number.isSafeInteger(min) && Number.isSafeInteger(max) && min <= max); + assert(min >= Number(info.min) && max <= Number(info.max), `range does not fit ${info.rust}`); + super( + info, + `::Bun::BindgenCustomEnforceRange<${info.cpp}, ${min}, ${max}, ::Bun::BindgenCustomEnforceRangeKind::${kind}>`, ); + this.kind = kind; } - - isNumberType() { - return numericTypes.has(this.kind); + getHeaders(out: Set): void { + out.add("BindgenCustomEnforceRange.h"); } - - isObjectType() { - return this.kind === "externalClass" || this.kind === "dictionary"; + cppConvert(value: string, ctx: string, name: string): string { + return this.kind === "Node" + ? `::WebCore::convert<${this.idlType}>(*globalObject, ${value}, []() { return ${asciiLiteral(name)}; })` + : `::WebCore::convert<${this.idlType}>(*globalObject, ${value})`; } +} - [Symbol.toStringTag] = "Type"; - [Bun.inspect.custom](depth, options, inspect) { - return ( - `${options.stylize("Type", "special")} ${ - this.lowersToNamedType() && this.nameDeduplicated - ? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " " - : "" - }${options.stylize( - `[${this.kind}${["required", "optional", "nullable"] - .filter(k => this.flags[k]) - .map(x => ", " + x) - .join("")}]`, - "regexp", - )}` + - (this.data - ? " " + - inspect(this.data, { - ...options, - depth: options.depth === null ? null : options.depth - 1, - }).replace(/\n/g, "\n") - : "") - ); +const integer = (rust: string, cpp: string, size: number, webidl: string, min: number | bigint, max: number | bigint) => + new IntegerType({ rust, cpp: `::std::${cpp}`, size, webidl, min, max }); + +export const integers = { + i8: integer("i8", "int8_t", 1, "IDLByte", -128, 127), + u8: integer("u8", "uint8_t", 1, "IDLOctet", 0, 255), + i16: integer("i16", "int16_t", 2, "IDLShort", -32768, 32767), + u16: integer("u16", "uint16_t", 2, "IDLUnsignedShort", 0, 65535), + i32: integer("i32", "int32_t", 4, "IDLLong", -2147483648, 2147483647), + u32: integer("u32", "uint32_t", 4, "IDLUnsignedLong", 0, 4294967295), + i64: integer("i64", "int64_t", 8, "IDLLongLong", Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), + u64: integer("u64", "uint64_t", 8, "IDLUnsignedLongLong", 0, Number.MAX_SAFE_INTEGER), + usize: integer("usize", "uint64_t", 8, "IDLUnsignedLongLong", 0, Number.MAX_SAFE_INTEGER), +}; + +export class DoubleType extends PrimitiveType { + constructor(idlType = "::Bun::IDLStrictDouble") { + super(idlType, "number", "f64", 8); } - - // Public interface definition API - get optional() { - if (this.flags.required) { - throw new Error("Cannot derive optional on a required type"); - } - if (this.flags.default) { - throw new Error("Cannot derive optional on a something with a default value (default implies optional)"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - optional: true, - }); + /** Rejects NaN and the infinities. */ + get finite(): DoubleType { + return new DoubleType("::Bun::IDLFiniteDouble"); } - - get finite() { - if (this.kind !== "f64") { - throw new Error("finite can only be used on f64"); - } - if (this.flags.finite) { - throw new Error("This type already has finite set"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - finite: true, - }); + /** `ToNumber`. */ + get loose(): DoubleType { + return new DoubleType("::WebCore::IDLUnrestrictedDouble"); } +} - get required() { - if (this.flags.required) { - throw new Error("This type already has required set"); - } - if (this.flags.required) { - throw new Error("Cannot derive required on an optional type"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - required: true, - }); +/** Only as a union alternative or a return type. */ +export class UnitType extends Type { + readonly idlType: string; + readonly tsType: string; + constructor(tsType: "null" | "undefined") { + super(); + this.tsType = tsType; + this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : "::Bun::IDLStrictUndefined"; } - - default(def: any) { - if ("default" in this.flags) { - throw new Error("This type already has a default value"); - } - if (this.flags.required) { - throw new Error("Cannot derive default on a required type"); - } - this.assertDefaultIsValid(def); - return new TypeImpl(this.kind, this.data, { - ...this.flags, - default: def, - }); + get permitsUndefined(): boolean { + return true; } - - clamp(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "clamp"); + toCpp(): string { + return this.tsType === "null" ? "nullptr" : "::std::monostate {}"; + } + layout(): Layout { + return { size: 1, align: 1 }; } + rustExternType(): string { + return "u8"; + } + rustType(): string { + return "()"; + } + rustFromExtern(): string { + return "()"; + } + rustToJS(value: string): string { + assert(this.tsType === "undefined", "return b.undefined, or a nullable type"); + return `bindgen::to_js::undefined(heap, ${value})`; + } +} - enforceRange(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "enforce"); +// ───────────────────────────── strings, values, buffers ───────────────────────────── + +/** An owned `bun_core::String` sharing the JS string's `StringImpl`. */ +export class StringType extends Type { + readonly idlType: string; + readonly tsType = "string"; + constructor(idlType = "::Bun::IDLStrictString") { + super(); + this.idlType = idlType; + } + /** `ToString`: any value is accepted. */ + get loose(): StringType { + return new StringType("::WebCore::IDLDOMString"); + } + get nullRepresentation() { + return "pointer" as const; + } + toCpp(value: unknown): string { + assert(typeof value === "string"); + return `::WTF::String { ${asciiLiteral(value)} }`; + } + toRust(value: unknown): string { + return value === "" ? "bun_core::String::EMPTY" : `bun_core::String::static_(${JSON.stringify(value)})`; } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "bindgen::extern_types::ExternString"; + } + rustType(): string { + return "bun_core::String"; + } + rustFromExtern(expr: string): string { + return `bindgen::extern_types::adopt_string(${expr})`; + } + rustToJS(value: string): string { + return `bindgen::to_js::string(heap, ${value})`; + } +} - get nonNull() { - if (this.flags.nonNull) { - throw new Error("Cannot derive nonNull on a nonNull type"); - } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - nonNull: true, - }); +/** + * UTF-8 bytes for the duration of the call: borrowed from the JS string when it is 8-bit ASCII, + * transcoded into the thunk's frame otherwise. + */ +export class Utf8Type extends Type { + readonly idlType: string; + readonly tsType = "string"; + constructor(idlType = "::Bun::Bindgen::IDLStrictUTF8String") { + super(); + this.idlType = idlType; + } + /** `ToString`: any value is accepted. */ + get loose(): Utf8Type { + return new Utf8Type("::Bun::Bindgen::IDLLooseUTF8String"); + } + get borrowsFrame(): boolean { + return true; + } + get nullRepresentation() { + return "pointer" as const; + } + getHeaders(out: Set): void { + out.add("Bindgen/IDLConvert.h"); + } + toCpp(value: unknown): string { + assert(typeof value === "string"); + return `::Bun::Bindgen::JSStringUTF8View { ${asciiLiteral(value)} }`; + } + toRust(value: unknown): string { + return `b${JSON.stringify(value)}`; + } + layout(): Layout { + return { size: 16, align: 8 }; + } + rustExternType(): string { + return "bindgen::extern_types::ExternBytes"; + } + rustType(): string { + return "&'a [u8]"; } + rustFromExtern(expr: string): string { + return `${expr}.get()`; + } + rustResultFieldType(): string { + return "Vec"; + } + rustReturnType(): string { + return "impl AsRef<[u8]>"; + } + rustToJS(value: string): string { + return `bindgen::to_js::utf8(heap, ${value})`; + } +} - validateInt32(min?: number, max?: number) { - if (this.kind !== "i32") { - throw new Error("validateInt32 can only be used on i32 or u32"); - } - const rangeInfo = cAbiIntegerLimits("i32"); - return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); +/** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ +export class BytesType extends Type { + readonly idlType = "::Bun::Bindgen::IDLBufferSourceBytes"; + readonly tsType = "ArrayBuffer | ArrayBufferView"; + get borrowsHeap(): boolean { + return true; + } + get nullRepresentation() { + return "pointer" as const; + } + getHeaders(out: Set): void { + out.add("Bindgen/IDLConvert.h"); + } + toCpp(): string { + throw new TypeError("b.bytes cannot have a default"); + } + layout(): Layout { + return { size: 16, align: 8 }; } + rustExternType(): string { + return "bindgen::extern_types::ExternBytes"; + } + rustType(): string { + return "&'a [u8]"; + } + rustFromExtern(expr: string): string { + return `${expr}.get()`; + } +} - validateUint32(min?: number, max?: number) { - if (this.kind !== "u32") { - throw new Error("validateUint32 can only be used on i32 or u32"); - } - const rangeInfo = cAbiIntegerLimits("u32"); - return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]); +/** A typed array, `DataView` or `ArrayBuffer`, as a handle whose bytes are read through `with_bytes`. */ +export class BufferSourceType extends Type { + readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; + readonly tsType = "ArrayBuffer | ArrayBufferView"; + get holdsJSValue(): boolean { + return true; + } + getHeaders(out: Set): void { + out.add("Bindgen/IDLConvert.h"); + } + toCpp(): string { + throw new TypeError("b.bufferSource cannot have a default"); + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "JSValue"; } + rustType(): string { + return "bindgen::JsValue<'a, bun_jsc::ArrayBuffer>"; + } + rustFromExtern(expr: string): string { + return `bindgen::JsValue::from_extern(${expr})`; + } + rustToJS(value: string): string { + return `bindgen::to_js::any(heap, ${value})`; + } +} - validateInteger(min?: number | bigint, max?: number | bigint) { - min ??= Number.MIN_SAFE_INTEGER; - max ??= Number.MAX_SAFE_INTEGER; - const enforceRange = this.#rangeModifier(min, max, "enforce") as TypeImpl; - enforceRange.flags.nodeValidator = NodeValidator.validateInteger; - return enforceRange; +/** Converts as `inner` does; the thunk passes the argument itself along. */ +export class WithValueType extends Type { + readonly inner: Type; + constructor(inner: Type) { + super(); + assert(!(inner instanceof DefaultType || inner instanceof WithValueType)); + this.inner = inner; + } + get idlType(): string { + return this.inner.idlType; + } + get tsType(): string { + return this.inner.tsType; + } + get permitsUndefined(): boolean { + return this.inner.permitsUndefined; + } + get holdsJSValue(): boolean { + return true; + } + get dependencies(): readonly Type[] { + return [this.inner]; + } + toCpp(): string { + throw new TypeError("put .withValue last"); + } + cppConvert(value: string, ctx: string, name: string): string { + return this.inner.cppConvert(value, ctx, name); + } + layout(): Layout { + return structLayout([this.inner.layout(), { size: 8, align: 8 }]); + } + rustExternType(scope: RustScope): string { + return `bindgen::extern_types::ExternWithValue<${this.inner.rustExternType(scope)}>`; + } + rustType(scope: RustScope): string { + return `bindgen::WithValue<'a, ${this.inner.rustType(scope)}>`; + } + rustFromExtern(expr: string, scope: RustScope): string { + return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsValue::from_extern(with_value.value) } }`; } } -export function cAbiIntegerLimits(type: CAbiType) { - switch (type) { - case "u8": - return [0, 255]; - case "u16": - return [0, 65535]; - case "u32": - return [0, 4294967295]; - case "u64": - return [0, 18446744073709551615n]; - case "usize": - return [0, 18446744073709551615n]; - case "i8": - return [-128, 127]; - case "i16": - return [-32768, 32767]; - case "i32": - return [-2147483648, 2147483647]; - case "i64": - return [-9223372036854775808n, 9223372036854775807n]; - case "f64": - return [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]; - default: - throw new Error(`Unexpected type ${type}`); +export class AnyType extends Type { + readonly idlType = "::Bun::IDLRawAny"; + readonly tsType = "any"; + get holdsJSValue(): boolean { + return true; + } + get permitsUndefined(): boolean { + return true; + } + get nullRepresentation() { + return "undefined" as const; + } + toCpp(): string { + throw new TypeError("b.any cannot have a default"); + } + toRust(): string { + return "bindgen::JsValue::UNDEFINED"; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "JSValue"; + } + rustType(): string { + return "bindgen::JsValue<'a>"; + } + rustFromExtern(expr: string): string { + return `bindgen::JsValue::from_extern(${expr})`; + } + rustToJS(value: string): string { + return `bindgen::to_js::any(heap, ${value})`; } } -export function cAbiTypeForEnum(length: number): CAbiType { - return ("u" + alignForward(length, 8)) as CAbiType; +/** A ref-counted native object taken out of its JS wrapper. */ +export class RefType extends Type { + readonly idlType: string; + readonly tsType: string; + readonly pointee: string; + readonly header: string | null; + constructor(idlType: string, tsType: string, pointee: string, header: string | null) { + super(); + this.idlType = idlType; + this.tsType = tsType; + this.pointee = pointee; + this.header = header; + } + get nullRepresentation() { + return "pointer" as const; + } + getHeaders(out: Set): void { + if (this.header) out.add(this.header); + } + toCpp(): string { + throw new TypeError(`${this.tsType} cannot have a default`); + } + layout(): Layout { + return { size: 8, align: 8 }; + } + /** Untyped: C++ never looks inside, and `improper_ctypes` would follow a typed pointer into the pointee. */ + rustExternType(): string { + return "*mut core::ffi::c_void"; + } + rustType(): string { + return `bun_core::ExternalShared<${this.pointee}>`; + } + rustFromExtern(expr: string): string { + return `bun_core::ExternalShared::<${this.pointee}>::adopt(${expr}.cast())`; + } } -export function inspect(value: any) { - return Bun.inspect(value, { colors: Bun.enableANSIColors }); +// ───────────────────────────── combinators ───────────────────────────── + +export class OptionalType extends Type { + readonly payload: Type; + readonly kind: "optional" | "nullable" | "looseNullable"; + constructor(payload: Type, kind: "optional" | "nullable" | "looseNullable") { + super(); + assert(!(payload instanceof OptionalType), "already optional"); + assert(!(payload instanceof DefaultType), "put .default() last"); + assert(!(payload instanceof AnyType), "b.any already accepts undefined and null"); + this.payload = payload; + this.kind = kind; + } + /** Every falsy value means "not provided". */ + get loose(): OptionalType { + assert(this.kind === "nullable", ".loose follows .nullable"); + return new OptionalType(this.payload, "looseNullable"); + } + get idlType(): string { + const template = { + optional: "::WebCore::IDLOptional", + nullable: "::WebCore::IDLNullable", + looseNullable: "::Bun::IDLLooseNullable", + }[this.kind]; + return `${template}<${this.payload.idlType}>`; + } + get tsType(): string { + return `${this.payload.tsType} | ${this.kind === "optional" ? "undefined" : "null | undefined"}`; + } + get permitsUndefined(): boolean { + return true; + } + get dependencies(): readonly Type[] { + return [this.payload]; + } + toCpp(value: unknown): string { + return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); + } + toRust(value: unknown, scope: RustScope): string { + return value == null ? "None" : `Some(${this.payload.toRust(value, scope)})`; + } + layout(): Layout { + return this.payload.nullRepresentation === "optional" + ? variantLayout([{ size: 1, align: 1 }, this.payload.layout()]) + : this.payload.layout(); + } + rustExternType(scope: RustScope): string { + return this.payload.nullRepresentation === "optional" + ? `bindgen::extern_types::ExternOptional<${this.payload.rustExternType(scope)}>` + : this.payload.rustExternType(scope); + } + rustType(scope: RustScope): string { + return `Option<${this.payload.rustType(scope)}>`; + } + rustFromExtern(expr: string, scope: RustScope): string { + if (this.payload.nullRepresentation === "optional") { + const inner = this.payload.rustFromExtern("value", scope); + return inner === "value" ? `${expr}.get()` : `${expr}.get().map(|value| ${inner})`; + } + if (this.payload instanceof Utf8Type) return `${expr}.get_optional()`; + return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.payload.rustFromExtern("value", scope)}) } }`; + } + rustResultFieldType(scope: RustScope): string { + return `Option<${this.payload.rustResultFieldType(scope)}>`; + } + rustReturnType(scope: RustScope): string { + return `Option<${this.payload.rustReturnType(scope)}>`; + } + rustToJS(value: string, scope: RustScope): string { + const none = this.kind === "optional" ? "UNDEFINED" : "NULL"; + return `match ${value} { Some(value) => ${this.payload.rustToJS("value", scope)}, None => Ok(bindgen::JsValue::${none}) }`; + } } -export function oneOfImpl(types: TypeImpl[]): TypeImpl { - const out: TypeImpl[] = []; - for (const type of types) { - if (type.kind === "oneOf") { - out.push(...type.data); +/** Not a type of its own: marks an argument or member whose absence has a value. */ +export class DefaultType extends Type { + readonly payload: Type; + readonly value: unknown; + /** `null` also takes the default. */ + readonly onNull: boolean; + constructor(payload: Type, value: unknown) { + super(); + assert(!(payload instanceof DefaultType)); + if (payload instanceof OptionalType) { + assert(payload.kind === "nullable", ".default() follows a type or .nullable"); + this.payload = payload.payload; + this.onNull = true; } else { - if (type.flags.default) { - throw new Error( - "Union type cannot include a default value. Instead, set a default value on the union type itself", - ); - } - if (type.isVirtualArgument()) { - throw new Error(`t.${type.kind} can only be used as a function argument type`); - } - out.push(type); + this.payload = payload; + this.onNull = false; } + this.value = value; + this.payload.toCpp(value); + } + get idlType(): string { + return this.payload.idlType; + } + get tsType(): string { + return `${this.payload.tsType} | ${this.onNull ? "null | undefined" : "undefined"}`; + } + get permitsUndefined(): boolean { + return true; + } + get dependencies(): readonly Type[] { + return [this.payload]; + } + toCpp(value: unknown): string { + return this.payload.toCpp(value); + } + toRust(value: unknown, scope: RustScope): string { + return this.payload.toRust(value, scope); + } + cppConvert(value: string, ctx: string, name: string): string { + return this.payload.cppConvert(value, ctx, name); + } + layout(): Layout { + return this.payload.layout(); + } + rustExternType(scope: RustScope): string { + return this.payload.rustExternType(scope); + } + rustType(scope: RustScope): string { + return this.payload.rustType(scope); + } + rustFromExtern(expr: string, scope: RustScope): string { + return this.payload.rustFromExtern(expr, scope); } - return new TypeImpl("oneOf", out); } -export function dictionaryImpl(record: Record): TypeImpl { - const out: DictionaryField[] = []; - for (const key in record) { - const type = record[key]; - if (type.isVirtualArgument()) { - throw new Error(`t.${type.kind} can only be used as a function argument type`); - } - out.push({ - key, - type: type, - }); +export class ArrayType extends Type { + readonly element: Type; + constructor(element: Type) { + super(); + assert(!(element instanceof DefaultType)); + this.element = element; + } + get idlType(): string { + assert(!this.element.holdsJSValue, "an array argument cannot hold JS values: the GC does not scan it"); + assert(!this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string"); + return `::Bun::IDLArray<${this.element.idlType}>`; + } + get tsType(): string { + return `(${this.element.tsType})[]`; + } + get dependencies(): readonly Type[] { + return [this.element]; + } + getHeaders(out: Set): void { + out.add("Bindgen/ExternVectorTraits.h"); + super.getHeaders(out); + } + toCpp(value: unknown): string { + assert(Array.isArray(value)); + return `${this.idlType}::ImplementationType { ${value.map(item => this.element.toCpp(item)).join(", ")} }`; + } + layout(): Layout { + return { size: 16, align: 8 }; + } + rustExternType(scope: RustScope): string { + return `bindgen::extern_types::ExternVector<${this.element.rustExternType(scope)}>`; + } + rustType(scope: RustScope): string { + return `Vec<${this.element.rustType(scope)}>`; + } + rustFromExtern(expr: string, scope: RustScope): string { + return `${expr}.into_vec(|value| ${this.element.rustFromExtern("value", scope)})`; + } + rustResultFieldType(scope: RustScope): string { + return `Vec<${this.element.rustResultFieldType(scope)}>`; + } + rustReturnType(scope: RustScope): string { + return `impl IntoIterator`; + } + rustToJS(value: string, scope: RustScope): string { + return `bindgen::to_js::array(heap, ${value}, |heap, value| ${this.element.rustToJS("value", scope)})`; } - return new TypeImpl("dictionary", out); } -export const isFunc = Symbol("isFunc"); +// ───────────────────────────── named types ───────────────────────────── -export interface Func { - [isFunc]: true; - name: string; - snapshot: string; - /** The `.bind.ts` that declared it, relative to `src/`. */ - sourceFile: string; - variants: Variant[]; +export abstract class NamedType extends Type { + readonly name: string; + /** Set when the generator finds the type among a file's exports. */ + origin: Origin | null = null; + constructor(name: string) { + super(); + assert(/^[A-Z][A-Za-z0-9]*$/.test(name), `type names are PascalCase: ${name}`); + this.name = name; + } + get cpp(): string { + return `::Bun::Bindgen::Generated::${this.name}`; + } + getHeaders(out: Set): void { + assert(this.origin, `${this.name} is used but not exported from a .bind.ts file`); + out.add(`Generated${this.origin.namespace}.h`); + } + protected path(scope: RustScope, name: string): string { + assert(this.origin, `${this.name} is used but not exported from a .bind.ts file`); + if (this.origin.crate !== scope.crate) + return `${this.origin.crate}::generated_bindgen::${this.origin.rustModule}::${name}`; + if (this.origin.rustModule !== scope.rustModule) return `super::${this.origin.rustModule}::${name}`; + return name; + } + rustType(scope: RustScope): string { + return this.path(scope, this.name) + (this.needsLifetime ? "<'a>" : ""); + } + get tsType(): string { + return this.name; + } } -export interface Variant { - suffix: string; - args: Arg[]; - ret: TypeImpl; - returnStrategy?: ReturnStrategy; - globalObjectArg?: number | "hidden"; - minRequiredArgs: number; - communicationStruct?: Struct; +export class EnumType extends NamedType { + /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ + readonly spellings: readonly (readonly string[])[]; + /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ + firstEnumerationValueIndex = -1; + constructor(name: string, values: readonly (string | readonly string[])[]) { + super(name); + assert(values.length > 0, `${name} is empty`); + this.spellings = values.map(value => (typeof value === "string" ? [value] : value)); + const all = this.spellings.flat(); + assert(new Set(all).size === all.length, `${name} has a duplicate value`); + assert(new Set(this.variants).size === this.variants.length, `${name}: two values make the same identifier`); + for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${name}: ${variant} is not an identifier`); + } + get variants(): string[] { + return this.spellings.map(([canonical]) => pascalCase(canonical)); + } + get idlType(): string { + return `::WebCore::IDLEnumeration<${this.cpp}>`; + } + get tsType(): string { + return this.spellings + .flat() + .map(value => JSON.stringify(value)) + .join(" | "); + } + toCpp(value: unknown): string { + const index = this.spellings.findIndex(spellings => spellings.includes(value as string)); + assert(index >= 0, `${util.inspect(value)} is not a value of ${this.name}`); + return `${this.cpp}::${this.variants[index]}`; + } + toRust(value: unknown, scope: RustScope): string { + const index = this.spellings.findIndex(spellings => spellings.includes(value as string)); + return `${this.path(scope, this.name)}::${this.variants[index]}`; + } + layout(): Layout { + return { size: 4, align: 4 }; + } + rustExternType(scope: RustScope): string { + return this.path(scope, this.name); + } + rustToJS(value: string, scope: RustScope): string { + return `${value}.to_js(heap)`; + } } -export interface Arg { - name: string; - type: TypeImpl; - loweringStrategy?: ArgStrategy; +export interface MemberOptions { + type: Type; + default?: unknown; + /** Name of the C++ field and, in snake_case, of the Rust one. Defaults to the JS name. */ + internalName?: string; + /** Other JS names, tried in order while the value is `undefined`. */ + altNames?: readonly string[]; } -/** - * The strategy for moving arguments over the ABI boundary are computed before - * any code is generated so that the proper definitions can be easily made, - * while allow new special cases to be added. - */ -export type ArgStrategy = - // The argument is communicated as a C parameter - | { type: "c-abi-pointer"; abiType: CAbiType } - // The argument is communicated as a C parameter - | { type: "c-abi-value"; abiType: CAbiType } - // The data is added as a field on `.communicationStruct` - | { - type: "uses-communication-buffer"; - /** - * Unique prefix for fields. For example, moving an optional over the ABI - * boundary uses two fields, `bool {prefix}_set` and `T {prefix}_value`. - */ - prefix: string; - }; -/** - * In addition to moving a payload over, an additional bit of information - * crosses the ABI boundary indicating if the function threw an exception. - * - * For simplicity, the possibility of any native binding returning an error/calling - * `throw` is assumed and there isnt a way to disable the exception check. - */ -export type ReturnStrategy = - // JSValue is special cased because it encodes exception as 0x0 - | { type: "jsvalue" } - // Return value doesnt exist. function returns a boolean indicating success/error. - | { type: "void" } - // For primitives and simple structures where direct assignment into a - // pointer is possible. function returns a boolean indicating success/error. - | { type: "basic-out-param"; abiType: CAbiType }; - -export interface File { - functions: Func[]; - typedefs: TypeDef[]; +export class Member { + readonly names: readonly string[]; + readonly internalName: string; + readonly type: Type; + constructor(name: string, spec: Type | MemberOptions) { + const options: MemberOptions = spec instanceof Type ? { type: spec } : spec; + this.names = [name, ...(options.altNames ?? [])]; + this.internalName = options.internalName ?? name; + this.type = Object.hasOwn(options, "default") ? options.type.default(options.default) : options.type; + } + get name(): string { + return this.names[0]; + } + get rustName(): string { + return rustIdent(snakeCase(this.internalName)); + } } -export interface TypeDef { +export interface DictionaryOptions { name: string; - type: TypeImpl; + /** What error messages call it. Defaults to `name`. */ + userFacingName?: string; + /** Also generate `Name::from_js`, for Rust that holds a JS value and no typed signature. */ + fromJS?: boolean; } -export function registerFunction(opts: FuncOptions) { - const snapshot = snapshotCallerLocation(); - const filename = stackTraceFileName(snapshot); - expect(filename).toEndWith(".bind.ts"); - const sourceFile = path.relative(src, filename); - let file = files.get(sourceFile); - if (!file) { - file = { functions: [], typedefs: [] }; - files.set(sourceFile, file); - } - const variants: Variant[] = []; - if ("variants" in opts) { - let i = 1; - for (const variant of opts.variants) { - const { minRequiredArgs } = validateVariant(variant); - variants.push({ - args: Object.entries(variant.args).map(([name, type]) => ({ name, type })) as Arg[], - ret: variant.ret as TypeImpl, - suffix: `${i}`, - minRequiredArgs, - } as unknown as Variant); - i++; - } - } else { - const { minRequiredArgs } = validateVariant(opts); - variants.push({ - suffix: "", - args: Object.entries(opts.args).map(([name, type]) => ({ name, type })) as Arg[], - ret: opts.ret as TypeImpl, - minRequiredArgs, +export class DictionaryType extends NamedType { + readonly userFacingName: string; + readonly fromJS: boolean; + readonly members: readonly Member[]; + /** Index of the first member name in `Tables::memberNames`. Set by the generator. */ + firstMemberNameIndex = -1; + /** Index in `Tables::objectStructures`. Set by the generator. */ + structureIndex = -1; + /** Reachable from an argument or `fromJS` / from a return type. Set by the generator. */ + accepted = false; + returned = false; + constructor(options: string | DictionaryOptions, members: Record) { + super(typeof options === "string" ? options : options.name); + this.userFacingName = (typeof options === "string" ? undefined : options.userFacingName) ?? this.name; + this.fromJS = typeof options !== "string" && !!options.fromJS; + this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); + assert(!this.fromJS || !this.borrowsFrame, `${this.name}: from_js outlives the frame b.utf8 borrows from`); + } + get idlType(): string { + return `::WebCore::IDLDictionary<${this.cpp}>`; + } + get dependencies(): readonly Type[] { + return this.members.map(member => member.type); + } + /** Every JS name read, in order. */ + get names(): string[] { + return this.members.flatMap(member => member.names); + } + toCpp(value: unknown): string { + assert(typeof value === "object" && value !== null); + const given = value as Record; + for (const key of Object.keys(given)) + assert( + this.members.some(m => m.name === key), + `unexpected key: ${key}`, + ); + const fields = this.members.map(member => { + let field: unknown; + if (Object.hasOwn(given, member.name)) field = given[member.name]; + else if (member.type instanceof DefaultType) field = member.type.value; + else assert(member.type.permitsUndefined, `missing key: ${member.name}`); + return `.${member.internalName} = ${member.type.toCpp(field)}`; }); + return `${this.cpp} { ${fields.join(", ")} }`; + } + /** Every member can be left out. */ + get hasDefault(): boolean { + return this.members.every(member => member.type.permitsUndefined); } + toRust(value: unknown, scope: RustScope): string { + const given = value as Record; + const fields = this.members + .filter(member => Object.hasOwn(given, member.name)) + .map(member => `${member.rustName}: ${member.type.toRust(given[member.name], scope)}, `); + return `${this.path(scope, this.name)} { ${fields.join("")}..Default::default() }`; + } + layout(): Layout { + return structLayout(this.members.map(member => member.type.layout())); + } + rustExternType(scope: RustScope): string { + return this.path(scope, `Extern${this.name}`); + } + rustFromExtern(expr: string, scope: RustScope): string { + return `${this.path(scope, this.name)}::from_extern(${expr})`; + } + rustToJS(value: string, scope: RustScope): string { + return `${value}.to_js(heap)`; + } +} - const func: Func = { - [isFunc]: true, - name: "", - snapshot, - sourceFile, - variants, - }; - file.functions.push(func); - return func; +/** Alternatives are tried in order; the first whose conversion accepts the value wins. */ +export class UnionType extends NamedType { + readonly alternatives: readonly { name: string; variant: string; type: Type }[]; + constructor(name: string, alternatives: Record) { + super(name); + this.alternatives = Object.entries(alternatives).map(([name, type]) => { + assert(!(type instanceof DefaultType)); + return { name, variant: pascalCase(name), type }; + }); + assert(this.alternatives.length > 0 && this.alternatives.length <= 16, `${name}: 1 to 16 alternatives`); + } + get idlType(): string { + return `::Bun::IDLOrderedUnion<${this.alternatives.map(a => a.type.idlType).join(", ")}>`; + } + get tsType(): string { + return this.alternatives.map(a => a.type.tsType).join(" | "); + } + get permitsUndefined(): boolean { + return this.alternatives.some(a => a.type.permitsUndefined); + } + get dependencies(): readonly Type[] { + return this.alternatives.map(a => a.type); + } + toCpp(value: unknown): string { + const unit = this.alternatives.find(a => a.type instanceof UnitType && a.type.tsType === String(value)); + assert(unit, `${this.name}: only null or undefined can be a default`); + return `${this.idlType}::ImplementationType { ${unit.type.toCpp(value)} }`; + } + toRust(value: unknown, scope: RustScope): string { + const unit = this.alternatives.find( + a => a.type instanceof UnitType && (value === undefined || a.type.tsType === String(value)), + ); + assert(unit, `${this.name}: only null or undefined can be a default`); + return `${this.path(scope, this.name)}::${unit.variant}`; + } + layout(): Layout { + return variantLayout(this.alternatives.map(a => a.type.layout())); + } + rustExternType(scope: RustScope): string { + return this.path(scope, `Extern${this.name}`); + } + rustFromExtern(expr: string, scope: RustScope): string { + return `${this.path(scope, this.name)}::from_extern(${expr})`; + } } -function validateVariant(variant: any) { - let minRequiredArgs = 0; - let seenOptionalArgument = false; - let i = 0; +// ───────────────────────────── functions ───────────────────────────── - for (const [name, type] of Object.entries(variant.args) as [string, TypeImpl][]) { - if (!(type instanceof TypeImpl)) { - throw new Error(`Expected type for argument ${name}, got ${inspect(type)}`); - } - i += 1; - if (type.isVirtualArgument()) { - continue; +/** + * What the Rust body is handed, and so what it is able to do: + * `"none"` computes; `"heap"` (`&JsHeap`) also allocates JS values; `"js"` (`&mut JsCall`) also runs JavaScript. + */ +export type Capability = "none" | "heap" | "js"; + +export interface SignatureOptions { + args?: Record; + returns?: Type; + capability?: Capability; + /** The body returns `Result` for any `E: bindgen::IntoError`. */ + throws?: boolean; + /** Path of the Rust function. Defaults to the module's path plus the export's name in snake_case. */ + rustPath?: string; + /** The JS function's `length`. Defaults to the number of required arguments. */ + length?: number; +} + +export class Signature { + readonly args: readonly { name: string; type: Type }[]; + readonly returns: Type; + readonly capability: Capability; + readonly throws: boolean; + readonly rustPath: string | undefined; + readonly length: number | undefined; + constructor(options: SignatureOptions) { + this.length = options.length; + this.args = Object.entries(options.args ?? {}).map(([name, type]) => ({ name, type })); + this.returns = options.returns ?? new UnitType("undefined"); + this.capability = options.capability ?? "none"; + this.throws = options.throws ?? false; + this.rustPath = options.rustPath; + let optional = false; + for (const { name, type } of this.args) { + if (type.permitsUndefined) optional = true; + else assert(!optional, `required argument ${name} follows an optional one`); } - if (!type.flags.optional && !("default" in type.flags)) { - if (seenOptionalArgument) { - throw new Error(`Required argument ${name} cannot follow an optional argument`); - } - minRequiredArgs++; - } else { - seenOptionalArgument = true; + for (const { name, type } of this.args) { + assert( + !type.borrowsHeap || this.capability !== "js", + `${name}: JavaScript can detach a buffer; take b.bufferSource and use with_bytes`, + ); } + assert(!(this.returns instanceof DefaultType)); + assert(!this.returns.holdsJSValue || this.capability !== "none", "returning a JS value takes a capability"); + } + get requiredCount(): number { + return this.args.filter(arg => !arg.type.permitsUndefined).length; } - - return { minRequiredArgs }; } -function snapshotCallerLocation(): string { - const stack = new Error().stack!; - const lines = stack.split("\n"); - let i = 1; - for (; i < lines.length; i++) { - if (!lines[i].includes(import.meta.dir)) { - return lines[i]; - } +export class FunctionDefinition { + /** More than one: picked by argument count, most arguments first. */ + readonly signatures: readonly Signature[]; + name = ""; + origin: Origin | null = null; + constructor(signatures: readonly SignatureOptions[]) { + assert(signatures.length > 0); + this.signatures = signatures.map(options => new Signature(options)).sort((a, b) => b.args.length - a.args.length); + const counts = this.signatures.map(signature => signature.requiredCount); + assert(new Set(counts).size === counts.length, "overloads must differ in their number of required arguments"); + } + get length(): number { + const given = this.signatures.find(signature => signature.length !== undefined); + return given?.length ?? Math.min(...this.signatures.map(signature => signature.requiredCount)); } - throw new Error("Couldn't find caller location in stack trace"); } -function stackTraceFileName(line: string): string { - const match = /(?:at\s+|\()(.:?[^:\n(\)]*)[^(\n]*$/i.exec(line); - assert(match, `Couldn't extract filename from stack trace line: ${line}`); - return match[1].replaceAll("\\", "/"); +// ───────────────────────────── classes ───────────────────────────── + +/** A native function. */ +export interface FunctionField extends Omit { + /** Name of the method in the generated trait. Defaults to the property's name in snake_case. */ + fn?: string; + /** `&mut self` instead of `&self`. Not with `capability: "js"`, which gets the wrapper. */ + mutates?: boolean; + /** Also pass the wrapper, for its `values`. `capability: "js"` always gets it. */ + passThis?: boolean; + /** Key the function by this private builtin name (`$name` in src/js) instead of a string. */ + privateSymbol?: string; } -export type CAbiType = - | "*anyopaque" - | "*JSGlobalObject" - | "JSValue" - | "JSValue.MaybeException" - | "u0" - | "bun.String" - | "bool" - | "u8" - | "u16" - | "u32" - | "u64" - | "usize" - | "i8" - | "i16" - | "i32" - | "i64" - | "f64" - | Struct; - -export function cAbiTypeInfo(type: CAbiType): [size: number, align: number] { - if (typeof type !== "string") { - return type.abiInfo(); - } - switch (type) { - case "u0": - return [0, 0]; // no-op - case "bool": - case "u8": - case "i8": - return [1, 1]; - case "u16": - case "i16": - return [2, 2]; - case "u32": - case "i32": - return [4, 4]; - case "usize": - case "u64": - case "i64": - case "f64": - return [8, 8]; - case "*anyopaque": - case "*JSGlobalObject": - case "JSValue": - case "JSValue.MaybeException": - return [8, 8]; // pointer size - case "bun.String": - return [24, 8]; - default: - throw new Error("unexpected: " + (type satisfies never)); - } +export interface GetterField { + getter: Type; + /** Also generate a setter, which takes `&mut self` (or the wrapper, with `capability: "js"`). */ + setter?: true; + /** Computed once per object, then kept in the wrapper. */ + cache?: true; + capability?: Capability; + throws?: boolean; } -export function cAbiTypeName(type: CAbiType) { - if (typeof type !== "string") { - return type.name(); - } - return ( - { - "*anyopaque": "void*", - "*JSGlobalObject": "JSC::JSGlobalObject*", - "JSValue": "JSValue", - "JSValue.MaybeException": "JSValue", - "bool": "bool", - "u8": "uint8_t", - "u16": "uint16_t", - "u32": "uint32_t", - "u64": "uint64_t", - "i8": "int8_t", - "i16": "int16_t", - "i32": "int32_t", - "i64": "int64_t", - "f64": "double", - "usize": "size_t", - "bun.String": "BunString", - u0: "void", - } satisfies Record, string> - )[type]; +/** A function written in JavaScript: the name of the code generator bundle-functions.ts emits. */ +export interface BuiltinField { + builtin: string; + length: number; } -export function alignForward(size: number, alignment: number) { - return Math.floor((size + alignment - 1) / alignment) * alignment; +export type Field = FunctionField | GetterField | BuiltinField; + +export interface ClassOptions { + /** Path of the Rust struct. It implements the generated `${name}Class` trait. */ + rustPath: string; + /** Absent: JavaScript cannot construct one, and there is no constructor object. */ + construct?: Omit; + /** Properties of the prototype. */ + proto?: Record; + /** Properties of the constructor. */ + klass?: Record; + /** JS values the object holds. The GC visits them; the wrapper gets `_get_cached` / `_set_cached` for each. */ + values?: readonly string[]; + /** `false`: prototype properties are non-configurable. */ + configurable?: boolean; } -export class Struct { - fields: StructField[] = []; - #hash?: string; - #name?: string; - - abiInfo(): [size: number, align: number] { - let size = 0; - let align = 0; - for (const field of this.fields) { - size = alignForward(size, field.naturalAlignment); - size += field.size; - align = Math.max(align, field.naturalAlignment); - } - return [size, align]; +export class Method extends Signature { + readonly name: string; + readonly rustName: string; + readonly receiver: "ref" | "mut" | "wrapper" | "static"; + readonly passThis: boolean; + readonly privateSymbol: string | undefined; + constructor(name: string, options: FunctionField, isStatic = false) { + super(options); + this.name = name; + this.rustName = options.fn ?? rustIdent(snakeCase(name)); + assert( + !(options.mutates && this.capability === "js"), + `${name}: a method that can run JavaScript gets the wrapper, not &mut self`, + ); + this.receiver = isStatic ? "static" : this.capability === "js" ? "wrapper" : options.mutates ? "mut" : "ref"; + this.passThis = !isStatic && this.capability !== "js" && !!options.passThis; + this.privateSymbol = options.privateSymbol; } +} - reorderForSmallestSize() { - // for conistency sort by alignment, then size, then name - this.fields.sort((a, b) => { - if (a.naturalAlignment !== b.naturalAlignment) { - return a.naturalAlignment - b.naturalAlignment; - } - if (a.size !== b.size) { - return a.size - b.size; - } - return a.name.localeCompare(b.name); - }); +export class Getter { + readonly name: string; + readonly type: Type; + readonly capability: Capability; + readonly throws: boolean; + readonly hasSetter: boolean; + /** Index of the internal field a cached value lives in, or -1. */ + internalFieldIndex = -1; + constructor(name: string, options: GetterField) { + this.name = name; + this.type = options.getter; + this.capability = options.capability ?? "none"; + this.throws = options.throws ?? false; + this.hasSetter = options.setter ?? false; + assert(!(options.cache && options.setter), `${name}: a cached getter has no setter`); + if (options.cache) this.internalFieldIndex = 0; } - - hash() { - return (this.#hash ??= String( - Bun.hash( - this.fields - .map(f => { - if (f.type instanceof Struct) { - return f.name + `:` + f.type.hash(); - } - return f.name + `:` + f.type; - }) - .join(","), - ), - )); + get rustName(): string { + return rustIdent(snakeCase(this.name)); } +} - name() { - if (this.#name) return this.#name; - const hash = this.hash(); - const existing = structHashToSelf.get(hash); - if (existing && existing !== this) return (this.#name = existing.name()); - return (this.#name = `anon_extern_struct_${hash}`); +/** Stands for the class being declared, in its own signatures. */ +class SelfType extends Type { + readonly idlType = ""; + readonly tsType = "this"; + toCpp(): string { + throw new TypeError("b.self is only for the arguments and results of a class's own members"); } - - assignName(name: string) { - if (this.#name) return; - const hash = this.hash(); - const existing = structHashToSelf.get(hash); - if (existing && existing.#name) name = existing.#name; - this.#name = name; - if (existing) existing.#name = name; + layout(): Layout { + return this.toCpp() as never; } - - assignGeneratedName(name: string) { - if (this.#name) return; - this.assignName(name); + rustExternType(): string { + return this.toCpp(); } - - add(name: string, cType: CAbiType) { - const [size, naturalAlignment] = cAbiTypeInfo(cType); - this.fields.push({ name, type: cType, size, naturalAlignment }); + rustType(): string { + return this.toCpp(); } - - emitCpp(cpp: CodeWriter, structName: string) { - cpp.line(`struct ${structName} {`); - cpp.indent(); - for (const field of this.fields) { - cpp.line(`${cAbiTypeName(field.type)} ${field.name};`); +} +export const selfType = new SelfType(); + +/** As an argument or a result: the wrapper, `JsWrapper<'a, T>`. */ +export class ClassType extends NamedType { + readonly rustPath: string; + readonly construct: Signature | null; + readonly methods: readonly Method[]; + readonly getters: readonly Getter[]; + readonly staticMethods: readonly Method[]; + readonly builtins: readonly ({ name: string } & BuiltinField)[]; + readonly values: readonly string[]; + /** `values`, then one per cached getter. */ + readonly internalFields: readonly string[]; + readonly configurable: boolean; + /** Index in `Tables::classes`. Set by the generator. */ + classIndex = -1; + constructor(name: string, options: ClassOptions) { + super(name); + this.rustPath = options.rustPath; + const own = (type: Type) => (type === selfType ? this : type); + const resolve = (signature: T): T => ({ + ...signature, + args: Object.fromEntries(Object.entries(signature.args ?? {}).map(([name, type]) => [name, own(type)])), + ...(signature.returns ? { returns: own(signature.returns) } : {}), + }); + this.construct = options.construct ? new Signature(resolve(options.construct)) : null; + const methods: Method[] = []; + const getters: Getter[] = []; + const builtins: ({ name: string } & BuiltinField)[] = []; + for (const [name, field] of Object.entries(options.proto ?? {})) { + if ("builtin" in field) builtins.push({ name, ...field }); + else if ("getter" in field) getters.push(new Getter(name, field)); + else methods.push(new Method(name, resolve(field))); + } + this.methods = methods; + this.getters = getters; + this.builtins = builtins; + this.staticMethods = Object.entries(options.klass ?? {}).map( + ([name, field]) => new Method(name, resolve(field), true), + ); + assert(this.construct || this.staticMethods.length === 0, `${name}: klass needs a constructor object`); + this.values = options.values ?? []; + const internalFields = [...this.values]; + for (const getter of getters) { + if (getter.internalFieldIndex >= 0) getter.internalFieldIndex = internalFields.push(getter.name) - 1; + } + this.internalFields = internalFields; + this.configurable = options.configurable ?? true; + for (const method of this.methods) { + assert( + method.receiver !== "mut" || + !method.args.some(arg => arg.type.dependencies.includes(this) || arg.type === this), + `${name}.${method.name}: &mut self and an argument of the same class may be the same object`, + ); } - cpp.dedent(); - cpp.line("};"); } -} - -export interface StructField { - /** camel case */ - name: string; - type: CAbiType; - size: number; - naturalAlignment: number; -} - -export class CodeWriter { - level = 0; - buffer = ""; - - temporaries = new Set(); - - line(s?: string) { - this.add((s ?? "") + "\n"); + get cpp(): string { + return `::Bun::Bindgen::Generated::JS${this.name}`; } - - add(s: string) { - this.buffer += (this.buffer.endsWith("\n") ? " ".repeat(this.level) : "") + s; + get idlType(): string { + return `::Bun::Bindgen::IDLRawInterface<${this.cpp}>`; } - - indent() { - this.level += 1; + get holdsJSValue(): boolean { + return true; } - - dedent() { - this.level -= 1; + get nullRepresentation() { + return "pointer" as const; } - - trimLastNewline() { - this.buffer = this.buffer.trimEnd(); + /** Signatures are not dependencies: a class may mention itself. */ + get signatureTypes(): Type[] { + return [ + ...(this.construct ? this.construct.args.map(arg => arg.type) : []), + ...[...this.methods, ...this.staticMethods].flatMap(method => [ + ...method.args.map(arg => arg.type), + method.returns, + ]), + ...this.getters.map(getter => getter.type), + ]; } - - resetTemporaries() { - this.temporaries.clear(); + toCpp(): string { + throw new TypeError(`${this.name} cannot have a default`); + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "*mut u8"; + } + rustType(): string { + return `bindgen::JsWrapper<'a, ${this.rustPath}>`; + } + rustFromExtern(expr: string): string { + return `bindgen::JsWrapper::<${this.rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(${expr}))`; } + rustToJS(value: string): string { + return `bindgen::to_js::any(heap, ${value}.as_value())`; + } +} - nextTemporaryName(label: string) { - let i = 0; - let name = `${label}_${i}`; - while (this.temporaries.has(name)) { - i++; - name = `${label}_${i}`; - } - this.temporaries.add(name); - return name; +export class ModuleDefinition { + /** Rust path the file's functions live under, e.g. `crate::node::node_os`. */ + readonly rustPath: string; + constructor(options: { rustPath: string }) { + this.rustPath = options.rustPath; } } diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 623c1e619ae9..126f1d42418b 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -1,339 +1,104 @@ -/** - * This is the public API for `bind.ts` files - * It is aliased as `import {} from 'bindgen'` - * @see https://bun.com/docs/project/bindgen - */ +// What a `.bind.ts` file imports: +// +// import * as b from "../../codegen/bindgen-lib.ts"; +// +// See docs/project/bindgen.mdx. import { - dictionaryImpl, - isFunc, - isType, - oneOfImpl, - registerFunction, - TypeImpl, - type TypeKind, -} from "./bindgen-lib-internal"; - -/** A type definition for argument parsing. See `bindgen.md` for usage details. */ -export type Type< - /** T = JavaScript type that the Type represents */ - T, - /** K = "kind" a string pertaining to the `t.` that created this type. affects method listing */ - K extends TypeKind = TypeKind, - /** F = "flags" defining if the value is optional. null = not set, false = required, true = optional. */ - F extends TypeFlag = null, -> = F extends null - ? Props - : F extends true - ? { - [isType]: true | [T, K, F]; - nonNull: Type; - } - : { [isType]: true | [T, K, F] }; - -type TypeFlag = boolean | "opt-nonnull" | null; - -// This needs to be exported to avoid error TS4023. -export interface BaseTypeProps { - [isType]: true | [T, K]; - /** - * Optional means the value may be omitted from a parameter definition. - * Parameters are required by default. - */ - optional: Type; - /** - * When this is used as a dictionary value, this makes that parameter - * required. Dictionary entries are optional by default. - */ - required: Type, K, false>; - - /** Implies `optional`, this sets a default value if omitted */ - default(def: T): Type; -} - -interface NumericTypeProps extends BaseTypeProps { - /** - * Applies [Clamp] semantics - * https://webidl.spec.whatwg.org/#Clamp - * If a custom numeric range is provided, it will be used instead of the built-in clamp rules. - */ - clamp(min?: T, max?: T): Type; - /** - * Applies [EnforceRange] semantics - * https://webidl.spec.whatwg.org/#EnforceRange - * If a custom numeric range is provided, it will be used instead of the built-in enforce rules. - */ - enforceRange(min?: T, max?: T): Type; - - /** - * Equivalent to calling Node.js' `validateInteger(val, prop, min, max)` - */ - validateInteger(min?: T, max?: T): Type; -} + AnyType, + ArrayType, + BoolType, + BufferSourceType, + BytesType, + type ClassOptions, + ClassType, + type DictionaryOptions, + DictionaryType, + DoubleType, + EnumType, + FunctionDefinition, + integers, + type MemberOptions, + ModuleDefinition, + RefType, + type SignatureOptions, + StringType, + type Type, + UnionType, + UnitType, + Utf8Type, +} from "./bindgen-lib-internal.ts"; + +export type { Capability, Type } from "./bindgen-lib-internal.ts"; +export { null_ as null, undefined_ as undefined }; + +/** Strict: only a boolean. `.loose` for `ToBoolean`. */ +export const bool = new BoolType(); -interface I32TypeProps extends NumericTypeProps { - /** - * Equivalent to calling Node.js' `validateInt32(val, prop, min, max)` - */ - validateInt32(min?: number, max?: number): Type; -} - -interface U32TypeProps extends NumericTypeProps { - /** - * Equivalent to calling Node.js' `validateUint32(val, prop, min, max)` - */ - validateUint32(min?: number, max?: number): Type; -} - -interface F64TypeProps extends NumericTypeProps { - /** - * Throws an error if the input is non-finite (NaN, ±Infinity) - */ - finite: Type; - /** - * Equivalent to calling Node.js' `validateNumber(val, prop, min, max)` - */ - validateNumber(min?: number, max?: number): Type; -} - -// If an entry does not exist, then `BaseTypeProps` is assumed. -// T = JavaScript type that the Type represents -interface TypePropsMap { - // Integer types are always numbers, so T is not passed - ["u8"]: NumericTypeProps; - ["i8"]: NumericTypeProps; - ["u16"]: NumericTypeProps; - ["i16"]: NumericTypeProps; - ["u32"]: U32TypeProps; - ["i32"]: I32TypeProps; - ["u64"]: NumericTypeProps; - ["i64"]: NumericTypeProps; - // F64 is always a number, so T is not passed. - ["f64"]: F64TypeProps; -} - -type PropertyMapKeys = keyof TypePropsMap; -type Props = K extends PropertyMapKeys ? TypePropsMap[K] : BaseTypeProps; +/** + * Strict: a number (or BigInt) that is an integer in range. `.loose`, `.wrap`, `.enforceRange()`, + * `.validateInteger()`. + */ +export const { i8, u8, i16, u16, i32, u32, i64, u64, usize } = integers; -export type AcceptedDictionaryTypeKind = Exclude; +/** Strict: only a number. `.finite`, `.loose`. */ +export const f64 = new DoubleType(); -function builtinType() { - return (kind: K) => new TypeImpl(kind, undefined as any, {}) as Type as Type; -} +/** An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`. */ +export const string = new StringType(); -/** Contains all primitive types provided by the bindings generator */ -export namespace t { - /** - * Can only be used as an argument type. The generated host function passes the - * `JSGlobalObject*` for this argument. - */ - export const globalObject = builtinType()("globalObject"); - /** - * Can only be used as an argument type. The generated host function passes the - * `JSGlobalObject*` for this argument; the native side takes its `VirtualMachine` from it. - */ - export const virtualMachine = builtinType()("virtualMachine"); +/** `&[u8]`, UTF-8, for the duration of the call. Strict: a string or `String` object. `.loose` for `ToString`. */ +export const utf8 = new Utf8Type(); - /** - * Provides the raw JSValue from the JavaScriptCore API. Avoid using this if - * possible. This indicates the bindings generator is incapable of processing - * your use case. - */ - export const any = builtinType()("any"); - /** Void function type */ - export const undefined = builtinType()("undefined"); - /** Does not throw on parse. Equivalent to `!!value` */ - export const boolean = builtinType()("boolean"); - /** Throws if the value is not a boolean. */ - export const strictBoolean = builtinType()("strictBoolean"); +/** The JS value itself, as a `JsValue<'a>`. */ +export const any = new AnyType(); - /** - * Equivalent to IDL's `unrestricted double`, allowing NaN and Infinity. - * To restrict to finite values, use `f64.finite`. - */ - export const f64 = builtinType()("f64"); +/** + * `&[u8]`: the bytes of a typed array, `DataView` or `ArrayBuffer`, for the duration of the call. Not shared + * ones. Not for `capability: "js"`, where JavaScript could detach them. + */ +export const bytes = new BytesType(); - export const u8 = builtinType()("u8"); - export const u16 = builtinType()("u16"); - export const u32 = builtinType()("u32"); - export const u64 = builtinType()("u64"); - export const i8 = builtinType()("i8"); - export const i16 = builtinType()("i16"); - export const i32 = builtinType()("i32"); - export const i64 = builtinType()("i64"); - export const usize = builtinType()("usize"); +/** The same values as a `JsValue<'a, ArrayBuffer>`, whose `with_bytes` looks the bytes up each time. */ +export const bufferSource = new BufferSourceType(); - /** - * The DOMString type corresponds to strings. - * - * @note A DOMString value might include unmatched surrogate code points. - * Use USVString if this is not desirable. - * - * @see https://webidl.spec.whatwg.org/#idl-DOMString - */ - export const DOMString = builtinType()("DOMString"); - /** - * The {@link USVString} type corresponds to scalar value strings. Depending on the - * context, these can be treated as sequences of code units or scalar values. - * - * Specifications should only use USVString for APIs that perform text - * processing and need a string of scalar values to operate on. Most APIs that - * use strings should instead be using {@link DOMString}, which does not make any - * interpretations of the code units in the string. When in doubt, use - * {@link DOMString} - * - * @see https://webidl.spec.whatwg.org/#idl-USVString - */ - export const USVString = builtinType()("USVString"); - /** - * The ByteString type corresponds to byte sequences. - * - * WARNING: Specifications should only use ByteString for interfacing with - * protocols that use bytes and strings interchangeably, such as HTTP. In - * general, strings should be represented with {@link DOMString} values, even - * if it is expected that values of the string will always be in ASCII or some - * 8-bit character encoding. Sequences or frozen arrays with octet or byte - * elements, {@link Uint8Array}, or {@link Int8Array} should be used for - * holding 8-bit data rather than `ByteString`. - * - * https://webidl.spec.whatwg.org/#idl-ByteString - */ - export const ByteString = builtinType()("ByteString"); - /** - * DOMString but encoded as `[]const u8` - * - * ```ts - * // foo.bind.ts - * import { fn, t } from "bindgen"; - * - * export const foo = fn({ - * args: { bar: t.UTF8String }, - * }) - * ``` - * - * ```rust - * pub fn foo(bar: &[u8]) { - * // ... - * } - * ``` - */ - export const UTF8String = builtinType()("UTF8String"); +/** The `JSC::ArrayBuffer` behind an `ArrayBuffer`, typed array or `DataView`. Not shared ones. */ +export const arrayBuffer = new RefType( + "::Bun::IDLArrayBufferRef", + "ArrayBuffer | ArrayBufferView", + "bun_jsc::JSCArrayBuffer", + null, +); - /** An array or iterable type of T */ - export function sequence(itemType: Type): Type, "sequence"> { - return new TypeImpl("sequence", { - element: itemType as TypeImpl, - repr: "slice", - }); - } +/** The native `Blob` behind a JS `Blob`. */ +export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h"); - /** Object with arbitrary keys but a specific value type */ - export function record(valueType: Type): Type, "record"> { - return new TypeImpl("record", { - value: valueType as TypeImpl, - repr: "kv-slices", - }); - } +const null_ = new UnitType("null"); +const undefined_ = new UnitType("undefined"); - /** - * Reference a type by string name instead of by object reference. This is - * required in some siutations like {@link Request} which can take an existing - * request object in as itself. - */ - export function ref(name: string): Type { - return new TypeImpl("ref", name); - } +export const array = (element: Type) => new ArrayType(element); - /** - * Reference an external class type that is not defined with `bindgen`, - * from either WebCore, JavaScriptCore, or Bun. - */ - export function externalClass(name: string): Type { - return new TypeImpl("ref", name); - } +/** An array in place of a value lists several spellings of it; the first is the one given back to JS. */ +export const enumeration = (name: string, values: readonly (string | readonly string[])[]) => + new EnumType(name, values); - export function oneOf[]>( - ...types: T - ): Type< - { - [K in keyof T]: T[K] extends Type ? U : never; - }[number], - "oneOf" - > { - return oneOfImpl(types as unknown[] as TypeImpl[]); - } +export const dictionary = (options: string | DictionaryOptions, members: Record) => + new DictionaryType(options, members); - export function dictionary>>( - fields: R, - ): Type< - { - [K in keyof R]?: R[K] extends Type ? T : never; - }, - "dictionary" - > { - return dictionaryImpl(fields as Record); - } +/** Alternatives are tried in order. */ +export const union = (name: string, alternatives: Record) => new UnionType(name, alternatives); - /** Create an enum from a list of strings. */ - export function stringEnum( - ...values: T - ): Type< - { - [K in keyof T]: K; - }[number], - "stringEnum" - > { - return new TypeImpl("stringEnum", values.sort()); - } -} +/** A JS class whose state is the Rust struct at `options.rustPath`, stored inside the wrapper object. */ +const class_ = (name: string, options: ClassOptions) => new ClassType(name, options); +export { class_ as class }; -interface FuncOptionsWithVariant { - /** - * Declare a function with multiple overloads. Each overload gets its own - * native function named "name`n`" where `n` is the 1-based index of the - * overload. - * - * ## Example - * ```ts - * // foo.bind.ts - * import { fn } from "bindgen"; - * - * export const foo = fn({ - * variants: [ - * { - * args: { a: t.i32 }, - * ret: t.i32, - * }, - * { - * args: { a: t.i32, b: t.i32 }, - * ret: t.boolean, - * } - * ] - * }); - * ``` - * - * ```rust - * pub fn foo1(a: i32) -> i32 { - * a - * } - * - * pub fn foo2(a: i32, b: i32) -> bool { - * a == b - * } - * ``` - */ - variants: FuncVariant[]; -} -export type FuncOptions = FuncOptionsWithVariant | FuncVariant; +/** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */ +export { selfType as self } from "./bindgen-lib-internal.ts"; -export type FuncReference = { [isFunc]: true }; +export const fn = (signature: SignatureOptions) => new FunctionDefinition([signature]); -export interface FuncVariant { - /** Ordered record. Cannot include ".required" types since required is the default. */ - args: Record>; - ret: Type; -} +/** One JS function, several signatures, told apart by how many arguments were passed. */ +export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); -export function fn(opts: FuncOptions) { - return registerFunction(opts) as FuncReference; -} +/** `export default b.module({ rustPath: "crate::node::node_os" })`: where the file's Rust functions are. */ +export const module = (options: { rustPath: string }) => new ModuleDefinition(options); diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index bb9837503b68..542142363e82 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1,1284 +1,113 @@ -// The binding generator to rule them all. -// Converts binding definition files (.bind.ts) into C++ and native binding code. +#!/usr/bin/env bun +// Usage: bindgen.ts --sources= --codegen-path= --types-path= // -// Generated bindings are available as `Generated::::*` in C++ from including -// `Generated.h`; the Rust side binds the emitted `bindgen_*` symbols by hand in -// `src/jsc/bindings/GeneratedBindings.rs` (`bun_jsc::r#gen::`). +// Writes, and scripts/build/codegen.ts declares: +// /Generated.{h,cpp} per source +// /GeneratedBindings+Tables.h, GeneratedBindings+Counts.h +// /generated_bindgen_.rs per crate that owns a source +// /Bindgen.d.ts + import assert from "node:assert"; -import * as path from "node:path"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; import { - CodeWriter, - Func, - NodeValidator, - Struct, - TypeImpl, - alignForward, - cAbiIntegerLimits, - cAbiTypeName, - cap, - extDispatchVariant, - extInternalDispatchVariant, - extJsFunction, - files, - inspect, - isFunc, - pascal, - src, - str, - typeHashToNamespace, - typeHashToReachableType, - type CAbiType, - type DictionaryField, - type ReturnStrategy, - type TypeKind, - type Variant, -} from "./bindgen-lib-internal"; -import { argParse, readdirRecursiveWithExclusionsAndExtensionsSync, writeIfNotChanged } from "./helpers"; - -// arg parsing. The build passes `--debug=ON|OFF`; the output does not depend on it. -const { "codegen-root": codegenRoot } = argParse(["codegen-root", "debug"]); -if (!codegenRoot) { - console.error("Missing --codegen-root=..."); - process.exit(1); -} - -function resolveVariantStrategies(vari: Variant, name: string) { - let argIndex = 0; - let communicationStruct: Struct | undefined; - for (const arg of vari.args) { - if (arg.type.isVirtualArgument() && vari.globalObjectArg === undefined) { - vari.globalObjectArg = argIndex; - } - argIndex += 1; - - // If `extern struct` can represent this type, that is the simplest way to cross the C-ABI boundary. - const isNullable = arg.type.flags.optional && !("default" in arg.type.flags); - const abiType = !isNullable && arg.type.canDirectlyMapToCAbi(); - if (abiType) { - arg.loweringStrategy = { - // Passing by-value extern structs in C functions did not work - // reliably in release builds. - // type: cAbiTypeInfo(abiType)[0] > 8 ? "c-abi-pointer" : "c-abi-value", - // Always pass an argument by-pointer for now. - type: abiType === "*anyopaque" || abiType === "*JSGlobalObject" ? "c-abi-value" : "c-abi-pointer", - abiType, - }; - continue; - } - - communicationStruct ??= new Struct(); - const prefix = `${arg.name}`; - if (isNullable) { - resolveNullableArgumentStrategy(arg.type, prefix, communicationStruct); - } else { - resolveComplexArgumentStrategy(arg.type, prefix, communicationStruct); - } - arg.loweringStrategy = { - type: "uses-communication-buffer", - prefix, - }; - } - - if (vari.globalObjectArg === undefined) { - vari.globalObjectArg = "hidden"; - } - - return_strategy: { - if (vari.ret.kind === "undefined") { - vari.returnStrategy = { type: "void" }; - break return_strategy; - } - if (vari.ret.kind === "any") { - vari.returnStrategy = { type: "jsvalue" }; - break return_strategy; - } - const abiType = vari.ret.canDirectlyMapToCAbi(); - if (abiType) { - vari.returnStrategy = { - type: "basic-out-param", - abiType, - }; - break return_strategy; - } - } - - communicationStruct?.reorderForSmallestSize(); - communicationStruct?.assignGeneratedName(name); - vari.communicationStruct = communicationStruct; -} - -function resolveNullableArgumentStrategy(type: TypeImpl, prefix: string, communicationStruct: Struct): void { - assert(type.flags.optional && !("default" in type.flags)); - communicationStruct.add(`${prefix}Set`, "bool"); - resolveComplexArgumentStrategy(type, `${prefix}Value`, communicationStruct); -} - -function resolveComplexArgumentStrategy(type: TypeImpl, prefix: string, communicationStruct: Struct): void { - const abiType = type.canDirectlyMapToCAbi(); - if (abiType) { - communicationStruct.add(prefix, abiType); - return; - } - - switch (type.kind) { - default: - throw new Error(`TODO: resolveComplexArgumentStrategy for ${type.kind}`); - } -} - -function collectStringTemps(args: readonly { type: TypeImpl }[]): Map { - const result = new Map(); - // Use the same JS argument indexing as the arg processing loop: - // skip virtual args entirely, increment for everything else. - let jsArgIdx = 0; - for (const arg of args) { - const type = arg.type; - if (type.isVirtualArgument()) continue; - if (type.isIgnoredUndefinedType()) { - jsArgIdx++; - continue; - } - if (type.isStringType()) { - result.set(jsArgIdx, [cpp.nextTemporaryName("wtfString")]); - } else if (type.kind === "dictionary") { - const temps: string[] = []; - for (const field of type.data as DictionaryField[]) { - if (field.type.isStringType()) { - temps.push(cpp.nextTemporaryName("wtfString")); - } - } - if (temps.length > 0) { - result.set(jsArgIdx, temps); - } - } - jsArgIdx++; - } - return result; -} - -function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionName: string) { - cpp.line(`auto& vm = JSC::getVM(global);`); - cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); - if (variant.minRequiredArgs > 0) { - cpp.line(`size_t argumentCount = callFrame->argumentCount();`); - cpp.line(`if (argumentCount < ${variant.minRequiredArgs}) {`); - cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); - cpp.line(`}`); - } - const communicationStruct = variant.communicationStruct; - if (communicationStruct) { - cpp.line(`${communicationStruct.name()} buf;`); - communicationStruct.emitCpp(cppInternal, communicationStruct.name()); - } - - // Hoist all WTF::String temps to the dispatch scope so they outlive any - // BunString values that reference them. Bun::toString() does not ref the - // StringImpl, so the WTF::String must stay alive until the dispatch call. - const hoistedTemps = collectStringTemps(variant.args); - for (const temps of hoistedTemps.values()) { - for (const temp of temps) { - cpp.line(`WTF::String ${temp};`); - } - } - - let i = 0; - for (const arg of variant.args) { - const type = arg.type; - if (type.isVirtualArgument()) continue; - if (type.isIgnoredUndefinedType()) { - i += 1; - continue; - } - - const exceptionContext: ExceptionContext = { - type: "argument", - argumentIndex: i, - name: arg.name, - functionName: name, - }; - - const strategy = arg.loweringStrategy!; - assert(strategy); - - const get = variant.minRequiredArgs > i ? "uncheckedArgument" : "argument"; - cpp.line(`JSC::EnsureStillAliveScope arg${i} = callFrame->${get}(${i});`); - - let storageLocation; - let needDeclare = true; - switch (strategy.type) { - case "c-abi-pointer": - case "c-abi-value": - storageLocation = "arg" + cap(arg.name); - break; - case "uses-communication-buffer": - storageLocation = `buf.${strategy.prefix}`; - needDeclare = false; - break; - default: - throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); - } - - const jsValueRef = `arg${i}.value()`; - - /** If JavaScript may pass null or undefined */ - const isOptionalToUser = type.flags.optional || "default" in type.flags; - /** If the final representation may include null */ - const isNullable = type.flags.optional && !("default" in type.flags); - - const argTemps = hoistedTemps.get(i); - - if (isOptionalToUser) { - if (needDeclare) { - addHeaderForType(type); - cpp.line(`${type.cppName()} ${storageLocation};`); - } - const isUndefinedOrNull = type.flags.nonNull ? "isUndefined" : "isUndefinedOrNull"; - if (isNullable) { - assert(strategy.type === "uses-communication-buffer"); - cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.${isUndefinedOrNull}())) {`); - storageLocation = `${storageLocation}Value`; - } else { - cpp.line(`if (!${jsValueRef}.${isUndefinedOrNull}()) {`); - } - cpp.indent(); - const hoistedTemp = type.isStringType() ? argTemps?.[0] : undefined; - emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, "assign", hoistedTemp, argTemps); - cpp.dedent(); - if ("default" in type.flags) { - cpp.line(`} else {`); - cpp.indent(); - cpp.add(`${storageLocation} = `); - type.emitCppDefaultValue(cpp); - cpp.line(";"); - cpp.dedent(); - } else { - assert(isNullable); - } - cpp.line(`}`); - } else { - const hoistedTemp = type.isStringType() ? argTemps?.[0] : undefined; - emitConvertValue( - storageLocation, - arg.type, - jsValueRef, - exceptionContext, - needDeclare ? "declare" : "assign", - hoistedTemp, - argTemps, - ); - } - - i += 1; - } - - const returnStrategy = variant.returnStrategy!; - switch (returnStrategy.type) { - case "jsvalue": - cpp.line(`return ${dispatchFunctionName}(`); - break; - case "basic-out-param": - cpp.line(`${cAbiTypeName(returnStrategy.abiType)} out;`); - cpp.line(`if (!${dispatchFunctionName}(`); - break; - case "void": - cpp.line(`if (!${dispatchFunctionName}(`); - break; - default: - throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); - } - - let emittedFirstArgument = false; - function addCommaAfterArgument() { - if (emittedFirstArgument) { - cpp.line(","); - } else { - emittedFirstArgument = true; - } - } - - const totalArgs = variant.args.length; - i = 0; - cpp.indent(); - - if (variant.globalObjectArg === "hidden") { - addCommaAfterArgument(); - cpp.add("global"); - } - - for (const arg of variant.args) { - i += 1; - if (arg.type.isIgnoredUndefinedType()) continue; - - if (arg.type.isVirtualArgument()) { - switch (arg.type.kind) { - case "virtualMachine": - case "globalObject": - addCommaAfterArgument(); - cpp.add("global"); - break; - default: - throw new Error(`TODO: emitCppCallToVariant for ${inspect(arg.type)}`); - } - } else { - const storageLocation = `arg${cap(arg.name)}`; - const strategy = arg.loweringStrategy!; - switch (strategy.type) { - case "c-abi-pointer": - addCommaAfterArgument(); - cpp.add(`&${storageLocation}`); - break; - case "c-abi-value": - addCommaAfterArgument(); - cpp.add(`${storageLocation}`); - break; - case "uses-communication-buffer": - break; - default: - throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); - } - } - } - - if (communicationStruct) { - addCommaAfterArgument(); - cpp.add("&buf"); - } - - switch (returnStrategy.type) { - case "jsvalue": - cpp.dedent(); - if (totalArgs === 0) { - cpp.trimLastNewline(); - } - cpp.line(");"); - break; - case "void": - cpp.dedent(); - cpp.line(")) {"); - cpp.line(` return {};`); - cpp.line("}"); - cpp.line("return JSC::JSValue::encode(JSC::jsUndefined());"); - break; - case "basic-out-param": - addCommaAfterArgument(); - cpp.add("&out"); - cpp.line(); - cpp.dedent(); - cpp.line(")) {"); - cpp.line(` return {};`); - cpp.line("}"); - const simpleType = getSimpleIdlType(variant.ret); - if (simpleType) { - cpp.line(`return JSC::JSValue::encode(WebCore::toJS<${simpleType}>(*global, out));`); - break; - } - switch (variant.ret.kind) { - case "UTF8String": - throw new Error("Memory lifetime is ambiguous when returning UTF8String"); - case "DOMString": - case "USVString": - case "ByteString": - cpp.line( - `return JSC::JSValue::encode(WebCore::toJS(*global, out.transferToWTFString()));`, - ); - break; - } - break; - default: - throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); - } -} - -/** If a simple IDL type mapping exists, it also currently means there is a direct C ABI mapping */ -function getSimpleIdlType(type: TypeImpl): string | undefined { - const map: { [K in TypeKind]?: string } = { - boolean: "WebCore::IDLBoolean", - undefined: "WebCore::IDLUndefined", - usize: "WebCore::IDLUnsignedLongLong", - u8: "WebCore::IDLOctet", - u16: "WebCore::IDLUnsignedShort", - u32: "WebCore::IDLUnsignedLong", - u64: "WebCore::IDLUnsignedLongLong", - i8: "WebCore::IDLByte", - i16: "WebCore::IDLShort", - i32: "WebCore::IDLLong", - i64: "WebCore::IDLLongLong", + analyze, + cppCounts, + cppHeader, + cppSource, + cppTables, + dts, + rustForCrate, + type SchemaFile, +} from "./bindgen-emit.ts"; +import { + FunctionDefinition, + ModuleDefinition, + NamedType, + type Origin, + pascalCase, + snakeCase, + type Type, +} from "./bindgen-lib-internal.ts"; +import { argParse, writeIfNotChanged } from "./helpers.ts"; + +const require = createRequire(import.meta.url); + +/** The Cargo package whose directory contains `file`. */ +function crateOf(file: string): string { + for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) { + const manifest = path.join(dir, "Cargo.toml"); + if (!fs.existsSync(manifest)) continue; + const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(fs.readFileSync(manifest, "utf8")); + if (name) return name[1]; + } + throw new Error(`${file} is not inside a Cargo package`); +} + +function load(file: string): SchemaFile { + const stem = path.basename(file, ".bind.ts"); + const origin: Origin = { + file, + stem, + namespace: pascalCase(stem), + rustModule: snakeCase(stem), + crate: crateOf(file), }; - let entry = map[type.kind]; - if (!entry) { - switch (type.kind) { - case "f64": - entry = type.flags.finite // - ? "WebCore::IDLDouble" - : "WebCore::IDLUnrestrictedDouble"; - break; - case "stringEnum": - entry = `WebCore::IDLEnumeration<${type.cppClassName()}>`; - break; - default: - return; - } - } - - if (type.flags.range) { - const { range, nodeValidator } = type.flags; - if ((range[0] === "enforce" && range[1] !== "abi") || nodeValidator) { - if (nodeValidator) assert(nodeValidator === NodeValidator.validateInteger); // TODO? - - const [abiMin, abiMax] = cAbiIntegerLimits(type.kind as CAbiType); - let [_, min, max] = range as [string, bigint | number | "abi", bigint | number | "abi"]; - if (min === "abi") min = abiMin; - if (max === "abi") max = abiMax; - - headers.add("BindgenCustomEnforceRange.h"); - entry = `Bun::BindgenCustomEnforceRange<${cAbiTypeName(type.kind as CAbiType)}, ${min}, ${max}, Bun::BindgenCustomEnforceRangeKind::${ - nodeValidator ? "Node" : "Web" - }>`; - } else { - const rangeAdaptor = { - "clamp": "WebCore::IDLClampAdaptor", - "enforce": "WebCore::IDLEnforceRangeAdaptor", - }[range[0]]; - assert(rangeAdaptor); - entry = `${rangeAdaptor}<${entry}>`; - } - } - - return entry; -} - -type ExceptionContext = - | { type: "none" } - | { type: "argument"; argumentIndex: number; name: string; functionName: string }; - -function emitConvertValue( - storageLocation: string, - type: TypeImpl, - jsValueRef: string, - exceptionContext: ExceptionContext, - decl: "declare" | "assign", - hoistedTemp?: string, - dictStringTemps?: string[], -) { - if (decl === "declare") { - addHeaderForType(type); - } - - const simpleType = getSimpleIdlType(type); - if (simpleType) { - const cAbiType = type.canDirectlyMapToCAbi(); - assert(cAbiType); - let exceptionHandler: ExceptionHandler | undefined; - switch (exceptionContext.type) { - case "none": - break; - case "argument": - exceptionHandler = getArgumentExceptionHandler( - type, - exceptionContext.argumentIndex, - exceptionContext.name, - exceptionContext.functionName, - ); - } - - if (decl === "declare") { - cpp.add(`${type.cppName()} `); - } - - let exceptionHandlerText = exceptionHandler ? `, ${exceptionHandler.params} { ${exceptionHandler.body} }` : ""; - cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandlerText});`); - - if (type.flags.range && type.flags.range[0] === "clamp" && type.flags.range[1] !== "abi") { - emitRangeModifierCheck(cAbiType, storageLocation, type.flags.range); - } - - cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); - } else { - switch (type.kind) { - case "any": { - if (decl === "declare") { - cpp.add(`${type.cppName()} `); - } - cpp.line(`${storageLocation} = JSC::JSValue::encode(${jsValueRef});`); - break; - } - case "USVString": - case "DOMString": - case "ByteString": { - const temp = hoistedTemp ?? cpp.nextTemporaryName("wtfString"); - if (hoistedTemp) { - cpp.line(`${temp} = WebCore::convert(*global, ${jsValueRef});`); - } else { - cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); - } - cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); - - if (decl === "declare") { - cpp.add(`${type.cppName()} `); - } - cpp.line(`${storageLocation} = Bun::toString(${temp});`); - break; - } - case "UTF8String": { - const temp = hoistedTemp ?? cpp.nextTemporaryName("wtfString"); - if (hoistedTemp) { - cpp.line(`${temp} = WebCore::convert(*global, ${jsValueRef});`); - } else { - cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); - } - cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); - - if (decl === "declare") { - cpp.add(`${type.cppName()} `); - } - cpp.line(`${storageLocation} = Bun::toString(${temp});`); - break; - } - case "dictionary": { - if (decl === "declare") { - cpp.line(`${type.cppName()} ${storageLocation};`); - } - if (dictStringTemps && dictStringTemps.length > 0) { - cpp.line( - `auto did_convert = convert${type.cppInternalName()}(&${storageLocation}, global, ${jsValueRef}, ${dictStringTemps.join(", ")});`, - ); - } else { - cpp.line(`auto did_convert = convert${type.cppInternalName()}(&${storageLocation}, global, ${jsValueRef});`); - } - cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); - cpp.line(`if (!did_convert) return {};`); - break; - } - default: - throw new Error(`TODO: emitConvertValue for Type ${type.kind}`); - } - } -} - -interface ExceptionHandler { - /** @example "[](JSC::JSGlobalObject& global, ThrowScope& scope)" */ - params: string; - /** @example "WebCore::throwTypeError(global, scope)" */ - body: string; -} - -function getArgumentExceptionHandler(type: TypeImpl, argumentIndex: number, name: string, functionName: string) { - const { nodeValidator } = type.flags; - if (nodeValidator) { - switch (nodeValidator) { - case NodeValidator.validateInteger: - headers.add("ErrorCode.h"); - return { - params: `[]()`, - body: `return ${str(name)}_s;`, - }; - default: - throw new Error(`TODO: implement exception thrower for node validator ${nodeValidator}`); - } - } - switch (type.kind) { - case "stringEnum": { - return { - params: `[](JSC::JSGlobalObject& global, JSC::ThrowScope& scope)`, - body: `WebCore::throwArgumentMustBeEnumError(${[ - `global`, - `scope`, - `${argumentIndex}`, - `${str(name)}_s`, - `${str(type.name())}_s`, - `${str(functionName)}_s`, - `WebCore::expectedEnumerationValues<${type.cppClassName()}>()`, - ].join(", ")});`, - }; - break; - } - } -} - -/** - * The built in WebCore range adaptors do not support arbitrary ranges, but that - * is something we want to have. They aren't common, so they are just tacked - * onto the webkit one. - */ -function emitRangeModifierCheck( - cAbiType: CAbiType, - storageLocation: string, - range: ["clamp" | "enforce", bigint, bigint], -) { - const [kind, min, max] = range; - if (kind === "clamp") { - cpp.line(`if (${storageLocation} < ${min}) ${storageLocation} = ${min};`); - cpp.line(`else if (${storageLocation} > ${max}) ${storageLocation} = ${max};`); - } else { - // Implemented in BindgenCustomEnforceRange - throw new Error(`This should not be called for 'enforceRange' types.`); - } -} - -function addHeaderForType(type: TypeImpl) { - if (type.lowersToNamedType() && type.ownerFile) { - headers.add(`Generated${pascal(type.ownerFile)}.h`); - } -} - -function emitConvertDictionaryFunction(type: TypeImpl) { - assert(type.kind === "dictionary"); - const fields = type.data as DictionaryField[]; - - addHeaderForType(type); - - // Build WTF::String& params for string fields (caller owns the storage). - const stringFieldParams: string[] = []; - for (const field of fields) { - if (field.type.isStringType()) { - stringFieldParams.push(`WTF::String& ${field.key}_str`); - } - } - - cpp.line(`// Internal dictionary parse for ${type.name()}`); - const params = [ - `${type.cppName()}* result`, - `JSC::JSGlobalObject* global`, - `JSC::JSValue value`, - ...stringFieldParams, - ]; - cpp.line(`bool convert${type.cppInternalName()}(${params.join(", ")}) {`); - cpp.indent(); - - cpp.line(`auto& vm = JSC::getVM(global);`); - cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); - cpp.line(`bool isNullOrUndefined = value.isUndefinedOrNull();`); - cpp.line(`auto* object = isNullOrUndefined ? nullptr : value.getObject();`); - cpp.line(`if (!isNullOrUndefined && !object) [[unlikely]] {`); - cpp.line(` throwTypeError(global, throwScope);`); - cpp.line(` return false;`); - cpp.line(`}`); - cpp.line(`JSC::JSValue propValue;`); - - for (const field of fields) { - const { key, type: fieldType } = field; - cpp.line("// " + key); - cpp.line(`if (isNullOrUndefined) {`); - cpp.line(` propValue = JSC::jsUndefined();`); - cpp.line(`} else {`); - headers.add("ObjectBindings.h"); - cpp.line( - ` propValue = Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, global, object, JSC::Identifier::fromString(vm, ${str(key)}_s));`, - ); - cpp.line(` RETURN_IF_EXCEPTION(throwScope, false);`); - cpp.line(`}`); - // For string fields, use the caller-owned WTF::String& ref so the - // string data outlives this function and the BunString in the result. - const hoistedTemp = fieldType.isStringType() ? `${key}_str` : undefined; - cpp.line(`if (!propValue.isUndefined()) {`); - cpp.indent(); - emitConvertValue(`result->${key}`, fieldType, "propValue", { type: "none" }, "assign", hoistedTemp); - cpp.dedent(); - cpp.line(`} else {`); - cpp.indent(); - if (type.flags.required) { - cpp.line(`throwTypeError(global, throwScope);`); - cpp.line(`return false;`); - } else if ("default" in fieldType.flags) { - cpp.add(`result->${key} = `); - fieldType.emitCppDefaultValue(cpp); - cpp.line(";"); - } else { - throw new Error(`TODO: optional dictionary field`); - } - cpp.dedent(); - cpp.line(`}`); - } - - cpp.line(`return true;`); - cpp.dedent(); - cpp.line(`}`); - cpp.line(); -} - -function emitCppStructHeader(w: CodeWriter, type: TypeImpl) { - if (type.kind === "stringEnum") { - emitCppEnumHeader(w, type); - return; - } - - const externLayout = type.canDirectlyMapToCAbi(); - if (externLayout) { - if (typeof externLayout === "string") { - w.line(`typedef ${externLayout} ${type.name()};`); - console.warn("should this really be done lol", type); + const result: SchemaFile = { origin, moduleDefinition: null, types: [], functions: [] }; + for (const [name, value] of Object.entries(require(file) as Record)) { + if (value instanceof ModuleDefinition) { + assert(name === "default", `${stem}.bind.ts: b.module() is the default export`); + result.moduleDefinition = value; + } else if (value instanceof FunctionDefinition) { + value.name = name; + value.origin = origin; + result.functions.push(value); + } else if (value instanceof NamedType) { + // Re-exporting an import is fine; the first file to export a type owns it. + if (value.origin) continue; + assert(name === value.name, `${stem}.bind.ts: export const ${name} declares a type named ${value.name}`); + value.origin = origin; + result.types.push(value); } else { - externLayout.emitCpp(w, type.name()); - w.line(); + throw new TypeError(`${stem}.bind.ts: export ${name} is not a type, a function or b.module()`); } - return; - } - - throw new Error(`TODO: emitCppStructHeader for Type ${type.kind}`); -} - -function emitCppEnumHeader(w: CodeWriter, type: TypeImpl) { - assert(type.kind === "stringEnum"); - assert(type.data.length > 0); - const signPrefix = "u"; - const intBits = alignForward(type.data.length, 8); - const tagType = `${signPrefix}int${intBits}_t`; - w.line(`enum class ${type.name()} : ${tagType} {`); - for (const value of type.data) { - w.line(` ${pascal(value)},`); - } - w.line(`};`); - w.line(); -} - -// This function assumes in the WebCore namespace -function emitConvertEnumFunction(w: CodeWriter, type: TypeImpl) { - assert(type.kind === "stringEnum"); - assert(type.data.length > 0); - - const name = "Generated::" + type.cppName(); - headers.add("JavaScriptCore/JSCInlines.h"); - headers.add("JavaScriptCore/JSString.h"); - headers.add("wtf/NeverDestroyed.h"); - headers.add("wtf/SortedArrayMap.h"); - - w.line(`String convertEnumerationToString(${name} enumerationValue) {`); - w.indent(); - w.line(` static const NeverDestroyed values[] = {`); - w.indent(); - for (const value of type.data) { - w.line(` MAKE_STATIC_STRING_IMPL(${str(value)}),`); - } - w.dedent(); - w.line(` };`); - w.line(` return values[static_cast(enumerationValue)];`); - w.dedent(); - w.line(`}`); - w.line(); - w.line(`template<> JSString* convertEnumerationToJS(JSC::JSGlobalObject& global, ${name} enumerationValue) {`); - w.line(` return jsStringWithCache(global.vm(), convertEnumerationToString(enumerationValue));`); - w.line(`}`); - w.line(); - w.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String& stringValue)`); - w.line(`{`); - w.line( - ` static constexpr SortedArrayMap enumerationMapping { std::to_array>({`, - ); - for (const value of type.data) { - w.line(` { ${str(value)}_s, ${name}::${pascal(value)} },`); - } - w.line(` }) };`); - w.line(` if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]`); - w.line(` return *enumerationValue;`); - w.line(` return std::nullopt;`); - w.line(`}`); - w.line(); - w.line( - `template<> std::optional<${name}> parseEnumeration<${name}>(JSGlobalObject& lexicalGlobalObject, JSValue value)`, - ); - w.line(`{`); - w.line(` return parseEnumerationFromString<${name}>(value.toWTFString(&lexicalGlobalObject));`); - w.line(`}`); - w.line(); - w.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>()`); - w.line(`{`); - w.line(` return ${str(type.data.map(value => `${str(value)}`).join(", "))}_s;`); - w.line(`}`); - w.line(); -} - -function returnStrategyCppType(strategy: ReturnStrategy): string { - switch (strategy.type) { - case "basic-out-param": - case "void": - return "bool"; // true=success, false=exception - case "jsvalue": - return "JSC::EncodedJSValue"; - default: - throw new Error( - `TODO: returnStrategyCppType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, - ); - } -} - -type DistinguishablePrimitive = "undefined" | "string" | "number" | "boolean" | "object"; -type DistinguishStrategy = DistinguishablePrimitive; - -function typeCanDistinguish(t: TypeImpl[]) { - const seen: Record = { - undefined: false, - string: false, - number: false, - boolean: false, - object: false, - }; - let strategies: DistinguishStrategy[] = []; - - for (const type of t) { - let primitive: DistinguishablePrimitive | null = null; - if (type.kind === "undefined") { - primitive = "undefined"; - } else if (type.isStringType()) { - primitive = "string"; - } else if (type.isNumberType()) { - primitive = "number"; - } else if (type.kind === "boolean") { - primitive = "boolean"; - } else if (type.isObjectType()) { - primitive = "object"; - } - if (primitive) { - if (seen[primitive]) { - return null; - } - seen[primitive] = true; - strategies.push(primitive); - continue; - } - return null; // TODO: - } - - return strategies; -} - -/** This is an arbitrary classifier to allow consistent sorting for distinguishing arguments */ -function typeDistinguishmentWeight(type: TypeImpl): number { - if (type.kind === "undefined") { - return 100; - } - - if (type.isObjectType()) { - return 10; - } - - if (type.isStringType()) { - return 5; - } - - if (type.isNumberType()) { - return 3; - } - - if (type.kind === "boolean") { - return -1; - } - - return 0; -} - -function getDistinguishCode(strategy: DistinguishStrategy, type: TypeImpl, value: string) { - switch (strategy) { - case "string": - return { condition: `${value}.isString()`, canThrow: false }; - case "number": - return { condition: `${value}.isNumber()`, canThrow: false }; - case "boolean": - return { condition: `${value}.isBoolean()`, canThrow: false }; - case "object": - return { condition: `${value}.isObject()`, canThrow: false }; - case "undefined": - return { condition: `${value}.isUndefined()`, canThrow: false }; - default: - throw new Error(`TODO: getDistinguishCode for ${strategy}`); } + return result; } -/** The variation selector implementation decides which variation dispatch to call. */ -function emitCppVariationSelector(fn: Func, namespaceVar: string) { - let minRequiredArgs = Infinity; - let maxArgs = 0; - - const variationsByArgumentCount = new Map(); - - const pushToList = (argCount: number, vari: Variant) => { - assert(typeof argCount === "number"); - let list = variationsByArgumentCount.get(argCount); - if (!list) { - list = []; - variationsByArgumentCount.set(argCount, list); - } - list.push(vari); +/** Dependencies before dependents. */ +function sortTypes(file: SchemaFile): void { + const sorted: NamedType[] = []; + const visit = (type: Type) => { + if (type instanceof NamedType && sorted.includes(type)) return; + for (const dependency of type.dependencies) visit(dependency); + if (type instanceof NamedType && type.origin === file.origin) sorted.push(type); }; - - for (const vari of fn.variants) { - const vmra = vari.minRequiredArgs; - minRequiredArgs = Math.min(minRequiredArgs, vmra); - maxArgs = Math.max(maxArgs, vari.args.length); - const allArgCount = vari.args.filter(arg => !arg.type.isVirtualArgument()).length; - pushToList(vmra, vari); - if (allArgCount != vmra) { - pushToList(allArgCount, vari); - } - } - - cpp.line(`auto& vm = JSC::getVM(global);`); - cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); - if (minRequiredArgs > 0) { - cpp.line(`size_t argumentCount = std::min(callFrame->argumentCount(), ${maxArgs});`); - cpp.line(`if (argumentCount < ${minRequiredArgs}) {`); - cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); - cpp.line(`}`); - } - - const sorted = [...variationsByArgumentCount.entries()] - .map(([key, value]) => ({ argCount: key, variants: value })) - .sort((a, b) => b.argCount - a.argCount); - let argCountI = 0; - for (const { argCount, variants } of sorted) { - argCountI++; - const checkArgCount = argCountI < sorted.length && argCount !== minRequiredArgs; - if (checkArgCount) { - cpp.line(`if (argumentCount >= ${argCount}) {`); - cpp.indent(); - } - - if (variants.length === 1) { - cpp.line( - `RELEASE_AND_RETURN(throwScope, ${extInternalDispatchVariant(namespaceVar, fn.name, variants[0].suffix)}(global, callFrame));`, - ); - } else { - let argIndex = 0; - let strategies: DistinguishStrategy[] | null = null; - while (argIndex < argCount) { - strategies = typeCanDistinguish( - variants.map(v => v.args.filter(v => !v.type.isVirtualArgument())[argIndex].type), - ); - if (strategies) { - break; - } - argIndex++; - } - if (!strategies) { - const err = new Error( - `\x1b[0mVariations with ${argCount} required arguments must have at least one argument that can distinguish between them.\n` + - `Variations:\n${variants.map(v => ` ${inspect(v.args.filter(a => !a.type.isVirtualArgument()).map(x => x.type))}`).join("\n")}`, - ); - err.stack = `Error: ${err.message}\n${fn.snapshot}`; - throw err; - } - - const getArgument = minRequiredArgs > 0 ? "uncheckedArgument" : "argument"; - cpp.line(`JSC::JSValue distinguishingValue = callFrame->${getArgument}(${argIndex});`); - const sortedVariants = variants - .map((v, i) => ({ - variant: v, - type: v.args.filter(a => !a.type.isVirtualArgument())[argIndex].type, - strategy: strategies[i], - })) - .sort((a, b) => typeDistinguishmentWeight(a.type) - typeDistinguishmentWeight(b.type)); - for (const { variant: v, strategy: s } of sortedVariants) { - const arg = v.args[argIndex]; - const { condition, canThrow } = getDistinguishCode(s, arg.type, "distinguishingValue"); - cpp.line(`if (${condition}) {`); - cpp.indent(); - cpp.line( - `RELEASE_AND_RETURN(throwScope, ${extInternalDispatchVariant(namespaceVar, fn.name, v.suffix)}(global, callFrame));`, - ); - cpp.dedent(); - cpp.line(`}`); - if (canThrow) { - cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); - } - } - } - - if (checkArgCount) { - cpp.dedent(); - cpp.line(`}`); - } - } + file.types.forEach(visit); + file.types = sorted; } -// BEGIN MAIN CODE GENERATION +const args = argParse(["sources", "codegen-path", "types-path"]); +const codegenPath: string = args["codegen-path"]; +const sources = (args.sources as string) + .split(",") + .filter(source => source.length > 0) + .sort(); -// Search for all .bind.ts files -const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["node_modules", ".git"], [".bind.ts"]); +const files = sources.map(load); +files.forEach(sortTypes); +const names = files.flatMap(file => file.types.map(type => type.name)); +assert(new Set(names).size === names.length, "two .bind.ts files export a type with the same name"); +const tables = analyze(files); -// Sort for deterministic output -for (const fileName of [...unsortedFiles].sort()) { - const sourceFile = path.relative(src, fileName); - let file = files.get(sourceFile); - if (!file) { - file = { functions: [], typedefs: [] }; - files.set(sourceFile, file); - } - - const exports = import.meta.require(fileName); - - // Mark all exported TypeImpl as reachable - for (let [key, value] of Object.entries(exports)) { - if (value == null || typeof value !== "object") continue; - - if (value instanceof TypeImpl) { - value.assignName(key); - value.markReachable(); - file.typedefs.push({ name: key, type: value }); - } - - if (value[isFunc]) { - const func = value as Func; - func.name = key; - } - } - - for (const fn of file.functions) { - if (fn.name === "") { - const err = new Error(`This function definition needs to be exported`); - err.stack = `Error: ${err.message}\n${fn.snapshot}`; - throw err; - } - } -} - -const cpp = new CodeWriter(); -const cppInternal = new CodeWriter(); -const headers = new Set(); - -cpp.line("namespace Generated {"); -cpp.line(); - -cppInternal.line('// These "Arguments" definitions are for communication between C++ and Rust.'); -cppInternal.line('// Field layout depends on implementation details in "bindgen.ts", and'); -cppInternal.line("// is not intended for usage outside generated binding code."); - -headers.add("root.h"); -headers.add("IDLTypes.h"); -headers.add("JSDOMBinding.h"); -headers.add("JSDOMConvertBase.h"); -headers.add("JSDOMConvertBoolean.h"); -headers.add("JSDOMConvertNumbers.h"); -headers.add("JSDOMConvertStrings.h"); -headers.add("JSDOMExceptionHandling.h"); -headers.add("JSDOMOperation.h"); - -/** Indexed by the `.bind.ts` path; values are the binding's namespace identifier, without collisions. */ -const fileMap = new Map(); -const fileNames = new Set(); - -for (const [filename, { functions, typedefs }] of files) { - const basename = path.basename(filename, ".bind.ts"); - let varName = basename; - if (fileNames.has(varName)) { - throw new Error(`File name collision: ${basename}.bind.ts`); - } - fileNames.add(varName); - fileMap.set(filename, varName); - - if (functions.length === 0) continue; - - for (const td of typedefs) { - typeHashToNamespace.set(td.type.hash(), varName); - } - - for (const fn of functions) { - for (const vari of fn.variants) { - for (const arg of vari.args) { - arg.type.markReachable(); - } - } - } -} - -let needsWebCore = false; -for (const type of typeHashToReachableType.values()) { - // Emit convert functions for compound types in the Generated namespace - switch (type.kind) { - case "dictionary": - emitConvertDictionaryFunction(type); - break; - case "stringEnum": - needsWebCore = true; - break; - } -} - -for (const [filename, { functions }] of files) { - const namespaceVar = fileMap.get(filename)!; - assert(namespaceVar, `namespaceVar not found for ${filename}, ${inspect(fileMap)}`); - - for (const fn of functions) { - cpp.line(`// Bindings for \"${fn.name}\" from \"src/${fn.sourceFile}\"`); - const externName = extJsFunction(namespaceVar, fn.name); - - // C++ forward declarations - let variNum = 1; - for (const vari of fn.variants) { - resolveVariantStrategies( - vari, - `${pascal(namespaceVar)}${pascal(fn.name)}Arguments${fn.variants.length > 1 ? variNum : ""}`, - ); - const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); - const internalDispatchName = extInternalDispatchVariant(namespaceVar, fn.name, variNum); - - const args: string[] = []; - - if (vari.globalObjectArg === "hidden") { - args.push("JSC::JSGlobalObject*"); - } - for (const arg of vari.args) { - if (arg.type.isIgnoredUndefinedType()) continue; - const strategy = arg.loweringStrategy!; - switch (strategy.type) { - case "c-abi-pointer": - addHeaderForType(arg.type); - args.push(`const ${arg.type.cppName()}*`); - break; - case "c-abi-value": - addHeaderForType(arg.type); - args.push(arg.type.cppName()); - break; - case "uses-communication-buffer": - break; - default: - throw new Error(`TODO: C++ dispatch function for ${inspect(strategy)}`); - } - } - const { communicationStruct } = vari; - if (communicationStruct) { - args.push(`${communicationStruct.name()}*`); - } - const returnStrategy = vari.returnStrategy!; - if (returnStrategy.type === "basic-out-param") { - args.push(cAbiTypeName(returnStrategy.abiType) + "*"); - } - - cpp.line(`extern "C" ${returnStrategyCppType(vari.returnStrategy!)} ${dispatchName}(${args.join(", ")});`); - - if (fn.variants.length > 1) { - // Emit separate variant dispatch functions - cpp.line( - `extern "C" SYSV_ABI JSC::EncodedJSValue ${internalDispatchName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, - ); - cpp.line(`{`); - cpp.indent(); - cpp.resetTemporaries(); - emitCppCallToVariant(fn.name, vari, dispatchName); - cpp.dedent(); - cpp.line(`}`); - } - variNum += 1; - } - - // Generated JSC host function - cpp.line( - `extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, - ); - cpp.line(`{`); - cpp.indent(); - cpp.resetTemporaries(); - - if (fn.variants.length === 1) { - emitCppCallToVariant(fn.name, fn.variants[0], extDispatchVariant(namespaceVar, fn.name, 1)); - } else { - emitCppVariationSelector(fn, namespaceVar); - } - - cpp.dedent(); - cpp.line(`}`); - cpp.line(); - } -} - -cpp.line("} // namespace Generated"); -cpp.line(); -if (needsWebCore) { - cpp.line(`namespace WebCore {`); - cpp.line(); - for (const [type, reachableType] of typeHashToReachableType) { - switch (reachableType.kind) { - case "stringEnum": - emitConvertEnumFunction(cpp, reachableType); - break; - } - } - cpp.line(`} // namespace WebCore`); - cpp.line(); +for (const file of files) { + writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.h`), cppHeader(file)); + writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.cpp`), cppSource(file)); } - -writeIfNotChanged( - path.join(codegenRoot, "GeneratedBindings.cpp"), - [...headers].map(name => `#include ${str(name)}\n`).join("") + "\n" + cppInternal.buffer + "\n" + cpp.buffer, -); - -// Headers -for (const [filename, { functions, typedefs }] of files) { - const namespaceVar = fileMap.get(filename)!; - const header = new CodeWriter(); - const headerIncludes = new Set(); - let needsWebCoreNamespace = false; - - headerIncludes.add("root.h"); - - header.line(`namespace {`); - header.line(); - for (const fn of functions) { - const externName = extJsFunction(namespaceVar, fn.name); - header.line(`extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject*, JSC::CallFrame*);`); - } - header.line(); - header.line(`} // namespace`); - header.line(); - - header.line(`namespace Generated {`); - header.line(); - header.line(`/// Generated binding code for src/${filename}`); - header.line(`namespace ${namespaceVar} {`); - header.line(); - for (const td of typedefs) { - emitCppStructHeader(header, td.type); - - switch (td.type.kind) { - case "stringEnum": - case "dictionary": - needsWebCoreNamespace = true; - break; - } - } - for (const fn of functions) { - const externName = extJsFunction(namespaceVar, fn.name); - header.line(`constexpr auto* js${cap(fn.name)} = &${externName};`); - } - header.line(); - header.line(`} // namespace ${namespaceVar}`); - header.line(); - header.line(`} // namespace Generated`); - header.line(); - - if (needsWebCoreNamespace) { - header.line(`namespace WebCore {`); - header.line(); - for (const td of typedefs) { - switch (td.type.kind) { - case "stringEnum": - headerIncludes.add("JSDOMConvertEnumeration.h"); - const basename = td.type.name(); - const name = `Generated::${namespaceVar}::${basename}`; - header.line(`// Implement WebCore::IDLEnumeration trait for ${basename}`); - header.line(`String convertEnumerationToString(${name});`); - header.line(`template<> JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, ${name});`); - header.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String&);`); - header.line( - `template<> std::optional<${name}> parseEnumeration<${name}>(JSC::JSGlobalObject&, JSC::JSValue);`, - ); - header.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>();`); - header.line(); - break; - case "dictionary": - // TODO: - // header.line(`// Implement WebCore::IDLDictionary trait for ${td.type.name()}`); - // header.line( - // "template<> FetchRequestInit convertDictionary(JSC::JSGlobalObject&, JSC::JSValue);", - // ); - // header.line(); - break; - default: - } - } - header.line(`} // namespace WebCore`); - } - - header.buffer = - "#pragma once\n" + [...headerIncludes].map(name => `#include ${str(name)}\n`).join("") + "\n" + header.buffer; - - writeIfNotChanged(path.join(codegenRoot, `Generated${pascal(namespaceVar)}.h`), header.buffer); +writeIfNotChanged(path.join(codegenPath, "GeneratedBindings+Tables.h"), cppTables(tables)); +writeIfNotChanged(path.join(codegenPath, "GeneratedBindings+Counts.h"), cppCounts(tables)); +for (const [crate, crateFiles] of Map.groupBy(files, file => file.origin.crate)) { + writeIfNotChanged(path.join(codegenPath, `generated_bindgen_${crate}.rs`), rustForCrate(crateFiles)); } +writeIfNotChanged(path.join(args["types-path"], "Bindgen.d.ts"), dts(files)); diff --git a/src/codegen/bindgenv2/internal/any.ts b/src/codegen/bindgenv2/internal/any.ts deleted file mode 100644 index b9a96965c22f..000000000000 --- a/src/codegen/bindgenv2/internal/any.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type CodeStyle, Type } from "./base.ts"; - -export const RawAny = new (class extends Type { - get idlType() { - return "::Bun::IDLRawAny"; - } - get bindgenType() { - return "bindgen.BindgenRawAny"; - } - zigType(style?: CodeStyle) { - return "bun.bun_js.jsc.JSValue"; - } - toCpp(value: any): string { - throw RangeError("`RawAny` cannot have a default value"); - } -})(); - -export const StrongAny = new (class extends Type { - get idlType() { - return "::Bun::Bindgen::IDLStrongAny"; - } - get bindgenType() { - return "bindgen.BindgenStrongAny"; - } - zigType(style?: CodeStyle) { - return "bun.bun_js.jsc.Strong"; - } - optionalZigType(style?: CodeStyle) { - return this.zigType(style) + ".Optional"; - } - toCpp(value: any): string { - throw RangeError("`StrongAny` cannot have a default value"); - } -})(); - -export function isAny(type: Type): boolean { - return type === RawAny || type === StrongAny; -} - -export function hasRawAny(type: Type): boolean { - return type === RawAny || type.dependencies.some(hasRawAny); -} diff --git a/src/codegen/bindgenv2/internal/array.ts b/src/codegen/bindgenv2/internal/array.ts deleted file mode 100644 index 73cf0fac24d3..000000000000 --- a/src/codegen/bindgenv2/internal/array.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { hasRawAny } from "./any.ts"; -import { type CodeStyle, Type } from "./base.ts"; - -export abstract class ArrayType extends Type {} - -export function Array(elemType: Type): ArrayType { - if (hasRawAny(elemType)) { - throw RangeError("arrays cannot contain `RawAny` (use `StrongAny`)"); - } - return new (class extends ArrayType { - get idlType() { - return `::Bun::IDLArray<${elemType.idlType}>`; - } - get bindgenType() { - return `bindgen.BindgenArray(${elemType.bindgenType})`; - } - zigType(style?: CodeStyle) { - return `bun.collections.ArrayListDefault(${elemType.zigType(style)})`; - } - toCpp(value: any[]): string { - const args = `${value.map(elem => elemType.toCpp(elem)).join(", ")}`; - return `${this.idlType}::ImplementationType { ${args} }`; - } - get dependencies() { - return [elemType]; - } - getHeaders(result: Set): void { - result.add("Bindgen/ExternVectorTraits.h"); - elemType.getHeaders(result); - } - })(); -} diff --git a/src/codegen/bindgenv2/internal/base.ts b/src/codegen/bindgenv2/internal/base.ts deleted file mode 100644 index 9fcfef6afa13..000000000000 --- a/src/codegen/bindgenv2/internal/base.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { createRequire } from "node:module"; -import util from "node:util"; -import type { NullableType, OptionalType } from "./optional.ts"; - -// optional.ts imports this module, so the getters below load it when they run. -const require = createRequire(import.meta.url); - -/** Default is "compact". */ -export type CodeStyle = "compact" | "pretty"; - -export abstract class Type { - /** Treats `undefined` as a not-provided value. */ - get optional(): OptionalType { - return require("./optional.ts").optional(this); - } - - /** Treats `null` or `undefined` as a not-provided value. */ - get nullable(): NullableType { - return require("./optional.ts").nullable(this); - } - - abstract readonly idlType: string; - abstract readonly bindgenType: string; - - /** - * This can be overridden to make the generated code clearer. If overridden, it must return an - * expression that evaluates to the same type as `${this.bindgenType}.ZigType`; it should not - * actually change the type. - */ - zigType(style?: CodeStyle): string { - return this.bindgenType + ".ZigType"; - } - - /** This must be overridden if a custom `OptionalZigType` is defined. */ - optionalZigType(style?: CodeStyle): string { - return `?${this.zigType(style)}`; - } - - /** Converts a JS value into a C++ expression. Used for default values. */ - abstract toCpp(value: any): string; - - /** Other types that this type contains or otherwise depends on. */ - get dependencies(): readonly Type[] { - return []; - } - - /** Headers required by users of this type. */ - getHeaders(result: Set): void { - for (const type of this.dependencies) { - type.getHeaders(result); - } - } -} - -export abstract class NamedType extends Type { - abstract readonly name: string; - get cppHeader(): string | null { - return null; - } - get cppSource(): string | null { - return null; - } - // These getters are faster than `.cppHeader != null` etc. - get hasCppHeader(): boolean { - return false; - } - get hasCppSource(): boolean { - return false; - } - getHeaders(result: Set): void { - result.add(`Generated${this.name}.h`); - } -} - -export function validateName(name: string): void { - const reservedPrefixes = ["IDL", "Bindgen", "Extern", "Generated", "MemberType"]; - const reservedNames = ["Bun", "WTF", "JSC", "WebCore", "Self"]; - if (!/^[A-Z]/.test(name)) { - throw RangeError(`name must start with a capital letter: ${name}`); - } - if (/[^a-zA-Z0-9_]/.test(name)) { - throw RangeError(`name may only contain letters, numbers, and underscores: ${name}`); - } - if (reservedPrefixes.some(s => name.startsWith(s))) { - throw RangeError(`name starts with reserved prefix: ${name}`); - } - if (reservedNames.includes(name)) { - throw RangeError(`cannot use reserved name: ${name}`); - } -} - -export function headersForTypes(types: readonly Type[]): string[] { - const headers = new Set(); - for (const type of types) { - type.getHeaders(headers); - } - return Array.from(headers); -} - -export function dedent(text: string): string { - const commonIndent = Math.min( - ...Array.from(text.matchAll(/\n( *)[^ \n]/g) ?? []).map(m => m[1].length), - ); - text = text.trim(); - if (commonIndent > 0 && commonIndent !== Infinity) { - text = text.replaceAll("\n" + " ".repeat(commonIndent), "\n"); - } - return text.replace(/^ +$/gm, ""); -} - -/** Converts indents from 2 spaces to 4. */ -export function reindent(text: string): string { - return dedent(text).replace(/^ +/gm, "$&$&"); -} - -/** Does not indent the first line. */ -export function addIndent(amount: number, text: string): string { - return text.replaceAll("\n", "\n" + " ".repeat(amount)); -} - -export function joinIndented(amount: number, pieces: readonly string[]): string { - return addIndent(amount, pieces.map(dedent).join("\n")); -} - -export function toQuotedLiteral(value: string): string { - return `"${util.inspect(value).slice(1, -1).replaceAll('"', '\\"')}"`; -} - -export function toASCIILiteral(value: string): string { - if (value[Symbol.iterator]().some(c => c.charCodeAt(0) >= 128)) { - throw RangeError(`string must be ASCII: ${util.inspect(value)}`); - } - return `${toQuotedLiteral(value)}_s`; -} diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts deleted file mode 100644 index f6bd38c721fa..000000000000 --- a/src/codegen/bindgenv2/internal/dictionary.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { hasRawAny, isAny } from "./any.ts"; -import { - addIndent, - type CodeStyle, - dedent, - headersForTypes, - joinIndented, - NamedType, - reindent, - toASCIILiteral, - toQuotedLiteral, - Type, - validateName, -} from "./base.ts"; -import * as optional from "./optional.ts"; -import { isUnion } from "./union.ts"; - -export interface DictionaryMember { - type: Type; - /** Optional default value to use when this member is missing or undefined. */ - default?: any; - /** The name used in generated code. Defaults to the public JS name. */ - internalName?: string; - /** Alternative JavaScript names for this member. */ - altNames?: string[]; -} - -export interface DictionaryMembers { - readonly [name: string]: Type | DictionaryMember; -} - -export interface DictionaryInstance { - readonly [name: string]: any; -} - -export abstract class DictionaryType extends NamedType {} - -interface DictionaryOptions { - name: string; - /** Used in error messages. Defaults to `name`. */ - userFacingName?: string; - /** Whether to generate a `fromJS` conversion function. */ - generateConversionFunction?: boolean; -} - -export function dictionary( - nameOrOptions: string | DictionaryOptions, - members: DictionaryMembers, -): DictionaryType { - let name: string; - let userFacingName: string; - let generateConversionFunction = false; - if (typeof nameOrOptions === "string") { - name = nameOrOptions; - userFacingName = name; - } else { - name = nameOrOptions.name; - userFacingName = nameOrOptions.userFacingName ?? name; - generateConversionFunction = !!nameOrOptions.generateConversionFunction; - } - validateName(name); - const fullMembers = Object.entries(members).map( - ([name, value]) => new FullDictionaryMember(name, value), - ); - - return new (class extends DictionaryType { - get name() { - return name; - } - get idlType() { - return `::Bun::Bindgen::Generated::IDL${name}`; - } - get bindgenType() { - return `bindgen_generated.internal.${name}`; - } - zigType(style?: CodeStyle) { - return `bindgen_generated.${name}`; - } - get dependencies() { - return fullMembers.map(m => m.type); - } - - toCpp(value: DictionaryInstance): string { - for (const memberName of Object.keys(value)) { - if (!(memberName in members)) throw RangeError(`unexpected key: ${memberName}`); - } - return reindent(`${name} { - ${joinIndented( - 8, - fullMembers.map(memberInfo => { - let memberValue; - if (Object.hasOwn(value, memberInfo.name)) { - memberValue = value[memberInfo.name]; - } else if (memberInfo.hasDefault) { - memberValue = memberInfo.default; - } else if (!permitsUndefined(memberInfo.type)) { - throw RangeError(`missing key: ${memberInfo.name}`); - } - const internalName = memberInfo.internalName; - return `.${internalName} = ${memberInfo.type.toCpp(memberValue)},`; - }), - )} - }`); - } - - get hasCppHeader() { - return true; - } - get cppHeader() { - return reindent(` - #pragma once - #include "Bindgen.h" - #include "JSDOMConvertDictionary.h" - ${headersForTypes(Object.values(fullMembers).map(m => m.type)) - .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) - .join("")} - namespace Bun { - namespace Bindgen { - namespace Generated { - struct ${name} { - ${joinIndented( - 10, - fullMembers.map((memberInfo, i) => { - return ` - using MemberType${i} = ${memberInfo.type.idlType}::ImplementationType; - MemberType${i} ${memberInfo.internalName}; - `; - }), - )} - }; - using IDL${name} = ::WebCore::IDLDictionary<${name}>; - struct Extern${name} { - ${joinIndented( - 10, - fullMembers.map((memberInfo, i) => { - return ` - using MemberType${i} = ExternTraits<${name}::MemberType${i}>::ExternType; - MemberType${i} ${memberInfo.internalName}; - `; - }), - )} - };${(() => { - if (!generateConversionFunction) { - return ""; - } - const result = dedent(` - extern "C" bool bindgenConvertJSTo${name}( - ::JSC::JSGlobalObject* globalObject, - ::JSC::EncodedJSValue value, - Extern${name}* result); - `); - return addIndent(8, "\n" + result); - })()} - } - - template<> struct ExternTraits { - using ExternType = Generated::Extern${name}; - static ExternType convertToExtern(Generated::${name}&& cppValue) - { - return ExternType { - ${joinIndented( - 14, - fullMembers.map((memberInfo, i) => { - const cppType = `Generated::${name}::MemberType${i}`; - const cppValue = `::std::move(cppValue.${memberInfo.internalName})`; - const rhs = `ExternTraits<${cppType}>::convertToExtern(${cppValue})`; - return `.${memberInfo.internalName} = ${rhs},`; - }), - )} - }; - } - }; - } - - template<> - struct IDLHumanReadableName<::WebCore::IDLDictionary> - : BaseIDLHumanReadableName { - static constexpr auto humanReadableName - = ::std::to_array(${toQuotedLiteral(userFacingName)}); - }; - } - - template<> Bun::Bindgen::Generated::${name} - WebCore::convertDictionary( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value); - - ${(() => { - if (!hasRawAny(this)) { - return ""; - } - const code = ` - template<> struct WebCore::IDLDictionary<::Bun::Bindgen::Generated::${name}> - : ::Bun::Bindgen::IDLStackOnlyDictionary<::Bun::Bindgen::Generated::${name}> {}; - `; - return joinIndented(8, [code]); - })()} - `); - } - - get hasCppSource() { - return true; - } - get cppSource() { - return reindent(` - #include "root.h" - #include "Generated${name}.h" - #include "Bindgen/IDLConvert.h" - #include - #include - - template<> Bun::Bindgen::Generated::${name} - WebCore::convertDictionary( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value) - { - ::JSC::VM& vm = globalObject.vm(); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto ctx = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(userFacingName)} }; - auto* object = value.getObject(); - if (!object) [[unlikely]] { - ctx.throwNotObject(globalObject, throwScope); - return {}; - } - bool isArray = JSC::isArray(&globalObject, value); - RETURN_IF_EXCEPTION(throwScope, {}); - if (isArray) [[unlikely]] { - ctx.throwNotObject(globalObject, throwScope); - return {}; - } - ::Bun::Bindgen::Generated::${name} result; - ${joinIndented( - 10, - fullMembers.map((m, i) => memberConversion(userFacingName, m, i)), - )} - return result; - } - - ${(() => { - if (!generateConversionFunction) { - return ""; - } - const result = ` - namespace Bun::Bindgen::Generated { - extern "C" bool bindgenConvertJSTo${name}( - ::JSC::JSGlobalObject* globalObject, - ::JSC::EncodedJSValue value, - Extern${name}* result) - { - ::JSC::VM& vm = globalObject->vm(); - auto throwScope = DECLARE_THROW_SCOPE(vm); - ${name} convertedValue = ::WebCore::convert>( - *globalObject, - JSC::JSValue::decode(value) - ); - RETURN_IF_EXCEPTION(throwScope, false); - *result = ExternTraits<${name}>::convertToExtern(::std::move(convertedValue)); - return true; - } - } - `; - return joinIndented(8, [result]); - })()} - `); - } - })(); -} - -class FullDictionaryMember { - names: string[]; - internalName: string; - type: Type; - hasDefault: boolean = false; - default?: any; - - constructor(name: string, member: Type | DictionaryMember) { - if (member instanceof Type) { - this.names = [name]; - this.internalName = name; - this.type = member; - } else { - this.names = [name, ...(member.altNames ?? [])]; - this.internalName = member.internalName ?? name; - this.type = member.type; - this.hasDefault = Object.hasOwn(member, "default"); - this.default = member.default; - } - } - - get name(): string { - return this.names[0]; - } -} - -function memberConversion( - userFacingDictName: string, - memberInfo: FullDictionaryMember, - memberIndex: number, -): string { - const i = memberIndex; - const internalName = memberInfo.internalName; - const idlType = memberInfo.type.idlType; - const qualifiedName = `${userFacingDictName}.${memberInfo.name}`; - - const start = ` - ::JSC::JSValue value${i}; - auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(qualifiedName)} }; - do { - ${joinIndented( - 6, - memberInfo.names.map((memberName, altNameIndex) => { - let result = ""; - if (altNameIndex > 0) { - result = `if (!value${i}.isUndefined()) break;\n`; - } - result += dedent(` - value${i} = object->get( - &globalObject, - ::JSC::Identifier::fromString(vm, ${toASCIILiteral(memberName)})); - RETURN_IF_EXCEPTION(throwScope, {}); - `); - return result; - }), - )} - } while (false); - `; - - let end: string; - if (memberInfo.hasDefault) { - end = ` - if (value${i}.isUndefined()) { - result.${internalName} = ${memberInfo.type.toCpp(memberInfo.default)}; - } else { - result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); - RETURN_IF_EXCEPTION(throwScope, {}); - } - `; - } else if (permitsUndefined(memberInfo.type)) { - end = ` - result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); - RETURN_IF_EXCEPTION(throwScope, {}); - `; - } else { - end = ` - if (value${i}.isUndefined()) { - ctx${i}.throwRequired(globalObject, throwScope); - return {}; - } - result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); - RETURN_IF_EXCEPTION(throwScope, {}); - `; - } - const body = dedent(start) + "\n" + dedent(end); - return addIndent(2, "{\n" + body) + "\n}"; -} - -function basicPermitsUndefined(type: Type): boolean { - return ( - type instanceof optional.OptionalType || - type instanceof optional.NullableType || - type instanceof optional.LooseNullableType || - type === optional.undefined || - type === optional.null || - isAny(type) - ); -} - -function permitsUndefined(type: Type): boolean { - if (isUnion(type)) { - return type.dependencies.some(basicPermitsUndefined); - } - return basicPermitsUndefined(type); -} diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts deleted file mode 100644 index c29aac74aedb..000000000000 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ /dev/null @@ -1,177 +0,0 @@ -import assert from "node:assert"; -import util from "node:util"; -import { - type CodeStyle, - joinIndented, - NamedType, - reindent, - toASCIILiteral, - toQuotedLiteral, -} from "./base.ts"; - -abstract class EnumType extends NamedType {} - -/** - * If `values[x]` is an array, all elements of that array will map to the same underlying integral - * value (that is, `x`). Essentially, they become different spellings of the same enum value. - */ -export function enumeration( - name: string, - values: readonly (string | readonly string[])[], -): EnumType { - const uniqueValues: string[] = values.map((v, i) => { - if (!Array.isArray(v)) return v; - if (v.length === 0) throw RangeError(`enum value cannot be empty (index ${i})`); - return v[0]; - }); - if (uniqueValues.length === 0) { - throw RangeError("enum cannot be empty: " + name); - } - - const indexedValues = values - .map(v => (Array.isArray(v) ? v : [v])) - .flatMap((arr, i) => arr.map((v): [string, number] => [v, i])); - const valueMap = new Map(); - for (const [value, index] of indexedValues) { - if (valueMap.size === valueMap.set(value, index).size) { - throw RangeError(`duplicate enum value: ${util.inspect(value)}`); - } - } - - const cppMemberSet = new Set(); - for (const value of uniqueValues) { - let cppName = "k"; - cppName += value - .split(/[^A-Za-z0-9]+/) - .filter(x => x) - .map(s => s[0].toUpperCase() + s.slice(1)) - .join(""); - if (cppMemberSet.size === cppMemberSet.add(cppName).size) { - let i = 2; - while (cppMemberSet.size === cppMemberSet.add(cppName + i).size) { - ++i; - } - } - } - const cppMembers = Array.from(cppMemberSet); - return new (class extends EnumType { - get name() { - return name; - } - get idlType() { - return `::Bun::Bindgen::Generated::IDL${name}`; - } - get bindgenType() { - return `bindgen_generated.internal.${name}`; - } - zigType(style?: CodeStyle) { - return `bindgen_generated.${name}`; - } - toCpp(value: string): string { - const index = valueMap.get(value); - if (index == null) { - throw RangeError(`not a member of ${name}: ${util.inspect(value)}`); - } - return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; - } - - get hasCppHeader() { - return true; - } - get cppHeader() { - const quotedValues = uniqueValues.map(v => `"${v}"`); - let humanReadableName; - if (quotedValues.length == 0) { - assert(false); // unreachable - } else if (quotedValues.length == 1) { - humanReadableName = quotedValues[0]; - } else if (quotedValues.length == 2) { - humanReadableName = quotedValues[0] + " or " + quotedValues[1]; - } else { - humanReadableName = - quotedValues.slice(0, -1).join(", ") + ", or " + quotedValues[quotedValues.length - 1]; - } - - return reindent(` - #pragma once - #include "Bindgen/ExternTraits.h" - #include "JSDOMConvertEnumeration.h" - - namespace Bun { - namespace Bindgen { - namespace Generated { - enum class ${name} : ::std::uint32_t { - ${joinIndented( - 10, - cppMembers.map(memberName => `${memberName},`), - )} - }; - using IDL${name} = ::WebCore::IDLEnumeration; - } - template<> struct ExternTraits : TrivialExtern {}; - } - template<> - struct IDLHumanReadableName<::WebCore::IDLEnumeration> - : BaseIDLHumanReadableName { - static constexpr auto humanReadableName - = std::to_array(${toQuotedLiteral(humanReadableName)}); - }; - } - - template<> std::optional - WebCore::parseEnumerationFromString( - const WTF::String&); - - template<> std::optional - WebCore::parseEnumeration( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value); - `); - } - - get hasCppSource() { - return true; - } - get cppSource() { - const qualifiedName = "Bun::Bindgen::Generated::" + name; - const pairType = `::std::pair<::WTF::ComparableASCIILiteral, ::${qualifiedName}>`; - return reindent(` - #include "root.h" - #include "Generated${name}.h" - #include - - template<> std::optional<${qualifiedName}> - WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal) - { - static constexpr ::WTF::SortedArrayMap enumerationMapping { ::std::to_array<${pairType}>({ - ${joinIndented( - 12, - Array.from(valueMap.entries()) - .sort(([v1], [v2]) => (v1 < v2 ? -1 : 1)) - .map(([value, i]) => { - return `${pairType} { - ${toASCIILiteral(value)}, - ::${qualifiedName}::${cppMembers[i]}, - },`; - }), - )} - }) }; - if (auto* enumerationValue = enumerationMapping.tryGet(stringVal)) [[likely]] { - return *enumerationValue; - } - return std::nullopt; - } - - template<> std::optional<${qualifiedName}> - WebCore::parseEnumeration<${qualifiedName}>( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value) - { - return parseEnumerationFromString<::${qualifiedName}>( - value.toWTFString(&globalObject) - ); - } - `); - } - })(); -} diff --git a/src/codegen/bindgenv2/internal/interfaces.ts b/src/codegen/bindgenv2/internal/interfaces.ts deleted file mode 100644 index a9f3b281ee13..000000000000 --- a/src/codegen/bindgenv2/internal/interfaces.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { type CodeStyle, Type } from "./base.ts"; - -export const ArrayBuffer = new (class extends Type { - get idlType() { - return `::Bun::IDLArrayBufferRef`; - } - get bindgenType() { - return `bindgen.BindgenArrayBuffer`; - } - zigType(style?: CodeStyle) { - return "bun.bun_js.jsc.JSCArrayBuffer.Ref"; - } - optionalZigType(style?: CodeStyle) { - return this.zigType(style) + ".Optional"; - } - toCpp(value: any): string { - throw RangeError("default values for `ArrayBuffer` are not supported"); - } -})(); - -export const Blob = new (class extends Type { - get idlType() { - return `::Bun::IDLBlobRef`; - } - get bindgenType() { - return `bindgen.BindgenBlob`; - } - zigType(style?: CodeStyle) { - return "bun.bun_js.webcore.Blob.Ref"; - } - optionalZigType(style?: CodeStyle) { - return this.zigType(style) + ".Optional"; - } - toCpp(value: any): string { - throw RangeError("default values for `Blob` are not supported"); - } - getHeaders(result: Set): void { - result.add("BunIDLConvertBlob.h"); - } -})(); diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts deleted file mode 100644 index 9fc64ac84cbf..000000000000 --- a/src/codegen/bindgenv2/internal/optional.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { isAny } from "./any.ts"; -import { type CodeStyle, Type } from "./base.ts"; - -function bindgenOptional(payload: Type): string { - return `bindgen.BindgenOptional(${payload.bindgenType})`; -} - -export abstract class OptionalType extends Type {} - -/** Treats `undefined` as a not-provided value. */ -export function optional(payload: Type): OptionalType { - if (isAny(payload)) { - throw RangeError("`Any` types are already optional"); - } - return new (class extends OptionalType { - get idlType() { - return `::WebCore::IDLOptional<${payload.idlType}>`; - } - get bindgenType() { - return bindgenOptional(payload); - } - zigType(style?: CodeStyle) { - return payload.optionalZigType(style); - } - toCpp(value: any): string { - if (value === undefined) { - return `::WebCore::IDLOptional<${payload.idlType}>::nullValue()`; - } - return payload.toCpp(value); - } - })(); -} - -export abstract class NullableType extends Type { - abstract loose: LooseNullableType; -} - -/** Treats `null` or `undefined` as a not-provided value. */ -export function nullable(payload: Type): NullableType { - return new (class extends NullableType { - /** Treats all falsy values as null. */ - get loose() { - return looseNullable(payload); - } - - get idlType() { - return `::WebCore::IDLNullable<${payload.idlType}>`; - } - get bindgenType() { - return bindgenOptional(payload); - } - zigType(style?: CodeStyle) { - return payload.optionalZigType(style); - } - toCpp(value: any): string { - if (value == null) { - return `::WebCore::IDLNullable<${payload.idlType}>::nullValue()`; - } - return payload.toCpp(value); - } - })(); -} - -export abstract class LooseNullableType extends Type {} - -/** Treats all falsy values as null. */ -export function looseNullable(payload: Type): LooseNullableType { - return new (class extends LooseNullableType { - get idlType() { - return `::Bun::IDLLooseNullable<${payload.idlType}>`; - } - get bindgenType() { - return bindgenOptional(payload); - } - zigType(style?: CodeStyle) { - return payload.optionalZigType(style); - } - toCpp(value: any): string { - if (!value) { - return `::Bun::IDLLooseNullable<${payload.idlType}>::nullValue()`; - } - return payload.toCpp(value); - } - })(); -} - -/** For use in unions, to represent an optional union. */ -const Undefined = new (class extends Type { - get idlType() { - return `::Bun::IDLStrictUndefined`; - } - get bindgenType() { - return `bindgen.BindgenNull`; - } - zigType(style?: CodeStyle) { - return "void"; - } - toCpp(value: undefined): string { - return `{}`; - } -})(); - -/** For use in unions, to represent a nullable union. */ -const Null = new (class extends Type { - get idlType() { - return `::Bun::IDLStrictNull`; - } - get bindgenType() { - return `bindgen.BindgenNull`; - } - zigType(style?: CodeStyle) { - return "void"; - } - toCpp(value: null): string { - return `nullptr`; - } -})(); - -export { Null as null, Undefined as undefined }; diff --git a/src/codegen/bindgenv2/internal/primitives.ts b/src/codegen/bindgenv2/internal/primitives.ts deleted file mode 100644 index 768f90e94e03..000000000000 --- a/src/codegen/bindgenv2/internal/primitives.ts +++ /dev/null @@ -1,229 +0,0 @@ -import assert from "node:assert"; -import util from "node:util"; -import { type CodeStyle, Type } from "./base.ts"; - -export const bool = new (class extends Type { - /** Converts to a boolean, as if by calling `Boolean`. */ - get loose() { - return LooseBool; - } - - get idlType() { - return "::Bun::IDLStrictBoolean"; - } - get bindgenType() { - return `bindgen.BindgenBool`; - } - zigType(style?: CodeStyle) { - return "bool"; - } - toCpp(value: boolean): string { - assert(typeof value === "boolean"); - return value ? "true" : "false"; - } -})(); - -export const LooseBool = new (class extends Type { - get idlType() { - return "::WebCore::IDLBoolean"; - } - get bindgenType() { - return bool.bindgenType; - } - zigType(style?: CodeStyle) { - return bool.zigType(style); - } - toCpp(value: boolean): string { - return bool.toCpp(value); - } -})(); - -export abstract class IntegerType extends Type { - abstract loose: LooseIntegerType; - abstract cppType: string; -} - -function makeUnsignedType(width: number): IntegerType { - assert(Number.isInteger(width) && width > 0); - return new (class extends IntegerType { - /** Converts to a number first. */ - get loose() { - return looseUnsignedTypes[width]; - } - - get idlType() { - return `::Bun::IDLStrictInteger<${this.cppType}>`; - } - get bindgenType() { - return `bindgen.BindgenU${width}`; - } - zigType(style?: CodeStyle) { - return `u${width}`; - } - get cppType() { - return `::std::uint${width}_t`; - } - toCpp(value: number | bigint): string { - assert(typeof value === "bigint" || Number.isSafeInteger(value)); - const intValue = BigInt(value); - if (intValue < 0) throw RangeError("unsigned int cannot be negative"); - const max = 1n << BigInt(width); - if (intValue >= max) throw RangeError("integer out of range"); - return intValue.toString(); - } - })(); -} - -function makeSignedType(width: number): IntegerType { - assert(Number.isInteger(width) && width > 0); - return new (class extends IntegerType { - /** Tries to convert to a number first. */ - get loose() { - return looseSignedTypes[width]; - } - - get idlType() { - return `::Bun::IDLStrictInteger<${this.cppType}>`; - } - get bindgenType() { - return `bindgen.BindgenI${width}`; - } - zigType(style?: CodeStyle) { - return `i${width}`; - } - get cppType() { - return `::std::int${width}_t`; - } - toCpp(value: number | bigint): string { - assert(typeof value === "bigint" || Number.isSafeInteger(value)); - const intValue = BigInt(value); - const max = 1n << BigInt(width - 1); - const min = -max; - if (intValue >= max || intValue < min) { - throw RangeError("integer out of range"); - } - if (width === 64 && intValue === min) { - return `(${intValue + 1n} - 1)`; - } - return intValue.toString(); - } - })(); -} - -export const u8: IntegerType = makeUnsignedType(8); -export const u16: IntegerType = makeUnsignedType(16); -export const u32: IntegerType = makeUnsignedType(32); -export const u64: IntegerType = makeUnsignedType(64); - -export const i8: IntegerType = makeSignedType(8); -export const i16: IntegerType = makeSignedType(16); -export const i32: IntegerType = makeSignedType(32); -export const i64: IntegerType = makeSignedType(64); - -export abstract class LooseIntegerType extends Type {} - -function makeLooseIntegerType(strict: IntegerType): LooseIntegerType { - return new (class extends LooseIntegerType { - get idlType() { - return `::Bun::IDLLooseInteger<${strict.cppType}>`; - } - get bindgenType() { - return strict.bindgenType; - } - zigType(style?: CodeStyle) { - return strict.zigType(style); - } - toCpp(value: number | bigint): string { - return strict.toCpp(value); - } - })(); -} - -export const LooseU8: LooseIntegerType = makeLooseIntegerType(u8); -export const LooseU16: LooseIntegerType = makeLooseIntegerType(u16); -export const LooseU32: LooseIntegerType = makeLooseIntegerType(u32); -export const LooseU64: LooseIntegerType = makeLooseIntegerType(u64); - -export const LooseI8: LooseIntegerType = makeLooseIntegerType(i8); -export const LooseI16: LooseIntegerType = makeLooseIntegerType(i16); -export const LooseI32: LooseIntegerType = makeLooseIntegerType(i32); -export const LooseI64: LooseIntegerType = makeLooseIntegerType(i64); - -const looseUnsignedTypes: { [width: number]: LooseIntegerType } = { - 8: LooseU8, - 16: LooseU16, - 32: LooseU32, - 64: LooseU64, -}; - -const looseSignedTypes: { [width: number]: LooseIntegerType } = { - 8: LooseI8, - 16: LooseI16, - 32: LooseI32, - 64: LooseI64, -}; - -export const f64 = new (class extends Type { - /** Does not allow NaN or infinities. */ - get finite() { - return FiniteF64; - } - /** Converts to a number, as if by calling `Number`. */ - get loose() { - return LooseF64; - } - - get idlType() { - return "::Bun::IDLStrictDouble"; - } - get bindgenType() { - return `bindgen.BindgenF64`; - } - zigType(style?: CodeStyle) { - return `f64`; - } - toCpp(value: number): string { - assert(typeof value === "number"); - if (Number.isNaN(value)) { - return "::std::numeric_limits::quiet_NaN()"; - } else if (value === Infinity) { - return "::std::numeric_limits::infinity()"; - } else if (value === -Infinity) { - return "-::std::numeric_limits::infinity()"; - } else { - return util.inspect(value); - } - } -})(); - -export const FiniteF64 = new (class extends Type { - get idlType() { - return "::Bun::IDLFiniteDouble"; - } - get bindgenType() { - return f64.bindgenType; - } - zigType(style?: CodeStyle) { - return f64.zigType(style); - } - toCpp(value: number): string { - assert(typeof value === "number"); - if (!Number.isFinite(value)) throw RangeError("number must be finite"); - return util.inspect(value); - } -})(); - -export const LooseF64 = new (class extends Type { - get idlType() { - return "::WebCore::IDLUnrestrictedDouble"; - } - get bindgenType() { - return f64.bindgenType; - } - zigType(style?: CodeStyle) { - return f64.zigType(style); - } - toCpp(value: number): string { - return f64.toCpp(value); - } -})(); diff --git a/src/codegen/bindgenv2/internal/string.ts b/src/codegen/bindgenv2/internal/string.ts deleted file mode 100644 index cf048566501a..000000000000 --- a/src/codegen/bindgenv2/internal/string.ts +++ /dev/null @@ -1,44 +0,0 @@ -import assert from "node:assert"; -import { type CodeStyle, Type, toASCIILiteral } from "./base.ts"; - -export const String = new (class extends Type { - /** Converts to a string, as if by calling `String`. */ - get loose() { - return LooseString; - } - - get idlType() { - return "::Bun::IDLStrictString"; - } - get bindgenType() { - return "bindgen.BindgenString"; - } - zigType(style?: CodeStyle) { - return "bun.string.WTFString"; - } - optionalZigType(style?: CodeStyle) { - return this.zigType(style) + ".Optional"; - } - toCpp(value: string): string { - assert(typeof value === "string"); - return toASCIILiteral(value); - } -})(); - -export const LooseString = new (class extends Type { - get idlType() { - return "::Bun::IDLDOMString"; - } - get bindgenType() { - return String.bindgenType; - } - zigType(style?: CodeStyle) { - return String.zigType(style); - } - optionalZigType(style?: CodeStyle) { - return String.optionalZigType(style); - } - toCpp(value: string): string { - return String.toCpp(value); - } -})(); diff --git a/src/codegen/bindgenv2/internal/union.ts b/src/codegen/bindgenv2/internal/union.ts deleted file mode 100644 index bb2734c42af6..000000000000 --- a/src/codegen/bindgenv2/internal/union.ts +++ /dev/null @@ -1,136 +0,0 @@ -import assert from "node:assert"; -import { - type CodeStyle, - dedent, - headersForTypes, - joinIndented, - NamedType, - reindent, - Type, - validateName, -} from "./base.ts"; - -export interface NamedAlternatives { - readonly [name: string]: Type; -} - -export interface UnionInstance { - readonly type: Type; - readonly value: any; -} - -export abstract class AnonymousUnionType extends Type {} -export abstract class NamedUnionType extends NamedType {} - -export function isUnion(type: Type): boolean { - return type instanceof AnonymousUnionType || type instanceof NamedUnionType; -} - -export function union(alternatives: Type[]): AnonymousUnionType; -export function union(name: string, alternatives: NamedAlternatives): NamedUnionType; - -/** - * The order of types in this union is significant. Each type is tried in order, and the first one - * that successfully converts determines the active field in the corresponding native tagged union. - * - * This means that it is an error to specify `RawAny` or `StrongAny` as anything other than the - * last alternative, as conversion to any subsequent types would never be attempted. - */ -export function union( - alternativesOrName: Type[] | string, - maybeNamedAlternatives?: NamedAlternatives, -): AnonymousUnionType | NamedUnionType { - let alternatives: Type[]; - - function toCpp(value: UnionInstance): string { - assert(alternatives.includes(value.type)); - return `${value.type.idlType}::ImplementationType { ${value.type.toCpp(value.value)} }`; - } - - function getUnionType() { - return `::Bun::IDLOrderedUnion<${alternatives.map(a => a.idlType).join(", ")}>`; - } - - function validateAlternatives(name?: string) { - const suffix = name == null ? "" : `: ${name}`; - if (alternatives.length === 0) { - throw RangeError("union cannot be empty" + suffix); - } - } - - if (typeof alternativesOrName !== "string") { - alternatives = alternativesOrName.slice(); - validateAlternatives(); - // anonymous union (neither union nor fields are named) - return new (class extends AnonymousUnionType { - get idlType() { - return getUnionType(); - } - get bindgenType() { - return `bindgen.BindgenUnion(&.{ ${alternatives.map(a => a.bindgenType).join(", ")} })`; - } - zigType(style?: CodeStyle) { - if (style !== "pretty") { - return `bun.meta.TaggedUnion(&.{ ${alternatives.map(a => a.zigType()).join(", ")} })`; - } - return dedent(`bun.meta.TaggedUnion(&.{ - ${joinIndented( - 10, - alternatives.map(a => a.zigType("pretty") + ","), - )} - })`); - } - get dependencies() { - return Object.freeze(alternatives); - } - toCpp(value: UnionInstance): string { - return toCpp(value); - } - })(); - } - - assert(maybeNamedAlternatives !== undefined); - const namedAlternatives: NamedAlternatives = maybeNamedAlternatives; - const name: string = alternativesOrName; - validateName(name); - alternatives = Object.values(namedAlternatives); - validateAlternatives(name); - // named union (both union and fields are named) - return new (class extends NamedUnionType { - get name() { - return name; - } - get idlType() { - return `::Bun::Bindgen::Generated::IDL${name}`; - } - get bindgenType() { - return `bindgen_generated.internal.${name}`; - } - zigType(style?: CodeStyle) { - return `bindgen_generated.${name}`; - } - get dependencies() { - return Object.freeze(alternatives); - } - toCpp(value: UnionInstance): string { - return toCpp(value); - } - - get hasCppHeader() { - return true; - } - get cppHeader() { - return reindent(` - #pragma once - #include "Bindgen/IDLTypes.h" - ${headersForTypes(alternatives) - .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) - .join("")} - namespace Bun::Bindgen::Generated { - using IDL${name} = ${getUnionType()}; - using ${name} = IDL${name}::ImplementationType; - } - `); - } - })(); -} diff --git a/src/codegen/bindgenv2/lib.ts b/src/codegen/bindgenv2/lib.ts deleted file mode 100644 index e9ca7628be9b..000000000000 --- a/src/codegen/bindgenv2/lib.ts +++ /dev/null @@ -1,10 +0,0 @@ -// organize-imports-ignore -export { bool, u8, u16, u32, u64, i8, i16, i32, i64, f64 } from "./internal/primitives.ts"; -export { RawAny, StrongAny } from "./internal/any.ts"; -export { String } from "./internal/string.ts"; -export { optional, nullable, undefined, null } from "./internal/optional.ts"; -export { union } from "./internal/union.ts"; -export { dictionary } from "./internal/dictionary.ts"; -export { enumeration } from "./internal/enumeration.ts"; -export { Array } from "./internal/array.ts"; -export { ArrayBuffer, Blob } from "./internal/interfaces.ts"; diff --git a/src/codegen/bindgenv2/script.ts b/src/codegen/bindgenv2/script.ts deleted file mode 100755 index 8376bf9cbbe1..000000000000 --- a/src/codegen/bindgenv2/script.ts +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bun -import { createRequire } from "node:module"; -import * as helpers from "../helpers.ts"; -import { NamedType, Type } from "./internal/base.ts"; - -const require = createRequire(import.meta.url); - -const USAGE = `\ -Usage: script.ts [options] - -Options (all required): - --command= Command to run (see below) - --sources= Comma-separated list of *.bindv2.ts files - --codegen-path= Path to build/*/codegen - -Commands: - list-outputs List files that will be generated, separated by semicolons - generate Generate all files -`; - -let codegenPath: string; -let sources: string[]; - -function getNamedExports(): NamedType[] { - return sources.flatMap(path => { - const exports = require(path); - return Object.values(exports).filter(v => v instanceof NamedType); - }); -} - -function getNamedDependencies(type: Type, result: Set): void { - for (const dependency of type.dependencies) { - if (dependency instanceof NamedType) { - result.add(dependency); - } - getNamedDependencies(dependency, result); - } -} - -function cppHeaderPath(type: NamedType): string { - return `${codegenPath}/Generated${type.name}.h`; -} - -function cppSourcePath(type: NamedType): string { - return `${codegenPath}/Generated${type.name}.cpp`; -} - -function toZigNamespace(name: string): string { - const result = name - .replace(/([^A-Z_])([A-Z])/g, "$1_$2") - .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") - .toLowerCase(); - if (result === name) { - return result + "_namespace"; - } - return result; -} - -/** Must name every file generate() writes: the build declares these as the outputs of the generate step. */ -function listOutputs(): void { - const outputs: string[] = []; - for (const type of getNamedExports()) { - if (type.hasCppHeader) outputs.push(cppHeaderPath(type)); - if (type.hasCppSource) outputs.push(cppSourcePath(type)); - } - process.stdout.write(outputs.join(";")); -} - -function generate(): void { - const names = new Set(); - - const namedExports = getNamedExports(); - { - const namedDependencies = new Set(); - for (const type of namedExports) { - getNamedDependencies(type, namedDependencies); - } - const namedExportsSet = new Set(namedExports); - for (const type of namedDependencies) { - if (!namedExportsSet.has(type)) { - console.error(`error: named type must be exported: ${type.name}`); - process.exit(1); - } - } - const namedTypeNames = new Set(); - for (const type of namedExports) { - if (namedTypeNames.size == namedTypeNames.add(type.name).size) { - console.error(`error: multiple types with same name: ${type.name}`); - process.exit(1); - } - } - } - - for (const type of namedExports) { - const zigNamespace = toZigNamespace(type.name); - const size = names.size; - names.add(type.name); - names.add(zigNamespace); - if (names.size !== size + 2) { - console.error(`error: duplicate name: ${type.name}`); - process.exit(1); - } - - const cppHeader = type.cppHeader; - const cppSource = type.cppSource; - if (cppHeader) { - helpers.writeIfNotChanged(cppHeaderPath(type), cppHeader); - } - if (cppSource) { - helpers.writeIfNotChanged(cppSourcePath(type), cppSource); - } - } -} - -function main(): void { - const args = helpers.argParse(["command", "codegen-path", "sources", "help"]); - if (Object.keys(args).length === 0) { - process.stderr.write(USAGE); - process.exit(1); - } - const { command, "codegen-path": codegenPathArg, sources: sourcesArg, help } = args; - if (help != null) { - process.stdout.write(USAGE); - process.exit(0); - } - - if (typeof codegenPathArg !== "string") { - console.error("error: missing --codegen-path"); - process.exit(1); - } - codegenPath = codegenPathArg; - - if (typeof sourcesArg !== "string") { - console.error("error: missing --sources"); - process.exit(1); - } - sources = sourcesArg.split(",").filter(x => x); - - switch (command) { - case "list-outputs": - listOutputs(); - break; - case "generate": - generate(); - break; - default: - if (typeof command === "string") { - console.error("error: unknown command: " + command); - } else { - console.error("error: missing --command"); - } - process.exit(1); - } -} - -main(); diff --git a/src/codegen/bindgenv2/tsconfig.json b/src/codegen/bindgenv2/tsconfig.json deleted file mode 100644 index b1484c3a8022..000000000000 --- a/src/codegen/bindgenv2/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "noUnusedLocals": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitAny": true, - "noImplicitThis": true, - "exactOptionalPropertyTypes": true, - "types": ["../../../packages/bun-types"] - }, - "include": ["**/*.ts", "../helpers.ts", "../../**/*.bindv2.ts"] -} diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index 343b603abced..2ba1c3577dc6 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -53,7 +53,6 @@ const rustIdentifierPaths: Record = { "MarkdownObject.rs": "runtime/api/MarkdownObject.rs", "SecureContext.rs": "runtime/api/bun/SecureContext.rs", "Stat.rs": "runtime/node/Stat.rs", - "bindgen_test.rs": "jsc/bindgen_test.rs", "collections/linear_fifo.rs": "collections/linear_fifo.rs", "crash_handler.rs": "crash_handler/crash_handler.rs", "css_internals.rs": "css_jsc/css_internals.rs", @@ -77,7 +76,6 @@ const rustIdentifierPaths: Record = { "node_fs_binding.rs": "runtime/node/node_fs_binding.rs", "node_http_binding.rs": "runtime/node/node_http_binding.rs", "node_net_binding.rs": "runtime/node/node_net_binding.rs", - "node_os.rs": "runtime/node/node_os.rs", "node_quic_binding.rs": "runtime/node/node_quic_binding.rs", "node_util_binding.rs": "runtime/node/node_util_binding.rs", "node_zlib_binding.rs": "runtime/node/node_zlib_binding.rs", diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index ecd55f327009..ed46f83e6eb0 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -2,6 +2,7 @@ /// /// /// +/// // Typedefs for JSC intrinsics. Instead of @, we use $ type TODO = any; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 2bbb5eabf317..353f5fa529cf 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -231,9 +231,25 @@ export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFramework }; }; -export const bindgen = $rust("bindgen_test.rs", "getBindgenTestFunctions") as { - add: (a: any, b: any) => number; - requiredAndOptionalArg: (a: any, b?: any, c?: any, d?: any) => number; +export const bindgen = { + add: $bindgenFn("bindgen_test.bind.ts", "add"), + requiredAndOptionalArg: $bindgenFn("bindgen_test.bind.ts", "requiredAndOptionalArg"), + utf8Echo: $bindgenFn("bindgen_test.bind.ts", "utf8Echo"), + utf8EchoLoose: $bindgenFn("bindgen_test.bind.ts", "utf8EchoLoose"), + utf8Optional: $bindgenFn("bindgen_test.bind.ts", "utf8Optional"), + stringEcho: $bindgenFn("bindgen_test.bind.ts", "stringEcho"), + nextColor: $bindgenFn("bindgen_test.bind.ts", "nextColor"), + optionsEcho: $bindgenFn("bindgen_test.bind.ts", "optionsEcho"), + describeValue: $bindgenFn("bindgen_test.bind.ts", "describeValue"), + sumArray: $bindgenFn("bindgen_test.bind.ts", "sumArray"), + range: $bindgenFn("bindgen_test.bind.ts", "range"), + words: $bindgenFn("bindgen_test.bind.ts", "words"), + sumBytes: $bindgenFn("bindgen_test.bind.ts", "sumBytes"), + sumBeforeAndAfterCallback: $bindgenFn("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), + withValueEcho: $bindgenFn("bindgen_test.bind.ts", "withValueEcho"), + fail: $bindgenFn("bindgen_test.bind.ts", "fail"), + TestCounter: $bindgenFn("bindgen_test.bind.ts", "TestCounter"), + makeToken: $bindgenFn("bindgen_test.bind.ts", "makeToken"), }; export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting"); diff --git a/src/js/node/os.ts b/src/js/node/os.ts index 3326d8093d54..d2c90632d36c 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -152,7 +152,22 @@ function bound(binding) { }; } -const out = bound($rust("node_os.rs", "createNodeOsBinding")); +const out = bound({ + hostCpuCount: $bindgenFn("node_os.bind.ts", "hostCpuCount")(), + cpus: $bindgenFn("node_os.bind.ts", "cpus"), + freemem: $bindgenFn("node_os.bind.ts", "freemem"), + getPriority: $bindgenFn("node_os.bind.ts", "getPriority"), + homedir: $bindgenFn("node_os.bind.ts", "homedir"), + hostname: $bindgenFn("node_os.bind.ts", "hostname"), + loadavg: $bindgenFn("node_os.bind.ts", "loadavg"), + networkInterfaces: $bindgenFn("node_os.bind.ts", "networkInterfaces"), + release: $bindgenFn("node_os.bind.ts", "release"), + totalmem: $bindgenFn("node_os.bind.ts", "totalmem"), + uptime: $bindgenFn("node_os.bind.ts", "uptime"), + userInfo: $bindgenFn("node_os.bind.ts", "userInfo"), + version: $bindgenFn("node_os.bind.ts", "version"), + setPriority: $bindgenFn("node_os.bind.ts", "setPriority"), +}); symbolToStringify(out, "arch"); symbolToStringify(out, "availableParallelism"); diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 0cc104cad778..b3a8321af321 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -275,13 +275,16 @@ declare function $newRustFunction any>( argCount: number, ): T; /** - * Retrieves a handle to a function defined in native code, defined in a + * Retrieves a handle to a function or class defined in native code, defined in a * `.bind.ts` file. For more information on how to define bindgen functions, see * [bindgen's documentation](https://bun.com/docs/project/bindgen). * @param filename - The basename of the `.bind.ts` file. - * @param symbol - The name of the function to call. + * @param symbol - The name of the export. */ -declare function $bindgenFn any>(filename: string, symbol: string): T; +declare function $bindgenFn( + filename: File, + symbol: Name, +): $Bindgen.Exports[File][Name]; // NOTE: $debug, $assert, and $isPromiseFulfilled omitted declare module "node:stream" { diff --git a/src/jsc/NodeModuleModule.bind.ts b/src/jsc/NodeModuleModule.bind.ts index 64e43a9ecad6..88ffd13f52d2 100644 --- a/src/jsc/NodeModuleModule.bind.ts +++ b/src/jsc/NodeModuleModule.bind.ts @@ -1,8 +1,9 @@ -import { fn, t } from "bindgen"; +import * as b from "../codegen/bindgen-lib.ts"; -export const _stat = fn({ +export const _stat = b.fn({ args: { - str: t.UTF8String, + str: b.utf8.loose, }, - ret: t.i32, + returns: b.i32, + rustPath: "crate::node_module_module::stat", }); diff --git a/src/jsc/Strong.rs b/src/jsc/Strong.rs index 58d2540773e5..49533182462a 100644 --- a/src/jsc/Strong.rs +++ b/src/jsc/Strong.rs @@ -34,16 +34,6 @@ impl Strong { debug_assert!(!new_value.is_empty()); Impl::set(self.handle, global, new_value); } - - /// Adopt an `Impl` handle allocated externally (e.g. by C++ bindgen glue), - /// taking ownership. The handle will be destroyed on `Drop`. - /// - /// # Safety - /// `handle` must have been produced by `Bun__StrongRef__new` (or equivalent) - /// and must not be owned by any other `Strong`/`Optional`. - pub(crate) unsafe fn adopt(handle: NonNull) -> Strong { - Strong { handle } - } } impl Drop for Strong { diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 996671297db0..c845bddb9f90 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -1,293 +1,674 @@ -use core::ffi::c_uint; -use core::marker::PhantomData; -use core::mem::{ManuallyDrop, align_of, size_of}; +//! What code generated by `src/codegen/bindgen.ts` hands to a binding's Rust body. +//! +//! Three rules, all enforced by the borrow checker: +//! +//! 1. Running JavaScript, or anything that can call back into Rust, takes `&mut JsCall`. +//! 2. A pointer into memory JavaScript can free, detach, resize or alias is only reachable inside a +//! closure that borrows a field of `JsCall`, so rule 1 cannot happen while it is live. +//! 3. A JS value is a [`JsValue<'a>`], tied to the host call. It cannot be stored anywhere that +//! outlives the call; a [`Strong`] and a class's `values` can hold one longer, and the GC visits +//! those. +//! +//! `test/internal/bindgen-compile-fail` holds one rejected program per hazard. + +use core::cell::UnsafeCell; +use core::ffi::c_void; +use core::marker::{PhantomData, PhantomPinned}; use core::ptr::NonNull; -use crate::{self as jsc, Strong}; -use bun_core::{WTFString, WTFStringImplStruct}; +use crate::{ + ArrayBuffer, JSFunction, JSGlobalObject, JSObject, JSString, JSValue, JsError, JsThread, + MarkedArgumentBuffer, Strong, +}; -// `BindgenArray::convert_from_extern` reuses C++-allocated buffers by adopting -// them into `Vec` even when `align_of::() != align_of::()`. -// That is only sound because mimalloc's `mi_free` ignores the allocation layout; -// the Rust `GlobalAlloc::dealloc` contract would otherwise be violated. The C++ side -// (`ExternVectorTraits.h`) always allocates with `mi_malloc`, so when the global -// allocator is not mimalloc the reuse path is skipped and the fallback frees the -// C++ buffer with `mi_free` directly. +#[path = "bindgen/extern_types.rs"] +pub mod extern_types; -// ────────────────────────────────────────────────────────────────────────── -// A `Bindgen` adapter supplies associated `ZigType`/`ExternType` plus -// `convert_from_extern`. -// ────────────────────────────────────────────────────────────────────────── +/// Makes `'a` invariant, so two calls' lifetimes never unify. +type InvariantLifetime<'a> = PhantomData &'a ()>; +type NotSend = PhantomData<*mut ()>; +/// As `bun_opaque::opaque_ffi!`: zero-sized, `!Freeze`, `!Unpin`, so a reference to it is a bare +/// pointer to LLVM (no `noalias`). +type OpaqueFfi = (UnsafeCell<[u8; 0]>, PhantomPinned); -pub trait Bindgen { - type ZigType; - type ExternType; +// ───────────────────────────── capabilities ───────────────────────────── - /// `true` when `ZigType` and `ExternType` are layout-identical. - /// Enables `BindgenArray`'s - /// allocation-reuse fast path. Defaults to `false`; override per adapter. - const SAME_REPR: bool = false; +/// The JS heap is alive and this is its thread: allocate, read JS-owned memory, describe an error +/// to throw. Cannot run JavaScript. +#[repr(C)] +pub struct JsHeap<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); + +/// Reach the Rust payloads of wrapper objects. +#[repr(C)] +pub struct JsPayloads<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType; +/// A call into Rust on the JS thread. `&mut JsCall` is what runs JavaScript. It is the +/// `JSGlobalObject*` the host function received: both fields are zero-sized at offset 0, and public +/// so a borrow of one leaves the other usable. +#[repr(C)] +pub struct JsCall<'a> { + pub heap: JsHeap<'a>, + pub payloads: JsPayloads<'a>, } -pub struct BindgenTrivial(PhantomData); +impl<'a> JsHeap<'a> { + #[inline(always)] + fn global(&self) -> &JSGlobalObject { + // SAFETY: a `JsHeap` only exists at the address of a live global (see `JsCall::with`). + unsafe { JSGlobalObject::opaque_ref_nn(core::ptr::from_ref(self).cast()) } + } -impl Bindgen for BindgenTrivial { - type ZigType = T; - type ExternType = T; - const SAME_REPR: bool = true; + /// For generated code. + #[doc(hidden)] + #[inline(always)] + pub fn global_ptr(&self) -> *mut JSGlobalObject { + self.global().as_mut_ptr() + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - extern_value + #[inline] + pub fn create_utf8_string(&self, utf8: &[u8]) -> Result<'a, JsValue<'a, JSString>> { + JsValue::from_js_result(crate::bun_string_jsc::create_utf8_for_js( + self.global(), + utf8, + )) + } + + #[cold] + pub fn throw(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::to_throw(self.global().create_error_instance(args)) } -} -// ────────────────────────────────────────────────────────────────────────── + #[cold] + pub fn throw_type_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::to_throw(self.global().create_type_error_instance(args)) + } -pub struct BindgenStrongAny; + #[cold] + pub fn throw_range_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::to_throw(self.global().create_range_error_instance(args)) + } -impl Bindgen for BindgenStrongAny { - type ZigType = Strong; - // `?*jsc.Strong.Impl` — must be single-word for #[repr(C)] union placement, so - // `Option>` (niche-optimized), NOT `Option<*mut T>` (two words). - type ExternType = Option>; + #[cold] + pub fn throw_out_of_memory(&self) -> Error<'a> { + Error::pending(JsError::OutOfMemory) + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - // SAFETY: bindgen contract — C++ passes a freshly-allocated Strong handle - // whose ownership is transferred to Rust here. - unsafe { Strong::adopt(extern_value.expect("non-null")) } + #[cold] + pub fn throw_value(&self, value: JsValue<'a, T>) -> Error<'a> { + Error::to_throw(value.0) } } -// ────────────────────────────────────────────────────────────────────────── +impl<'a> JsCall<'a> { + /// For generated thunks and event-loop dispatch. + /// + /// # Safety + /// `global` is the current thread's live global object, and no borrow of an outer `JsCall` is + /// in use. That holds wherever the engine or the event loop calls into Rust: getting there took + /// the outer `&mut JsCall`. + #[inline(always)] + pub unsafe fn with( + global: *mut JSGlobalObject, + f: impl for<'call> FnOnce(&mut JsCall<'call>) -> R, + ) -> R { + // SAFETY: `JsCall` is zero-sized and `global` is non-null. + f(unsafe { &mut *global.cast::>() }) + } -/// This represents both `IDLNull` and `IDLMonostateUndefined`. -pub struct BindgenNull; + /// For code that only has a `&JSGlobalObject`. + /// + /// # Safety + /// As [`with`](Self::with). + #[inline(always)] + pub unsafe fn from_global(global: &'a JSGlobalObject) -> &'a mut JsCall<'a> { + // SAFETY: `JsCall` is zero-sized; caller upholds exclusivity. + unsafe { &mut *global.as_mut_ptr().cast::>() } + } -impl Bindgen for BindgenNull { - type ZigType = (); - type ExternType = u8; + /// Anything can be done with a `&JSGlobalObject`, so it costs the whole `JsCall` for as long as + /// the reference lives. + #[inline(always)] + pub fn global(&mut self) -> &JSGlobalObject { + self.heap.global() + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - let _ = extern_value; + #[inline] + pub fn js_thread(&mut self) -> JsThread<'_> { + self.heap.global().js_thread_of_caller_no_frame() + } + + /// Runs `f`, which takes a `&JSGlobalObject`. + #[inline] + pub fn with_global( + &mut self, + f: impl FnOnce(&JSGlobalObject) -> crate::JsResult, + ) -> Result<'a, R> { + f(self.heap.global()).map_err(Error::pending) + } + + /// [`with_global`](Self::with_global) for an `f` that returns a JS value. + #[inline] + pub fn with_global_value( + &mut self, + f: impl FnOnce(&JSGlobalObject) -> crate::JsResult, + ) -> Result<'a, JsValue<'a>> { + JsValue::from_js_result(f(self.heap.global())) + } + + /// Takes the pending exception off the VM. + pub fn take_exception(&mut self, error: Error<'a>) -> Option> { + match error.0 { + ErrorState::ToThrow(value) => Some(JsValue::wrap(value)), + ErrorState::Pending(JsError::Thrown) => { + self.heap.global().try_take_exception().map(JsValue::wrap) + } + ErrorState::Pending(_) => None, + } } } -// ────────────────────────────────────────────────────────────────────────── +// ───────────────────────────── errors ───────────────────────────── -pub struct BindgenOptional(PhantomData); +#[derive(Debug)] +enum ErrorState { + /// Already pending on the VM (or, for the other `JsError`s, what the thunk must do). + Pending(JsError), + /// Made under `&JsHeap`; the thunk throws it once the body has returned. `VM::throwException` + /// notifies the debugger, which can run code, so it must not happen while a body holds borrows. + ToThrow(JSValue), +} -// Wrap in `ExternTaggedUnion<(u8, Child::ExternType)>` and produce -// `Option`. -impl Bindgen for BindgenOptional { - type ZigType = Option; - type ExternType = ExternTaggedUnion2; +/// The call failed with a JS exception. Made only by this module; tied to the call, so it cannot be +/// kept and replayed. +#[derive(Debug)] +#[must_use = "dropping this leaves an exception pending (or never throws it)"] +pub struct Error<'a>(ErrorState, InvariantLifetime<'a>, NotSend); +pub type Result<'a, T> = core::result::Result>; + +impl Error<'_> { + #[inline(always)] + fn to_throw(value: JSValue) -> Self { + Error(ErrorState::ToThrow(value), PhantomData, PhantomData) + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - if extern_value.tag == 0 { - return None; + #[inline(always)] + fn pending(error: JsError) -> Self { + Error(ErrorState::Pending(error), PhantomData, PhantomData) + } + + /// For generated thunks: leaves the exception pending and returns what the host function + /// returns to JSC. + #[doc(hidden)] + #[cold] + #[inline(never)] + pub fn into_host_fn_return(self, global: *mut JSGlobalObject) -> JSValue { + // SAFETY: the thunk's own global. + let global = unsafe { JSGlobalObject::opaque_ref_nn(global) }; + match self.0 { + ErrorState::Pending(error) => crate::host_fn::host_call_error_value(global, error), + ErrorState::ToThrow(value) => { + let _ = global.throw_value(value); + JSValue::ZERO + } } - debug_assert_eq!(extern_value.tag, 1); - // SAFETY: tag == 1 means the `_1` arm of the union is initialized. - Some(Child::convert_from_extern(unsafe { - ManuallyDrop::into_inner(extern_value.data._1) - })) + } + + #[cold] + pub fn into_js_error(self, global: &JSGlobalObject) -> JsError { + match self.0 { + ErrorState::Pending(error) => error, + ErrorState::ToThrow(value) => global.throw_value(value), + } + } +} + +/// An error type a body may return instead of [`Error`]. The thunk converts it. +pub trait IntoError<'a> { + fn into_error(self, heap: &JsHeap<'a>) -> Error<'a>; +} +impl<'a> IntoError<'a> for Error<'a> { + #[inline(always)] + fn into_error(self, _: &JsHeap<'a>) -> Error<'a> { + self + } +} +impl<'a> IntoError<'a> for JsError { + #[inline(always)] + fn into_error(self, _: &JsHeap<'a>) -> Error<'a> { + Error::pending(self) } } +impl<'a> IntoError<'a> for bun_sys::Error { + #[cold] + fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { + Error::to_throw(crate::SysErrorJsc::to_js(&self, heap.global())) + } +} +impl<'a> IntoError<'a> for core::convert::Infallible { + fn into_error(self, _: &JsHeap<'a>) -> Error<'a> { + match self {} + } +} + +// ───────────────────────────── values ───────────────────────────── + +/// A [`JSValue`] held by a host call, known to be a `T` ([`JSString`], [`JSObject`], [`JSFunction`], +/// [`ArrayBuffer`]; `JSValue` is "anything"). `Copy`, one register, no rooting: JSC scans the stack. +/// Keep lists of them in a [`MarkedArgumentBuffer`]; the GC does not look inside a `Vec`. +#[repr(transparent)] +pub struct JsValue<'a, T = JSValue>(JSValue, InvariantLifetime<'a>, PhantomData<*const T>); +impl Clone for JsValue<'_, T> { + #[inline(always)] + fn clone(&self) -> Self { + *self + } +} +impl Copy for JsValue<'_, T> {} + +impl<'a, T> JsValue<'a, T> { + #[inline(always)] + fn wrap(value: JSValue) -> Self { + JsValue(value, PhantomData, PhantomData) + } + + #[inline(always)] + fn from_js_result(result: crate::JsResult) -> Result<'a, Self> { + result.map(Self::wrap).map_err(Error::pending) + } + + /// For generated thunks. + /// + /// # Safety + /// `value` is a live, non-empty `T` belonging to the call `'a`. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn from_extern(value: JSValue) -> Self { + Self::wrap(value) + } + + #[inline(always)] + pub fn to_js(self) -> JSValue { + self.0 + } + + #[inline(always)] + pub fn as_value(self) -> JsValue<'a> { + JsValue::wrap(self.0) + } + + /// Keeps the value past the call. + #[inline] + pub fn to_strong(self, heap: &JsHeap<'a>) -> Strong { + Strong::create(self.0, heap.global()) + } +} + +impl<'a> JsValue<'a> { + pub const UNDEFINED: Self = JsValue(JSValue::UNDEFINED, PhantomData, PhantomData); + pub const NULL: Self = JsValue(JSValue::NULL, PhantomData, PhantomData); + + #[inline(always)] + pub fn new(_: &JsHeap<'a>, value: JSValue) -> Self { + debug_assert!(!value.is_empty()); + Self::wrap(value) + } -// ────────────────────────────────────────────────────────────────────────── + #[inline] + pub fn from_strong(_: &JsHeap<'a>, strong: &Strong) -> Self { + Self::wrap(strong.get()) + } -pub struct BindgenString; + #[inline(always)] + pub fn is_undefined(self) -> bool { + self.0.is_undefined() + } + #[inline(always)] + pub fn is_undefined_or_null(self) -> bool { + self.0.is_undefined_or_null() + } + #[inline] + pub fn as_function(self) -> Option> { + self.0.is_callable().then(|| JsValue::wrap(self.0)) + } +} -impl Bindgen for BindgenString { - type ZigType = WTFString; - // `?bun.string.WTFStringImpl` — `Option>` for single-word FFI layout. - type ExternType = Option>; +impl<'a> JsValue<'a, JSFunction> { + /// Runs JavaScript. + pub fn call( + self, + cx: &mut JsCall<'a>, + this: JsValue<'a, T>, + args: &[JsValue<'a>], + ) -> Result<'a, JsValue<'a>> { + // SAFETY: `JsValue` is `repr(transparent)` over `JSValue`. + let args = unsafe { &*(core::ptr::from_ref(args) as *const [JSValue]) }; + JsValue::from_js_result(self.0.call(cx.heap.global(), this.0, args)) + } +} - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - // SAFETY: bindgen contract — C++ passes a `StringImpl*` with one ref already - // taken for us; `adopt` consumes that ref. - unsafe { WTFString::adopt(extern_value.expect("non-null").as_ptr()) } +impl<'a> JsValue<'a, ArrayBuffer> { + /// The bytes of a typed array, `DataView` or `ArrayBuffer`, or `None` if it is detached. + /// Pointer and length are read on every call: JavaScript may have run since the last one. + /// Nothing that could detach, resize or re-home the storage can run inside `f`. + #[inline] + pub fn with_bytes(self, heap: &JsHeap<'a>, f: impl FnOnce(&[u8]) -> R) -> Option { + let array_buffer = self.0.as_array_buffer(heap.global())?; + if array_buffer.shared || array_buffer.ptr.is_null() && array_buffer.byte_len != 0 { + return None; + } + let result = f(array_buffer.byte_slice()); + self.0.ensure_still_alive(); + Some(result) } } -// ────────────────────────────────────────────────────────────────────────── -// BindgenUnion / ExternTaggedUnion / ExternUnion -// -// These are generated per arity (or by a proc-macro from the bindgen codegen). -// ────────────────────────────────────────────────────────────────────────── +/// An argument in both forms: converted for use now, and the value itself to hand to something that +/// holds it longer (a class's `values`, a [`Strong`], a JS call, the return value). +#[derive(Clone, Copy)] +pub struct WithValue<'a, T> { + pub converted: T, + pub value: JsValue<'a>, +} -// The bindgen TS codegen emits a concrete `enum` + `#[repr(C)]` union pair -// per call site rather than a generic Rust combinator (see -// `src/jsc/generated.rs`). This marker type exists for documentation parity. +// ───────────────────────────── classes ───────────────────────────── -/// `extern struct { data: ExternUnion(field_types), tag: u8 }` +/// The Rust struct of a generated class, stored inside its JS wrapper. `'static`, so it cannot hold a +/// [`JsValue`]. The sweeper drops it with no capability in scope, so `Drop` cannot touch the JS heap. /// -/// We provide fixed-arity instantiations; the 2-ary case is the only -/// one used directly in this file (by `BindgenOptional`). Higher arities are -/// emitted by codegen alongside their consumers. -#[repr(C)] -pub struct ExternTaggedUnion2 { - pub(crate) data: ExternUnion2, - pub(crate) tag: u8, +/// # Safety +/// Implemented by generated code only: the offsets describe the generated C++ cell. +pub unsafe trait JsPayload: Sized + 'static { + #[doc(hidden)] + const PAYLOAD_OFFSET: usize; + #[doc(hidden)] + const INTERNAL_FIELDS_OFFSET: usize; + /// A new cell whose payload is uninitialized. A null `structure` is the class's own. + /// + /// # Safety + /// The caller writes the payload before anything else runs. + #[doc(hidden)] + unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut c_void) -> NonNull; + /// `value`'s cell if it is a wrapper of `Self`, else null. + #[doc(hidden)] + fn cell_from_js(value: JSValue) -> *mut u8; + + /// A new JS object that owns `self`. + #[inline] + fn to_js<'a>(self, heap: &JsHeap<'a>) -> JsWrapper<'a, Self> { + // SAFETY: a null structure is always valid. + unsafe { JsWrapper::create_with_structure(heap, core::ptr::null_mut(), self) } + } + + /// `value` as a wrapper of `Self`, if it is one. + #[inline] + fn from_js<'a, T>(value: JsValue<'a, T>) -> Option> { + // SAFETY: C++ checked the class. + NonNull::new(Self::cell_from_js(value.0)) + .map(|cell| unsafe { JsWrapper::from_extern(cell) }) + } } -/// Union fields are wrapped in `ManuallyDrop` so non-`Copy` payloads -/// (e.g. nested `ExternTaggedUnion2`, `ExternArrayList`) are permitted without -/// trait bounds. There is no auto-drop — the active arm must be dropped -/// explicitly by whoever knows the tag. -#[repr(C)] -pub union ExternUnion2 { - pub _0: ManuallyDrop, - pub(crate) _1: ManuallyDrop, +/// The JS object that wraps a `T`. A cell pointer; the payload is reached only through +/// [`JsPayloads`]. +#[repr(transparent)] +pub struct JsWrapper<'a, T: JsPayload>(NonNull, InvariantLifetime<'a>, PhantomData<*const T>); +impl Clone for JsWrapper<'_, T> { + #[inline(always)] + fn clone(&self) -> Self { + *self + } } +impl Copy for JsWrapper<'_, T> {} + +impl<'a, T: JsPayload> JsWrapper<'a, T> { + /// For generated thunks. + /// + /// # Safety + /// `cell` is a live wrapper of `T` belonging to the call `'a`. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn from_extern(cell: NonNull) -> Self { + JsWrapper(cell, PhantomData, PhantomData) + } + + /// For generated constructors. + /// + /// # Safety + /// `structure` is null or a Structure of `T`'s cell class. + #[doc(hidden)] + #[inline] + pub unsafe fn create_with_structure( + heap: &JsHeap<'a>, + structure: *mut c_void, + payload: T, + ) -> Self { + // SAFETY: nothing runs between the allocation and the write, which cannot panic. + unsafe { + let this = Self::from_extern(T::create_cell(heap.global_ptr(), structure)); + this.payload().write(payload); + this + } + } -// ────────────────────────────────────────────────────────────────────────── + /// For the generated thunk of a method that cannot run JavaScript. + /// + /// # Safety + /// Nothing can reach the payload mutably for `'b`, and the caller's frame holds the cell. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn payload_ref<'b>(self) -> &'b T { + // SAFETY: forwarded. + unsafe { &*self.payload() } + } + + /// As [`payload_ref`](Self::payload_ref). + /// + /// # Safety + /// Nothing else can reach the payload for `'b`, and the caller's frame holds the cell. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn payload_mut<'b>(self) -> &'b mut T { + // SAFETY: forwarded. + unsafe { &mut *self.payload() } + } -pub struct BindgenArray(PhantomData); + #[inline(always)] + fn payload(self) -> *mut T { + // SAFETY: generated offset inside the cell. + unsafe { self.0.as_ptr().add(T::PAYLOAD_OFFSET).cast() } + } + + #[inline(always)] + pub fn as_value(self) -> JsValue<'a, JSObject> { + JsValue::wrap(JSValue::from_cell(self.0.as_ptr())) + } + + #[inline(always)] + pub fn with(self, _: &JsPayloads<'a>, f: impl FnOnce(&T) -> R) -> R { + // SAFETY: every `&mut T` holds `JsPayloads` exclusively. + let result = f(unsafe { &*self.payload() }); + self.as_value().0.ensure_still_alive(); + result + } + + #[inline(always)] + pub fn with_mut(self, _: &mut JsPayloads<'a>, f: impl FnOnce(&mut T) -> R) -> R { + // SAFETY: the exclusive borrow of `JsPayloads` excludes every other payload reference. + let result = f(unsafe { &mut *self.payload() }); + self.as_value().0.ensure_still_alive(); + result + } -impl Bindgen for BindgenArray { - type ZigType = bun_collections::ArrayListDefault; - type ExternType = ExternArrayList; + /// Keeps the wrapper, and so the payload, alive past the call. + #[inline] + pub fn to_strong(self, heap: &JsHeap<'a>) -> JsWrapperStrong { + JsWrapperStrong(self.as_value().to_strong(heap), PhantomData) + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - let length = usize::try_from(extern_value.length).expect("int cast"); - let capacity = usize::try_from(extern_value.capacity).expect("int cast"); + /// For the generated `_get_cached`. + /// + /// # Safety + /// `index` is one of the class's `values`. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn internal_field(self, _: &JsHeap<'a>, index: usize) -> JsValue<'a> { + // SAFETY: generated offset inside the cell; `values` are initialized to `undefined`. + JsValue::wrap(unsafe { + self.0 + .as_ptr() + .add(T::INTERNAL_FIELDS_OFFSET) + .cast::() + .add(index) + .read() + }) + } - if extern_value.data.is_null() { - return Self::ZigType::init(); + /// For the generated `_set_cached`. + /// + /// # Safety + /// As [`internal_field`](Self::internal_field). + #[doc(hidden)] + #[inline(always)] + pub unsafe fn set_internal_field( + self, + heap: &JsHeap<'a>, + index: usize, + value: JsValue<'a, V>, + ) { + // SAFETY: as above; C++ stores through the `WriteBarrier`. + unsafe { + extern_types::Bun__Bindgen__setInternalField( + heap.global_ptr(), + self.0.as_ptr(), + (T::INTERNAL_FIELDS_OFFSET + index * 8) as u32, + value.0, + ) } - let data = extern_value.data; - debug_assert!( - length <= capacity, - "length ({}) should not exceed capacity ({})", - length, - capacity, - ); - // SAFETY: C++ side guarantees `data` points to `capacity` elements with - // `length` initialized; allocation came from mimalloc (when `USE_MIMALLOC`). - let unmanaged: Vec = - unsafe { Vec::from_raw_parts(data, length, capacity) }; - - if !bun_alloc::USE_MIMALLOC { - // Don't reuse memory in this case; it would be freed by the wrong allocator. - } else if size_of::() == size_of::() - && align_of::() == align_of::() - // Rust has no stable type-equality test in generic context, so this - // fast-path is gated on the `const SAME_REPR: bool` opt-in: it only - // fires when the bindgen codegen has proven layout identity. - && Child::SAME_REPR - { - // The layouts are identical, so the Vec is returned as-is. - let (ptr, len, cap) = { - let mut v = ManuallyDrop::new(unmanaged); - (v.as_mut_ptr(), v.len(), v.capacity()) - }; - // SAFETY: `SAME_REPR` ⇒ same layout; `from_raw_parts` round-trip. - let reused: Vec = - unsafe { Vec::from_raw_parts(ptr.cast::(), len, cap) }; - return Self::ZigType::from_unmanaged(reused); - } else if size_of::() <= size_of::() - && align_of::() <= bun_alloc::mimalloc::MI_MAX_ALIGN_SIZE - { - // We can reuse the allocation, but we still need to convert the elements. - // - // Materializing a `&mut [u8]` over the full capacity would assert that - // every byte — including uninitialized tail elements and `ExternType` - // padding — is a valid `u8`, which is UB. Work entirely through raw - // `*mut u8` and `ptr::copy_nonoverlapping` instead; no reference to - // the storage is ever formed. - let mut v = ManuallyDrop::new(unmanaged); - let mut storage_ptr: *mut u8 = v.as_mut_ptr().cast::(); - let storage_len = v.capacity() * size_of::(); - - // Convert the elements. - for i in 0..length { - // Byte-wise copy: this is an in-place reinterpretation of - // overlapping element slots, so each element is copied out - // before conversion. - let mut old_elem = core::mem::MaybeUninit::::uninit(); - // SAFETY: source range lies within the mimalloc block and holds a - // valid (C++-initialized) `ExternType` for `i < length`. - unsafe { - core::ptr::copy_nonoverlapping( - storage_ptr.add(i * size_of::()), - old_elem.as_mut_ptr().cast::(), - size_of::(), - ); - } - // SAFETY: bytes for element `i` were just copied from initialized storage. - let new_elem = ManuallyDrop::new(Child::convert_from_extern(unsafe { - old_elem.assume_init() - })); - // SAFETY: dest range lies within the block; `size_of ZigType <= - // size_of ExternType` so slot `i` of the new layout never overruns - // slot `i` of the old layout (and never clobbers slot `i+1`). - unsafe { - core::ptr::copy_nonoverlapping( - (&raw const *new_elem).cast::(), - storage_ptr.add(i * size_of::()), - size_of::(), - ); - } + } +} + +/// A [`Strong`] on a wrapper; gives it back to code that has a capability again. +pub struct JsWrapperStrong(Strong, PhantomData<*const T>); +impl JsWrapperStrong { + #[inline] + pub fn get<'a>(&self, _: &JsHeap<'a>) -> JsWrapper<'a, T> { + // SAFETY: made from a `JsWrapper`; a cell value is its pointer. + unsafe { JsWrapper::from_extern(NonNull::new_unchecked(self.0.get().encoded() as *mut u8)) } + } +} + +// ───────────────────────────── results ───────────────────────────── + +/// Rust to JS, one function per schema type. The schema picks the function, not the Rust type: a +/// `Vec` is a string under `b.utf8` and an array of numbers under `b.array(b.u8)`. +pub mod to_js { + use super::*; + + macro_rules! number { + ($($name:ident: $t:ty => $f:expr;)*) => {$( + #[inline(always)] + pub fn $name<'a>(_: &JsHeap<'a>, value: $t) -> Result<'a, JsValue<'a>> { + Ok(JsValue::wrap($f(value))) } + )*}; + } + number! { + bool: bool => JSValue::from; + i8: i8 => |n| JSValue::js_number_from_int32(i32::from(n)); + u8: u8 => |n| JSValue::js_number_from_int32(i32::from(n)); + i16: i16 => |n| JSValue::js_number_from_int32(i32::from(n)); + u16: u16 => |n| JSValue::js_number_from_int32(i32::from(n)); + i32: i32 => JSValue::js_number_from_int32; + u32: u32 => |n| JSValue::js_number(f64::from(n)); + i64: i64 => JSValue::js_number_from_int64; + u64: u64 => JSValue::js_number_from_uint64; + usize: usize => |n| JSValue::js_number_from_uint64(n as u64); + f64: f64 => JSValue::js_number; + } - let new_size_is_multiple = - size_of::() % size_of::() == 0; - let new_capacity = if new_size_is_multiple { - capacity * (size_of::() / size_of::()) - } else { - let new_capacity = storage_len / size_of::(); - let new_alloc_size = new_capacity * size_of::(); - if new_alloc_size != storage_len { - // Allocation isn't a multiple of `size_of::()`; we have to - // resize it. - // SAFETY: `storage_ptr` is the original mimalloc block (the - // `USE_MIMALLOC` guard above gates entry to this path); shrinking - // with `mi_realloc` preserves the prefix bytes. - storage_ptr = bun_core::handle_oom(unsafe { - bun_alloc::realloc_raw(storage_ptr, new_alloc_size) - }); - } - new_capacity - }; - - let items_ptr = storage_ptr.cast::(); - // SAFETY: `storage_ptr` is aligned to ≥ `MI_MAX_ALIGN_SIZE` ≥ - // `align_of::()`; the first `length` slots were just written - // with valid `ZigType` values; the block is mimalloc-owned and the - // global allocator is mimalloc (the `if !bun_alloc::USE_MIMALLOC` - // guard above gates entry to this path), so `Vec`'s eventual dealloc - // — even with `ZigType`'s layout — routes to `mi_free`, which - // ignores layout. - let new_unmanaged: Vec = - unsafe { Vec::from_raw_parts(items_ptr, length, new_capacity) }; - return Self::ZigType::from_unmanaged(new_unmanaged); - } + #[inline(always)] + pub fn undefined<'a>(_: &JsHeap<'a>, (): ()) -> Result<'a, JsValue<'a>> { + Ok(JsValue::UNDEFINED) + } - // Fallback: allocate fresh, convert, free old. `data` was `mi_malloc`'d - // by the C++ side regardless of the Rust global allocator, so free it - // with `mi_free` directly instead of `Vec::drop`. - let mut result = bun_core::handle_oom(Self::ZigType::init_capacity(length)); - let mut unmanaged = ManuallyDrop::new(unmanaged); - for item in unmanaged.iter_mut() { - // SAFETY: each slot holds a C++-initialized `ExternType`; `ManuallyDrop` ensures it isn't read twice. - result.append_assume_capacity(Child::convert_from_extern(unsafe { - core::ptr::read(item) - })); - } - // SAFETY: `data` is the live `mi_malloc`'d block from `ExternVectorTraits::convertToExtern`. - unsafe { bun_alloc::mimalloc::mi_free(data.cast()) }; - result + #[inline(always)] + pub fn any<'a, T>(_: &JsHeap<'a>, value: JsValue<'a, T>) -> Result<'a, JsValue<'a>> { + Ok(value.as_value()) + } + + #[inline] + pub fn utf8<'a>(heap: &JsHeap<'a>, value: impl AsRef<[u8]>) -> Result<'a, JsValue<'a>> { + heap.create_utf8_string(value.as_ref()) + .map(JsValue::as_value) + } + + #[inline] + pub fn string<'a>(heap: &JsHeap<'a>, value: bun_core::String) -> Result<'a, JsValue<'a>> { + JsValue::from_js_result(crate::StringJsc::into_js(value, heap.global())) + } + + #[inline] + pub fn array<'a, I: IntoIterator>( + heap: &JsHeap<'a>, + items: I, + mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsValue<'a>>, + ) -> Result<'a, JsValue<'a>> { + MarkedArgumentBuffer::new(|values| { + for item in items { + values.append(convert(heap, item)?.0); + } + // SAFETY: `values` is live for the call. + JsValue::from_js_result(crate::call_zero_is_throw(heap.global(), || unsafe { + extern_types::Bun__Bindgen__constructArray(heap.global_ptr(), values) + })) + }) + } + + /// For generated dictionaries: an object with the generated Structure at `structure_index`. An + /// empty value is a member that is absent. + #[doc(hidden)] + #[inline] + pub fn object_with_structure<'a>( + heap: &JsHeap<'a>, + structure_index: u32, + values: &[JSValue], + ) -> Result<'a, JsValue<'a>> { + // SAFETY: `values` is on the caller's stack, where the GC sees it; this cannot throw. + Ok(JsValue::wrap(unsafe { + extern_types::Bun__Bindgen__createObject( + heap.global_ptr(), + structure_index, + values.as_ptr(), + values.len(), + ) + })) + } + + /// For generated enums. + #[doc(hidden)] + #[inline] + pub fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsValue<'a>> { + // SAFETY: a generated index; this cannot throw. + Ok(JsValue::wrap(unsafe { + extern_types::Bun__Bindgen__enumerationValueToJS(heap.global_ptr(), index) + })) } } -#[repr(C)] -pub struct ExternArrayList { - // Single-word nullable pointer. `Option<*mut T>` has no niche - // (two words) and would break the C ABI; use raw `*mut T` and check `.is_null()`. - pub(crate) data: *mut Child, - pub(crate) length: c_uint, - pub(crate) capacity: c_uint, +/// For generated thunks: a body's result as what the host function returns to JSC. +#[doc(hidden)] +#[inline(always)] +pub fn host_fn_return<'a, T, E: IntoError<'a>>( + cx: &mut JsCall<'a>, + result: core::result::Result, + convert: impl FnOnce(&JsHeap<'a>, T) -> Result<'a, JsValue<'a>>, +) -> JSValue { + let error = match result { + Ok(value) => match convert(&cx.heap, value) { + Ok(value) => return value.0, + Err(error) => error, + }, + Err(error) => error.into_error(&cx.heap), + }; + error.into_host_fn_return(cx.heap.global_ptr()) } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs new file mode 100644 index 000000000000..aa0258dcc156 --- /dev/null +++ b/src/jsc/bindgen/extern_types.rs @@ -0,0 +1,131 @@ +//! Rust side of `src/jsc/bindings/Bindgen/Extern*.h`: the C layouts generated code passes between +//! C++ and Rust. Everything here is plain data; ownership is taken by the generated conversion that +//! reads it, exactly once. + +use core::ffi::c_uint; + +use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; + +/// `WTF::StringImpl*` carrying one ref, or null. +pub type ExternString = *mut bun_core::WTFStringImplStruct; + +/// An empty `StringImpl` becomes `String::EMPTY`, as `Bun::toString` does: a WTF-backed `String` is +/// never empty. +#[inline] +pub fn adopt_string(ptr: ExternString) -> bun_core::String { + let string = bun_core::String::adopt_wtf_impl(ptr); + if string.is_empty() { + return bun_core::String::EMPTY; + } + string +} + +/// `ExternVariant`, which is what `std::optional` lowers to. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ExternOptional { + data: ExternOptionalData, + tag: u8, +} +#[repr(C)] +#[derive(Clone, Copy)] +union ExternOptionalData { + none: u8, + some: T, +} +impl ExternOptional { + #[inline(always)] + pub fn get(self) -> Option { + // SAFETY: C++ initialized the arm `tag` names. + (self.tag != 0).then(|| unsafe { self.data.some }) + } +} + +/// A mimalloc buffer this side now owns. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ExternVector { + data: *mut T, + length: c_uint, + capacity: c_uint, +} +impl ExternVector { + /// Converts every element and frees the buffer. + #[inline] + pub fn into_vec(self, mut convert: impl FnMut(T) -> U) -> Vec { + if self.data.is_null() { + return Vec::new(); + } + // SAFETY: C++ initialized `length` elements. + let items = unsafe { core::slice::from_raw_parts(self.data, self.length as usize) }; + let result = items.iter().map(|item| convert(*item)).collect(); + // SAFETY: allocated by `Bun::MimallocMalloc`; ownership was transferred. + unsafe { bun_alloc::basic::free_without_size(self.data.cast()) }; + result + } +} + +/// Bytes in the C++ host function's frame, or in a JS value that frame keeps alive, until it returns. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ExternBytes { + data: *const u8, + length: usize, +} +impl ExternBytes { + /// # Safety + /// `'a` ends before the host function that passed this returns. + #[inline(always)] + pub unsafe fn get<'a>(self) -> &'a [u8] { + // SAFETY: C++ never passes a null pointer for a present value. + unsafe { core::slice::from_raw_parts(self.data, self.length) } + } + /// A null pointer is "absent". + /// + /// # Safety + /// As [`get`](Self::get). + #[inline(always)] + pub unsafe fn get_optional<'a>(self) -> Option<&'a [u8]> { + if self.data.is_null() { + return None; + } + // SAFETY: forwarded. + Some(unsafe { self.get() }) + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ExternWithValue { + pub converted: T, + pub value: JSValue, +} + +/// Runs a generated `Bun__Bindgen__*__fromJS`, which returns false exactly when it threw. +#[inline] +pub fn from_js<'a>( + cx: &mut super::JsCall<'a>, + convert: impl FnOnce(*mut JSGlobalObject) -> bool, +) -> super::Result<'a, ()> { + cx.with_global(|global| crate::call_false_is_throw(global, || convert(global.as_mut_ptr()))) +} + +unsafe extern "C" { + pub fn Bun__Bindgen__constructArray( + global: *mut JSGlobalObject, + values: *mut MarkedArgumentBuffer, + ) -> JSValue; + pub fn Bun__Bindgen__createObject( + global: *mut JSGlobalObject, + structure_index: u32, + values: *const JSValue, + count: usize, + ) -> JSValue; + pub fn Bun__Bindgen__enumerationValueToJS(global: *mut JSGlobalObject, index: u32) -> JSValue; + pub fn Bun__Bindgen__setInternalField( + global: *mut JSGlobalObject, + cell: *mut u8, + offset: u32, + value: JSValue, + ); +} diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index ee61e0733c09..33419135212b 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -1,20 +1,109 @@ -import { fn, t } from "bindgen"; +// Functions that exist to test the binding generator: test/internal/bindgen.test.ts +import * as b from "../codegen/bindgen-lib.ts"; -export const add = fn({ +export default b.module({ rustPath: "crate::bindgen_test" }); + +export const add = b.fn({ args: { - global: t.globalObject, - a: t.i32, - b: t.i32.default(-1), + a: b.i32.wrap, + b: b.i32.wrap.nullable.default(-1), }, - ret: t.i32, + returns: b.i32, + capability: "heap", + throws: true, }); -export const requiredAndOptionalArg = fn({ +export const requiredAndOptionalArg = b.fn({ args: { - a: t.boolean, - b: t.usize.optional, - c: t.i32.enforceRange(0, 100).default(42), - d: t.u8.optional, + a: b.bool.loose, + b: b.usize.wrap.nullable, + c: b.i32.enforceRange(0, 100).nullable.default(42), + d: b.u8.wrap.nullable, + }, + returns: b.i32, +}); + +export const utf8Echo = b.fn({ args: { text: b.utf8 }, returns: b.utf8 }); +export const utf8EchoLoose = b.fn({ args: { text: b.utf8.loose }, returns: b.utf8 }); +export const utf8Optional = b.fn({ args: { text: b.utf8.nullable }, returns: b.utf8.nullable }); +export const stringEcho = b.fn({ args: { text: b.string }, returns: b.string }); + +export const TestColor = b.enumeration("TestColor", ["red", ["green", "lime"], "blue"]); +export const nextColor = b.fn({ args: { color: TestColor }, returns: TestColor }); + +export const TestOptions = b.dictionary("TestOptions", { + count: b.i32.default(1), + flag: b.bool.loose.default(false), + name: b.utf8.optional, + color: TestColor.default("red"), + ratio: b.f64.nullable, + payload: b.any, + renamed: { type: b.u8.optional, altNames: ["oldName"] }, +}); +export const TestOptionsEcho = b.dictionary("TestOptionsEcho", { + count: b.i32, + flag: b.bool, + name: b.utf8.optional, + color: TestColor, + ratio: b.f64.nullable, + payload: b.any, + renamed: b.u8.optional, +}); +export const optionsEcho = b.fn({ args: { options: TestOptions }, returns: TestOptionsEcho, capability: "heap" }); + +export const TestValue = b.union("TestValue", { + number: b.f64, + flag: b.bool, + text: b.utf8, + list: b.array(b.i32), +}); +export const describeValue = b.fn({ args: { value: TestValue }, returns: b.utf8 }); + +export const sumArray = b.fn({ args: { numbers: b.array(b.i32) }, returns: b.f64 }); +export const range = b.fn({ args: { length: b.u32 }, returns: b.array(b.u32) }); +export const words = b.fn({ args: { text: b.utf8 }, returns: b.array(b.utf8) }); + +export const sumBytes = b.fn({ args: { bytes: b.bytes }, returns: b.u32 }); +/** The sum before `callback` runs, then after it, or `null` once the buffer is detached. */ +export const sumBeforeAndAfterCallback = b.fn({ + args: { buffer: b.bufferSource, callback: b.any }, + returns: b.array(b.u32.nullable), + capability: "js", + throws: true, +}); + +/** The original value if the converted text is not empty. */ +export const withValueEcho = b.fn({ args: { text: b.utf8.loose.withValue }, returns: b.any, capability: "heap" }); + +export const TestFailure = b.enumeration("TestFailure", ["type", "range", "plain", "syscall", "value"]); +export const fail = b.fn({ args: { how: TestFailure, value: b.any }, capability: "heap", throws: true }); + +export const TestCounter = b.class("TestCounter", { + rustPath: "crate::bindgen_test::TestCounter", + construct: { args: { start: b.i32.default(0) }, capability: "heap", throws: true }, + values: ["listener"], + proto: { + count: { getter: b.i32 }, + label: { getter: b.utf8, setter: true }, + tag: { getter: b.any, capability: "heap", cache: true }, + increment: { args: { by: b.i32.default(1) }, returns: b.i32, mutates: true }, + listen: { args: { listener: b.any }, capability: "heap", passThis: true }, + /** Calls the listener, which may call back into this object, then returns the count. */ + notify: { returns: b.i32, capability: "js", throws: true }, + /** `other` may be `this`. */ + plus: { args: { other: b.self }, returns: b.i32, capability: "js" }, + }, + klass: { + starting: { args: { at: b.i32 }, returns: b.self, capability: "heap" }, + alive: { returns: b.usize }, + }, +}); + +/** Only native code makes one. */ +export const TestToken = b.class("TestToken", { + rustPath: "crate::bindgen_test::TestToken", + proto: { + id: { getter: b.u32 }, }, - ret: t.i32, }); +export const makeToken = b.fn({ args: { id: b.u32 }, returns: TestToken, capability: "heap" }); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index cab573ad8b66..497599aed0a5 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -1,47 +1,20 @@ //! This namespace is used to test binding generator -use crate::js_object::PojoFields; -use crate::{JSGlobalObject, JSObject, JSValue, JsResult}; - -use crate::r#gen::bindgen_test as generated; - -pub fn get_bindgen_test_functions(global: &JSGlobalObject) -> JsResult { - // Rust has no field reflection, so a local `PojoFields` impl enumerates - // the fields. - struct Fns { - add: JSValue, - required_and_optional_arg: JSValue, - } - impl PojoFields for Fns { - const FIELD_COUNT: usize = 2; - fn put_fields( - &self, - _global: &JSGlobalObject, - mut put: impl FnMut(&'static [u8], JSValue) -> JsResult<()>, - ) -> JsResult<()> { - put(b"add", self.add)?; - put(b"requiredAndOptionalArg", self.required_and_optional_arg)?; - Ok(()) - } - } - let pojo = Fns { - add: generated::create_add_callback(global), - required_and_optional_arg: generated::create_required_and_optional_arg_callback(global), - }; - Ok(JSObject::create(&pojo, global)?.to_js()) -} +use core::sync::atomic::{AtomicUsize, Ordering}; + +use crate::ArrayBuffer; +use crate::bindgen::{ + self, IntoError as _, JsCall, JsHeap, JsPayload as _, JsValue, JsWrapper, WithValue, +}; +use crate::generated_bindgen::bindgen_test::{ + TestColor, TestCounterClass, TestCounterValues as _, TestFailure, TestOptions, TestOptionsEcho, + TestTokenClass, TestValue, +}; // This example should be kept in sync with bindgen's documentation -pub fn add(global: &JSGlobalObject, a: i32, b: i32) -> JsResult { - 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"))) - } - } +pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { + a.checked_add(b) + .ok_or_else(|| heap.throw(format_args!("Integer overflow while adding"))) } pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { @@ -61,3 +34,201 @@ pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option &[u8] { + text +} + +pub fn utf8_echo_loose(text: &[u8]) -> &[u8] { + text +} + +pub fn utf8_optional(text: Option<&[u8]>) -> Option<&[u8]> { + text +} + +pub fn string_echo(text: bun_core::String) -> bun_core::String { + text +} + +pub fn next_color(color: TestColor) -> TestColor { + match color { + TestColor::Red => TestColor::Green, + TestColor::Green => TestColor::Blue, + TestColor::Blue => TestColor::Red, + } +} + +pub fn options_echo<'a>(_: &JsHeap<'a>, options: TestOptions<'a>) -> TestOptionsEcho<'a> { + TestOptionsEcho { + count: options.count, + flag: options.flag, + name: options.name.map(<[u8]>::to_vec), + color: options.color, + ratio: options.ratio, + payload: options.payload, + renamed: options.renamed, + } +} + +pub fn describe_value(value: TestValue<'_>) -> Vec { + match value { + TestValue::Number(number) => format!("number {number}").into_bytes(), + TestValue::Flag(flag) => format!("flag {flag}").into_bytes(), + TestValue::Text(text) => [b"text ", text].concat(), + TestValue::List(list) => format!("list {list:?}").into_bytes(), + } +} + +pub fn sum_array(numbers: Vec) -> f64 { + numbers + .into_iter() + .fold(0.0, |sum, number| sum + f64::from(number)) +} + +pub fn range(length: u32) -> core::ops::Range { + 0..length +} + +pub fn words(text: &[u8]) -> impl Iterator { + bun_core::strings::tokenize(text, b" ") +} + +fn sum(bytes: &[u8]) -> u32 { + bytes.iter().map(|&byte| u32::from(byte)).sum() +} + +pub fn sum_bytes(bytes: &[u8]) -> u32 { + sum(bytes) +} + +pub fn sum_before_and_after_callback<'a>( + cx: &mut JsCall<'a>, + buffer: JsValue<'a, ArrayBuffer>, + callback: JsValue<'a>, +) -> bindgen::Result<'a, [Option; 2]> { + let before = buffer.with_bytes(&cx.heap, sum); + let Some(callback) = callback.as_function() else { + return Err(cx + .heap + .throw_type_error(format_args!("callback must be a function"))); + }; + callback.call(cx, JsValue::UNDEFINED, &[])?; + Ok([before, buffer.with_bytes(&cx.heap, sum)]) +} + +pub fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsValue<'a> { + if text.converted.is_empty() { + return JsValue::UNDEFINED; + } + text.value +} + +pub fn fail<'a>( + heap: &JsHeap<'a>, + how: TestFailure, + value: JsValue<'a>, +) -> bindgen::Result<'a, ()> { + Err(match how { + TestFailure::Type => heap.throw_type_error(format_args!("a type error")), + TestFailure::Range => heap.throw_range_error(format_args!("a range error")), + TestFailure::Plain => heap.throw(format_args!("an error")), + TestFailure::Syscall => { + bun_sys::Error::from_code(bun_sys::E::ENOENT, bun_sys::Tag::open).into_error(heap) + } + TestFailure::Value => heap.throw_value(value), + }) +} + +static ALIVE: AtomicUsize = AtomicUsize::new(0); + +pub struct TestCounter { + count: i32, + label: Vec, +} + +impl TestCounter { + fn new(count: i32) -> Self { + ALIVE.fetch_add(1, Ordering::Relaxed); + TestCounter { + count, + label: Vec::new(), + } + } +} + +impl Drop for TestCounter { + fn drop(&mut self) { + ALIVE.fetch_sub(1, Ordering::Relaxed); + } +} + +impl TestCounterClass for TestCounter { + type Error<'a> = bindgen::Error<'a>; + + fn constructor<'a>(heap: &JsHeap<'a>, start: i32) -> bindgen::Result<'a, Self> { + if start < 0 { + return Err(heap.throw_range_error(format_args!("start must not be negative"))); + } + Ok(Self::new(start)) + } + + fn count(&self) -> i32 { + self.count + } + + fn label(&self) -> impl AsRef<[u8]> { + &self.label + } + + fn set_label<'a>(&mut self, value: &'a [u8]) { + self.label = value.to_vec(); + } + + fn tag<'a>(&self, heap: &JsHeap<'a>) -> JsValue<'a> { + bindgen::to_js::array(heap, [self.count], bindgen::to_js::i32).unwrap_or(JsValue::NULL) + } + + fn increment(&mut self, by: i32) -> i32 { + self.count = self.count.wrapping_add(by); + self.count + } + + fn listen<'a>(&self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, listener: JsValue<'a>) { + this.listener_set_cached(heap, listener); + } + + fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { + if let Some(listener) = this.listener_get_cached(&cx.heap).as_function() { + listener.call(cx, this.as_value(), &[])?; + } + Ok(this.with(&cx.payloads, |counter| counter.count)) + } + + fn plus<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>, other: JsWrapper<'a, Self>) -> i32 { + let other = other.with(&cx.payloads, |counter| counter.count); + this.with(&cx.payloads, |counter| counter.count.wrapping_add(other)) + } + + fn starting<'a>(heap: &JsHeap<'a>, at: i32) -> JsWrapper<'a, Self> { + Self::new(at).to_js(heap) + } + + fn alive() -> usize { + ALIVE.load(Ordering::Relaxed) + } +} + +pub struct TestToken { + id: u32, +} + +impl TestTokenClass for TestToken { + fn id(&self) -> u32 { + self.id + } +} + +pub fn make_token<'a>(heap: &JsHeap<'a>, id: u32) -> JsWrapper<'a, TestToken> { + TestToken { id }.to_js(heap) +} diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h new file mode 100644 index 000000000000..0aeb4857bb51 --- /dev/null +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -0,0 +1,68 @@ +#pragma once +#include "root.h" +#include "Bindgen/Identifiers.h" +#include "ObjectBindings.h" +#include +#include + +namespace Bun::Bindgen { + +// Reads the members of a dictionary, by index into its run of Tables::memberNames. +// +// Every lookup follows one rule: own properties, then the prototype chain up to but not including +// Object.prototype. For a plain object whose prototype is Object.prototype or null, the own property +// table is therefore the whole answer, and it is read directly: no PropertySlot, no walk. Reading +// data properties is unobservable, so they are all read up front. +template +class DictionaryMemberReader { +public: + DictionaryMemberReader(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, unsigned firstMemberNameIndex) + : m_vm(vm) + , m_globalObject(globalObject) + , m_object(object) + , m_memberNames(identifiers(vm).identifiersAt(firstMemberNameIndex, memberNameCount)) + , m_isPlainObject(readPlainObject()) + { + } + + // May throw. + ALWAYS_INLINE JSC::JSValue get(unsigned index) + { + if (m_isPlainObject) [[likely]] + return m_values[index]; + return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); + } + +private: + bool readPlainObject() + { + if (m_object->type() != JSC::FinalObjectType) + return false; + JSC::Structure* structure = m_object->structure(); + if (structure->hasPolyProto()) + return false; + JSC::JSValue prototype = structure->storedPrototype(); + if (prototype != m_globalObject->objectPrototype() && !prototype.isNull()) + return false; + for (unsigned i = 0; i < memberNameCount; ++i) { + unsigned attributes = 0; + JSC::PropertyOffset offset = structure->get(m_vm, m_memberNames[i], attributes); + if (offset == JSC::invalidOffset) + m_values[i] = JSC::jsUndefined(); + else if (attributes & (JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::CustomAccessorOrValue)) [[unlikely]] + return false; + else + m_values[i] = m_object->getDirect(offset); + } + return true; + } + + JSC::VM& m_vm; + JSC::JSGlobalObject* m_globalObject; + JSC::JSObject* m_object; + const JSC::Identifier* m_memberNames; + JSC::JSValue m_values[memberNameCount]; + bool m_isPlainObject; +}; + +} diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 0bac90fdf14f..84ada1d82ea5 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,8 +11,8 @@ #include #include #include -#include #include "ExternUnion.h" +#include "JSStringUTF8View.h" namespace Bun::Bindgen { @@ -122,15 +123,37 @@ template<> struct ExternTraits { } }; -template<> struct ExternTraits { - using ExternType = Bun::StrongRefImpl*; +struct ExternBytes { + const std::uint8_t* data; + std::size_t length; +}; + +template<> struct ExternTraits> { + using ExternType = ExternBytes; + + static ExternType convertToExtern(std::span cppValue) + { + return { cppValue.data(), cppValue.size() }; + } +}; + +template<> struct ExternTraits { + using ExternType = ExternBytes; - static ExternType convertToExtern(Bun::StrongRef&& cppValue) + // The storage stays with `cppValue`, which outlives the call this is an argument of. + static ExternType convertToExtern(JSStringUTF8View&& cppValue) { - return cppValue.release(); + return ExternTraits>::convertToExtern(cppValue.bytes()); } }; +// A converted argument and the argument itself. +template +struct ExternWithValue { + T converted; + JSC::EncodedJSValue value; +}; + template struct ExternTraits> { using ExternType = T*; diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 669b74c2d893..4a878e7b2858 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -1,19 +1,128 @@ #pragma once #include +#include +#include #include "IDLTypes.h" #include "IDLConvertBase.h" namespace Bun { -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("any"); +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("string"); }; +template<> struct IDLHumanReadableName : IDLHumanReadableName {}; +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("ArrayBuffer or ArrayBufferView"); +}; +template<> struct IDLHumanReadableName : IDLHumanReadableName {}; + +namespace Bindgen { + +// std::nullopt for anything else, a shared buffer included: another thread may be writing to it. A +// detached or out-of-bounds view is empty. +inline std::optional> unsharedBufferSourceSpan(JSC::JSValue value) +{ + if (auto* view = dynamicDowncast(value)) { + if (view->isShared()) + return std::nullopt; + return view->span(); + } + if (auto* buffer = dynamicDowncast(value)) { + if (buffer->isShared()) + return std::nullopt; + return buffer->impl()->span(); + } + return std::nullopt; } -template<> struct WebCore::Converter - : WebCore::DefaultConverter { +} +} + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + template + static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) + { + if (value.isString()) [[likely]] + return Bun::Bindgen::JSStringUTF8View::create(globalObject, JSC::asString(value)); + if (!value.isCell() || (value.asCell()->type() != JSC::StringObjectType && value.asCell()->type() != JSC::DerivedStringObjectType)) + return std::nullopt; + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSString* string = value.toString(&globalObject); + RETURN_IF_EXCEPTION(scope, Bun::Bindgen::JSStringUTF8View {}); + RELEASE_AND_RETURN(scope, Bun::Bindgen::JSStringUTF8View::create(globalObject, string)); + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwNotString(globalObject, scope); + } +}; + +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static Bun::Bindgen::JSStringUTF8View convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSString* string = value.toString(&globalObject); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, Bun::Bindgen::JSStringUTF8View::create(globalObject, string)); + } +}; + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional> tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + auto bytes = Bun::Bindgen::unsharedBufferSourceSpan(value); + // Null data is the null value. + if (bytes && !bytes->data()) + return std::span { reinterpret_cast(""), 0 }; + return bytes; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView that is not shared"_s); + } +}; + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (!Bun::Bindgen::unsharedBufferSourceSpan(value)) + return std::nullopt; + return value; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView that is not shared"_s); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (auto* object = dynamicDowncast(value)) + return object; + return std::nullopt; + } - static Bun::StrongRef convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { - return Bun::StrongRef { Bun__StrongRef__new(&globalObject, JSC::JSValue::encode(value)) }; + ctx.throwTypeMustBe(globalObject, scope, WTF::makeString("an instance of "_s, JSClass::info()->className)); } }; diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 93089efbe6e6..c82d3824eac4 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -1,22 +1,10 @@ #pragma once #include -#include +#include +#include "JSStringUTF8View.h" namespace Bun::Bindgen { -// See also: Bun::IDLRawAny -struct IDLStrongAny : WebCore::IDLType { - using NullableType = Bun::StrongRef; - using NullableInnerParameterType = NullableType; - - static inline std::nullptr_t nullValue() { return nullptr; } - template static inline bool isNullValue(U&& value) { return !value; } - template static inline U&& extractValueFromNullable(U&& value) - { - return std::forward(value); - } -}; - // Dictionaries that contain raw `JSValue`s must live on the stack. template struct IDLStackOnlyDictionary : WebCore::IDLType { @@ -25,4 +13,43 @@ struct IDLStackOnlyDictionary : WebCore::IDLType { using NullableParameterType = const T&; }; +// A string or a `String` object, which is what `JSValue::is_string` in Rust has always meant. +struct IDLStrictUTF8String : WebCore::IDLType { + using SequenceStorageType = void; + using NullableType = JSStringUTF8View; + using NullableInnerParameterType = JSStringUTF8View; + static JSStringUTF8View nullValue() { return {}; } + static bool isNullValue(const JSStringUTF8View& value) { return value.isNull(); } + template static U&& extractValueFromNullable(U&& value) { return std::forward(value); } +}; +// `ToString`. +struct IDLLooseUTF8String : IDLStrictUTF8String {}; + +// The bytes of a BufferSource that is not shared. Valid while the argument is alive and no JavaScript +// runs; the generator refuses this type to a function that can run any. +struct IDLBufferSourceBytes : WebCore::IDLType> { + using SequenceStorageType = void; + using NullableType = std::span; + using NullableInnerParameterType = std::span; + static NullableType nullValue() { return {}; } + static bool isNullValue(NullableType value) { return !value.data(); } + static ImplementationType extractValueFromNullable(NullableType value) { return value; } +}; + +// The same values, as the `JSValue` itself. See Bun::IDLRawAny. +struct IDLRawBufferSource : WebCore::IDLType { + using SequenceStorageType = void; +}; + +// An instance of a generated class, as the cell itself. The caller's frame keeps it alive. +template +struct IDLRawInterface : WebCore::IDLType { + using SequenceStorageType = void; + using NullableType = JSClass*; + using NullableInnerParameterType = JSClass*; + static JSClass* nullValue() { return nullptr; } + static bool isNullValue(JSClass* value) { return !value; } + static JSClass* extractValueFromNullable(JSClass* value) { return value; } +}; + } diff --git a/src/jsc/bindings/Bindgen/Identifiers.h b/src/jsc/bindings/Bindgen/Identifiers.h new file mode 100644 index 000000000000..b7ef047863a5 --- /dev/null +++ b/src/jsc/bindings/Bindgen/Identifiers.h @@ -0,0 +1,52 @@ +#pragma once +#include "root.h" +#include +#include + +namespace Bun::Bindgen { + +// Identifiers for dictionary member names and JSStrings for enumeration values, each made on first +// use. Atom tables are per thread, so this lives in JSVMClientData, never in a process global. +class Identifiers { + WTF_MAKE_NONCOPYABLE(Identifiers); + WTF_MAKE_NONMOVABLE(Identifiers); + +public: + explicit Identifiers(JSC::VM&); + + // `count` identifiers starting at Tables::memberNames[first] (GeneratedBindings+Tables.h). + ALWAYS_INLINE const JSC::Identifier* identifiersAt(unsigned first, unsigned count) + { + if (m_memberNames[first].isNull()) [[unlikely]] + initializeMemberNames(first, count); + return &m_memberNames[first]; + } + + // Tables::enumerationValues[index]. + ALWAYS_INLINE JSC::JSString* stringAt(unsigned index) + { + if (!m_enumerationValues[index]) [[unlikely]] + initializeEnumerationValue(index); + return m_enumerationValues[index]; + } + + // Called from a marking constraint, with the world stopped. + template void visit(Visitor&); + +private: + void initializeMemberNames(unsigned first, unsigned count); + void initializeEnumerationValue(unsigned index); + + JSC::VM& m_vm; + // Not arrays sized by the tables: most of the C++ includes this through BunClientData.h. + const std::unique_ptr m_memberNames; + const std::unique_ptr m_enumerationValues; +}; + +Identifiers& identifiers(JSC::VM&); + +// The Structures of returned dictionaries, by index into Tables::objectStructures. +unsigned objectStructureCount(); +JSC::Structure* createObjectStructure(JSC::VM&, JSC::JSGlobalObject*, unsigned index); + +} diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h new file mode 100644 index 000000000000..05b09ebf4282 --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -0,0 +1,166 @@ +#pragma once +#include "root.h" +#include "BunClientData.h" +#include "Bindgen/ExternTraits.h" +#include "Bindgen/IDLConvert.h" +#include "ErrorCode.h" +#include "ZigGlobalObject.h" +#include +#include +#include + +namespace Bun::Bindgen { + +// What is the same for every generated class, as data. One per class, defined by generated code. +struct ClassDefinition { + WTF::ASCIILiteral className; + const JSC::ClassInfo* info; + JSC::Structure* (*createStructure)(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + std::span prototypeTableValues; + std::span constructorTableValues; + // Properties a static table cannot describe (private names). May be null. + void (*finishPrototypeCreation)(JSC::VM&, JSC::JSGlobalObject*, JSC::JSObject* prototype); + // Null: `new` from JavaScript is not allowed, and there is no constructor object. + JSC::NativeFunction construct; + unsigned constructorLength; +}; + +// The wrapper of a generated class: +// +// JSDestructibleObject | WriteBarrier internalFields[numberOfInternalFields] | the Rust payload +// +// The payload is stored in the cell, so a wrapper and its state have one lifetime, and a pointer +// into the payload keeps the wrapper alive: the conservative scan maps interior pointers to their +// cell. The collector reads the internal fields and nothing else; no Rust runs on its threads. +// +// Traits, generated: +// static constexpr ASCIILiteral className; +// static constexpr unsigned classIndex, numberOfInternalFields, numberOfValues; +// static size_t payloadSize(); size_of::(), a constant Rust defines +// static void finalize(void* payload); drop_in_place:: +template +class JSBindgenObject final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + // A payload is not one of a few sizes shared with other cells, so a precise-allocation tier buys nothing. + static constexpr uint8_t numberOfLowerTierPreciseCells = 0; + static constexpr unsigned numberOfInternalFields = Traits::numberOfInternalFields; + + DECLARE_INFO; + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + auto& clientData = *downcast(vm.clientData); + if (auto* space = clientData.clientSubspaces().m_clientSubspaceForBindgenClass[Traits::classIndex]) [[likely]] + return space; + return WebCore::subspaceForImplSlow( + vm, + WebCore::SubspaceForInit { static_cast(allocationSize()), numberOfLowerTierPreciseCells, WebCore::SubspaceForInit::CellType::Destructible, false }, + BUN_SUBSPACE_SLOTS(m_clientSubspaceForBindgenClass[Traits::classIndex], m_subspaceForBindgenClass[Traits::classIndex]), + nullptr); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + + // The payload is uninitialized: the caller writes it before anything else can run. + static JSBindgenObject* create(JSC::VM& vm, JSC::Structure* structure) + { + auto* object = new (NotNull, JSC::allocateCell(vm, allocationSize())) JSBindgenObject(vm, structure); + object->finishCreation(vm); + return object; + } + + static void destroy(JSC::JSCell* cell) + { + Traits::finalize(static_cast(cell)->payload()); + } + + static constexpr size_t offsetOfInternalFields() { return sizeof(Base); } + static constexpr size_t offsetOfPayload() { return offsetOfInternalFields() + numberOfInternalFields * sizeof(JSC::WriteBarrier); } + static size_t allocationSize() { return offsetOfPayload() + Traits::payloadSize(); } + + void* payload() { return reinterpret_cast(this) + offsetOfPayload(); } + JSC::WriteBarrier& internalField(unsigned index) { return internalFields()[index]; } + std::span, numberOfInternalFields> internalFields() + { + return std::span, numberOfInternalFields> { reinterpret_cast*>(reinterpret_cast(this) + offsetOfInternalFields()), numberOfInternalFields }; + } + +private: + JSBindgenObject(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + // Rust reads the schema's `values`, which start as `undefined`. The rest hold cached getters: empty until computed. + unsigned index = 0; + for (auto& field : internalFields()) { + new (NotNull, &field) JSC::WriteBarrier; + if (index++ < Traits::numberOfValues) + field.setWithoutWriteBarrier(JSC::jsUndefined()); + } + } +}; + +template +template +void JSBindgenObject::visitChildrenImpl(JSC::JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + for (auto& field : thisObject->internalFields()) + visitor.append(field); +} + +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template, JSBindgenObject); + +template +const JSC::ClassInfo JSBindgenObject::s_info = { Traits::className, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBindgenObject) }; + +template struct ExternTraits*> : TrivialExtern*> {}; + +// The constructor object of every generated class that has one. +class JSBindgenConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, BUN_SUBSPACE_SLOTS(m_clientSubspaceForBunClassConstructor, m_subspaceForBunClassConstructor)); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + + static JSBindgenConstructor* create(JSC::VM&, JSC::JSGlobalObject*, const ClassDefinition&, JSC::JSObject* prototype); + +private: + JSBindgenConstructor(JSC::VM& vm, JSC::Structure* structure, JSC::NativeFunction construct); +}; + +// By index into Tables::classes (GeneratedBindings+Tables.h). +unsigned classCount(); +void setupClassStructure(JSC::LazyClassStructure::Initializer&, unsigned classIndex); +JSC::Structure* structureForClass(JSC::JSGlobalObject*, unsigned classIndex); +JSC::JSObject* constructorForClass(JSC::JSGlobalObject*, unsigned classIndex); +// The class's own, or one for `new.target` (a subclass, Reflect.construct). May throw. +JSC::Structure* structureForNewTarget(JSC::JSGlobalObject*, JSC::CallFrame*, unsigned classIndex); + +} diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h new file mode 100644 index 000000000000..2461dc7a1740 --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -0,0 +1,68 @@ +#pragma once +#include "root.h" +#include +#include +#include + +namespace Bun::Bindgen { + +// Bun::UTF8View for an argument of a host call: the JS string's own bytes when it is 8-bit ASCII, a +// converted copy otherwise, which for a short string needs no allocation. Lives in the host +// function's frame. Holding the JSString until destruction is what keeps borrowed bytes valid: if JSC +// swaps the string's impl for an atom, it keeps the old one for as long as the conservative scan +// still finds the JSString. +class JSStringUTF8View { + WTF_MAKE_NONCOPYABLE(JSStringUTF8View); + +public: + JSStringUTF8View() = default; + JSStringUTF8View(JSStringUTF8View&&) = default; + JSStringUTF8View& operator=(JSStringUTF8View&&) = default; + explicit JSStringUTF8View(WTF::ASCIILiteral literal) + : m_borrowed(literal.span8()) + , m_isNull(false) + { + } + ~JSStringUTF8View() { JSC::ensureStillAliveHere(m_string); } + + // Resolving a rope can throw. + static JSStringUTF8View create(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSStringUTF8View result; + auto view = string->view(&globalObject); + RETURN_IF_EXCEPTION(scope, result); + result.m_string = string; + result.m_isNull = false; + if (view->is8Bit() && WTF::charactersAreAllASCII(view->span8())) [[likely]] + result.m_borrowed = view->span8(); + else + result.convert(view); + return result; + } + + bool isNull() const { return m_isNull; } + + // Null data only when isNull(). + std::span bytes() const + { + if (m_isNull) + return {}; + auto span = m_isConverted ? m_converted.span() : byteCast(m_borrowed); + if (!span.data()) + return { reinterpret_cast(""), 0 }; + return span; + } + +private: + void convert(WTF::StringView); + + JSC::JSString* m_string { nullptr }; + std::span m_borrowed; + WTF::Vector m_converted; + bool m_isConverted { false }; + bool m_isNull { true }; +}; + +} diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/BindgenIdentifiers.cpp new file mode 100644 index 000000000000..728fcec402ff --- /dev/null +++ b/src/jsc/bindings/BindgenIdentifiers.cpp @@ -0,0 +1,132 @@ +#include "root.h" +#include "Bindgen/Identifiers.h" +#include "Bindgen/JSStringUTF8View.h" +#include "GeneratedBindings+Tables.h" +#include "BunClientData.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +Identifiers::Identifiers(VM& vm) + : m_vm(vm) + , m_memberNames(std::make_unique(std::size(Tables::memberNames))) + , m_enumerationValues(std::make_unique(std::size(Tables::enumerationValues))) +{ +} + +void Identifiers::initializeMemberNames(unsigned first, unsigned count) +{ + ASSERT(m_vm.currentThreadIsHoldingAPILock()); + for (unsigned i = first; i < first + count; ++i) + m_memberNames[i] = Identifier::fromString(m_vm, Tables::memberNames[i]); +} + +void Identifiers::initializeEnumerationValue(unsigned index) +{ + ASSERT(m_vm.currentThreadIsHoldingAPILock()); + m_enumerationValues[index] = jsString(m_vm, AtomString(Tables::enumerationValues[index])); +} + +template +void Identifiers::visit(Visitor& visitor) +{ + for (unsigned i = 0; i < Tables::enumerationValueCount; ++i) { + if (auto* string = m_enumerationValues[i]) + visitor.appendUnbarriered(string); + } +} +template void Identifiers::visit(JSC::AbstractSlotVisitor&); +template void Identifiers::visit(JSC::SlotVisitor&); + +Identifiers& identifiers(VM& vm) +{ + return static_cast(vm.clientData)->bindgenIdentifiers; +} + +unsigned objectStructureCount() +{ + return Tables::objectStructureCount; +} + +Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) +{ + auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; + const Identifier* memberNames = identifiers(vm).identifiersAt(firstMemberNameIndex, memberCount); + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); + for (unsigned i = 0; i < memberCount; ++i) { + PropertyOffset offset; + structure = Structure::addPropertyTransition(vm, structure, memberNames[i], 0, offset); + RELEASE_ASSERT(offset == static_cast(i)); + } + return structure; +} + +void JSStringUTF8View::convert(StringView view) +{ + m_isConverted = true; + if (view.is8Bit()) { + auto span = view.span8(); + m_converted.grow(span.size() * 2); + m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span.data()), span.size(), byteCast(m_converted.mutableSpan().data()))); + return; + } + auto span = view.span16(); + m_converted.grow(span.size() * 3); + size_t length = simdutf::convert_utf16le_to_utf8(span.data(), span.size(), byteCast(m_converted.mutableSpan().data())); + if (length) [[likely]] { + m_converted.shrink(length); + return; + } + // simdutf refuses unpaired surrogates; each becomes U+FFFD. + auto lenient = view.utf8(WTF::ConversionMode::StrictConversionReplacingUnpairedSurrogatesWithFFFD); + m_converted.shrink(0); + m_converted.append(byteCast(lenient.span())); +} + +} + +using namespace JSC; + +// An empty value is a member that is absent. +extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) +{ + VM& vm = getVM(globalObject); + ASSERT(count == Bun::Bindgen::Tables::objectStructures[structureIndex].memberCount); + bool hasEveryMember = true; + for (size_t i = 0; i < count; ++i) + hasEveryMember &= !!values[i]; + if (hasEveryMember) [[likely]] { + JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->bindgenObjectStructure(structureIndex)); + for (size_t i = 0; i < count; ++i) + object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); + return JSValue::encode(object); + } + const Identifier* memberNames = Bun::Bindgen::identifiers(vm).identifiersAt(Bun::Bindgen::Tables::objectStructures[structureIndex].firstMemberNameIndex, count); + JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count); + for (size_t i = 0; i < count; ++i) { + if (values[i]) + object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); + } + return JSValue::encode(object); +} + +extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* globalObject, unsigned index) +{ + return JSValue::encode(Bun::Bindgen::identifiers(getVM(globalObject)).stringAt(index)); +} + +extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (values->hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + RELEASE_AND_RETURN(scope, JSValue::encode(constructArray(globalObject, static_cast(nullptr), *values))); +} diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index a7040df327de..22f1ea9de5b3 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -57,6 +57,7 @@ JSHeapData::~JSHeapData() = default; JSVMClientData::JSVMClientData(VM& vm, RefPtr sourceProvider) : commonStrings(vm) + , bindgenIdentifiers(vm) , m_builtinNames(vm) , m_builtinFunctions(makeUnique(vm, sourceProvider, m_builtinNames)) , m_heapData(JSHeapData::ensureHeapData(vm.heap)) @@ -183,6 +184,7 @@ void JSVMClientData::create(VM* vm, void* bunVM, WorkerMessagingProxy* worker) MAKE_MARKING_CONSTRAINT_EXECUTOR_PAIR(([clientData](auto& visitor) { JSC::SetRootMarkReasonScope rootScope(visitor, JSC::RootMarkReason::StrongHandles); clientData->commonStrings.visit(visitor); + clientData->bindgenIdentifiers.visit(visitor); })), JSC::ConstraintVolatility::GreyedByExecution)); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index f696a0d7973e..83b590079c8b 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -72,6 +72,7 @@ class DOMWrapperWorld; #include "JSCTaskScheduler.h" #include "HTTPHeaderIdentifiers.h" #include "BunCommonStrings.h" +#include "Bindgen/Identifiers.h" #include "DOMURLBaseCache.h" #include "NodeVMOptionNames.h" #include "NodeVMSourceOriginCache.h" @@ -244,6 +245,7 @@ class JSVMClientData : public JSC::VM::ClientData { // Public so Bun::commonStrings(vm) below is a static_cast and a member load. Bun::CommonStrings commonStrings; + Bun::Bindgen::Identifiers bindgenIdentifiers; WebCore::DOMURLBaseCache& urlBaseCache() { return m_urlBaseCache; } Bun::NodeVMOptionNames& nodeVMOptionNames() { return m_nodeVMOptionNames; } diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 42a00f2b4ebf..120b79a7e1ec 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -376,7 +376,7 @@ static JSValue constructBunShell(VM& vm, JSObject* bunObject) return {}; } - bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "braces"_s), 1, Generated::BunObject::jsBraces, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); + bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "braces"_s), 1, Bun::Bindgen::Generated::BunObject::jsBraces, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); bunShell->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "escape"_s), 1, BunObject_callback_shellEscape, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); bunShell->putDirect(vm, JSC::Identifier::fromString(vm, "ShellError"_s), ShellError.getObject(), JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly | 0); @@ -965,7 +965,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj fetch constructBunFetchObject ReadOnly|DontDelete|PropertyCallback file BunObject_callback_file DontDelete|Function 1 fileURLToPath functionFileURLToPath DontDelete|Function 1 - gc Generated::BunObject::jsGc DontDelete|Function 1 + gc Bun::Bindgen::Generated::BunObject::jsGc DontDelete|Function 1 generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 2 gunzipSync BunObject_callback_gunzipSync DontDelete|Function 1 gzipSync BunObject_callback_gzipSync DontDelete|Function 1 diff --git a/src/jsc/bindings/GeneratedBindings.rs b/src/jsc/bindings/GeneratedBindings.rs deleted file mode 100644 index 4ab057be718c..000000000000 --- a/src/jsc/bindings/GeneratedBindings.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Hand-written surface of `src/codegen/bindgen.ts` output (bindgen dispatch shims). -//! -//! Until the generator grows a `.rs` backend, the modules required by -//! downstream Rust callers are ported here by hand. Each `create_*_callback` -//! is a thin wrapper over `host_fn::new_runtime_function` binding a C++-side -//! `JSHostFn` (the `bindgen_*_js*` symbols emitted by the C++ dispatch shim) -//! to a named JS function value. - -use crate::{JSGlobalObject, JSHostFn, JSValue, host_fn}; -use bun_core::EncodedSlice; - -/// Generated for "src/jsc/bindgen_test.rs" -pub mod bindgen_test { - use super::*; - - crate::jsc_abi_extern! { - #[link_name = "bindgen_Bindgen_test_jsAdd"] - fn jsAdd(global: *mut JSGlobalObject, frame: *mut crate::CallFrame) -> JSValue; - #[link_name = "bindgen_Bindgen_test_jsRequiredAndOptionalArg"] - fn jsRequiredAndOptionalArg(global: *mut JSGlobalObject, frame: *mut crate::CallFrame) -> JSValue; - } - - const JS_ADD: JSHostFn = jsAdd; - const JS_REQUIRED_AND_OPTIONAL_ARG: JSHostFn = jsRequiredAndOptionalArg; - - pub(crate) fn create_add_callback(global: &JSGlobalObject) -> JSValue { - host_fn::new_runtime_function( - global, - Some(&EncodedSlice::latin1(b"add")), - 3, - JS_ADD, - false, - None, - ) - } - - pub(crate) fn create_required_and_optional_arg_callback(global: &JSGlobalObject) -> JSValue { - host_fn::new_runtime_function( - global, - Some(&EncodedSlice::latin1(b"requiredAndOptionalArg")), - 4, - JS_REQUIRED_AND_OPTIONAL_ARG, - false, - None, - ) - } -} diff --git a/src/jsc/bindings/IDLTypes.h b/src/jsc/bindings/IDLTypes.h index 888901589ed3..bd32cb4064f2 100644 --- a/src/jsc/bindings/IDLTypes.h +++ b/src/jsc/bindings/IDLTypes.h @@ -71,7 +71,7 @@ struct IDLType { struct IDLNull : IDLType { }; -// See also: Bun::IDLRawAny, Bun::Bindgen::IDLStrongAny +// See also: Bun::IDLRawAny struct IDLAny : IDLType> { // SequenceStorageType must be left as JSC::Strong; otherwise // IDLSequence would yield a Vector, whose contents diff --git a/src/jsc/bindings/JSBindgenObject.cpp b/src/jsc/bindings/JSBindgenObject.cpp new file mode 100644 index 000000000000..95e07e84ed2e --- /dev/null +++ b/src/jsc/bindings/JSBindgenObject.cpp @@ -0,0 +1,90 @@ +#include "root.h" +#include "Bindgen/JSBindgenObject.h" +#include "GeneratedBindings+Tables.h" +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +const ClassInfo JSBindgenConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBindgenConstructor) }; + +static JSC_DECLARE_HOST_FUNCTION(callJSBindgenConstructor); +JSC_DEFINE_HOST_FUNCTION(callJSBindgenConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, makeString(constructor->name(), " constructor cannot be invoked without 'new'"_s)); + return {}; +} + +JSBindgenConstructor::JSBindgenConstructor(VM& vm, Structure* structure, NativeFunction construct) + : Base(vm, structure, callJSBindgenConstructor, construct) +{ +} + +JSBindgenConstructor* JSBindgenConstructor::create(VM& vm, JSGlobalObject* globalObject, const ClassDefinition& definition, JSObject* prototype) +{ + auto* structure = createStructure(vm, globalObject, globalObject->functionPrototype()); + auto* constructor = new (NotNull, allocateCell(vm)) JSBindgenConstructor(vm, structure, definition.construct); + constructor->finishCreation(vm, definition.constructorLength, definition.className, PropertyAdditionMode::WithoutStructureTransition); + Bun::reifyStaticPropertyTable(vm, info(), definition.constructorTableValues, *constructor); + constructor->putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + return constructor; +} + +unsigned classCount() +{ + return Tables::classCount; +} + +void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIndex) +{ + const ClassDefinition& definition = *Tables::classes[classIndex]; + VM& vm = init.vm; + auto* prototypeStructure = JSFinalObject::createStructure(vm, init.global, init.global->objectPrototype(), 0); + prototypeStructure->setMayBePrototype(true); + JSObject* prototype = JSFinalObject::create(vm, prototypeStructure); + Bun::reifyStaticPropertyTable(vm, definition.info, definition.prototypeTableValues, *prototype); + if (definition.finishPrototypeCreation) + definition.finishPrototypeCreation(vm, init.global, prototype); + Bun::putToStringTagWithoutTransition(vm, prototype, definition.info); + init.setPrototype(prototype); + init.setStructure(definition.createStructure(vm, init.global, prototype)); + if (definition.construct) + init.setConstructor(JSBindgenConstructor::create(vm, init.global, definition, prototype)); +} + +Structure* structureForClass(JSGlobalObject* globalObject, unsigned classIndex) +{ + auto* global = defaultGlobalObject(globalObject); + return global->m_bindgenLazyClasses[classIndex].getInitializedOnMainThread(global); +} + +JSObject* constructorForClass(JSGlobalObject* globalObject, unsigned classIndex) +{ + auto* global = defaultGlobalObject(globalObject); + return global->m_bindgenLazyClasses[classIndex].constructorInitializedOnMainThread(global); +} + +Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFrame, unsigned classIndex) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* newTarget = asObject(callFrame->newTarget()); + if (newTarget == constructorForClass(globalObject, classIndex)) [[likely]] + return structureForClass(globalObject, classIndex); + // ShadowRealm functions belong to a different global object. + auto* functionGlobalObject = getFunctionRealm(globalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, InternalFunction::createSubclassStructure(globalObject, newTarget, structureForClass(functionGlobalObject, classIndex))); +} + +} + +extern "C" void Bun__Bindgen__setInternalField(JSC::JSGlobalObject* globalObject, JSC::JSCell* cell, uint32_t offset, JSC::EncodedJSValue value) +{ + auto* field = reinterpret_cast*>(reinterpret_cast(cell) + offset); + field->set(JSC::getVM(globalObject), cell, JSC::JSValue::decode(value)); +} diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 96ef06f074c2..06dbb4562083 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -70,6 +70,7 @@ #include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" +#include "Bindgen/JSBindgenObject.h" #include "BunPlugin.h" #include "BunProcess.h" #include "BunSecureContextCache.h" @@ -2752,6 +2753,19 @@ void GlobalObject::finishCreation(VM& vm) this->initGeneratedLazyClasses(); + for (auto& lazyClass : m_bindgenLazyClasses) { + lazyClass.initLater([](LazyClassStructure::Initializer& init) { + auto* globalObject = uncheckedDowncast(init.global); + Bun::Bindgen::setupClassStructure(init, &init.classStructure - globalObject->m_bindgenLazyClasses.begin()); + }); + } + for (auto& structure : m_bindgenObjectStructures) { + structure.initLater([](const Initializer& init) { + auto* globalObject = uncheckedDowncast(init.owner); + init.set(Bun::Bindgen::createObjectStructure(init.vm, globalObject, &init.property - globalObject->m_bindgenObjectStructures.begin())); + }); + } + m_napiTypeTags.initLater([](const JSC::LazyProperty::Initializer& init) { init.set(JSC::JSWeakMap::create(init.vm, init.owner->weakMapStructure())); }); @@ -3323,6 +3337,10 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) WebCore::clientData(visitor.collector().heap().vm())->httpHeaderIdentifiers().template visit(visitor); thisObject->visitGeneratedLazyClasses(thisObject, visitor); + for (auto& structure : thisObject->m_bindgenObjectStructures) + structure.visit(visitor); + for (auto& lazyClass : thisObject->m_bindgenLazyClasses) + lazyClass.visit(visitor); thisObject->visitAdditionalChildrenInGCThread(visitor); } diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 1d04744f57b8..de6bd286e572 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -73,6 +73,7 @@ struct node_module; #include "BakeAdditionsToGlobalObject.h" #include "NativeModuleList.h" #include "streams/JSStreamsRuntime.h" +#include namespace Bun { class JSCommonJSExtensions; @@ -97,6 +98,11 @@ extern "C" bool Bun__VirtualMachine__isShuttingDown(void* /* BunVM */); extern "C" uv_loop_t* Bun__ZigGlobalObject__uvLoop(void* /* BunVM */); #endif +namespace Bun::Bindgen { +unsigned objectStructureCount(); +unsigned classCount(); +} + namespace Zig { class JSCStackTrace; @@ -784,6 +790,13 @@ class GlobalObject : public Bun::GlobalScope { Bun::MarkdownTagStrings& markdownTagStrings() { return m_markdownTagStrings; } #include "ZigGeneratedClasses+lazyStructureHeader.h" + // Structures of the dictionaries src/codegen/bindgen.ts returns, by index into Bun::Bindgen::Tables::objectStructures. + // Sized in the member initializer, so the marker never sees it change. + JSC::Structure* bindgenObjectStructure(unsigned index) const { return m_bindgenObjectStructures[index].getInitializedOnMainThread(this); } + WTF::FixedVector> m_bindgenObjectStructures { Bun::Bindgen::objectStructureCount() }; + // The classes it generates, by index into Bun::Bindgen::Tables::classes. + WTF::FixedVector m_bindgenLazyClasses { Bun::Bindgen::classCount() }; + void finishCreation(JSC::VM&); private: diff --git a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h index ee30ad6b1a8d..33cc60ba36e5 100644 --- a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h @@ -1,6 +1,7 @@ #pragma once #include "root.h" +#include "GeneratedBindings+Counts.h" #include #include @@ -79,6 +80,7 @@ class DOMClientIsoSubspaces { GCClient::IsoSubspace* m_clientSubspaceForJSWebView { nullptr }; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" + GCClient::IsoSubspace* m_clientSubspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClassSubspaces] {}; /* --- bun --- */ GCClient::IsoSubspace* m_clientSubspaceForDOMException { nullptr }; diff --git a/src/jsc/bindings/webcore/DOMIsoSubspaces.h b/src/jsc/bindings/webcore/DOMIsoSubspaces.h index 3860de3220bd..4e4d706ad9cd 100644 --- a/src/jsc/bindings/webcore/DOMIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMIsoSubspaces.h @@ -2,6 +2,7 @@ #pragma once #include "root.h" +#include "GeneratedBindings+Counts.h" #include #include @@ -76,6 +77,7 @@ class DOMIsoSubspaces { IsoSubspace* m_subspaceForWasmStreamingCompiler { nullptr }; IsoSubspace* m_subspaceForJSWebView { nullptr }; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" + IsoSubspace* m_subspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClassSubspaces] {}; /*-- BUN --*/ IsoSubspace* m_subspaceForFetchHeaders { nullptr }; diff --git a/src/jsc/fmt_jsc.bind.ts b/src/jsc/fmt_jsc.bind.ts index 79a7a2aa3daa..b83c12d33cfd 100644 --- a/src/jsc/fmt_jsc.bind.ts +++ b/src/jsc/fmt_jsc.bind.ts @@ -1,12 +1,19 @@ -import { fn, t } from "bindgen"; +import * as b from "../codegen/bindgen-lib.ts"; -export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell"); +export default b.module({ rustPath: "crate::fmt_jsc::js_bindings" }); -export const fmtString = fn({ +export const Formatter = b.enumeration("Formatter", [ + "highlight-javascript", + "highlight-javascript-redacted", + "escape-powershell", +]); + +export const fmtString = b.fn({ args: { - global: t.globalObject, - code: t.UTF8String, + code: b.utf8.loose, formatter: Formatter, }, - ret: t.DOMString, + returns: b.string, + capability: "heap", + throws: true, }); diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index 3d65201fc24c..a17362872d9a 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -1,35 +1,24 @@ //! Bindgen target for `fmt_jsc.bind.ts`. The actual formatters live in -//! `src/bun_core/fmt.rs`; only the JS-facing wrapper that takes a -//! `&JSGlobalObject` lives here so `bun_core/` stays JSC-free. +//! `src/bun_core/fmt.rs`; only the JS-facing wrapper lives here so `bun_core/` +//! stays JSC-free. use std::io::Write as _; -use crate::{JSGlobalObject, JsResult}; +use crate::bindgen::{self, JsHeap}; use bun_core::fmt; use bun_core::{MutableString, String}; pub mod js_bindings { use super::*; - /// `bun.gen.fmt_jsc.Formatter` — bindgen-emitted enum from `fmt_jsc.bind.ts`. - /// Mirrored locally until `bun_gen` is reachable from this tier. - /// NOTE: bindgen sorts `t.stringEnum` values alphabetically before emitting - /// the C++ `enum class`, so discriminants must match `GeneratedFmtJsc.h` - /// (EscapePowershell first), not the `.bind.ts` declaration order. - #[repr(u8)] - #[derive(Copy, Clone, Eq, PartialEq)] - pub enum Formatter { - EscapePowershell = 0, - HighlightJavascript = 1, - HighlightJavascriptRedacted = 2, - } + pub use crate::generated_bindgen::fmt_jsc::Formatter; /// Internal function for testing in highlighter.test.ts - pub fn fmt_string( - global: &JSGlobalObject, + pub fn fmt_string<'a>( + heap: &JsHeap<'a>, code: &[u8], formatter_id: Formatter, - ) -> JsResult { + ) -> bindgen::Result<'a, String> { let mut buffer = MutableString::init_empty(); let writer = buffer.writer(); @@ -43,7 +32,7 @@ pub mod js_bindings { ..Default::default() }, ); - write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| heap.throw_out_of_memory())?; } Formatter::HighlightJavascriptRedacted => { let formatter = fmt::fmt_javascript( @@ -54,11 +43,11 @@ pub mod js_bindings { redact_sensitive_information: true, }, ); - write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| heap.throw_out_of_memory())?; } Formatter::EscapePowershell => { write!(writer, "{}", fmt::escape_powershell(code)) - .map_err(|_| global.throw_out_of_memory())?; + .map_err(|_| heap.throw_out_of_memory())?; } } diff --git a/src/jsc/generated.rs b/src/jsc/generated.rs index 6e7e53843139..142fc5efb9cf 100644 --- a/src/jsc/generated.rs +++ b/src/jsc/generated.rs @@ -1,711 +1,16 @@ -//! Hand-written Rust surface for the `.bindv2.ts` / `.classes.ts` codegen. +//! Hand-written Rust surface for the `.classes.ts` codegen. //! -//! Two distinct generators feed this file: -//! -//! 1. **bindgen option-structs** (`src/codegen/bindgen.ts`). Each emits a -//! `bindgenConvertJSTo` C++ shim that fills a `#[repr(C)]` extern -//! struct, plus a `convertFromExtern` that reshapes it into the public -//! Rust type. `from_js` glues the two. -//! -//! 2. **per-class accessor modules** (`src/codegen/generate-classes.ts`). -//! Each `JS${Type}` exposes -//! `from_js` / `from_js_direct` / `to_js` / `get_constructor` thin-wrapping -//! the `${Type}__fromJS` / `__fromJSDirect` / `__create` / `__getConstructor` -//! C++ exports, plus one `${name}_get_cached` / `${name}_set_cached` pair -//! per `cache: true` property. -//! -//! Until both generators grow a `.rs` backend, this file ports their output by -//! hand for the handful of shapes downstream crates name directly -//! (`bun_jsc::generated::{SocketConfig*, SSLConfig*, JSTimeout, JSImmediate, -//! JSBlob, JSResponse, JSRequest}`). +//! Each `JS${Type}` module exposes `from_js` / `from_js_direct` / `to_js` / `get_constructor`, +//! thin-wrapping the `${Type}__fromJS` / `__fromJSDirect` / `__create` / `__getConstructor` C++ +//! exports of `src/codegen/generate-classes.ts`, plus one `${name}_get_cached` / +//! `${name}_set_cached` pair per `cache: true` property, for the classes downstream crates name +//! directly (`bun_jsc::generated::{JSTimeout, JSImmediate, JSBlob, JSResponse, JSRequest}`). //! //! Symbol-naming contract (kept in sync with generate-classes.ts): //! `${T}Prototype__${name}GetCachedValue(JSValue) -> JSValue` //! `${T}Prototype__${name}SetCachedValue(JSValue, *JSGlobalObject, JSValue)` //! `${T}__fromJS` / `${T}__fromJSDirect` / `${T}__create` / `${T}__getConstructor` -use core::ffi::c_uint; -use core::mem::MaybeUninit; - -use crate::{JSCArrayBuffer, JSGlobalObject, JSValue, JsResult}; - -// ────────────────────────────────────────────────────────────────────────── -// Generic accessor wrappers. -// -// Per-field accessors come in two shapes: `.get()` (optional) / -// `.items()` (array). Dependents pattern-match on these without naming the -// wrapper type directly, so a pair of generic carriers covers every field. -// ────────────────────────────────────────────────────────────────────────── - -/// Optional-value accessor: `field.get() -> Option`. -#[derive(Debug, Default)] -pub struct GenOpt(Option); - -impl GenOpt { - #[inline] - pub fn as_ref(&self) -> Option<&T> { - self.0.as_ref() - } - #[inline] - pub fn into_inner(self) -> Option { - self.0 - } -} - -/// Required-value accessor: `field.get() -> T` (used inside tagged-union arms). -#[derive(Debug)] -pub struct GenVal(T); - -impl GenVal { - #[inline] - pub fn get(&self) -> T - where - T: Copy, - { - self.0 - } - #[inline] - pub fn as_ref(&self) -> &T { - &self.0 - } -} - -/// Array accessor: `field.items() -> &[T]`. -#[derive(Debug, Default)] -pub struct GenList(Vec); - -impl GenList { - #[inline] - pub fn items(&self) -> &[T] { - &self.0 - } -} - -/// Bindgen option-structs for `BunObject.bind.ts`. -pub mod bun_object { - /// `gen.BunObject.BracesOptions` — `#[repr(C)]` extern struct passed by - /// pointer from the C++ dispatch shim. Field order is - /// `parse`, `tokenize`. - #[repr(C)] - #[derive(Default, Clone, Copy)] - pub struct BracesOptions { - pub parse: bool, - pub tokenize: bool, - } -} - -// Shorthand for the bindgen string payload. The real generator hands back a -// `bun.String` / `WTFStringImpl`; downstream code only calls `.length()` / -// `.to_utf8()` / `.to_owned_slice_z()` on it, all of which `bun_core::String` -// already provides. -pub type GenString = bun_core::String; - -/// `bun.bun_js.jsc.JSCArrayBuffer.Ref` — adopted `*mut JSC::ArrayBuffer` -/// (refcount already +1 from C++). Stays a raw `Copy` pointer because it is -/// also embedded in the `#[repr(C)]` extern unions below, which require POD -/// arms; the +1 ref is released exactly once via -/// `release_gen_val_array_buffer` in the owning containers' `Drop` impls. -pub type GenArrayBuffer = *mut JSCArrayBuffer; - -/// The codegen `m_ctx` `*mut webcore::Blob`, erased: `Blob` lives in `bun_runtime`. -pub type GenBlob = *mut core::ffi::c_void; - -// ────────────────────────────────────────────────────────────────────────── -// Extern-ABI helper layouts. -// -// The bindgen's tagged-union extern layout is `{ data: union, tag: u8 }`. -// Rust has no variadic -// `#[repr(C)] union`, so we hand-roll the few arities the bindgen actually -// emits for the structs below. All extern types are `Copy` POD; the -// `convert_from_extern` step takes ownership of any embedded heap refs. -// ────────────────────────────────────────────────────────────────────────── - -type RawWTFStringImpl = *mut bun_core::WTFStringImplStruct; - -/// `BindgenOptional(BindgenTrivial).ExternType` — `T` has no custom -/// `OptionalExternType`, so the C++ side wraps it in a 2-arm tagged union -/// `{ data: union { _0: u8 /* unused */, _1: T }, tag: u8 }`. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternOptional { - data: ExternOptionalData, - tag: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union ExternOptionalData { - _0: u8, - _1: T, -} - -impl ExternOptional { - #[inline] - fn get(self) -> Option { - if self.tag == 0 { - return None; - } - debug_assert_eq!(self.tag, 1); - // SAFETY: tag == 1 ⇒ C++ initialized the `_1` arm. - Some(unsafe { self.data._1 }) - } -} - -/// `bindgen.ExternArrayList(T)` — `extern struct { data: ?[*]T, length: c_uint, -/// capacity: c_uint }`. -// Clone/Copy: bitwise OK — FFI mirror of a C++ buffer; Rust treats it as a -// borrowed view and adopts ownership exactly once at the call site. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternArrayList { - data: *mut T, // null = empty - length: c_uint, - capacity: c_uint, -} - -#[inline] -fn adopt_string(ptr: RawWTFStringImpl) -> GenString { - // C++ hands back a +1 ref; `adopt_wtf_impl` takes ownership (no inc). - bun_core::String::adopt_wtf_impl(ptr) -} - -#[inline] -fn adopt_opt_string(ptr: RawWTFStringImpl) -> GenOpt { - // `BindgenOptional(BindgenString).ExternType` is `?WTFStringImpl` — single-word - // nullable ptr (custom `OptionalExternType`), NOT an `ExternTaggedUnion`. - GenOpt(if ptr.is_null() { - None - } else { - Some(adopt_string(ptr)) - }) -} - -// ────────────────────────────────────────────────────────────────────────── -// SocketConfigHandlers -// ────────────────────────────────────────────────────────────────────────── - -#[repr(u32)] -#[derive(Debug, Default, Clone, Copy)] -pub enum SocketConfigHandlersBinaryType { - Arraybuffer = 0, - #[default] - Buffer = 1, - Uint8array = 2, -} - -pub struct SocketConfigHandlers { - pub on_open: JSValue, - pub on_close: JSValue, - pub on_data: JSValue, - pub on_writable: JSValue, - pub on_timeout: JSValue, - pub on_connect_error: JSValue, - pub on_end: JSValue, - pub on_error: JSValue, - pub on_handshake: JSValue, - pub on_session: JSValue, - pub on_keylog: JSValue, - pub on_server_name: JSValue, - pub on_alpn_callback: JSValue, - pub binary_type: SocketConfigHandlersBinaryType, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSocketConfigHandlers { - onOpen: JSValue, - onClose: JSValue, - onError: JSValue, - onData: JSValue, - onWritable: JSValue, - onHandshake: JSValue, - onEnd: JSValue, - onConnectError: JSValue, - onTimeout: JSValue, - onSession: JSValue, - onKeylog: JSValue, - onServerName: JSValue, - onALPNCallback: JSValue, - binary_type: SocketConfigHandlersBinaryType, -} - -// safe: `JSGlobalObject` is an opaque `UnsafeCell`-backed ZST handle (`&` is -// ABI-identical to non-null `*mut`); `&mut MaybeUninit` is ABI-identical to -// non-null `*mut T` (`MaybeUninit` is layout-transparent over `T`). The C++ -// side fully initializes `*result` iff it returns `true`. -unsafe extern "C" { - safe fn bindgenConvertJSToSocketConfigHandlers( - global: &JSGlobalObject, - value: JSValue, - result: &mut MaybeUninit, - ) -> bool; -} - -impl SocketConfigHandlers { - fn convert_from_extern(ext: &ExternSocketConfigHandlers) -> Self { - Self { - on_open: ext.onOpen, - on_close: ext.onClose, - on_error: ext.onError, - on_data: ext.onData, - on_writable: ext.onWritable, - on_handshake: ext.onHandshake, - on_end: ext.onEnd, - on_connect_error: ext.onConnectError, - on_timeout: ext.onTimeout, - on_session: ext.onSession, - on_keylog: ext.onKeylog, - on_server_name: ext.onServerName, - on_alpn_callback: ext.onALPNCallback, - binary_type: ext.binary_type, - } - } - - pub fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult { - // Wrapped in an `ExceptionValidationScope` for the - // `assertExceptionPresenceMatches(!success)` check; the scope must exist - // *before* the FFI call so the C++ ThrowScope's `simulateThrow()` is - // satisfied under `validateExceptionChecks=1`. - let mut ext = MaybeUninit::::uninit(); - crate::call_false_is_throw(global, || { - bindgenConvertJSToSocketConfigHandlers(global, value, &mut ext) - })?; - // SAFETY: success ⇒ C++ initialized `ext`. - Ok(Self::convert_from_extern(unsafe { ext.assume_init_ref() })) - } -} - -// ────────────────────────────────────────────────────────────────────────── -// SSLConfig -// ────────────────────────────────────────────────────────────────────────── - -pub enum SSLConfigAlpnProtocols { - None, - String(GenVal), - Buffer(GenVal), -} - -/// `string | Buffer | BunFile` element of an SSL file-ish option. -pub enum SSLConfigSingleFile { - String(GenVal), - Buffer(GenVal), - File(GenVal), -} - -/// `string | Buffer | BunFile | Array<...>` — the full file-ish option. -pub enum SSLConfigFile { - None, - String(GenVal), - Buffer(GenVal), - File(GenVal), - Array(GenList), -} - -pub struct SSLConfig { - pub passphrase: GenOpt, - pub dh_params_file: GenOpt, - pub server_name: GenOpt, - pub low_memory_mode: bool, - pub reject_unauthorized: Option, - pub request_cert: bool, - pub secure_options: u32, - pub ssl_min_version: i32, - pub ssl_max_version: i32, - pub ca: SSLConfigFile, - pub cert: SSLConfigFile, - pub key: SSLConfigFile, - pub key_file: GenOpt, - pub cert_file: GenOpt, - pub ca_file: GenOpt, - pub alpn_protocols: SSLConfigAlpnProtocols, - pub ciphers: GenOpt, - pub client_renegotiation_limit: u32, - pub client_renegotiation_window: u32, - pub crl: SSLConfigFile, - pub allow_partial_trust_chain: bool, - pub session_timeout: i32, - pub sigalgs: GenOpt, - pub ecdh_curve: GenOpt, -} - -// ── refcount release on drop ────────────────────────────────────────────── -// -// `adopt_string` adopts a +1 `WTF::StringImpl` ref into a `GenString` -// (= `bun_core::String`), which releases it on drop. -// -// `GenArrayBuffer` / `GenBlob` raw-pointer payloads carry an adopted +1 ref -// (C++ `ExternTraits>::convertToExtern` calls `leakRef()`); -// released here via the matching `ExternalSharedDescriptor::ext_deref`. - -#[inline] -fn release_gen_val_array_buffer(b: &GenVal) { - if !b.0.is_null() { - // SAFETY: `b.0` is the `RefPtr::leakRef()` result from - // C++ `ExternTraits` — a live `JSC::ArrayBuffer*` carrying +1. - unsafe { ::ext_deref(b.0) }; - } -} - -#[inline] -fn release_gen_val_blob(b: &GenVal) { - if !b.0.is_null() { - // SAFETY: `b.0` is the `RefPtr::leakRef()` result from C++ - // `ExternTraits` — a live heap-allocated `Blob*` carrying +1. - unsafe { - ::ext_deref( - b.0.cast::(), - ) - }; - } -} - -impl Drop for SSLConfigAlpnProtocols { - fn drop(&mut self) { - match self { - SSLConfigAlpnProtocols::None | SSLConfigAlpnProtocols::String(_) => {} - SSLConfigAlpnProtocols::Buffer(v) => release_gen_val_array_buffer(v), - } - } -} - -impl Drop for SSLConfigSingleFile { - fn drop(&mut self) { - match self { - SSLConfigSingleFile::String(_) => {} - SSLConfigSingleFile::Buffer(v) => release_gen_val_array_buffer(v), - SSLConfigSingleFile::File(v) => release_gen_val_blob(v), - } - } -} - -impl Drop for SSLConfigFile { - fn drop(&mut self) { - // `Array` recursively drops each `SSLConfigSingleFile`. - match self { - SSLConfigFile::None | SSLConfigFile::Array(_) | SSLConfigFile::String(_) => {} - SSLConfigFile::Buffer(v) => release_gen_val_array_buffer(v), - SSLConfigFile::File(v) => release_gen_val_blob(v), - } - } -} - -// ── extern layouts ──────────────────────────────────────────────────────── - -/// `BindgenSSLConfigSingleFile.ExternType` = -/// `ExternTaggedUnion(&.{ ?WTFStringImpl, ?*JSCArrayBuffer, ?*Blob })`. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSSLConfigSingleFile { - data: ExternSSLConfigSingleFileData, - tag: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union ExternSSLConfigSingleFileData { - _0: RawWTFStringImpl, - _1: GenArrayBuffer, - _2: GenBlob, -} - -impl SSLConfigSingleFile { - fn convert_from_extern(ext: ExternSSLConfigSingleFile) -> Self { - // SAFETY: each arm reads the union field selected by `tag`, which C++ - // guarantees is the initialized one. - match ext.tag { - // SAFETY: tag == 0 ⇒ `_0` is the initialized union arm. - 0 => Self::String(GenVal(adopt_string(unsafe { ext.data._0 }))), - // SAFETY: tag == 1 ⇒ `_1` is the initialized union arm. - 1 => Self::Buffer(GenVal(unsafe { ext.data._1 })), - // SAFETY: tag == 2 ⇒ `_2` is the initialized union arm. - 2 => Self::File(GenVal(unsafe { ext.data._2 })), - // SAFETY: tag space is 0..=2 per bindgen contract. - _ => unsafe { core::hint::unreachable_unchecked() }, - } - } -} - -/// `BindgenSSLConfigFile.ExternType` = `ExternTaggedUnion(&.{ u8, ?WTFStringImpl, -/// ?*JSCArrayBuffer, ?*Blob, ExternArrayList })`. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSSLConfigFile { - data: ExternSSLConfigFileData, - tag: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union ExternSSLConfigFileData { - _0: u8, // BindgenNull - _1: RawWTFStringImpl, - _2: GenArrayBuffer, - _3: GenBlob, - _4: ExternArrayList, -} - -impl SSLConfigFile { - fn convert_from_extern(ext: ExternSSLConfigFile) -> Self { - // SAFETY: each arm reads the union field selected by `tag`. - match ext.tag { - 0 => Self::None, - // SAFETY: tag == 1 ⇒ `_1` is the initialized union arm. - 1 => Self::String(GenVal(adopt_string(unsafe { ext.data._1 }))), - // SAFETY: tag == 2 ⇒ `_2` is the initialized union arm. - 2 => Self::Buffer(GenVal(unsafe { ext.data._2 })), - // SAFETY: tag == 3 ⇒ `_3` is the initialized union arm. - 3 => Self::File(GenVal(unsafe { ext.data._3 })), - 4 => { - // SAFETY: tag == 4 ⇒ `_4` is the initialized arm. - let arr = unsafe { ext.data._4 }; - let len = arr.length as usize; - let mut out = Vec::with_capacity(len); - if !arr.data.is_null() { - for i in 0..len { - // SAFETY: `arr.data` points to `length` initialized elements - // (mimalloc-backed; C++ transferred ownership). - let elem = unsafe { *arr.data.add(i) }; - out.push(SSLConfigSingleFile::convert_from_extern(elem)); - } - // This path copies-then-frees the - // source buffer; profile if it shows up on a hot path. - // SAFETY: `arr.data` was allocated by `WTF::fastMalloc` ≡ mimalloc - // (per crate prereq); `mi_free` is size-agnostic. - unsafe { bun_alloc::basic::free_without_size(arr.data.cast()) }; - } - Self::Array(GenList(out)) - } - // SAFETY: tag space is 0..=4 per bindgen contract. - _ => unsafe { core::hint::unreachable_unchecked() }, - } - } -} - -/// `BindgenALPNProtocols.ExternType` = -/// `ExternTaggedUnion(&.{ u8, ?WTFStringImpl, ?*JSCArrayBuffer })`. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternALPNProtocols { - data: ExternALPNProtocolsData, - tag: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union ExternALPNProtocolsData { - _0: u8, - _1: RawWTFStringImpl, - _2: GenArrayBuffer, -} - -impl SSLConfigAlpnProtocols { - fn convert_from_extern(ext: ExternALPNProtocols) -> Self { - // SAFETY: each arm reads the union field selected by `tag`. - match ext.tag { - 0 => Self::None, - // SAFETY: tag == 1 ⇒ `_1` is the initialized union arm. - 1 => Self::String(GenVal(adopt_string(unsafe { ext.data._1 }))), - // SAFETY: tag == 2 ⇒ `_2` is the initialized union arm. - 2 => Self::Buffer(GenVal(unsafe { ext.data._2 })), - // SAFETY: tag space is 0..=2 per bindgen contract. - _ => unsafe { core::hint::unreachable_unchecked() }, - } - } -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSSLConfig { - passphrase: RawWTFStringImpl, - dh_params_file: RawWTFStringImpl, - server_name: RawWTFStringImpl, - low_memory_mode: bool, - reject_unauthorized: ExternOptional, - request_cert: bool, - ca: ExternSSLConfigFile, - cert: ExternSSLConfigFile, - key: ExternSSLConfigFile, - secure_options: u32, - ssl_min_version: i32, - ssl_max_version: i32, - key_file: RawWTFStringImpl, - cert_file: RawWTFStringImpl, - ca_file: RawWTFStringImpl, - alpn_protocols: ExternALPNProtocols, - ciphers: RawWTFStringImpl, - client_renegotiation_limit: u32, - client_renegotiation_window: u32, - crl: ExternSSLConfigFile, - allow_partial_trust_chain: bool, - session_timeout: i32, - sigalgs: RawWTFStringImpl, - ecdh_curve: RawWTFStringImpl, -} - -// safe: same handle/out-param contract as -// `bindgenConvertJSToSocketConfigHandlers` above. -unsafe extern "C" { - safe fn bindgenConvertJSToSSLConfig( - global: &JSGlobalObject, - value: JSValue, - result: &mut MaybeUninit, - ) -> bool; -} - -impl SSLConfig { - fn convert_from_extern(ext: &ExternSSLConfig) -> Self { - Self { - passphrase: adopt_opt_string(ext.passphrase), - dh_params_file: adopt_opt_string(ext.dh_params_file), - server_name: adopt_opt_string(ext.server_name), - low_memory_mode: ext.low_memory_mode, - reject_unauthorized: ext.reject_unauthorized.get(), - request_cert: ext.request_cert, - ca: SSLConfigFile::convert_from_extern(ext.ca), - cert: SSLConfigFile::convert_from_extern(ext.cert), - key: SSLConfigFile::convert_from_extern(ext.key), - secure_options: ext.secure_options, - ssl_min_version: ext.ssl_min_version, - ssl_max_version: ext.ssl_max_version, - key_file: adopt_opt_string(ext.key_file), - cert_file: adopt_opt_string(ext.cert_file), - ca_file: adopt_opt_string(ext.ca_file), - alpn_protocols: SSLConfigAlpnProtocols::convert_from_extern(ext.alpn_protocols), - ciphers: adopt_opt_string(ext.ciphers), - client_renegotiation_limit: ext.client_renegotiation_limit, - client_renegotiation_window: ext.client_renegotiation_window, - crl: SSLConfigFile::convert_from_extern(ext.crl), - allow_partial_trust_chain: ext.allow_partial_trust_chain, - session_timeout: ext.session_timeout, - sigalgs: adopt_opt_string(ext.sigalgs), - ecdh_curve: adopt_opt_string(ext.ecdh_curve), - } - } - - pub fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult { - let mut ext = MaybeUninit::::uninit(); - crate::call_false_is_throw(global, || { - bindgenConvertJSToSSLConfig(global, value, &mut ext) - })?; - // SAFETY: success ⇒ C++ initialized `ext`. - Ok(Self::convert_from_extern(unsafe { ext.assume_init_ref() })) - } -} - -// ────────────────────────────────────────────────────────────────────────── -// SocketConfig -// ────────────────────────────────────────────────────────────────────────── - -pub enum SocketConfigTls { - None, - Boolean(bool), - Object(Box), -} - -pub struct SocketConfig { - pub tls: SocketConfigTls, - pub fd: Option, - pub handlers: SocketConfigHandlers, - pub data: JSValue, - pub unix_: GenOpt, - pub hostname: GenOpt, - pub port: Option, - pub exclusive: bool, - pub allow_half_open: bool, - pub reuse_port: bool, - pub ipv6_only: bool, - pub pause_on_connect: bool, -} - -/// `BindgenSocketConfigTLS.ExternType` = -/// `ExternTaggedUnion(&.{ u8, bool, ExternSSLConfig })`. -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSocketConfigTLS { - data: ExternSocketConfigTLSData, - tag: u8, -} - -#[repr(C)] -#[derive(Clone, Copy)] -union ExternSocketConfigTLSData { - _0: u8, - _1: bool, - _2: ExternSSLConfig, -} - -impl SocketConfigTls { - fn convert_from_extern(ext: &ExternSocketConfigTLS) -> Self { - // SAFETY: each arm reads the union field selected by `tag`. - match ext.tag { - 0 => Self::None, - // SAFETY: tag == 1 ⇒ `_1` is the initialized union arm. - 1 => Self::Boolean(unsafe { ext.data._1 }), - // SAFETY: tag == 2 ⇒ `_2` is the initialized union arm. - 2 => Self::Object(Box::new(SSLConfig::convert_from_extern(unsafe { - &ext.data._2 - }))), - // SAFETY: tag space is 0..=2 per bindgen contract. - _ => unsafe { core::hint::unreachable_unchecked() }, - } - } -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ExternSocketConfig { - handlers: ExternSocketConfigHandlers, - data: JSValue, - allow_half_open: bool, - hostname: RawWTFStringImpl, - port: ExternOptional, - tls: ExternSocketConfigTLS, - exclusive: bool, - reuse_port: bool, - ipv6_only: bool, - pause_on_connect: bool, - unix_: RawWTFStringImpl, - fd: ExternOptional, -} - -// safe: same handle/out-param contract as -// `bindgenConvertJSToSocketConfigHandlers` above. -unsafe extern "C" { - safe fn bindgenConvertJSToSocketConfig( - global: &JSGlobalObject, - value: JSValue, - result: &mut MaybeUninit, - ) -> bool; -} - -impl SocketConfig { - fn convert_from_extern(ext: &ExternSocketConfig) -> Self { - Self { - handlers: SocketConfigHandlers::convert_from_extern(&ext.handlers), - data: ext.data, - allow_half_open: ext.allow_half_open, - hostname: adopt_opt_string(ext.hostname), - port: ext.port.get(), - tls: SocketConfigTls::convert_from_extern(&ext.tls), - exclusive: ext.exclusive, - reuse_port: ext.reuse_port, - ipv6_only: ext.ipv6_only, - pause_on_connect: ext.pause_on_connect, - unix_: adopt_opt_string(ext.unix_), - fd: ext.fd.get(), - } - } - - pub fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult { - let mut ext = MaybeUninit::::uninit(); - crate::call_false_is_throw(global, || { - bindgenConvertJSToSocketConfig(global, value, &mut ext) - })?; - // SAFETY: success ⇒ C++ initialized `ext`. - Ok(Self::convert_from_extern(unsafe { ext.assume_init_ref() })) - } -} - // ────────────────────────────────────────────────────────────────────────── // Per-class cached-accessor modules. // diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index c15ddb5406fa..a07914cc0d72 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1579,8 +1579,16 @@ unsafe extern "C" { #[path = "generated.rs"] pub mod generated; -/// `bun.gen` — bindgen dispatch shims. -/// Hand-written per-module until `src/codegen/bindgen.ts` grows a `.rs` backend. -/// (`gen` is a reserved keyword in edition 2024; use `r#gen` at call sites.) -#[path = "bindings/GeneratedBindings.rs"] -pub mod r#gen; +/// What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. +#[allow( + non_snake_case, + unused_unsafe, + clippy::missing_safety_doc, + clippy::identity_op +)] +pub mod generated_bindgen { + include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_jsc.rs" + )); +} diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index 99ffb5f3f6c3..91e7b5d0dd03 100644 --- a/src/jsc/modules/NodeModuleModule.cpp +++ b/src/jsc/modules/NodeModuleModule.cpp @@ -988,7 +988,7 @@ _pathCache getPathCacheObject PropertyCallback _preloadModules jsFunctionPreloadModules Function 0 _resolveFilename nodeModuleResolveFilename CustomAccessor _resolveLookupPaths jsFunctionResolveLookupPaths Function 2 -_stat &Generated::NodeModuleModule::js_stat Function 1 +_stat &Bun::Bindgen::Generated::NodeModuleModule::js_stat Function 1 builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 14bfbea46095..5f6f59600634 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -1,23 +1,27 @@ -import { fn, t } from "bindgen"; +import * as b from "../../codegen/bindgen-lib.ts"; -export const BracesOptions = t.dictionary({ - tokenize: t.boolean.default(false), - parse: t.boolean.default(false), +export default b.module({ rustPath: "crate::api::bun_object" }); + +export const BracesOptions = b.dictionary("BracesOptions", { + tokenize: b.bool.loose.default(false), + parse: b.bool.loose.default(false), }); -export const braces = fn({ +export const braces = b.fn({ args: { - global: t.globalObject, - input: t.DOMString, - options: BracesOptions.default({}), + input: b.string.loose, + options: BracesOptions.nullable.default({}), }, - ret: t.any, + returns: b.any, + capability: "js", + throws: true, }); -export const gc = fn({ +export const gc = b.fn({ args: { - vm: t.virtualMachine, - force: t.boolean.default(false), + force: b.bool.loose.default(false), }, - ret: t.usize, + returns: b.usize, + capability: "js", + rustPath: "crate::api::bun_object::gc_from_js", }); diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 6724f2592b95..1ba707bfc74d 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -92,10 +92,9 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::call_frame::ArgumentsSlice; use bun_jsc::{StringJsc as _, bun_string_jsc}; -/// Bindgen-generated option-structs for this module (`BunObject.bind.ts`). -pub(crate) mod r#gen { - pub(crate) use bun_jsc::generated::bun_object::BracesOptions; -} +use crate::generated_bindgen::bun_object::BracesOptions; +use crate::generated_bindgen::glob::GlobClass as _; +use bun_jsc::bindgen::{self, JsCall, JsValue}; // ─── wrap_static_method adapters ─────────────────────────────────────────── // `#[bun_jsc::host_fn(static)]` is not yet emitted, @@ -387,10 +386,23 @@ fn shell_escape(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult Ok(jsval) } -pub(crate) fn braces( +pub(crate) fn braces<'a>( + cx: &mut JsCall<'a>, + input: BunString, + options: BracesOptions, +) -> bindgen::Result<'a, JsValue<'a>> { + cx.with_global_value(|global| braces_impl(global, &input, options)) +} + +/// `Bun.gc(force)`: heap size after the collection. +pub(crate) fn gc_from_js(cx: &mut JsCall<'_>, force: bool) -> usize { + cx.global().bun_vm().as_mut().garbage_collect_from_js(force) +} + +fn braces_impl( global: &JSGlobalObject, brace_str: &BunString, - opts: r#gen::BracesOptions, + opts: BracesOptions, ) -> JsResult { let brace_slice = brace_str.to_utf8(); @@ -1738,7 +1750,9 @@ fn get_archive_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValu } fn get_glob_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - jsc::codegen::js::get_constructor::(global_this) + // SAFETY: a lazy property callback, entered from JSC with no `JsCall` borrowed. + let cx = unsafe { JsCall::from_global(global_this) }; + crate::api::glob::Glob::get_constructor(&cx.heap).to_js() } fn get_image_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { diff --git a/src/runtime/api/Glob.bind.ts b/src/runtime/api/Glob.bind.ts new file mode 100644 index 000000000000..5d00ab951ce2 --- /dev/null +++ b/src/runtime/api/Glob.bind.ts @@ -0,0 +1,55 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export const GlobScanOptions = b.dictionary("GlobScanOptions", { + onlyFiles: b.bool.loose.nullable.default(true), + throwErrorOnBrokenSymlink: b.bool.loose.nullable.default(false), + followSymlinks: b.bool.loose.nullable.default(false), + absolute: b.bool.loose.nullable.default(false), + cwd: b.utf8.nullable, + dot: b.bool.loose.nullable.default(false), +}); + +export const GlobScanArgument = b.union("GlobScanArgument", { + cwd: b.utf8, + options: GlobScanOptions, +}); + +export const Glob = b.class("Glob", { + rustPath: "crate::api::glob::Glob", + configurable: false, + construct: { + args: { pattern: b.utf8 }, + }, + proto: { + scan: { + builtin: "globScanCodeGenerator", + length: 1, + }, + scanSync: { + builtin: "globScanSyncCodeGenerator", + length: 1, + }, + __scan: { + fn: "scan", + privateSymbol: "pull", + args: { options: GlobScanArgument.nullable }, + returns: b.any, + capability: "js", + throws: true, + length: 1, + }, + __scanSync: { + fn: "scan_sync", + privateSymbol: "resolveSync", + args: { options: GlobScanArgument.nullable }, + returns: b.any, + capability: "heap", + throws: true, + length: 1, + }, + match: { + args: { str: b.utf8 }, + returns: b.bool, + }, + }, +}); diff --git a/src/runtime/api/Glob.classes.ts b/src/runtime/api/Glob.classes.ts deleted file mode 100644 index 9b7ca6359033..000000000000 --- a/src/runtime/api/Glob.classes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { define } from "../../codegen/class-definitions.ts"; - -export default [ - define({ - name: "Glob", - construct: true, - finalize: true, - hasPendingActivity: true, - configurable: false, - klass: {}, - JSType: "0b11101110", - proto: { - scan: { - builtin: "globScanCodeGenerator", - length: 1, - }, - scanSync: { - builtin: "globScanSyncCodeGenerator", - length: 1, - }, - __scan: { - fn: "__scan", - length: 1, - // Wanted to use `resolve` and `resolveSync` but for some reason the - // resolve symbol was not working, even though `resolveSync` was. - privateSymbol: "pull", - }, - __scanSync: { - fn: "__scanSync", - length: 1, - privateSymbol: "resolveSync", - }, - match: { - fn: "match", - length: 1, - }, - }, - }), -]; diff --git a/src/runtime/api/glob.rs b/src/runtime/api/glob.rs index 377ed8787212..911b02546c75 100644 --- a/src/runtime/api/glob.rs +++ b/src/runtime/api/glob.rs @@ -1,24 +1,39 @@ -use core::sync::atomic::{AtomicUsize, Ordering}; - -use bun_alloc::Arena; -use bun_core::String as BunString; use bun_glob::BunGlobWalker as GlobWalker; use bun_glob::walk; +use bun_jsc::bindgen::{self, IntoError, JsCall, JsHeap, JsValue, JsWrapper}; use bun_jsc::bun_string_jsc; use bun_jsc::{ - ArgumentsSlice, CallFrame, JSGlobalObject, JSPromiseStrong, JSValue, Job, JobContext, JsPtr, - JsResult, JsThread, StringJsc as _, SysErrorJsc as _, + JSGlobalObject, JSPromiseStrong, JSValue, Job, JobContext, JsResult, JsThread, SysErrorJsc as _, }; use bun_paths::resolve_path::join_string_buf; use bun_paths::{self as resolve_path, MAX_PATH_BYTES, platform}; use bun_sys as syscall; -// Codegen hooks (JSGlob): toJS / fromJS / fromJSDirect are provided by the -// generated C++ wrapper. See PORTING.md §JSC ".classes.ts-backed types". -#[bun_jsc::JsClass] +use crate::generated_bindgen::glob::{GlobClass, GlobScanArgument, GlobScanOptions}; + +/// `Bun.Glob`, declared in `Glob.bind.ts`. pub(crate) struct Glob { pattern: Box<[u8]>, - has_pending_activity: AtomicUsize, +} + +pub(crate) enum ScanError<'a> { + Sys(syscall::Error), + Runtime(crate::Error), + CwdTooLong(&'static str), + Js(bindgen::Error<'a>), +} +impl<'a> IntoError<'a> for ScanError<'a> { + #[cold] + fn into_error(self, heap: &JsHeap<'a>) -> bindgen::Error<'a> { + match self { + ScanError::Sys(err) => err.into_error(heap), + ScanError::Runtime(err) => bun_jsc::JsError::from(err).into_error(heap), + ScanError::CwdTooLong(fn_name) => heap.throw(format_args!( + "{fn_name}: invalid `cwd`, longer than {MAX_PATH_BYTES} bytes" + )), + ScanError::Js(err) => err, + } + } } struct ScanOpts { @@ -31,164 +46,62 @@ struct ScanOpts { } impl ScanOpts { - fn parse_cwd( - global_this: &JSGlobalObject, - _arena: &Arena, - cwd_val: JSValue, + /// `None` for the empty string, which means "not given". + fn parse_cwd<'a>( + cwd: &[u8], absolute: bool, fn_name: &'static str, - ) -> JsResult> { - let cwd_string = BunString::from_js(cwd_val, global_this)?; - if cwd_string.is_empty() { - return Ok(Box::default()); + ) -> Result>, ScanError<'a>> { + if cwd.is_empty() { + return Ok(None); + } + if cwd.len() > MAX_PATH_BYTES { + return Err(ScanError::CwdTooLong(fn_name)); + } + if resolve_path::Platform::AUTO.is_absolute(cwd) { + return Ok(Some(Box::from(cwd))); } - let cwd_str: Box<[u8]> = 'cwd_str: { - let cwd_utf8 = cwd_string.to_utf8(); - - if cwd_utf8.slice().len() > MAX_PATH_BYTES { - return Err(global_this.throw(format_args!( - "{}: invalid `cwd`, longer than {} bytes", - fn_name, MAX_PATH_BYTES - ))); - } - - // If its absolute return as is - if resolve_path::Platform::AUTO.is_absolute(cwd_utf8.slice()) { - break 'cwd_str Box::<[u8]>::from(cwd_utf8.slice()); - } - - // `cwd_utf8` drops at scope exit. - let mut path_buf2 = [0u8; MAX_PATH_BYTES * 2]; - - if !absolute { - let parts: &[&[u8]] = &[cwd_utf8.slice()]; - let cwd_str = join_string_buf::(&mut path_buf2, parts); - break 'cwd_str Box::<[u8]>::from(cwd_str); - } - - // Convert to an absolute path + let mut path_buf2 = [0u8; MAX_PATH_BYTES * 2]; + let cwd_str: Box<[u8]> = if !absolute { + Box::from(join_string_buf::(&mut path_buf2, &[cwd])) + } else { let mut path_buf = bun_paths::path_buffer_pool::get(); - let cwd_len = match bun_sys::getcwd(&mut path_buf[..]) { - bun_sys::Result::Ok(len) => len, - bun_sys::Result::Err(err) => { - let err_js = err.to_js(global_this); - return Err(global_this.throw_value(err_js)); - } - }; - - let cwd_str = join_string_buf::( + let cwd_len = bun_sys::getcwd(&mut path_buf[..]).map_err(ScanError::Sys)?; + Box::from(join_string_buf::( &mut path_buf2, - &[&path_buf[..cwd_len], cwd_utf8.slice()], - ); - break 'cwd_str Box::<[u8]>::from(cwd_str); + &[&path_buf[..cwd_len], cwd], + )) }; - if cwd_str.len() > MAX_PATH_BYTES { - return Err(global_this.throw(format_args!( - "{}: invalid `cwd`, longer than {} bytes", - fn_name, MAX_PATH_BYTES - ))); + return Err(ScanError::CwdTooLong(fn_name)); } - - Ok(cwd_str) + Ok(Some(cwd_str).filter(|cwd| !cwd.is_empty())) } - fn from_js( - global_this: &JSGlobalObject, - arguments: &mut ArgumentsSlice, + fn new<'a>( + argument: Option>, fn_name: &'static str, - arena: &mut Arena, - ) -> JsResult> { - let Some(opts_obj) = arguments.next_eat() else { - return Ok(None); - }; - let mut out = ScanOpts { - cwd: None, - dot: false, - absolute: false, - follow_symlinks: false, - error_on_broken_symlinks: false, - only_files: true, + ) -> Result> { + let options = match argument { + Some(GlobScanArgument::Options(options)) => options, + Some(GlobScanArgument::Cwd(cwd)) => GlobScanOptions { + cwd: Some(cwd), + ..Default::default() + }, + None => Default::default(), }; - if opts_obj.is_undefined_or_null() { - return Ok(Some(out)); - } - if !opts_obj.is_object() { - if opts_obj.is_string() { - { - let result = - Self::parse_cwd(global_this, arena, opts_obj, out.absolute, fn_name)?; - if !result.is_empty() { - out.cwd = Some(result); - } - } - return Ok(Some(out)); - } - return Err(global_this.throw(format_args!( - "{}: expected first argument to be an object", - fn_name - ))); - } - - if let Some(only_files) = opts_obj.get_truthy(global_this, "onlyFiles")? { - out.only_files = if only_files.is_boolean() { - only_files.as_boolean() - } else { - false - }; - } - - if let Some(error_on_broken) = - opts_obj.get_truthy(global_this, "throwErrorOnBrokenSymlink")? - { - out.error_on_broken_symlinks = if error_on_broken.is_boolean() { - error_on_broken.as_boolean() - } else { - false - }; - } - - if let Some(follow_symlinks_val) = opts_obj.get_truthy(global_this, "followSymlinks")? { - out.follow_symlinks = if follow_symlinks_val.is_boolean() { - follow_symlinks_val.as_boolean() - } else { - false - }; - } - - if let Some(absolute_val) = opts_obj.get_truthy(global_this, "absolute")? { - out.absolute = if absolute_val.is_boolean() { - absolute_val.as_boolean() - } else { - false - }; - } - - if let Some(cwd_val) = opts_obj.get_truthy(global_this, "cwd")? { - if !cwd_val.is_string() { - return Err( - global_this.throw(format_args!("{}: invalid `cwd`, not a string", fn_name)) - ); - } - - { - let result = Self::parse_cwd(global_this, arena, cwd_val, out.absolute, fn_name)?; - if !result.is_empty() { - out.cwd = Some(result); - } - } - } - - if let Some(dot) = opts_obj.get_truthy(global_this, "dot")? { - out.dot = if dot.is_boolean() { - dot.as_boolean() - } else { - false - }; - } - - Ok(Some(out)) + Ok(ScanOpts { + cwd: match options.cwd { + Some(cwd) => Self::parse_cwd(cwd, options.absolute, fn_name)?, + None => None, + }, + dot: options.dot, + absolute: options.absolute, + only_files: options.only_files, + follow_symlinks: options.follow_symlinks, + error_on_broken_symlinks: options.throw_error_on_broken_symlink, + }) } } @@ -347,10 +260,16 @@ impl AnyGlobWalker { } } - fn result_to_js(&self, global_this: &JSGlobalObject) -> JsResult { + fn matched_paths_to_js<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a>> { + fn to_js<'a, A: walk::Accessor>( + walker: &walk::GlobWalker, + heap: &JsHeap<'a>, + ) -> bindgen::Result<'a, JsValue<'a>> { + bindgen::to_js::array(heap, walker.matched_paths.keys(), bindgen::to_js::utf8) + } match self { - AnyGlobWalker::Fs(walker) => glob_walk_result_to_js(walker, global_this), - AnyGlobWalker::Standalone(walker) => glob_walk_result_to_js(walker, global_this), + AnyGlobWalker::Fs(walker) => to_js(walker, heap), + AnyGlobWalker::Standalone(walker) => to_js(walker, heap), } } } @@ -364,31 +283,9 @@ pub(crate) struct WalkTask { // SAFETY: the walker owns its pattern/arena; nothing in it is thread-affine. unsafe impl Send for WalkTask {} -/// While a scan is pending the `Glob` wrapper reports `hasPendingActivity` -/// (so it is not collected); released on the JS thread with the completion. -pub(crate) struct PendingScan(JsPtr); -// SAFETY: a counter inside the Glob's native part, which its wrapper owns. -unsafe impl bun_jsc::job::JsAffine for PendingScan {} -impl PendingScan { - fn new(counter: &AtomicUsize) -> Self { - let _ = counter.fetch_add(1, Ordering::SeqCst); - // SAFETY: the Glob's m_ctx, kept alive by hasPendingActivity while > 0. - Self(unsafe { JsPtr::new(core::ptr::NonNull::from(counter)) }) - } -} -impl Drop for PendingScan { - fn drop(&mut self) { - // Only ever dropped on the JS thread (a job's Js side); the pointer is - // live because the count we hold kept the wrapper alive. - // SAFETY: as above. - let _ = unsafe { &*self.0.as_ptr() }.fetch_sub(1, Ordering::SeqCst); - } -} - #[derive(bun_jsc::JsAffine)] pub(crate) struct WalkJs { promise: JSPromiseStrong, - _pending: PendingScan, } pub(crate) enum WalkTaskErr { @@ -432,48 +329,27 @@ impl JobContext for WalkTask { promise.reject_with_async_stack(global, err.to_js(global))?; return Ok(()); } - let js_strings = match this.walker.result_to_js(global) { - Ok(v) => v, - Err(e) => return promise.reject(global, Err(e)), - }; - promise.resolve(global, js_strings) - } -} - -fn glob_walk_result_to_js( - glob_walk: &walk::GlobWalker, - global_this: &JSGlobalObject, -) -> JsResult { - let keys = glob_walk.matched_paths.keys(); - if keys.is_empty() { - return JSValue::create_empty_array(global_this, 0); + // SAFETY: a job's completion, entered from the event loop with no `JsCall` borrowed. + let heap = &unsafe { JsCall::from_global(global) }.heap; + match this.walker.matched_paths_to_js(heap) { + Ok(paths) => promise.resolve(global, paths.to_js()), + Err(err) => promise.reject(global, Err(err.into_js_error(global))), + } } - - JSValue::create_array_from_iter(global_this, keys.iter(), |key| { - bun_string_jsc::create_utf8_for_js(global_this, key) - }) } impl Glob { - /// The reference to the arena is not used after the scope because it is copied - /// by `GlobWalker.init`/`GlobWalker.initWithCwd` if all allocations work and no - /// errors occur - fn make_glob_walker( + fn make_glob_walker<'a>( &self, - global_this: &JSGlobalObject, - arguments: &mut ArgumentsSlice, + argument: Option>, fn_name: &'static str, - arena: &mut Arena, - ) -> JsResult> { - let Some(match_opts) = ScanOpts::from_js(global_this, arguments, fn_name, arena)? else { - return Ok(None); - }; + ) -> Result> { + let match_opts = ScanOpts::new(argument, fn_name)?; - fn init_walker( - global_this: &JSGlobalObject, + fn init_walker<'a, A: walk::Accessor>( pattern: &[u8], opts: &ScanOpts, - ) -> JsResult>> { + ) -> Result>, ScanError<'a>> { let result = match &opts.cwd { Some(cwd) => walk::GlobWalker::::init_with_cwd( pattern, @@ -495,10 +371,10 @@ impl Glob { None, ), }; - match result.map_err(crate::Error::from)? { - bun_sys::Result::Err(err) => Err(global_this.throw_value(err.to_js(global_this))), - bun_sys::Result::Ok(gw) => Ok(Box::new(gw)), - } + result + .map_err(|err| ScanError::Runtime(err.into()))? + .map(Box::new) + .map_err(ScanError::Sys) } // Match the walker's root choice (`Iterator::init`): an absolute @@ -517,169 +393,56 @@ impl Glob { }; if in_standalone_graph { - return Ok(Some(AnyGlobWalker::Standalone(init_walker::< - StandaloneAccessor, - >( - global_this, - &self.pattern, - &match_opts, - )?))); - } - Ok(Some(AnyGlobWalker::Fs( - init_walker::(global_this, &self.pattern, &match_opts)?, - ))) + return Ok(AnyGlobWalker::Standalone( + init_walker::(&self.pattern, &match_opts)?, + )); + } + Ok(AnyGlobWalker::Fs(init_walker::( + &self.pattern, + &match_opts, + )?)) } +} - // No `#[bun_jsc::host_fn]` here — the `#[bun_jsc::JsClass]` derive on - // the struct already emits the `GlobClass__construct` shim that calls - // `::constructor(..)`. The free-fn `host_fn` expansion can't name an - // associated fn without a receiver. - pub(crate) fn constructor( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult> { - // SAFETY: bun_vm() returns a non-null *mut to the live VirtualMachine for this global. - let mut arguments = ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); - // `arguments` drops at scope exit. - let Some(pat_arg) = arguments.next_eat() else { - return Err(global_this.throw(format_args!( - "Glob.constructor: expected 1 arguments, got 0" - ))); - }; +impl GlobClass for Glob { + type Error<'a> = ScanError<'a>; - if !pat_arg.is_string() { - return Err(global_this.throw(format_args!( - "Glob.constructor: first argument is not a string" - ))); + fn constructor<'a>(pattern: &'a [u8]) -> Self { + Glob { + pattern: Box::from(pattern), } - - let pat_str: Box<[u8]> = pat_arg - .to_bun_string(global_this)? - .to_owned_slice() - .into_boxed_slice(); - - Ok(Box::new(Glob { - pattern: pat_str, - has_pending_activity: AtomicUsize::new(0), - })) } - /// Called on the GC thread concurrently with the mutator. Reads only the - /// atomic counter; never allocates, locks, or touches JS. The codegen shim - /// (`Glob__hasPendingActivity`) handles the `callconv(.c)` ABI and passes - /// `&*this`. - pub(crate) fn has_pending_activity(&self) -> bool { - self.has_pending_activity.load(Ordering::SeqCst) > 0 + fn r#match<'a>(&self, str: &'a [u8]) -> bool { + bun_glob::r#match(&self.pattern, str).matches() } -} - -impl Glob { - // R-2 (host-fn re-entrancy): all JS-exposed methods take `&self`. `Glob`'s - // fields are read-only after construction (`pattern`) or already atomic - // (`has_pending_activity`), so no `Cell`/`JsCell` wrapping is needed — the - // `&mut self` receivers were vestigial. The codegen shim still emits - // `this: &mut Glob`; `&mut T` auto-derefs to `&T`. - #[bun_jsc::host_fn(method)] - pub(crate) fn __scan( - &self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - // SAFETY: bun_vm() returns a non-null *mut to the live VirtualMachine for this global. - let mut arguments = ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); - // `arguments` drops at scope exit. - - let mut arena = Arena::new(); - // GlobWalker::init/init_with_cwd own their allocations (Box); the - // arena here is vestigial. - let glob_walker = - match self.make_glob_walker(global_this, &mut arguments, "scan", &mut arena) { - Err(err) => { - drop(arena); - return Err(err); - } - Ok(None) => { - drop(arena); - return Ok(JSValue::UNDEFINED); - } - Ok(Some(gw)) => gw, - }; - let cx = global_this.js_thread_of_caller(callframe); - let promise = JSPromiseStrong::init(global_this); + fn scan<'a>( + this: JsWrapper<'a, Self>, + cx: &mut JsCall<'a>, + options: Option>, + ) -> Result, ScanError<'a>> { + let walker = this.with(&cx.payloads, |glob| glob.make_glob_walker(options, "scan"))?; + let promise = JSPromiseStrong::init(cx.global()); let value = promise.value(); Job::::schedule( - &cx, - WalkTask { - walker: glob_walker, - err: None, - }, - WalkJs { - promise, - _pending: PendingScan::new(&self.has_pending_activity), - }, + &cx.js_thread(), + WalkTask { walker, err: None }, + WalkJs { promise }, ); - Ok(value) - } - - #[bun_jsc::host_fn(method)] - pub(crate) fn __scan_sync( - &self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - // SAFETY: bun_vm() returns a non-null *mut to the live VirtualMachine for this global. - let mut arguments = ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); - - let mut arena = Arena::new(); - let mut glob_walker = - match self.make_glob_walker(global_this, &mut arguments, "scanSync", &mut arena) { - Err(err) => { - drop(arena); - return Err(err); - } - Ok(None) => { - drop(arena); - return Ok(JSValue::UNDEFINED); - } - Ok(Some(gw)) => gw, - }; - // Box drops at scope exit. - - match glob_walker.walk().map_err(crate::Error::from)? { - bun_sys::Result::Err(err) => { - return Err(global_this.throw_value(err.to_js(global_this))); - } - bun_sys::Result::Ok(()) => {} - } - - glob_walker.result_to_js(global_this) + Ok(JsValue::new(&cx.heap, value)) } - #[bun_jsc::host_fn(method)] - pub(crate) fn r#match( + fn scan_sync<'a>( &self, - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - // SAFETY: bun_vm() returns a non-null *mut to the live VirtualMachine for this global. - let mut arguments = ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); - let Some(str_arg) = arguments.next_eat() else { - return Err(global_this.throw(format_args!( - "Glob.matchString: expected 1 arguments, got 0" - ))); - }; - - if !str_arg.is_string() { - return Err(global_this.throw(format_args!( - "Glob.matchString: first argument is not a string" - ))); - } - - let str = str_arg.to_utf8(global_this)?; - - Ok(JSValue::from( - bun_glob::r#match(&self.pattern, str.slice()).matches(), - )) + heap: &JsHeap<'a>, + options: Option>, + ) -> Result, ScanError<'a>> { + let mut walker = self.make_glob_walker(options, "scanSync")?; + walker + .walk() + .map_err(|err| ScanError::Runtime(err.into()))? + .map_err(ScanError::Sys)?; + walker.matched_paths_to_js(heap).map_err(ScanError::Js) } } diff --git a/src/runtime/bake/DevServer.bind.ts b/src/runtime/bake/DevServer.bind.ts index df89b41febea..cb74513f48a3 100644 --- a/src/runtime/bake/DevServer.bind.ts +++ b/src/runtime/bake/DevServer.bind.ts @@ -1,6 +1,6 @@ -// @ts-ignore -import { fn, t } from "bindgen"; -export const getDeinitCountForTesting = fn({ - args: {}, - ret: t.usize, +import * as b from "../../codegen/bindgen-lib.ts"; + +export const getDeinitCountForTesting = b.fn({ + returns: b.usize, + rustPath: "crate::bake::get_deinit_count_for_testing", }); diff --git a/src/runtime/bake/tsconfig.json b/src/runtime/bake/tsconfig.json index 25ae601f5537..80c72d3da2d5 100644 --- a/src/runtime/bake/tsconfig.json +++ b/src/runtime/bake/tsconfig.json @@ -6,8 +6,7 @@ "ignoreDeprecations": "6.0", "baseUrl": ".", "paths": { - "bun-framework-react/*": ["./bun-framework-react/*"], - "bindgen": ["../../codegen/bindgen-lib"] + "bun-framework-react/*": ["./bun-framework-react/*"] }, "jsx": "react-jsx", // This code is bundled into the binary and never emits declarations, so the base config's `composite` diff --git a/src/runtime/dispatch_js2native.rs b/src/runtime/dispatch_js2native.rs index ad58e5a3d8d4..d0f2ad50d1c9 100644 --- a/src/runtime/dispatch_js2native.rs +++ b/src/runtime/dispatch_js2native.rs @@ -34,7 +34,6 @@ pub(crate) use bun_install_jsc::npm_jsc::package_manifest_bindings_generate as i pub(crate) use bun_install_jsc::ini_jsc::ini_testing_load_npmrc_from_js as ini_ini_ini_testing_ap_is_load_npmrc_from_js; pub(crate) use bun_install_jsc::ini_jsc::ini_testing_parse as ini_ini_ini_testing_ap_is_parse; -pub(crate) use bun_jsc::bindgen_test::get_bindgen_test_functions as jsc_bindgen_test_get_bindgen_test_functions; pub(crate) use bun_jsc::bytecode_order_recorder::names_for_testing as jsc_bytecode_order_recorder_names_for_testing; pub(crate) use bun_jsc::counters::create_counters_object as jsc_counters_create_counters_object; pub(crate) use bun_jsc::event_loop::get_active_tasks as jsc_event_loop_get_active_tasks; diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index ed1404e8a2c0..d8ca73b17fc5 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -351,384 +351,6 @@ pub(crate) fn on_reject_entry_point_result( bun_core::Global::exit(u32::from(global.bun_vm().as_mut().exit_handler.exit_code)); } -// ─── bindgenv2 dispatch shims (`bindgen_*_dispatch*`) ──────────────────────── -// -// These satisfy the `extern "C"` refs C++ emits from -// `Generated*Bindings.cpp`. Each forwards to the real Rust port of the named -// fn and maps `JsResult` → bool/JSValue per the bindgen ABI. - -/// `NodeModuleModule._stat(path) -> i32` (0=file, 1=dir, -ENOENT otherwise). -/// -/// # Safety -/// `out` must be a valid C++ stack local. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_NodeModuleModule_dispatch_stat1( - _global: *mut JSGlobalObject, - arg_str: &bun_core::String, - out: *mut i32, -) -> bool { - let s = arg_str.to_utf8(); - // SAFETY: `out` is a valid C++ stack out-param. - unsafe { *out = bun_jsc::node_module_module::stat(s.slice()) }; - true -} - -/// `BunObject.braces(input, options) -> JSValue`. -/// -/// # Safety -/// `arg_options` must be a valid C++ stack local. -// HOST_EXPORT(bindgen_BunObject_dispatchBraces1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_bunobject_dispatch_braces( - global: &JSGlobalObject, - input: &bun_core::String, - arg_options: *const crate::api::bun_object::r#gen::BracesOptions, -) -> JSValue { - // SAFETY: `arg_options` points to a `BracesOptions` on the C++ caller's stack. - let opts = unsafe { *arg_options }; - bun_jsc::host_fn::to_js_host_call(global, || { - crate::api::bun_object::braces(global, input, opts) - }) -} - -/// `BunObject.gc(force) -> usize` (heap size after collection). -/// -/// # Safety -/// `arg_force` and `out` must be valid C++ stack locals. -// HOST_EXPORT(bindgen_BunObject_dispatchGc1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_bunobject_dispatch_gc( - global: &JSGlobalObject, - arg_force: *const bool, - out: *mut usize, -) -> bool { - // SAFETY: `arg_force`/`out` are valid C++ stack locals. - let force = unsafe { *arg_force }; - // `garbage_collect(force)`: mimalloc cleanup, then sync `runGC(true)` - // when `force`, else `collect_async()` + `heap_size()`. - // SAFETY: bun_vm() never null for a Bun-owned global. - unsafe { *out = global.bun_vm().as_mut().garbage_collect_from_js(force) }; - true -} - -/// `fmt_jsc.js_bindings.fmtString(code, formatter) -> bun.String` -/// (highlighter.test.ts internal). -/// -/// # Safety -/// `arg_formatter` and `out` must be valid C++ stack locals. -// HOST_EXPORT(bindgen_Fmt_jsc_dispatchFmtString1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_fmt_jsc_dispatch_fmt_string( - global: &JSGlobalObject, - arg_code: &bun_core::String, - arg_formatter: *const bun_jsc::fmt_jsc::js_bindings::Formatter, - out: *mut bun_core::String, -) -> bool { - let code = arg_code.to_utf8(); - // SAFETY: `arg_formatter` points to a `Formatter` on the C++ caller's stack. - let formatter = unsafe { *arg_formatter }; - bindgen_out( - global, - out, - bun_jsc::fmt_jsc::js_bindings::fmt_string(global, code.slice(), formatter), - ) -} - -/// `DevServer.getDeinitCountForTesting() -> usize`. -/// -/// # Safety -/// `out` must be a valid C++ stack out-param. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_DevServer_dispatchGetDeinitCountForTesting1( - _global: *mut JSGlobalObject, - out: *mut usize, -) -> bool { - // SAFETY: `out` is a valid C++ stack local out-param. - unsafe { *out = crate::bake::get_deinit_count_for_testing() }; - true -} - -// ─── bindgen dispatch shims: Bindgen_test (src/jsc/bindgen_test.rs) ────────── - -/// `bindgen_test.add(a, b) -> i32` — bindgen self-test (overflow throws). -/// -/// # Safety -/// `arg_a`, `arg_b`, and `out` must be valid C++ stack locals. -// HOST_EXPORT(bindgen_Bindgen_test_dispatchAdd1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_bindgen_test_dispatch_add( - global: &JSGlobalObject, - arg_a: *const i32, - arg_b: *const i32, - out: *mut i32, -) -> bool { - // SAFETY: `arg_a`/`arg_b`/`out` are valid C++ stack locals (see - // GeneratedBindings.cpp:149). - match bun_jsc::bindgen_test::add(global, unsafe { *arg_a }, unsafe { *arg_b }) { - Ok(v) => { - // SAFETY: `out` is a valid C++ stack out-param. - unsafe { *out = v }; - true - } - Err(bun_jsc::JsError::OutOfMemory) => { - let _ = global.throw_out_of_memory(); - false - } - Err(_) => false, - } -} - -/// `#[repr(C)]` mirror of the bindgen optional-arg communication -/// buffer. Field order is {b_set, d_set, d_value, -/// b_value} — declaration-ordered, NOT size-sorted. -#[repr(C)] -struct BindgenTestRequiredAndOptionalArgArguments { - pub b_set: bool, - pub d_set: bool, - pub d_value: u8, - pub b_value: usize, -} - -/// `bindgen_test.requiredAndOptionalArg(a, b?, c, d?) -> i32`. -/// -/// # Safety -/// `arg_a`, `arg_c`, `buf`, and `out` must be valid C++ stack locals. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_Bindgen_test_dispatchRequiredAndOptionalArg1( - _global: *mut JSGlobalObject, - arg_a: *const bool, - arg_c: *const i32, - buf: *mut BindgenTestRequiredAndOptionalArgArguments, - out: *mut i32, -) -> bool { - // SAFETY: all pointers are valid C++ stack locals; `buf` fields are read - // gated on their `_set` flags (matching `if buf.b_set buf.b_value else null`). - let buf = unsafe { &*buf }; - // SAFETY: `arg_a`/`arg_c` point to scalars on the C++ caller's stack. - let v = bun_jsc::bindgen_test::required_and_optional_arg( - unsafe { *arg_a }, - if buf.b_set { Some(buf.b_value) } else { None }, - unsafe { *arg_c }, - if buf.d_set { Some(buf.d_value) } else { None }, - ); - // SAFETY: `out` is a valid C++ stack out-param. - unsafe { *out = v }; - true -} - -// ─── bindgen dispatch shims: Node_os (src/runtime/node/node_os.rs) ─────────── - -use crate::node::os as node_os; - -/// Maps `JsResult` → bindgen's bool-return ABI: `true` writes `*out`, -/// `false` leaves a pending exception (throwing OOM if needed). -#[inline] -fn bindgen_out(global: &JSGlobalObject, out: *mut T, r: bun_jsc::JsResult) -> bool { - match r { - Ok(v) => { - // SAFETY: `out` is a valid C++ stack out-param. - unsafe { out.write(v) }; - true - } - Err(bun_jsc::JsError::OutOfMemory) => { - let _ = global.throw_out_of_memory(); - false - } - Err(_) => false, - } -} - -// HOST_EXPORT(bindgen_Node_os_dispatchCpus1) -pub(crate) fn bindgen_node_os_cpus(global: &JSGlobalObject) -> bun_jsc::JsResult { - node_os::cpus(global) -} - -/// # Safety -/// `out` must be a valid C++ stack out-param. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_Node_os_dispatchFreemem1( - _global: *mut JSGlobalObject, - out: *mut u64, -) -> bool { - // SAFETY: `out` is a valid C++ stack out-param. `freemem()` is infallible. - unsafe { *out = node_os::freemem() }; - true -} - -/// # Safety -/// `arg_pid` and `out` must be valid C++ stack locals. -// HOST_EXPORT(bindgen_Node_os_dispatchGetPriority1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_node_os_dispatch_get_priority( - global: &JSGlobalObject, - arg_pid: *const i32, - out: *mut i32, -) -> bool { - // SAFETY: `arg_pid`/`out` are valid C++ stack locals. - bindgen_out( - global, - out, - node_os::get_priority(global, unsafe { *arg_pid }), - ) -} - -// HOST_EXPORT(bindgen_Node_os_dispatchHomedir1, c) -pub(crate) fn bindgen_node_os_dispatch_homedir( - global: &JSGlobalObject, - out: *mut bun_core::String, -) -> bool { - bindgen_out(global, out, node_os::homedir(global)) -} - -// HOST_EXPORT(bindgen_Node_os_dispatchHostname1) -pub(crate) fn bindgen_node_os_hostname(global: &JSGlobalObject) -> bun_jsc::JsResult { - node_os::hostname(global) -} - -// HOST_EXPORT(bindgen_Node_os_dispatchLoadavg1) -pub(crate) fn bindgen_node_os_loadavg(global: &JSGlobalObject) -> bun_jsc::JsResult { - node_os::loadavg(global) -} - -// HOST_EXPORT(bindgen_Node_os_dispatchNetworkInterfaces1) -pub(crate) fn bindgen_node_os_network_interfaces( - global: &JSGlobalObject, -) -> bun_jsc::JsResult { - node_os::network_interfaces(global) -} - -/// # Safety -/// `out` must be a valid C++ stack out-param. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_Node_os_dispatchRelease1( - _global: *mut JSGlobalObject, - out: *mut bun_core::String, -) -> bool { - // SAFETY: `out` is a valid C++ stack out-param. `release()` is infallible. - unsafe { out.write(node_os::release()) }; - true -} - -/// # Safety -/// `out` must be a valid C++ stack out-param. -#[unsafe(no_mangle)] -unsafe extern "C" fn bindgen_Node_os_dispatchTotalmem1( - _global: *mut JSGlobalObject, - out: *mut u64, -) -> bool { - // SAFETY: `out` is a valid C++ stack out-param. `totalmem()` is infallible. - unsafe { *out = node_os::totalmem() }; - true -} - -// HOST_EXPORT(bindgen_Node_os_dispatchUptime1, c) -pub(crate) fn bindgen_node_os_dispatch_uptime(global: &JSGlobalObject, out: *mut f64) -> bool { - bindgen_out(global, out, node_os::uptime(global)) -} - -/// # Safety -/// `arg_options` must be a valid C++ stack local. -// HOST_EXPORT(bindgen_Node_os_dispatchUserInfo1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_node_os_dispatch_user_info( - global: &JSGlobalObject, - arg_options: *const crate::node::os::gen_::UserInfoOptions, -) -> JSValue { - // SAFETY: `arg_options` is a valid C++ stack local; `UserInfoOptions` is - // `#[repr(C)]` matching the bindgen `extern struct`. Borrowed: its strings - // are `Bun::toString` views owned by the C++ frame. - let options = unsafe { &*arg_options }; - bun_jsc::host_fn::to_js_host_call(global, || node_os::user_info(global, options)) -} - -// HOST_EXPORT(bindgen_Node_os_dispatchVersion1, c) -pub(crate) fn bindgen_node_os_dispatch_version( - global: &JSGlobalObject, - out: *mut bun_core::String, -) -> bool { - bindgen_out(global, out, node_os::version()) -} - -/// # Safety -/// `arg_pid` and `arg_priority` must be valid C++ stack locals. -// HOST_EXPORT(bindgen_Node_os_dispatchSetPriority1, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_node_os_dispatch_set_priority1( - global: &JSGlobalObject, - arg_pid: *const i32, - arg_priority: *const i32, -) -> bool { - // SAFETY: `arg_pid`/`arg_priority` are valid C++ stack locals. - bindgen_out( - global, - std::ptr::from_mut::<()>(&mut ()), - node_os::set_priority1(global, unsafe { *arg_pid }, unsafe { *arg_priority }), - ) -} - -/// # Safety -/// `arg_priority` must be a valid C++ stack local. -// HOST_EXPORT(bindgen_Node_os_dispatchSetPriority2, c) -// Called only from the generated `extern "C"` thunk; C++ guarantees non-null stack locals. -pub(crate) fn bindgen_node_os_dispatch_set_priority2( - global: &JSGlobalObject, - arg_priority: *const i32, -) -> bool { - // SAFETY: `arg_priority` is a valid C++ stack local. - bindgen_out( - global, - std::ptr::from_mut::<()>(&mut ()), - node_os::set_priority2(global, unsafe { *arg_priority }), - ) -} - -// ─── js2native bindgen create-callback exports (GeneratedJS2Native.h) ──────── -// -// `js2native_bindgen__` returns a freshly-minted `JSFunction` wrapping -// the C++-side `bindgen__js` host fn. The C++ side already exports the -// host fn (it lives in `Generated*Bindings.cpp`); we just call -// `NewRuntimeFunction` here. -// -// ABI: `generate-js2native.ts` declares these on the C++ side as -// `extern "C" SYSV_ABI ...(Zig::GlobalObject*)` (the `callJS2Native` switch -// dispatches through them), so the Rust thunk MUST be `jsc` (sysv64 on -// win-x64), not plain `c`. With `c`, the win-x64 callee read `global` from -// RCX while C++ passed it in RDI → garbage `&JSGlobalObject` propagated into -// `Bun__CreateFFIFunctionValue` → `getVM(garbage)` segfault on first -// `bun:internal-for-testing` import. - -bun_jsc::jsc_abi_extern! { - // C++-side host fns (Generated*Bindings.cpp). - fn bindgen_Fmt_jsc_jsFmtString(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_DevServer_jsGetDeinitCountForTesting(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; -} - -// HOST_EXPORT(js2native_bindgen_fmt_jsc_fmtString, jsc) -pub(crate) fn js2native_bindgen_fmt_jsc_fmt_string(global: &JSGlobalObject) -> JSValue { - let name = bun_core::EncodedSlice::latin1(b"fmtString"); - bun_jsc::host_fn::new_runtime_function( - global, - Some(&name), - 3, - bindgen_Fmt_jsc_jsFmtString, - false, - None, - ) -} - -// HOST_EXPORT(js2native_bindgen_DevServer_getDeinitCountForTesting, jsc) -pub(crate) fn js2native_bindgen_dev_server_get_deinit_count(global: &JSGlobalObject) -> JSValue { - let name = bun_core::EncodedSlice::latin1(b"getDeinitCountForTesting"); - bun_jsc::host_fn::new_runtime_function( - global, - Some(&name), - 0, - bindgen_DevServer_jsGetDeinitCountForTesting, - false, - None, - ) -} - // `Bun__Chrome__autoDetect` / `Bun__Chrome__ensure` — exported from // `crate::webview::chrome_process` (mod webview is declared in lib.rs). // diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index 22f3000363af..c6174a017a45 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -64,6 +64,21 @@ pub(crate) mod napi; pub(crate) mod run_main; pub(crate) mod timer; +/// What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. +#[allow( + non_snake_case, + unreachable_pub, + dead_code, + unused_unsafe, + clippy::missing_safety_doc, + clippy::identity_op +)] +pub(crate) mod generated_bindgen { + include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_runtime.rs" + )); +} pub(crate) mod generated_classes; // include!()s ${BUN_CODEGEN_DIR}/generated_classes.rs pub(crate) mod generated_host_exports; // include!()s ${BUN_CODEGEN_DIR}/generated_host_exports.rs pub(crate) mod generated_js2native; // include!()s ${BUN_CODEGEN_DIR}/generated_js2native.rs diff --git a/src/runtime/node.rs b/src/runtime/node.rs index 73d5e8fa5b61..059a73a7f570 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -25,8 +25,6 @@ pub(crate) mod path; #[path = "node/node_os.rs"] pub(crate) mod os; -// codegen (`generated_js2native.rs`) addresses this by its file-stem name. -pub(crate) use os as node_os; #[path = "node/node_process.rs"] pub(crate) mod process; diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index db3082c4b7a6..90a7889ce70a 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -1,94 +1,62 @@ // Internal bindings for node:os // The entrypoint for node:os is `src/js/node/os.ts` -import { fn, t } from "bindgen"; +import * as b from "../../codegen/bindgen-lib.ts"; -export const cpus = fn({ - args: { - global: t.globalObject, - }, - ret: t.any, -}); -export const freemem = fn({ - args: {}, - ret: t.u64, -}); -export const getPriority = fn({ - args: { - global: t.globalObject, - pid: t.i32.validateInt32().default(0).nonNull, - }, - ret: t.i32, -}); -export const homedir = fn({ - args: { - global: t.globalObject, - }, - ret: t.DOMString, -}); -export const hostname = fn({ - args: { - global: t.globalObject, - }, - ret: t.any, -}); -export const loadavg = fn({ - args: { - global: t.globalObject, - }, - ret: t.any, -}); -export const networkInterfaces = fn({ - args: { - global: t.globalObject, - }, - ret: t.any, -}); -export const release = fn({ - args: {}, - ret: t.DOMString, -}); -export const totalmem = fn({ - args: {}, - ret: t.u64, -}); -export const uptime = fn({ - args: { - global: t.globalObject, - }, - ret: t.f64, -}); -export const UserInfoOptions = t.dictionary({ - encoding: t.DOMString.default(""), +export default b.module({ rustPath: "crate::node::os::js_bindings" }); + +export const hostCpuCount = b.fn({ returns: b.i32 }); +export const cpus = b.fn({ returns: b.any, capability: "js", throws: true }); +export const freemem = b.fn({ returns: b.u64 }); +export const getPriority = b.fn({ + args: { + pid: b.i32.validateInteger().default(0), + }, + returns: b.i32, + capability: "js", + throws: true, + length: 1, +}); +export const homedir = b.fn({ returns: b.string, capability: "js", throws: true }); +export const hostname = b.fn({ returns: b.any, capability: "js", throws: true }); +export const loadavg = b.fn({ returns: b.array(b.f64) }); +export const networkInterfaces = b.fn({ returns: b.any, capability: "js", throws: true }); +export const release = b.fn({ returns: b.string }); +export const totalmem = b.fn({ returns: b.u64 }); +export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); + +export const UserInfoOptions = b.dictionary("UserInfoOptions", { + encoding: b.string.loose.default(""), }); -export const userInfo = fn({ +export const userInfo = b.fn({ args: { - global: t.globalObject, - options: UserInfoOptions.default({}), + options: UserInfoOptions.nullable.default({}), }, - ret: t.any, -}); -export const version = fn({ - args: {}, - ret: t.DOMString, + returns: b.any, + capability: "js", + throws: true, + length: 1, }); +export const version = b.fn({ returns: b.string, throws: true }); + const PRI_MIN = -20; const PRI_MAX = 19; -export const setPriority = fn({ - variants: [ - { - args: { - global: t.globalObject, - pid: t.i32.validateInt32(), - priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), - }, - ret: t.undefined, +export const setPriority = b.overloads( + { + args: { + pid: b.i32.validateInteger(), + priority: b.i32.validateInteger(PRI_MIN, PRI_MAX), }, - { - args: { - global: t.globalObject, - priority: t.i32.validateInt32(PRI_MIN, PRI_MAX), - }, - ret: t.undefined, + capability: "js", + throws: true, + rustPath: "crate::node::os::js_bindings::set_priority", + length: 2, + }, + { + args: { + priority: b.i32.validateInteger(PRI_MIN, PRI_MAX), }, - ], -}); + capability: "js", + throws: true, + rustPath: "crate::node::os::js_bindings::set_priority_of_current_process", + }, +); diff --git a/src/runtime/node/node_os.rs b/src/runtime/node/node_os.rs index f5abb8887154..98a75665de0c 100644 --- a/src/runtime/node/node_os.rs +++ b/src/runtime/node/node_os.rs @@ -29,13 +29,12 @@ pub(crate) fn freemem() -> u64 { mod _impl { use super::*; - use bun_core::EncodedSlice; #[cfg(any(target_os = "linux", target_os = "android"))] use bun_core::ZStr; #[cfg(not(windows))] use bun_core::strings; use bun_core::{env_var, fmt as bun_fmt}; - use bun_jsc::{CallFrame, JSArray, StringJsc as _, SysErrorJsc as _, SystemError}; + use bun_jsc::{StringJsc as _, SysErrorJsc as _, SystemError}; #[cfg(windows)] use bun_sys::ReturnCodeExt as _; @@ -74,114 +73,57 @@ mod _impl { // safe upper bound for the stack buffer on every platform. const HOST_NAME_MAX: usize = 256; - // Generated bindings (emitted from `node_os.bind.ts` via - // `src/codegen/bindgen.ts`). The C++ side - // (`GeneratedBindings.cpp`) defines the SYSV-ABI `bindgen_Node_os_js*` host - // functions, which validate/decode arguments and call back into the - // `bindgen_Node_os_dispatch*` entry points. This module provides the - // public surface: `js*` extern pointers + `create*Callback` wrappers - // + the `UserInfoOptions` dictionary. - pub(crate) mod gen_ { - use super::{BunString, CallFrame, EncodedSlice, JSGlobalObject, JSValue}; - use bun_jsc::host_fn; - - // C++-side host fns (GeneratedBindings.cpp). `bindgen.ts` emits these as - // `extern "C" SYSV_ABI` (the `JSHostFunctionType` shape) — `jsc.conv` is - // the System V ABI on Windows-x64 and the C ABI everywhere else, matching - // `bun_jsc::host_fn::JsHostFn`. - bun_jsc::jsc_abi_extern! { - fn bindgen_Node_os_jsCpus(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsFreemem(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsGetPriority(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsHomedir(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsHostname(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsLoadavg(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsNetworkInterfaces(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsRelease(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsTotalmem(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsUptime(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsUserInfo(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsVersion(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - fn bindgen_Node_os_jsSetPriority(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue; - } + /// What `node_os.bind.ts` binds. + pub(crate) mod js_bindings { + use super::BunString; + use crate::generated_bindgen::node_os::UserInfoOptions; + use bun_jsc::bindgen::{self, JsCall, JsValue}; - // Each `create*Callback` is identical modulo (display name, min arg - // count, host-fn symbol) — see `bindgen.ts:1538`. Generate them with - // the exact triples the codegen would have produced. - macro_rules! create_callback { - ($($fn_name:ident, $js_name:literal, $argc:literal, $sym:ident;)*) => {$( - pub(crate) fn $fn_name(global: &JSGlobalObject) -> JSValue { - host_fn::new_runtime_function( - global, - Some(&EncodedSlice::latin1($js_name.as_bytes())), - $argc, - $sym, - false, - None, - ) - } - )*}; - } - create_callback! { - create_cpus_callback, "cpus", 1, bindgen_Node_os_jsCpus; - create_freemem_callback, "freemem", 0, bindgen_Node_os_jsFreemem; - create_get_priority_callback, "getPriority", 2, bindgen_Node_os_jsGetPriority; - create_homedir_callback, "homedir", 1, bindgen_Node_os_jsHomedir; - create_hostname_callback, "hostname", 1, bindgen_Node_os_jsHostname; - create_loadavg_callback, "loadavg", 1, bindgen_Node_os_jsLoadavg; - create_network_interfaces_callback, "networkInterfaces", 1, bindgen_Node_os_jsNetworkInterfaces; - create_release_callback, "release", 0, bindgen_Node_os_jsRelease; - create_totalmem_callback, "totalmem", 0, bindgen_Node_os_jsTotalmem; - create_uptime_callback, "uptime", 1, bindgen_Node_os_jsUptime; - create_user_info_callback, "userInfo", 2, bindgen_Node_os_jsUserInfo; - create_version_callback, "version", 0, bindgen_Node_os_jsVersion; - create_set_priority_callback, "setPriority", 2, bindgen_Node_os_jsSetPriority; - } + pub(crate) use super::super::freemem; + pub(crate) use super::{loadavg, release, totalmem, version}; - /// `t.dictionary({ encoding: t.DOMString.default("") })` from - /// `node_os.bind.ts`. Mirrors the extern struct emitted by bindgen; - /// the C++ side passes a pointer to this layout, so it must stay - /// `#[repr(C)]`. - #[repr(C)] - pub(crate) struct UserInfoOptions { - pub(crate) encoding: BunString, + pub(crate) fn host_cpu_count() -> i32 { + super::bun_sysconf__SC_NPROCESSORS_ONLN().max(1) + } + pub(crate) fn cpus<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { + cx.with_global_value(super::cpus) + } + pub(crate) fn get_priority<'a>(cx: &mut JsCall<'a>, pid: i32) -> bindgen::Result<'a, i32> { + cx.with_global(|global| super::get_priority(global, pid)) + } + pub(crate) fn homedir<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, BunString> { + cx.with_global(super::homedir) + } + pub(crate) fn hostname<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { + cx.with_global_value(super::hostname) + } + pub(crate) fn network_interfaces<'a>( + cx: &mut JsCall<'a>, + ) -> bindgen::Result<'a, JsValue<'a>> { + cx.with_global_value(super::network_interfaces) + } + pub(crate) fn uptime<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, f64> { + cx.with_global(super::uptime) + } + pub(crate) fn user_info<'a>( + cx: &mut JsCall<'a>, + options: UserInfoOptions, + ) -> bindgen::Result<'a, JsValue<'a>> { + cx.with_global_value(|global| super::user_info(global, &options)) + } + pub(crate) fn set_priority<'a>( + cx: &mut JsCall<'a>, + pid: i32, + priority: i32, + ) -> bindgen::Result<'a, ()> { + cx.with_global(|global| super::set_priority(global, pid, priority)) + } + pub(crate) fn set_priority_of_current_process<'a>( + cx: &mut JsCall<'a>, + priority: i32, + ) -> bindgen::Result<'a, ()> { + cx.with_global(|global| super::set_priority(global, 0, priority)) } - } - - pub(crate) fn create_node_os_binding(global: &JSGlobalObject) -> JsResult { - let obj = JSValue::create_empty_object(global, 14); - // SAFETY: pure FFI getter - obj.put( - global, - b"hostCpuCount", - JSValue::js_number(1i32.max(bun_sysconf__SC_NPROCESSORS_ONLN()) as f64), - ); - obj.put(global, b"cpus", gen_::create_cpus_callback(global)); - obj.put(global, b"freemem", gen_::create_freemem_callback(global)); - obj.put( - global, - b"getPriority", - gen_::create_get_priority_callback(global), - ); - obj.put(global, b"homedir", gen_::create_homedir_callback(global)); - obj.put(global, b"hostname", gen_::create_hostname_callback(global)); - obj.put(global, b"loadavg", gen_::create_loadavg_callback(global)); - obj.put( - global, - b"networkInterfaces", - gen_::create_network_interfaces_callback(global), - ); - obj.put(global, b"release", gen_::create_release_callback(global)); - obj.put(global, b"totalmem", gen_::create_totalmem_callback(global)); - obj.put(global, b"uptime", gen_::create_uptime_callback(global)); - obj.put(global, b"userInfo", gen_::create_user_info_callback(global)); - obj.put(global, b"version", gen_::create_version_callback(global)); - obj.put( - global, - b"setPriority", - gen_::create_set_priority_callback(global), - ); - Ok(obj) } impl CPUTimes { @@ -781,7 +723,7 @@ mod _impl { } } - pub(crate) fn loadavg(global: &JSGlobalObject) -> JsResult { + pub(crate) fn loadavg() -> [f64; 3] { #[cfg(target_os = "macos")] let result: [f64; 3] = 'loadavg: { let mut avg: c::struct_loadavg = bun_core::ffi::zeroed(); @@ -831,14 +773,7 @@ mod _impl { #[cfg(windows)] let result: [f64; 3] = [0.0, 0.0, 0.0]; - JSArray::create( - global, - &[ - JSValue::js_number(result[0]), - JSValue::js_number(result[1]), - JSValue::js_number(result[2]), - ], - ) + result } #[cfg(unix)] @@ -1372,7 +1307,7 @@ mod _impl { return bun_sys::get_errno(code); } - pub(crate) fn set_priority1(global: &JSGlobalObject, pid: i32, priority: i32) -> JsResult<()> { + pub(crate) fn set_priority(global: &JSGlobalObject, pid: i32, priority: i32) -> JsResult<()> { let errno = set_process_priority_impl(pid, priority); if errno == bun_sys::E::SUCCESS { return Ok(()); @@ -1385,10 +1320,6 @@ mod _impl { Err(global.throw_value(sys_err.to_error_instance_with_info_object(global))) } - pub(crate) fn set_priority2(global: &JSGlobalObject, priority: i32) -> JsResult<()> { - set_priority1(global, 0, priority) - } - pub(crate) fn totalmem() -> u64 { #[cfg(target_os = "macos")] { @@ -1460,7 +1391,7 @@ mod _impl { pub(crate) fn user_info( global_this: &JSGlobalObject, - options: &gen_::UserInfoOptions, + options: &crate::generated_bindgen::node_os::UserInfoOptions, ) -> JsResult { let _ = options; // TODO: diff --git a/src/runtime/socket/Handlers.rs b/src/runtime/socket/Handlers.rs index 1b737bb33f1a..f82375736f30 100644 --- a/src/runtime/socket/Handlers.rs +++ b/src/runtime/socket/Handlers.rs @@ -2,11 +2,13 @@ use core::cell::Cell; use core::ptr::NonNull; use std::rc::Rc; +use crate::generated_bindgen::socket_config::{ + SocketConfig as GeneratedSocketConfig, SocketConfigBinaryType as GeneratedBinaryType, + SocketConfigHandlers as GeneratedSocketConfigHandlers, SocketConfigTLS as GeneratedTls, +}; use bun_core::Utf8Bytes; use bun_jsc::array_buffer::BinaryType; -use bun_jsc::generated::{ - SocketConfig as GeneratedSocketConfig, SocketConfigHandlers as GeneratedSocketConfigHandlers, -}; +use bun_jsc::bindgen::{JsCall, JsValue}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{GlobalRef, JSGlobalObject, JSValue, JsResult, Strong}; use bun_sys::Fd; @@ -310,13 +312,17 @@ impl Handlers { opts: JSValue, mode: SocketMode, ) -> JsResult> { - let generated = GeneratedSocketConfigHandlers::from_js(global_object, opts)?; + // SAFETY: reached from a host function with no `JsCall` borrowed. + let cx = unsafe { JsCall::from_global(global_object) }; + let opts = JsValue::new(&cx.heap, opts); + let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) + .map_err(|e| e.into_js_error(global_object))?; Self::from_generated(global_object, &generated, mode) } pub(crate) fn from_generated( global_object: &JSGlobalObject, - generated: &GeneratedSocketConfigHandlers, + generated: &GeneratedSocketConfigHandlers<'_>, mode: SocketMode, ) -> JsResult> { let callbacks = Self::validate_callbacks(global_object, generated)?; @@ -343,11 +349,11 @@ impl Handlers { /// `JSValue::ZERO`. Array order matches `Bun::JSSocketHandlers::Field`. fn validate_callbacks( global_object: &JSGlobalObject, - generated: &GeneratedSocketConfigHandlers, + generated: &GeneratedSocketConfigHandlers<'_>, ) -> JsResult<[JSValue; CALLBACK_COUNT]> { macro_rules! validated_callback { ($field:ident, $name:literal) => {{ - let value = generated.$field; + let value = generated.$field.to_js(); if value.is_undefined_or_null() { JSValue::ZERO } else if !value.is_callable() { @@ -406,7 +412,11 @@ impl Handlers { global_object: &JSGlobalObject, opts: JSValue, ) -> JsResult { - let generated = GeneratedSocketConfigHandlers::from_js(global_object, opts)?; + // SAFETY: reached from a host function with no `JsCall` borrowed. + let cx = unsafe { JsCall::from_global(global_object) }; + let opts = JsValue::new(&cx.heap, opts); + let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) + .map_err(|e| e.into_js_error(global_object))?; let callbacks = Self::validate_callbacks(global_object, &generated)?; Ok(ReloadedHandlers { callbacks, @@ -450,8 +460,6 @@ impl Scope { } } -use bun_jsc::generated::SocketConfigHandlersBinaryType as GeneratedBinaryType; - pub(crate) struct SocketConfig { pub(crate) hostname_or_unix: Utf8Bytes<'static>, pub(crate) port: Option, @@ -495,7 +503,7 @@ impl SocketConfig { pub(crate) fn from_generated( vm: &'static VirtualMachine, global: &JSGlobalObject, - generated: GeneratedSocketConfig, + generated: GeneratedSocketConfig<'_>, mode: SocketMode, ) -> JsResult { let mut result: SocketConfig = 'blk: { @@ -530,7 +538,7 @@ impl SocketConfig { default_data: if generated.data.is_undefined() { JSValue::ZERO } else { - generated.data + generated.data.to_js() }, exclusive: false, allow_half_open: false, @@ -550,7 +558,7 @@ impl SocketConfig { if result.fd.is_some() { // If a user passes a file descriptor then prefer it over hostname or unix - } else if let Some(unix) = generated.unix_.into_inner() { + } else if let Some(unix) = generated.unix_ { if unix.length() == 0 { return Err(global .throw_invalid_arguments(format_args!("Expected a non-empty \"unix\" path"))); @@ -564,7 +572,7 @@ impl SocketConfig { let without_prefix = slice[7..].to_vec(); result.hostname_or_unix = Utf8Bytes::Owned(without_prefix); } - } else if let Some(hostname) = generated.hostname.into_inner() { + } else if let Some(hostname) = generated.hostname { if hostname.length() == 0 { return Err(global .throw_invalid_arguments(format_args!("Expected a non-empty \"hostname\""))); @@ -601,9 +609,11 @@ impl SocketConfig { global_object: &JSGlobalObject, mode: SocketMode, ) -> JsResult { - let generated = GeneratedSocketConfig::from_js(global_object, opts)?; + // SAFETY: reached from a host function with no `JsCall` borrowed. + let cx = unsafe { JsCall::from_global(global_object) }; + let opts = JsValue::new(&cx.heap, opts); + let generated = + GeneratedSocketConfig::from_js(cx, opts).map_err(|e| e.into_js_error(global_object))?; Self::from_generated(vm, global_object, generated, mode) } } - -use bun_jsc::generated::SocketConfigTls as GeneratedTls; diff --git a/src/runtime/socket/SSLConfig.bindv2.ts b/src/runtime/socket/SSLConfig.bind.ts similarity index 77% rename from src/runtime/socket/SSLConfig.bindv2.ts rename to src/runtime/socket/SSLConfig.bind.ts index caee0e9bb506..62fd21a3273e 100644 --- a/src/runtime/socket/SSLConfig.bindv2.ts +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -1,39 +1,39 @@ -import * as b from "../../codegen/bindgenv2/lib.ts"; +import * as b from "../../codegen/bindgen-lib.ts"; export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { - string: b.String, - buffer: b.ArrayBuffer, - file: b.Blob, + string: b.string, + buffer: b.arrayBuffer, + file: b.blob, }); export const SSLConfigFile = b.union("SSLConfigFile", { none: b.null, - string: b.String, - buffer: b.ArrayBuffer, - file: b.Blob, - array: b.Array(SSLConfigSingleFile), + string: b.string, + buffer: b.arrayBuffer, + file: b.blob, + array: b.array(SSLConfigSingleFile), }); export const ALPNProtocols = b.union("ALPNProtocols", { none: b.null, - string: b.String, - buffer: b.ArrayBuffer, + string: b.string, + buffer: b.arrayBuffer, }); export const SSLConfig = b.dictionary( { name: "SSLConfig", userFacingName: "TLSOptions", - generateConversionFunction: true, + fromJS: true, }, { - passphrase: b.String.nullable, + passphrase: b.string.nullable, dhParamsFile: { - type: b.String.nullable, + type: b.string.nullable, internalName: "dh_params_file", }, serverName: { - type: b.String.nullable, + type: b.string.nullable, internalName: "server_name", altNames: ["servername"], }, @@ -70,22 +70,22 @@ export const SSLConfig = b.dictionary( internalName: "ssl_max_version", }, keyFile: { - type: b.String.nullable, + type: b.string.nullable, internalName: "key_file", }, certFile: { - type: b.String.nullable, + type: b.string.nullable, internalName: "cert_file", }, caFile: { - type: b.String.nullable, + type: b.string.nullable, internalName: "ca_file", }, ALPNProtocols: { type: ALPNProtocols, internalName: "alpn_protocols", }, - ciphers: b.String.nullable, + ciphers: b.string.nullable, clientRenegotiationLimit: { type: b.u32, default: 0, @@ -107,9 +107,9 @@ export const SSLConfig = b.dictionary( default: 0, internalName: "session_timeout", }, - sigalgs: b.String.nullable, + sigalgs: b.string.nullable, ecdhCurve: { - type: b.String.nullable, + type: b.string.nullable, internalName: "ecdh_curve", }, }, diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index 970aee2095d2..dd01ee90a64b 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -16,6 +16,7 @@ use core::ffi::c_char; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsError, JsResult, SysErrorJsc}; +use crate::generated_bindgen::ssl_config as generated; use crate::node::fs as node_fs; use crate::webcore::Blob; use crate::webcore::blob::store::Data as StoreData; @@ -132,7 +133,7 @@ pub(crate) trait SSLConfigFromJs: Sized { fn from_generated( vm: &VirtualMachine, global: &JSGlobalObject, - generated: &jsc::generated::SSLConfig, + generated: &generated::SSLConfig, is_server: bool, ) -> JsResult>; } @@ -144,7 +145,11 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { - let generated = jsc::generated::SSLConfig::from_js(global, value)?; + // SAFETY: reached from a host function with no `JsCall` borrowed. + let cx = unsafe { jsc::bindgen::JsCall::from_global(global) }; + let value = jsc::bindgen::JsValue::new(&cx.heap, value); + let generated = + generated::SSLConfig::from_js(cx, value).map_err(|e| e.into_js_error(global))?; // `generated` dropped at scope exit Self::from_generated(vm, global, &generated, is_server) } @@ -152,7 +157,7 @@ impl SSLConfigFromJs for SSLConfig { fn from_generated( vm: &VirtualMachine, global: &JSGlobalObject, - generated: &jsc::generated::SSLConfig, + generated: &generated::SSLConfig, is_server: bool, ) -> JsResult> { let mut result = SSLConfig::zero(); @@ -245,13 +250,10 @@ impl SSLConfigFromJs for SSLConfig { } let protocols: *const c_char = match &generated.alpn_protocols { - jsc::generated::SSLConfigAlpnProtocols::None => core::ptr::null(), - jsc::generated::SSLConfigAlpnProtocols::String(val) => { - zbox_into_raw(&val.as_ref().to_owned_slice_z()) - } - jsc::generated::SSLConfigAlpnProtocols::Buffer(val) => { - // SAFETY: `val.get()` returns a non-null `*mut JSCArrayBuffer` - // owned by the GenVal for the duration of `generated`. + generated::ALPNProtocols::None => core::ptr::null(), + generated::ALPNProtocols::String(val) => zbox_into_raw(&val.to_owned_slice_z()), + generated::ALPNProtocols::Buffer(val) => { + // SAFETY: `generated` holds a ref on the buffer, and nothing else borrows it here. let buffer: jsc::ArrayBuffer = unsafe { (*val.get()).as_array_buffer() }; dupe_z(buffer.byte_slice()) } @@ -277,7 +279,7 @@ impl SSLConfigFromJs for SSLConfig { return Ok(None); } // After `any`: the fallback does not make `tls: {}` a TLS configuration. - if generated.ciphers.as_ref().is_none() { + if generated.ciphers.is_none() { apply_default_ciphers(vm, &mut result); } Ok(Some(result)) @@ -370,7 +372,7 @@ fn handle_path( fn handle_file_for_field( global: &JSGlobalObject, field: &'static str, - file: &jsc::generated::SSLConfigFile, + file: &generated::SSLConfigFile, ) -> JsResult { match handle_file(global, file) { Ok(v) => Ok(v), @@ -389,25 +391,23 @@ fn handle_file_for_field( fn handle_file( global: &JSGlobalObject, - file: &jsc::generated::SSLConfigFile, + file: &generated::SSLConfigFile, ) -> Result { let single = handle_single_file( global, match file { - jsc::generated::SSLConfigFile::None => return Ok(None), - jsc::generated::SSLConfigFile::String(val) => SingleFile::String(val.as_ref()), - jsc::generated::SSLConfigFile::Buffer(val) => { - // SAFETY: GenVal::get() yields a non-null pointer valid for the - // lifetime of `generated`; we narrow it to `&mut` for the call. + generated::SSLConfigFile::None => return Ok(None), + generated::SSLConfigFile::String(val) => SingleFile::String(val), + generated::SSLConfigFile::Buffer(val) => { + // SAFETY: `file` holds a ref on the buffer, and nothing else borrows it here. SingleFile::Buffer(unsafe { &mut *val.get() }) } - jsc::generated::SSLConfigFile::File(val) => { - // SAFETY: opaque `GenBlob` (`*mut c_void`) is the JS class `m_ctx` - // pointer, layout-identical to `crate::webcore::Blob`. - SingleFile::File(unsafe { &mut *val.get().cast::() }) + generated::SSLConfigFile::File(val) => { + // SAFETY: as above, for the `Blob`. + SingleFile::File(unsafe { &mut *val.get() }) } - jsc::generated::SSLConfigFile::Array(list) => { - return handle_file_array(global, list.items()); + generated::SSLConfigFile::Array(list) => { + return handle_file_array(global, list); } }, )?; @@ -418,7 +418,7 @@ fn handle_file( fn handle_file_array( global: &JSGlobalObject, - elements: &[jsc::generated::SSLConfigSingleFile], + elements: &[generated::SSLConfigSingleFile], ) -> Result { if elements.is_empty() { return Ok(None); @@ -436,18 +436,14 @@ fn handle_file_array( guard.push(handle_single_file( global, match elem { - jsc::generated::SSLConfigSingleFile::String(val) => { - SingleFile::String(val.as_ref()) - } - jsc::generated::SSLConfigSingleFile::Buffer(val) => { - // SAFETY: see `handle_file` above — non-null GenVal pointers - // valid for the lifetime of `generated`. + generated::SSLConfigSingleFile::String(val) => SingleFile::String(val), + generated::SSLConfigSingleFile::Buffer(val) => { + // SAFETY: see `handle_file`. SingleFile::Buffer(unsafe { &mut *val.get() }) } - jsc::generated::SSLConfigSingleFile::File(val) => { - // SAFETY: opaque `GenBlob` (`*mut c_void`) is layout-identical - // to `crate::webcore::Blob`. - SingleFile::File(unsafe { &mut *val.get().cast::() }) + generated::SSLConfigSingleFile::File(val) => { + // SAFETY: see `handle_file`. + SingleFile::File(unsafe { &mut *val.get() }) } }, )?); diff --git a/src/runtime/socket/SocketConfig.bind.ts b/src/runtime/socket/SocketConfig.bind.ts new file mode 100644 index 000000000000..135ee2b388f2 --- /dev/null +++ b/src/runtime/socket/SocketConfig.bind.ts @@ -0,0 +1,92 @@ +import * as b from "../../codegen/bindgen-lib.ts"; +import { SSLConfig } from "./SSLConfig.bind.ts"; + +export const SocketConfigBinaryType = b.enumeration("SocketConfigBinaryType", [ + ["arraybuffer", "ArrayBuffer"], + ["buffer", "Buffer"], + ["uint8array", "Uint8Array"], +]); + +export const SocketConfigHandlers = b.dictionary( + { + name: "SocketConfigHandlers", + userFacingName: "SocketHandler", + fromJS: true, + }, + { + open: { type: b.any, internalName: "onOpen" }, + close: { type: b.any, internalName: "onClose" }, + error: { type: b.any, internalName: "onError" }, + data: { type: b.any, internalName: "onData" }, + drain: { type: b.any, internalName: "onWritable" }, + handshake: { type: b.any, internalName: "onHandshake" }, + end: { type: b.any, internalName: "onEnd" }, + connectError: { type: b.any, internalName: "onConnectError" }, + timeout: { type: b.any, internalName: "onTimeout" }, + session: { type: b.any, internalName: "onSession" }, + keylog: { type: b.any, internalName: "onKeylog" }, + serverName: { type: b.any, internalName: "onServerName" }, + alpnCallback: { type: b.any, internalName: "onALPNCallback" }, + binaryType: { + type: SocketConfigBinaryType, + default: "buffer", + internalName: "binary_type", + }, + }, +); + +export const SocketConfigTLS = b.union("SocketConfigTLS", { + none: b.null, + boolean: b.bool, + object: SSLConfig, +}); + +export const SocketConfig = b.dictionary( + { + name: "SocketConfig", + userFacingName: "SocketOptions", + fromJS: true, + }, + { + socket: { + type: SocketConfigHandlers, + internalName: "handlers", + }, + data: b.any, + allowHalfOpen: { + type: b.bool, + default: false, + internalName: "allow_half_open", + }, + hostname: { + type: b.string.loose.nullable.loose, + altNames: ["host"], + }, + port: b.u16.loose.nullable, + tls: SocketConfigTLS, + exclusive: { + type: b.bool, + default: false, + }, + reusePort: { + type: b.bool, + default: false, + internalName: "reuse_port", + }, + ipv6Only: { + type: b.bool, + default: false, + internalName: "ipv6_only", + }, + pauseOnConnect: { + type: b.bool.loose, + default: false, + internalName: "pause_on_connect", + }, + unix: { + type: b.string.nullable.loose, + internalName: "unix_", // `unix` is a predefined C macro... + }, + fd: b.i32.optional, + }, +); diff --git a/src/runtime/socket/SocketConfig.bindv2.ts b/src/runtime/socket/SocketConfig.bindv2.ts deleted file mode 100644 index 56da03e1501c..000000000000 --- a/src/runtime/socket/SocketConfig.bindv2.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as b from "../../codegen/bindgenv2/lib.ts"; -import { SSLConfig } from "./SSLConfig.bindv2.ts"; - -export const BinaryType = b.enumeration("SocketConfigBinaryType", [ - ["arraybuffer", "ArrayBuffer"], - ["buffer", "Buffer"], - ["uint8array", "Uint8Array"], -]); - -export const Handlers = b.dictionary( - { - name: "SocketConfigHandlers", - userFacingName: "SocketHandler", - generateConversionFunction: true, - }, - { - open: { type: b.RawAny, internalName: "onOpen" }, - close: { type: b.RawAny, internalName: "onClose" }, - error: { type: b.RawAny, internalName: "onError" }, - data: { type: b.RawAny, internalName: "onData" }, - drain: { type: b.RawAny, internalName: "onWritable" }, - handshake: { type: b.RawAny, internalName: "onHandshake" }, - end: { type: b.RawAny, internalName: "onEnd" }, - connectError: { type: b.RawAny, internalName: "onConnectError" }, - timeout: { type: b.RawAny, internalName: "onTimeout" }, - session: { type: b.RawAny, internalName: "onSession" }, - keylog: { type: b.RawAny, internalName: "onKeylog" }, - serverName: { type: b.RawAny, internalName: "onServerName" }, - alpnCallback: { type: b.RawAny, internalName: "onALPNCallback" }, - binaryType: { - type: BinaryType, - default: "buffer", - internalName: "binary_type", - }, - }, -); - -export const TLS = b.union("SocketConfigTLS", { - none: b.null, - boolean: b.bool, - object: SSLConfig, -}); - -export const SocketConfig = b.dictionary( - { - name: "SocketConfig", - userFacingName: "SocketOptions", - generateConversionFunction: true, - }, - { - socket: { - type: Handlers, - internalName: "handlers", - }, - data: b.RawAny, - allowHalfOpen: { - type: b.bool, - default: false, - internalName: "allow_half_open", - }, - hostname: { - type: b.String.loose.nullable.loose, - altNames: ["host"], - }, - port: b.u16.loose.nullable, - tls: TLS, - exclusive: { - type: b.bool, - default: false, - }, - reusePort: { - type: b.bool, - default: false, - internalName: "reuse_port", - }, - ipv6Only: { - type: b.bool, - default: false, - internalName: "ipv6_only", - }, - pauseOnConnect: { - type: b.bool.loose, - default: false, - internalName: "pause_on_connect", - }, - unix: { - type: b.String.nullable.loose, - internalName: "unix_", // `unix` is a predefined C macro... - }, - fd: b.i32.optional, - }, -); diff --git a/src/runtime/test_runner/timers/FakeTimersConfig.bindv2.ts b/src/runtime/test_runner/timers/FakeTimersConfig.bindv2.ts deleted file mode 100644 index 7fadd8b35ee9..000000000000 --- a/src/runtime/test_runner/timers/FakeTimersConfig.bindv2.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as b from "../../../codegen/bindgenv2/lib.ts"; - -export const FakeTimersConfig = b.dictionary( - { - name: "FakeTimersConfig", - userFacingName: "FakeTimersOptions", - generateConversionFunction: true, - }, - { - now: { - type: b.RawAny, - internalName: "now", - }, - }, -); diff --git a/src/tsconfig.json b/src/tsconfig.json index 4d2b492972f9..0fa424a7e570 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,10 +5,7 @@ "ignoreDeprecations": "6.0", "baseUrl": ".", // bun-types comes in through `include` below, by path: see src/js/tsconfig.json. - "types": [], - "paths": { - "bindgen": ["./codegen/bindgen-lib.ts"] - } + "types": [] }, "include": ["**/*.ts", "**/*.tsx", "../packages/bun-types/index.d.ts"], // js and runtime/bake are projects of their own, with settings that only apply there. The `bun init` and diff --git a/test/internal/bindgen-compile-fail.test.ts b/test/internal/bindgen-compile-fail.test.ts new file mode 100644 index 000000000000..8c243706676d --- /dev/null +++ b/test/internal/bindgen-compile-fail.test.ts @@ -0,0 +1,96 @@ +// `bun_jsc::bindgen` makes rustc enforce how native code may touch the JS heap. Each file in +// bindgen-compile-fail/fail is a mistake it must refuse, for the reason on the file's first line and no +// other; each file in bindgen-compile-fail/pass is the right way to do the same work. +// +// The cases are type-checked against the metadata `cargo check` leaves behind; nothing is linked. +import { which } from "bun"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +const cargo = which("cargo"); +const rustc = which("rustc"); +const repoRoot = join(import.meta.dir, "..", ".."); +const cases = join(import.meta.dir, "bindgen-compile-fail"); +const codegenDir = join(repoRoot, "build", "debug", "codegen"); +// Test-only CI lanes run a prebuilt binary: no toolchain, no vendored crates, no generated code. +const canCheck = + !!cargo && + !!rustc && + existsSync(join(repoRoot, "vendor", "lolhtml", "Cargo.toml")) && + existsSync(join(codegenDir, "generated_bindgen_bun_jsc.rs")); + +const env = { ...process.env, CARGO_TERM_COLOR: "never", BUN_CODEGEN_DIR: codegenDir }; + +describe.skipIf(!canCheck)("bun_jsc::bindgen", () => { + let metadata = ""; + const searchPaths = new Set(); + const prelude = readFileSync(join(cases, "prelude.rs"), "utf8"); + + beforeAll(async () => { + await using proc = Bun.spawn({ + cmd: [cargo!, "check", "--locked", "-p", "bun_jsc", "--message-format=json"], + cwd: repoRoot, + env, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stderr, exitCode }).toMatchObject({ exitCode: 0 }); + for (const line of stdout.split("\n")) { + if (!line.startsWith("{")) continue; + const message = JSON.parse(line); + if (message.reason !== "compiler-artifact") continue; + for (const name of message.filenames as string[]) searchPaths.add(dirname(name)); + if (message.target.name === "bun_jsc" && message.target.kind.includes("lib")) + metadata = message.filenames.find((name: string) => name.endsWith(".rmeta")); + } + expect(metadata).toEndWith(".rmeta"); + }, 600_000); + + /** The distinct errors rustc reports for the case: a code, or "lifetime" for the one that has none. */ + async function check(kind: string, file: string): Promise<{ codes: string[]; stderr: string }> { + using dir = tempDir("bindgen-compile-fail", { [file]: prelude + readFileSync(join(cases, kind, file), "utf8") }); + const source = join(String(dir), file); + await using proc = Bun.spawn({ + cmd: [ + rustc!, + "--edition=2024", + "--crate-type=bin", + "--emit=metadata", + "--error-format=short", + `--out-dir=${String(dir)}`, + "-Cpanic=abort", + ...Array.from(searchPaths, path => `-Ldependency=${path}`), + "--extern", + `bun_jsc=${metadata}`, + source, + ], + cwd: repoRoot, + env, + stdout: "ignore", + stderr: "pipe", + }); + const [stderr] = await Promise.all([proc.stderr.text(), proc.exited]); + const errors = stderr + .split("\n") + .filter(line => /\berror\b/.test(line) && !line.startsWith("error: aborting due to")); + const code = (line: string) => + /error\[(E\d+)\]/.exec(line)?.[1] ?? (line.includes("lifetime may not live long enough") ? "lifetime" : line); + return { codes: [...new Set(errors.map(code))], stderr }; + } + + describe("pass", () => { + test.concurrent.each(readdirSync(join(cases, "pass")))("%s", async file => { + expect(await check("pass", file)).toMatchObject({ codes: [] }); + }); + }); + + describe("fail", () => { + test.concurrent.each(readdirSync(join(cases, "fail")))("%s", async file => { + const expected = /^\/\/ (E\d+|lifetime): /.exec(readFileSync(join(cases, "fail", file), "utf8"))![1]; + expect(await check("fail", file)).toMatchObject({ codes: [expected] }); + }); + }); +}); diff --git a/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs b/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs new file mode 100644 index 000000000000..493c342bb454 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs @@ -0,0 +1,7 @@ +// E0500: ArrayBuffer bytes in use while JavaScript runs (it can detach, resize or transfer them). +fn f<'a>(cx: &mut JsCall<'a>, buffer: JsValue<'a, ArrayBuffer>, callback: JsValue<'a, JSFunction>) { + buffer.with_bytes(&cx.heap, |bytes| { + let _ = callback.call(cx, JsValue::UNDEFINED, &[]); + bytes[0] + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/bytes_escape.rs b/test/internal/bindgen-compile-fail/fail/bytes_escape.rs new file mode 100644 index 000000000000..f95c1b67f4b2 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/bytes_escape.rs @@ -0,0 +1,6 @@ +// E0521: ArrayBuffer bytes carried out of the closure that bounds them. +fn f<'a>(cx: &mut JsCall<'a>, buffer: JsValue<'a, ArrayBuffer>) -> u8 { + let mut escaped: &[u8] = &[]; + buffer.with_bytes(&cx.heap, |bytes| escaped = bytes); + escaped[0] +} diff --git a/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs b/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs new file mode 100644 index 000000000000..d6584a87deb0 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs @@ -0,0 +1,5 @@ +// E0521: a pending exception carried out of the call it belongs to. +fn f(global: *mut JSGlobalObject) { + let mut escaped = None; + unsafe { JsCall::with(global, |cx| escaped = Some(cx.heap.throw_out_of_memory())) }; +} diff --git a/test/internal/bindgen-compile-fail/fail/error_to_thread.rs b/test/internal/bindgen-compile-fail/fail/error_to_thread.rs new file mode 100644 index 000000000000..93a80b7479d4 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/error_to_thread.rs @@ -0,0 +1,4 @@ +// E0277: a pending exception sent to another thread. +fn f(error: bindgen::Error<'static>) { + std::thread::spawn(move || drop(error)); +} diff --git a/test/internal/bindgen-compile-fail/fail/forge_capability.rs b/test/internal/bindgen-compile-fail/fail/forge_capability.rs new file mode 100644 index 000000000000..b714b58b40d8 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/forge_capability.rs @@ -0,0 +1,4 @@ +// E0423: making a capability out of nothing. +fn f<'a>() -> JsHeap<'a> { + JsHeap(Default::default(), Default::default(), Default::default()) +} diff --git a/test/internal/bindgen-compile-fail/fail/forge_error.rs b/test/internal/bindgen-compile-fail/fail/forge_error.rs new file mode 100644 index 000000000000..dbf81977ae93 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/forge_error.rs @@ -0,0 +1,4 @@ +// E0423: an `Err` that says something was thrown when nothing was. +fn f<'a>() -> bindgen::Result<'a, ()> { + Err(bindgen::Error(Default::default(), Default::default(), Default::default())) +} diff --git a/test/internal/bindgen-compile-fail/fail/forge_value.rs b/test/internal/bindgen-compile-fail/fail/forge_value.rs new file mode 100644 index 000000000000..9261218d5d71 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/forge_value.rs @@ -0,0 +1,4 @@ +// E0423: a value made without a capability. +fn f<'a>() -> JsValue<'a> { + JsValue(JSValue::UNDEFINED, Default::default(), Default::default()) +} diff --git a/test/internal/bindgen-compile-fail/fail/global_across_payload.rs b/test/internal/bindgen-compile-fail/fail/global_across_payload.rs new file mode 100644 index 000000000000..9a8718ac64d3 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/global_across_payload.rs @@ -0,0 +1,7 @@ +// E0502: a `&JSGlobalObject`, with which anything can run, held while a wrapper's payload is borrowed. +fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { + let global = cx.global(); + this.with(&cx.payloads, |socket| { + let _ = (global, socket); + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs b/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs new file mode 100644 index 000000000000..a221845be49c --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs @@ -0,0 +1,4 @@ +// E0308: running JavaScript with the capability to allocate only. +fn f<'a>(heap: &JsHeap<'a>, callback: JsValue<'a, JSFunction>) { + let _ = callback.call(heap, JsValue::UNDEFINED, &[]); +} diff --git a/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs new file mode 100644 index 000000000000..d41a0392b285 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs @@ -0,0 +1,4 @@ +// E0133: reading an internal field by number; only the generated accessors know how many there are. +fn f<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>) -> JsValue<'a> { + this.internal_field(heap, 7) +} diff --git a/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs b/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs new file mode 100644 index 000000000000..98979c2afbc9 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs @@ -0,0 +1,6 @@ +// E0609: a `passThis` method with `&mut self` aliasing itself. `JsHeap` leads to no `JsPayloads`. +impl Socket { + fn f<'a>(&mut self, this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>) { + self.bytes_written = this.with(&heap.payloads, |socket| socket.bytes_written); + } +} diff --git a/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs b/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs new file mode 100644 index 000000000000..c2b46a64b6e6 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs @@ -0,0 +1,7 @@ +// E0500: `&mut T` into a wrapper's payload in use while JavaScript re-enters. +fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>, callback: JsValue<'a, JSFunction>) { + this.with_mut(&mut cx.payloads, |socket| { + let _ = callback.call(cx, JsValue::UNDEFINED, &[]); + socket.bytes_written += 1; + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs new file mode 100644 index 000000000000..613615cd5832 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs @@ -0,0 +1,7 @@ +// E0500: `&T` into a wrapper's payload in use while JavaScript re-enters and may mutate it. +fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>, callback: JsValue<'a, JSFunction>) { + this.with(&cx.payloads, |socket| { + let _ = callback.call(cx, JsValue::UNDEFINED, &[]); + socket.bytes_written + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs new file mode 100644 index 000000000000..7f6c759aa1ef --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs @@ -0,0 +1,6 @@ +// E0500: `&T` and `&mut T`; the two wrappers may be the same object. +fn f<'a>(a: JsWrapper<'a, Socket>, b: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { + a.with(&cx.payloads, |a| { + b.with_mut(&mut cx.payloads, |b| b.bytes_written = a.bytes_written); + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs new file mode 100644 index 000000000000..852591aed17f --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs @@ -0,0 +1,6 @@ +// E0521: a reference into a wrapper's payload carried out of the closure that bounds it. +fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) -> usize { + let mut escaped: &[u8] = &[]; + this.with(&cx.payloads, |socket| escaped = &socket.pending); + escaped.len() +} diff --git a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs new file mode 100644 index 000000000000..83d7dcfb10f3 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs @@ -0,0 +1,12 @@ +// E0200: claiming a cell layout; only generated code knows it. +struct Fake; +impl JsPayload for Fake { + const INTERNAL_FIELDS_OFFSET: usize = 0; + const PAYLOAD_OFFSET: usize = 0; + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unreachable!() + } + fn cell_from_js(_: JSValue) -> *mut u8 { + unreachable!() + } +} diff --git a/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs b/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs new file mode 100644 index 000000000000..3d025f238fa2 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs @@ -0,0 +1,4 @@ +// E0308: running JavaScript through a shared reference to the capability. +fn f<'a>(cx: &JsCall<'a>, callback: JsValue<'a, JSFunction>) { + let _ = callback.call(cx, JsValue::UNDEFINED, &[]); +} diff --git a/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs b/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs new file mode 100644 index 000000000000..1980c35c8084 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs @@ -0,0 +1,9 @@ +// lifetime: the UTF-8 view of a string argument stored past the call. +struct Route { + path: &'static [u8], +} +impl Route { + fn configure<'a>(&mut self, path: &'a [u8]) { + self.path = path; + } +} diff --git a/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs b/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs new file mode 100644 index 000000000000..4c2309ef8ad0 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs @@ -0,0 +1,4 @@ +// E0277: a wrapper's `Strong` sent to another thread; it is made, read and dropped on the JS thread. +fn f(strong: JsWrapperStrong) { + std::thread::spawn(move || drop(strong)); +} diff --git a/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs b/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs new file mode 100644 index 000000000000..c3bc03e0e605 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs @@ -0,0 +1,6 @@ +// E0500: two `&mut T`; the two wrappers may be the same object. +fn f<'a>(a: JsWrapper<'a, Socket>, b: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { + a.with_mut(&mut cx.payloads, |a| { + b.with_mut(&mut cx.payloads, |b| core::mem::swap(a, b)); + }); +} diff --git a/test/internal/bindgen-compile-fail/fail/value_between_calls.rs b/test/internal/bindgen-compile-fail/fail/value_between_calls.rs new file mode 100644 index 000000000000..c82fba9d57de --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/value_between_calls.rs @@ -0,0 +1,9 @@ +// E0521: a value of one call used in another. +fn f(global: *mut JSGlobalObject) { + unsafe { + JsCall::with(global, |outer| { + let error = outer.heap.throw_value(JsValue::NULL); + JsCall::with(global, |inner| drop(inner.take_exception(error))); + }) + }; +} diff --git a/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs b/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs new file mode 100644 index 000000000000..e1f2b9f0089d --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs @@ -0,0 +1,5 @@ +// E0521: a value carried out of the call it belongs to. +fn f(global: *mut JSGlobalObject) { + let mut escaped = None; + unsafe { JsCall::with(global, |cx| escaped = cx.heap.create_utf8_string(b"text").ok()) }; +} diff --git a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs new file mode 100644 index 000000000000..3d35c6b65adf --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs @@ -0,0 +1,14 @@ +// E0478: a payload that holds a value the GC would not see. Values go in the class's `values`. +struct Holder<'a> { + callback: JsValue<'a>, +} +unsafe impl<'a> JsPayload for Holder<'a> { + const INTERNAL_FIELDS_OFFSET: usize = 24; + const PAYLOAD_OFFSET: usize = 24; + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unreachable!() + } + fn cell_from_js(_: JSValue) -> *mut u8 { + unreachable!() + } +} diff --git a/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs b/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs new file mode 100644 index 000000000000..de2592a5f879 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs @@ -0,0 +1,5 @@ +// lifetime: a value captured by a task that runs later. A `Strong` is what outlives the call. +fn queue(_: Box) {} +fn f<'a>(cx: &mut JsCall<'a>, value: JsValue<'a>) { + queue(Box::new(move || drop(value))); +} diff --git a/test/internal/bindgen-compile-fail/fail/value_to_thread.rs b/test/internal/bindgen-compile-fail/fail/value_to_thread.rs new file mode 100644 index 000000000000..4e3f2308edc9 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/value_to_thread.rs @@ -0,0 +1,4 @@ +// E0277: a value sent to another thread. +fn f(value: JsValue<'static>) { + std::thread::spawn(move || drop(value)); +} diff --git a/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs b/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs new file mode 100644 index 000000000000..91c0149417c5 --- /dev/null +++ b/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs @@ -0,0 +1,9 @@ +// lifetime: of a `withValue` argument, the converted form stored instead of the value. +struct Route { + path: &'static [u8], +} +impl Route { + fn configure<'a>(&mut self, path: WithValue<'a, &'a [u8]>) { + self.path = path.converted; + } +} diff --git a/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs b/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs new file mode 100644 index 000000000000..ce31d78ee53f --- /dev/null +++ b/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs @@ -0,0 +1,16 @@ +// What outlives a call is a `Strong`; a capability turns it back into a value. +struct Pending { + socket: JsWrapperStrong, + callback: Strong, +} +fn start<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<'a, JSFunction>) -> Pending { + Pending { socket: this.to_strong(heap), callback: callback.to_strong(heap) } +} +fn finish<'a>(pending: Pending, cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { + let socket = pending.socket.get(&cx.heap); + socket.with_mut(&mut cx.payloads, |socket| socket.pending.clear()); + let Some(callback) = JsValue::from_strong(&cx.heap, &pending.callback).as_function() else { + return Ok(JsValue::UNDEFINED); + }; + callback.call(cx, socket.as_value(), &[]) +} diff --git a/test/internal/bindgen-compile-fail/pass/lists_of_values.rs b/test/internal/bindgen-compile-fail/pass/lists_of_values.rs new file mode 100644 index 000000000000..36732d145777 --- /dev/null +++ b/test/internal/bindgen-compile-fail/pass/lists_of_values.rs @@ -0,0 +1,12 @@ +// A list of values the GC can see. +fn pair<'a>(heap: &JsHeap<'a>, a: JsValue<'a>, b: JsValue<'a>) -> bindgen::Result<'a, JsValue<'a>> { + to_js::array(heap, [a, b], to_js::any) +} +fn collect<'a>(heap: &JsHeap<'a>, names: &[&[u8]]) -> bindgen::Result<'a, ()> { + MarkedArgumentBuffer::new(|values| { + for name in names { + values.append(heap.create_utf8_string(name)?.to_js()); + } + Ok(()) + }) +} diff --git a/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs b/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs new file mode 100644 index 000000000000..4e34399cee6c --- /dev/null +++ b/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs @@ -0,0 +1,24 @@ +// Methods that cannot run JavaScript get plain references. +impl Socket { + fn bytes_written(&self) -> u64 { + self.bytes_written + } + fn write<'a>(&mut self, data: &'a [u8]) -> usize { + self.pending.extend_from_slice(data); + data.len() + } + fn pending<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a, JSString>> { + heap.create_utf8_string(&self.pending) + } + fn check<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { + if self.pending.is_empty() { + return Err(heap.throw_type_error(format_args!("nothing is pending"))); + } + Ok(()) + } + // `passThis`: the wrapper reaches the class's `values`, not the payload. + fn listen<'a>(&mut self, this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<'a>) { + self.bytes_written = 0; + this.ondata_set_cached(heap, callback); + } +} diff --git a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs new file mode 100644 index 000000000000..5d5a928505af --- /dev/null +++ b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs @@ -0,0 +1,23 @@ +// A method that can run JavaScript: the wrapper, its payload borrowed for as long as nothing else can run. +fn flush<'a>( + this: JsWrapper<'a, Socket>, + cx: &mut JsCall<'a>, + extra: JsValue<'a, ArrayBuffer>, +) -> bindgen::Result<'a, JsValue<'a>> { + // Payload borrowed mutably and bytes viewed at once: disjoint fields of `cx`. + extra.with_bytes(&cx.heap, |bytes| { + this.with_mut(&mut cx.payloads, |socket| socket.pending.extend_from_slice(bytes)) + }); + let chunk = this.with(&cx.payloads, |socket| cx.heap.create_utf8_string(&socket.pending))?; + let Some(callback) = this.ondata_get_cached(&cx.heap).as_function() else { + return Ok(JsValue::UNDEFINED); + }; + let result = callback.call(cx, this.as_value(), &[chunk.as_value()])?; + // Afterwards everything is looked up again. A `JsValue` stays usable: the stack is scanned. + let written = extra.with_bytes(&cx.heap, <[u8]>::len).unwrap_or(0); + this.with_mut(&mut cx.payloads, |socket| { + socket.pending.clear(); + socket.bytes_written += written as u64; + }); + Ok(result) +} diff --git a/test/internal/bindgen-compile-fail/pass/with_value.rs b/test/internal/bindgen-compile-fail/pass/with_value.rs new file mode 100644 index 000000000000..82b9df1374ed --- /dev/null +++ b/test/internal/bindgen-compile-fail/pass/with_value.rs @@ -0,0 +1,18 @@ +// The converted form for now, the value itself for whatever holds it longer. +fn to_a_class_value<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> usize { + this.ondata_set_cached(heap, name.value); + name.converted.len() +} +fn to_a_strong<'a>(heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> (Vec, Strong) { + (name.converted.to_vec(), name.value.to_strong(heap)) +} +fn to_a_call<'a>( + cx: &mut JsCall<'a>, + callback: JsValue<'a, JSFunction>, + name: WithValue<'a, &'a [u8]>, +) -> bindgen::Result<'a, JsValue<'a>> { + if name.converted.is_empty() { + return Ok(name.value); + } + callback.call(cx, JsValue::UNDEFINED, &[name.value]) +} diff --git a/test/internal/bindgen-compile-fail/prelude.rs b/test/internal/bindgen-compile-fail/prelude.rs new file mode 100644 index 000000000000..b1d4c97557af --- /dev/null +++ b/test/internal/bindgen-compile-fail/prelude.rs @@ -0,0 +1,32 @@ +// Shared by every case: a class as its author writes it, and what the generator adds. +#![allow(unused)] +use bun_jsc::bindgen::{self, *}; +use bun_jsc::{ArrayBuffer, JSFunction, JSGlobalObject, JSString, JSValue, MarkedArgumentBuffer, Strong}; + +pub struct Socket { + pub pending: Vec, + pub bytes_written: u64, +} +unsafe impl JsPayload for Socket { + const INTERNAL_FIELDS_OFFSET: usize = 24; + const PAYLOAD_OFFSET: usize = 32; + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unreachable!() + } + fn cell_from_js(_: JSValue) -> *mut u8 { + unreachable!() + } +} +pub trait SocketValues<'a>: Copy { + fn ondata_get_cached(self, heap: &JsHeap<'a>) -> JsValue<'a>; + fn ondata_set_cached(self, heap: &JsHeap<'a>, value: JsValue<'a, T>); +} +impl<'a> SocketValues<'a> for JsWrapper<'a, Socket> { + fn ondata_get_cached(self, heap: &JsHeap<'a>) -> JsValue<'a> { + unsafe { self.internal_field(heap, 0) } + } + fn ondata_set_cached(self, heap: &JsHeap<'a>, value: JsValue<'a, T>) { + unsafe { self.set_internal_field(heap, 0, value) } + } +} +fn main() {} diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 4beefdead8f8..f7f06ace8c98 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -86,3 +86,606 @@ it("unsigned integer dictionary fields reject negative values", () => { expect(caught.message).toBe(`TLSOptions.${field} must be in the range [0, 4294967295] (received -1)`); } }); + +describe("strings", () => { + const rope = (a: string, b: string) => a + b + a; // long enough that JSC keeps it a rope + const texts = [ + "", + "ascii", + "latin1 éÿ", + "utf16 你好 \u{1f600}", + rope("a".repeat(40), "b".repeat(40)), + rope("é".repeat(40), "你".repeat(40)), + "x".repeat(1000), + "é".repeat(1000), + "nul \0 inside", + ]; + + it.each(texts)("round trips %j", text => { + expect(bindgen.utf8Echo(text)).toBe(text); + expect(bindgen.utf8EchoLoose(text)).toBe(text); + expect(bindgen.utf8Optional(text)).toBe(text); + expect(bindgen.stringEcho(text)).toBe(text); + }); + + it("replaces an unpaired surrogate", () => { + expect(bindgen.utf8Echo("a\ud800b")).toBe("a�b"); + expect(bindgen.utf8Echo("\udc00")).toBe("�"); + }); + + it("strict takes strings and String objects only", () => { + expect(bindgen.utf8Echo(new String("boxed"))).toBe("boxed"); + class Path extends String {} + expect(bindgen.utf8Echo(new Path("derived"))).toBe("derived"); + for (const value of [1, true, null, undefined, {}, [], Symbol("s"), { toString: () => "no" }]) { + expect(() => bindgen.utf8Echo(value)).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", message: "text must be a string" }), + ); + } + }); + + it("loose is ToString", () => { + expect(bindgen.utf8EchoLoose(12)).toBe("12"); + expect(bindgen.utf8EchoLoose(null)).toBe("null"); + expect(bindgen.utf8EchoLoose({ toString: () => "custom" })).toBe("custom"); + expect(() => bindgen.utf8EchoLoose(Symbol("s"))).toThrow(TypeError); + expect(() => + bindgen.utf8EchoLoose({ + toString() { + throw new RangeError("from toString"); + }, + }), + ).toThrow(new RangeError("from toString")); + }); + + it("tells an absent string from an empty one", () => { + expect(bindgen.utf8Optional(null)).toBe(null); + expect(bindgen.utf8Optional(undefined)).toBe(null); + expect(bindgen.utf8Optional()).toBe(null); + expect(bindgen.utf8Optional("")).toBe(""); + }); + + it("checks the argument count", () => { + expect(() => bindgen.utf8Echo()).toThrow("Not enough arguments"); + }); +}); + +describe("enumerations", () => { + it("parses and returns the canonical spelling", () => { + expect(bindgen.nextColor("red")).toBe("green"); + expect(bindgen.nextColor("green")).toBe("blue"); + expect(bindgen.nextColor("lime")).toBe("blue"); + expect(bindgen.nextColor("blue")).toBe("red"); + }); + + it("rejects anything else", () => { + for (const value of ["Red", "", "purple", 0, null, undefined, {}]) { + expect(() => bindgen.nextColor(value)).toThrow(TypeError); + } + }); +}); + +describe("dictionaries", () => { + const defaults = { count: 1, flag: false, color: "red", ratio: null, payload: undefined }; + + it("fills in defaults and leaves out what is absent", () => { + expect(bindgen.optionsEcho({})).toStrictEqual(defaults); + expect(Object.keys(bindgen.optionsEcho({}))).toEqual(["count", "flag", "color", "ratio", "payload"]); + }); + + it("reads every member", () => { + const payload = { any: "value" }; + const seen = bindgen.optionsEcho({ + count: 7, + flag: "truthy", + name: "né", + color: "lime", + ratio: 0.5, + payload, + renamed: 9, + }); + expect(seen).toStrictEqual({ + count: 7, + flag: true, + name: "né", + color: "green", + ratio: 0.5, + payload, + renamed: 9, + }); + expect(seen.payload).toBe(payload); + expect(Object.keys(seen)).toEqual(["count", "flag", "name", "color", "ratio", "payload", "renamed"]); + }); + + it("returns ordinary objects", () => { + const seen = bindgen.optionsEcho({ name: "a", renamed: 1 }); + expect(Object.getPrototypeOf(seen)).toBe(Object.prototype); + expect(Object.getOwnPropertyDescriptor(seen, "count")).toEqual({ + value: 1, + writable: true, + enumerable: true, + configurable: true, + }); + seen.count = 2; + seen.extra = 3; + delete seen.flag; + expect(bindgen.optionsEcho({ name: "a", renamed: 1 })).toStrictEqual({ ...defaults, name: "a", renamed: 1 }); + }); + + it("falls back to the other spelling of a member", () => { + expect(bindgen.optionsEcho({ oldName: 4 }).renamed).toBe(4); + expect(bindgen.optionsEcho({ renamed: 5, oldName: 4 }).renamed).toBe(5); + expect(bindgen.optionsEcho({ renamed: undefined, oldName: 4 }).renamed).toBe(4); + }); + + it("names the member in an error", () => { + expect(() => bindgen.optionsEcho({ count: "1" })).toThrow("TestOptions.count must be"); + expect(() => bindgen.optionsEcho({ name: 1 })).toThrow("TestOptions.name must be a string"); + expect(() => bindgen.optionsEcho({ color: "mauve" })).toThrow("TestOptions.color must be"); + for (const value of [1, "s", null, undefined, true, [], Symbol("s")]) { + expect(() => bindgen.optionsEcho(value)).toThrow("TestOptions must be an object"); + } + }); + + // The direct read of a plain object and the generic lookup must be indistinguishable. + describe("every kind of object gives the same answer", () => { + const members = { count: 3, flag: true, name: "x", color: "blue", ratio: 2, payload: "p", renamed: 8 }; + const expected = { ...members }; + const shapes: Record object> = { + "literal": () => ({ ...members }), + "other order": () => Object.fromEntries(Object.entries(members).reverse()), + "null prototype": () => Object.assign(Object.create(null), members), + "extra members": () => ({ a: 1, ...members, z: 2, [Symbol("s")]: 3, 0: 4 }), + "frozen": () => Object.freeze({ ...members }), + "sealed": () => Object.seal({ ...members }), + "non-enumerable": () => + Object.defineProperties( + {}, + Object.fromEntries(Object.entries(members).map(([key, value]) => [key, { value, enumerable: false }])), + ), + "dictionary mode": () => { + const object: Record = {}; + for (let i = 0; i < 200; i++) object["k" + i] = i; + Object.assign(object, members); + for (let i = 0; i < 200; i++) delete object["k" + i]; + return object; + }, + "getters": () => + Object.defineProperties( + {}, + Object.fromEntries(Object.entries(members).map(([key, value]) => [key, { get: () => value }])), + ), + "one getter": () => Object.defineProperty({ ...members }, "ratio", { get: () => 2 }), + "class instance": () => Object.assign(new (class Options {})(), members), + "class getters": () => + new (class Options { + get count() { + return 3; + } + get flag() { + return true; + } + get name() { + return "x"; + } + get color() { + return "blue"; + } + get ratio() { + return 2; + } + get payload() { + return "p"; + } + get renamed() { + return 8; + } + })(), + "inherited": () => Object.create({ ...members }), + "half inherited": () => + Object.assign(Object.create({ count: 3, flag: true, name: "x" }), { + color: "blue", + ratio: 2, + payload: "p", + renamed: 8, + }), + "proxy": () => new Proxy({ ...members }, {}), + "proxy with a get trap": () => new Proxy({}, { get: (_, key) => (members as any)[key] }), + "function": () => { + const { name, ...rest } = members; + return Object.assign({ x() {} }.x, rest); + }, + "map with members": () => Object.assign(new Map(), members), + "error with members": () => Object.assign(new Error("e"), members), + }; + for (const [title, make] of Object.entries(shapes)) { + it(title, () => { + expect(bindgen.optionsEcho(make())).toStrictEqual(expected); + }); + } + }); + + it("does not see Object.prototype", () => { + const prototype = Object.prototype as any; + try { + prototype.count = 99; + prototype.name = "polluted"; + Object.defineProperty(prototype, "ratio", { get: () => 99, configurable: true }); + expect(bindgen.optionsEcho({})).toStrictEqual(defaults); + expect(bindgen.optionsEcho(new (class Options {})())).toStrictEqual(defaults); + expect(bindgen.optionsEcho(Object.create({}))).toStrictEqual(defaults); + // A proxy answers for itself: its target's lookup is not ours to cut short. + expect(bindgen.optionsEcho(new Proxy({}, {}))).toMatchObject({ count: 99, name: "polluted", ratio: 99 }); + } finally { + delete prototype.count; + delete prototype.name; + delete prototype.ratio; + } + }); + + it("sees a change made after an earlier read", () => { + const options: Record = { count: 1, name: "a" }; + expect(bindgen.optionsEcho(options)).toMatchObject({ count: 1, name: "a" }); + options.count = 2; + expect(bindgen.optionsEcho(options)).toMatchObject({ count: 2, name: "a" }); + delete options.name; + expect(bindgen.optionsEcho(options)).toStrictEqual({ ...defaults, count: 2 }); + options.name = "b"; + expect(bindgen.optionsEcho(options)).toMatchObject({ count: 2, name: "b" }); + Object.defineProperty(options, "count", { get: () => 3 }); + expect(bindgen.optionsEcho(options)).toMatchObject({ count: 3, name: "b" }); + Object.setPrototypeOf(options, { renamed: 6 }); + expect(bindgen.optionsEcho(options)).toMatchObject({ count: 3, name: "b", renamed: 6 }); + }); + + it("reads and converts members in declaration order, each once", () => { + const log: string[] = []; + const logged = (key: string, value: unknown) => ({ + get() { + log.push(key); + return value; + }, + enumerable: true, + }); + const flag = { + valueOf() { + log.push("never: ToBoolean calls nothing"); + }, + }; + bindgen.optionsEcho( + Object.defineProperties( + {}, + { + renamed: logged("renamed", undefined), + oldName: logged("oldName", 1), + payload: logged("payload", 1), + ratio: logged("ratio", 1), + color: logged("color", "red"), + name: logged("name", "n"), + flag: logged("flag", flag), + count: logged("count", 1), + }, + ), + ); + expect(log).toEqual(["count", "flag", "name", "color", "ratio", "payload", "renamed", "oldName"]); + }); + + it("stops at the first member that throws", () => { + const log: string[] = []; + expect(() => + bindgen.optionsEcho({ + get count() { + log.push("count"); + return 1; + }, + get flag() { + log.push("flag"); + throw new RangeError("from a getter"); + }, + get name() { + log.push("name"); + return "n"; + }, + }), + ).toThrow(new RangeError("from a getter")); + expect(log).toEqual(["count", "flag"]); + }); + + it("a getter that changes the object is seen by the members after it", () => { + const options = { + get count() { + (this as any).name = "added"; + delete (this as any).ratio; + return 1; + }, + ratio: 5, + }; + expect(bindgen.optionsEcho(options)).toStrictEqual({ ...defaults, name: "added" }); + }); +}); + +describe("unions", () => { + it("picks the first alternative that fits", () => { + expect(bindgen.describeValue(1.5)).toBe("number 1.5"); + expect(bindgen.describeValue(true)).toBe("flag true"); + expect(bindgen.describeValue("téxt")).toBe("text téxt"); + expect(bindgen.describeValue([1, 2, 3])).toBe("list [1, 2, 3]"); + expect(bindgen.describeValue([])).toBe("list []"); + }); + + it("rejects what fits none", () => { + for (const value of [null, undefined, {}, Symbol("s"), 1n]) { + expect(() => bindgen.describeValue(value)).toThrow(TypeError); + } + expect(() => bindgen.describeValue(["not a number"])).toThrow(TypeError); + }); +}); + +describe("arrays", () => { + it("in", () => { + expect(bindgen.sumArray([])).toBe(0); + expect(bindgen.sumArray([1, 2, 3])).toBe(6); + expect(bindgen.sumArray(Array.from({ length: 1000 }, (_, i) => i))).toBe(499500); + expect(() => bindgen.sumArray(new Set([4, 5]))).toThrow("numbers must be an array of integer"); + expect(() => bindgen.sumArray([1, "2"])).toThrow(TypeError); + expect(() => bindgen.sumArray(5)).toThrow(TypeError); + }); + + it("out", () => { + expect(bindgen.range(0)).toEqual([]); + expect(bindgen.range(4)).toEqual([0, 1, 2, 3]); + expect(bindgen.range(5000)).toEqual(Array.from({ length: 5000 }, (_, i) => i)); + expect(bindgen.words("one twö 三")).toEqual(["one", "twö", "三"]); + expect(Array.isArray(bindgen.range(1))).toBe(true); + }); +}); + +describe("buffers", () => { + it("views every kind of buffer", () => { + const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + expect(bindgen.sumBytes(bytes)).toBe(36); + expect(bindgen.sumBytes(bytes.buffer)).toBe(36); + expect(bindgen.sumBytes(bytes.subarray(2, 4))).toBe(7); + expect(bindgen.sumBytes(new DataView(bytes.buffer, 6))).toBe(15); + expect(bindgen.sumBytes(new Uint16Array(bytes.buffer, 0, 1))).toBe(3); + expect(bindgen.sumBytes(Buffer.from([9, 9]))).toBe(18); + expect(bindgen.sumBytes(new Uint8Array(0))).toBe(0); + expect(bindgen.sumBytes(new ArrayBuffer(0))).toBe(0); + }); + + it("a detached or shrunk view is empty", () => { + const detached = new Uint8Array([1, 2, 3]); + detached.buffer.transfer(); + expect(bindgen.sumBytes(detached)).toBe(0); + + const resizable = new ArrayBuffer(4, { maxByteLength: 8 }); + const tracking = new Uint8Array(resizable); + const fixed = new Uint8Array(resizable, 2, 2); + tracking.fill(1); + expect(bindgen.sumBytes(tracking)).toBe(4); + resizable.resize(3); + expect(bindgen.sumBytes(tracking)).toBe(3); + expect(bindgen.sumBytes(fixed)).toBe(0); + }); + + it("rejects shared memory and everything else", () => { + for (const value of [new SharedArrayBuffer(4), new Uint8Array(new SharedArrayBuffer(4)), [1], "s", null, {}]) { + expect(() => bindgen.sumBytes(value)).toThrow( + "bytes must be an ArrayBuffer or ArrayBufferView that is not shared", + ); + expect(() => bindgen.sumBeforeAndAfterCallback(value, () => {})).toThrow(TypeError); + } + }); + + it("looks the bytes up again after JavaScript ran", () => { + const same = new Uint8Array([1, 2, 3]); + expect(bindgen.sumBeforeAndAfterCallback(same, () => {})).toEqual([6, 6]); + expect(bindgen.sumBeforeAndAfterCallback(same, () => void same.fill(2))).toEqual([6, 6]); + + const transferred = new Uint8Array([1, 2, 3]); + expect(bindgen.sumBeforeAndAfterCallback(transferred, () => void transferred.buffer.transfer())).toEqual([6, 0]); + + const resizable = new ArrayBuffer(4, { maxByteLength: 1 << 20 }); + const view = new Uint8Array(resizable).fill(1); + expect(bindgen.sumBeforeAndAfterCallback(view, () => resizable.resize(1 << 20))).toEqual([4, 4]); + expect(bindgen.sumBeforeAndAfterCallback(view, () => resizable.resize(1))).toEqual([4, 1]); + + expect(() => + bindgen.sumBeforeAndAfterCallback(same, () => { + throw new RangeError("from the callback"); + }), + ).toThrow(new RangeError("from the callback")); + }); +}); + +describe("withValue arguments", () => { + it("hands back the very value that was converted", () => { + const boxed = new String("boxed"); + const custom = { toString: () => "custom" }; + for (const value of ["plain", boxed, custom, 5]) expect(bindgen.withValueEcho(value)).toBe(value); + expect(bindgen.withValueEcho("")).toBe(undefined); + }); +}); + +describe("errors", () => { + it("throws what the function describes", () => { + expect(() => bindgen.fail("type")).toThrow(new TypeError("a type error")); + expect(() => bindgen.fail("range")).toThrow(new RangeError("a range error")); + expect(() => bindgen.fail("plain")).toThrow(new Error("an error")); + expect(() => bindgen.fail("syscall")).toThrow(expect.objectContaining({ code: "ENOENT", syscall: "open" })); + const value = { thrown: true }; + let caught; + try { + bindgen.fail("value", value); + } catch (error) { + caught = error; + } + expect(caught).toBe(value); + for (const primitive of [1, "s", null, undefined]) { + try { + bindgen.fail("value", primitive); + expect.unreachable(); + } catch (error) { + expect(error).toBe(primitive); + } + } + }); +}); + +describe("classes", () => { + const { TestCounter } = bindgen as any; + + it("has the shape of a class", () => { + expect(typeof TestCounter).toBe("function"); + expect(TestCounter.name).toBe("TestCounter"); + expect(TestCounter.length).toBe(0); + expect(TestCounter.prototype.constructor).toBe(TestCounter); + expect(Object.prototype.toString.call(new TestCounter())).toBe("[object TestCounter]"); + expect(Object.getOwnPropertyNames(TestCounter.prototype).sort()).toEqual( + ["constructor", "count", "increment", "label", "listen", "notify", "plus", "tag"].sort(), + ); + expect(Object.getOwnPropertyNames(new TestCounter())).toEqual([]); + expect(TestCounter.prototype.increment.length).toBe(0); + expect(TestCounter.prototype.plus.length).toBe(1); + expect(() => TestCounter()).toThrow("TestCounter constructor cannot be invoked without 'new'"); + }); + + it("constructs, reads and mutates", () => { + const counter = new TestCounter(5); + expect(counter).toBeInstanceOf(TestCounter); + expect(counter.count).toBe(5); + expect(counter.increment()).toBe(6); + expect(counter.increment(10)).toBe(16); + expect(counter.count).toBe(16); + expect(new TestCounter().count).toBe(0); + }); + + it("a constructor that throws makes nothing", () => { + Bun.gc(true); + const before = TestCounter.alive(); + expect(() => new TestCounter(-1)).toThrow(new RangeError("start must not be negative")); + expect(() => new TestCounter("1")).toThrow(TypeError); + expect(TestCounter.alive()).toBe(before); + }); + + it("setters convert like arguments", () => { + const counter = new TestCounter(); + expect(counter.label).toBe(""); + counter.label = "näme"; + expect(counter.label).toBe("näme"); + expect(() => { + counter.label = 5; + }).toThrow("TestCounter.label must be a string"); + expect(counter.label).toBe("näme"); + expect(() => { + "use strict"; + counter.count = 1; + }).toThrow(TypeError); + }); + + it("a cached getter is computed once per object", () => { + const counter = new TestCounter(3); + const tag = counter.tag; + expect(tag).toEqual([3]); + counter.increment(); + expect(counter.tag).toBe(tag); + Bun.gc(true); + expect(counter.tag).toBe(tag); + expect(new TestCounter(3).tag).not.toBe(tag); + }); + + it("checks the receiver", () => { + const { increment } = TestCounter.prototype; + const count = Object.getOwnPropertyDescriptor(TestCounter.prototype, "count")!.get!; + const setLabel = Object.getOwnPropertyDescriptor(TestCounter.prototype, "label")!.set!; + for (const receiver of [ + undefined, + null, + 1, + {}, + TestCounter.prototype, + Object.create(new TestCounter()), + new Bun.Glob("*"), + ]) { + expect(() => increment.call(receiver)).toThrow(TypeError); + expect(() => count.call(receiver)).toThrow(TypeError); + expect(() => setLabel.call(receiver, "x")).toThrow(TypeError); + } + expect(() => new TestCounter().plus({})).toThrow("other must be an instance of TestCounter"); + expect(() => new TestCounter().plus(new Bun.Glob("*"))).toThrow(TypeError); + }); + + it("can be subclassed and made with Reflect.construct", () => { + class Sub extends TestCounter { + extra = 1; + constructor(start: number) { + super(start); + } + twice() { + return this.increment() + this.increment(); + } + } + const sub = new Sub(1); + expect(sub).toBeInstanceOf(Sub); + expect(sub).toBeInstanceOf(TestCounter); + expect(sub.extra).toBe(1); + expect(sub.twice()).toBe(5); + function Other() {} + const other = Reflect.construct(TestCounter, [2], Other); + expect(Object.getPrototypeOf(other)).toBe(Other.prototype); + expect(TestCounter.prototype.increment.call(other)).toBe(3); + }); + + it("takes and returns instances", () => { + const made = TestCounter.starting(40); + expect(made).toBeInstanceOf(TestCounter); + expect(made.plus(new TestCounter(2))).toBe(42); + expect(made.plus(made)).toBe(80); + }); + + it("holds a value in a slot and lets JavaScript re-enter", () => { + const counter = new TestCounter(1); + expect(counter.notify()).toBe(1); + const receivers: unknown[] = []; + counter.listen(function (this: unknown) { + receivers.push(this); + counter.increment(10); + counter.label = "changed inside"; + if (receivers.length < 3) counter.notify(); + }); + Bun.gc(true); + expect(counter.notify()).toBe(31); + expect(receivers).toEqual([counter, counter, counter]); + expect(counter.label).toBe("changed inside"); + counter.listen(() => { + throw new RangeError("from the listener"); + }); + expect(() => counter.notify()).toThrow(new RangeError("from the listener")); + }); + + it("without `construct` there is no constructor", () => { + const token = bindgen.makeToken(7); + expect(token.id).toBe(7); + expect(Object.prototype.toString.call(token)).toBe("[object TestToken]"); + const prototype = Object.getPrototypeOf(token); + expect(Object.getOwnPropertyNames(prototype)).toEqual(["id"]); + expect(token.constructor).toBe(Object); + expect(Object.getPrototypeOf(bindgen.makeToken(8))).toBe(prototype); + }); + + it("drops the Rust value when the object is collected", () => { + Bun.gc(true); + const before = TestCounter.alive(); + (function make() { + for (let i = 0; i < 1000; i++) { + const counter = new TestCounter(i); + counter.label = "label " + i; + counter.listen(() => counter); + counter.tag; + } + })(); + expect(TestCounter.alive()).toBeGreaterThan(before); + Bun.gc(true); + expect(TestCounter.alive()).toBeLessThan(before + 100); + }); +}); diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index 7da469bb0c1a..a7297f49fd60 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -1,36 +1,32 @@ /** - * The two bindgen steps in scripts/build/codegen.ts have to declare every file - * their script writes, the Generated*.h headers included, not only the .cpp - * that gets compiled. + * The bindgen step in scripts/build/codegen.ts has to declare every file its + * script writes, the Generated*.h headers included, not only the .cpp files + * that get compiled. * * Compiles only order-depend on codegen; the compiler's depfile is what rebuilds * a TU when a generated header changes, and within one build ninja re-checks a * depfile entry after the step that writes it only if the entry is a declared * output of that step. A header no edge declares is stat'd once at startup, so - * a TU including one (BunObject.cpp includes GeneratedBunObject.h; the generated - * GeneratedSSLConfig.cpp includes the header-only union types next to it) was + * a TU including one (BunObject.cpp includes GeneratedBunObject.h) was * recompiled by the build AFTER the one that regenerated the header, and the * first build linked the stale object against the fresh generated code. * - * Each step is checked twice: what its emitter declares, and what its script - * writes when run for real into a scratch dir. The bindgenv2 cases use a probe - * .bindv2.ts with one header-only type (a union) and one type with a header and - * a .cpp (an enumeration); bindgen.ts finds the repo's .bind.ts files on its own, - * so its step is checked against those. + * The step is checked twice: what its emitter declares, and what its script + * writes when run for real into a scratch dir. */ import { describe, expect, test } from "bun:test"; import { bunExe, bunRun, tempDir } from "harness"; -import { readdirSync, writeFileSync } from "node:fs"; +import { readdirSync } from "node:fs"; import { isAbsolute, resolve } from "node:path"; -import { emitBindgen, emitBindgenV2, registerCodegenRules, type CodegenOutputs } from "../../scripts/build/codegen.ts"; +import { emitBindgen, registerCodegenRules, type CodegenOutputs } from "../../scripts/build/codegen.ts"; import { registerDirStamps } from "../../scripts/build/compile.ts"; import { resolveConfig, type Config, type Toolchain } from "../../scripts/build/config.ts"; import { Ninja } from "../../scripts/build/ninja.ts"; import { quote } from "../../scripts/build/shell.ts"; import type { Sources } from "../../scripts/glob-sources.ts"; -/** A fully-populated fake toolchain; `jsRuntime` is the one entry that gets spawned (bindgenv2 list-outputs). */ +/** A fully-populated fake toolchain. */ function mockToolchain(): Toolchain { return { cc: "/fake/llvm/bin/clang", @@ -97,7 +93,6 @@ function configure(buildDir: string): Configured { cppSources: [], cppHeaders: [], cppAll: [], - bindgenV2Cpp: [], internalModulesAsm: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.S"), internalModulesBin: resolve(cfg.codegenDir, "InternalModuleRegistryConstants.bin"), rootInstall: resolve(buildDir, "stamps", "install.stamp"), @@ -142,7 +137,7 @@ function inCodegenDir({ cfg, n }: Configured, names: string[]): string[] { return names.map(name => n.rel(resolve(cfg.codegenDir, name))).sort(); } -/** `paths` normalized (bindgenv2 reports its outputs as `/`), sorted. */ +/** `paths` normalized, sorted. */ function normalized(paths: string[]): string[] { return paths.map(p => resolve(p)).sort(); } @@ -156,97 +151,39 @@ function repoBindFiles(cfg: Config): string[] { return Array.from(new Bun.Glob("src/**/*.bind.ts").scanSync({ cwd: cfg.cwd }), p => resolve(cfg.cwd, p)).sort(); } -/** Writes the probe .bindv2.ts into `dir` and returns its path. */ -function writeProbe(dir: string, cfg: Config): string { - // The probe is outside src/, so it imports lib.ts by its absolute path. - const lib = resolve(cfg.cwd, "src", "codegen", "bindgenv2", "lib.ts"); - const probe = resolve(dir, "probe.bindv2.ts"); - writeFileSync( - probe, - `import * as b from ${JSON.stringify(lib)};\n` + - `export const ProbeUnion = b.union("ProbeUnion", { string: b.String, buffer: b.ArrayBuffer });\n` + - `export const ProbeEnum = b.enumeration("ProbeEnum", ["one", "two"]);\n`, - ); - return probe; -} - -const probeFiles = ["GeneratedProbeEnum.cpp", "GeneratedProbeEnum.h", "GeneratedProbeUnion.h"]; - // The tests below that run a generator script run it under the build being // tested; a debug build takes a few seconds to load one, more than the 5s // default allows for. const generatorTimeout = 60_000; -describe("emitBindgenV2", () => { - test( - "declares the header of every type and the .cpp of the types that have one, on one edge", - () => { - using dir = tempDir("build-codegen-bindgenv2", {}); - const c = configure(String(dir)); - const { cfg, n, o, dirStamp } = c; - const probe = writeProbe(String(dir), cfg); - - emitBindgenV2({ n, cfg, sources: sourceLists({ bindgenV2: [probe], bindgenV2Internal: [] }), o, dirStamp }); - - const cpp = resolve(cfg.codegenDir, "GeneratedProbeEnum.cpp"); - const headers = [ - resolve(cfg.codegenDir, "GeneratedProbeEnum.h"), - resolve(cfg.codegenDir, "GeneratedProbeUnion.h"), - ]; - expect(edgeOutputs(n, cpp)).toEqual(inCodegenDir(c, probeFiles)); - // Headers go in the group the PCH and every cxx edge order-depend on; the - // .cpp is compiled like the other bindgenv2 sources. - expect(normalized(o.cppHeaders)).toEqual(headers); - expect(normalized(o.bindgenV2Cpp)).toEqual([cpp]); - expect(normalized(o.all)).toEqual([cpp, ...headers]); - }, - generatorTimeout, - ); - - test.concurrent( - "list-outputs names exactly the files generate writes", - async () => { - using dir = tempDir("build-codegen-bindgenv2-generate", {}); - const { cfg } = configure(String(dir)); - const probe = writeProbe(String(dir), cfg); - const script = resolve(cfg.cwd, "src", "codegen", "bindgenv2", "script.ts"); - const args = [`--sources=${probe}`, `--codegen-path=${cfg.codegenDir}`]; - - const [listed, generated] = await Promise.all([ - bunRun(["run", script, "--command=list-outputs", ...args]), - bunRun(["run", script, "--command=generate", ...args]), - ]); - expect(listed).toSpawn(); - expect(generated).toSpawn(""); - - const expected = probeFiles.map(name => resolve(cfg.codegenDir, name)); - expect(normalized(listed.stdout.split(";"))).toEqual(expected); - expect(readdirSync(cfg.codegenDir).sort()).toEqual(probeFiles); - }, - generatorTimeout, - ); -}); - describe("emitBindgen", () => { - test("declares Generated.h for every .bind.ts on the GeneratedBindings.cpp edge", () => { + test("declares a .cpp and a .h per .bind.ts, the tables, a .rs per crate and the .d.ts, on one edge", () => { using dir = tempDir("build-codegen-bindgen", {}); const c = configure(String(dir)); const { cfg, n, o, dirStamp } = c; const bindgen = [ resolve(cfg.cwd, "src", "runtime", "node", "node_os.bind.ts"), - resolve(cfg.cwd, "src", "runtime", "api", "BunObject.bind.ts"), + resolve(cfg.cwd, "src", "jsc", "fmt_jsc.bind.ts"), ]; - emitBindgen({ n, cfg, sources: sourceLists({ bindgen }), o, dirStamp }); - - const cpp = resolve(cfg.codegenDir, "GeneratedBindings.cpp"); - const headers = [resolve(cfg.codegenDir, "GeneratedBunObject.h"), resolve(cfg.codegenDir, "GeneratedNodeOs.h")]; - expect(edgeOutputs(n, cpp)).toEqual( - inCodegenDir(c, ["GeneratedBindings.cpp", "GeneratedBunObject.h", "GeneratedNodeOs.h"]), - ); + emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenInternal: [] }), o, dirStamp }); + + const inDir = (name: string) => resolve(cfg.codegenDir, name); + const cpp = ["GeneratedFmtJsc.cpp", "GeneratedNodeOs.cpp"].map(inDir); + const headers = [ + "GeneratedBindings+Counts.h", + "GeneratedBindings+Tables.h", + "GeneratedFmtJsc.h", + "GeneratedNodeOs.h", + ].map(inDir); + const rust = ["generated_bindgen_bun_jsc.rs", "generated_bindgen_bun_runtime.rs"].map(inDir); + const types = resolve(cfg.typesDir, "Bindgen.d.ts"); + expect(edgeOutputs(n, cpp[0])).toEqual([...cpp, ...headers, ...rust, types].map(p => n.rel(p)).sort()); + expect(normalized(o.cppSources)).toEqual(cpp); expect(normalized(o.cppHeaders)).toEqual(headers); - expect(o.cppSources).toEqual([cpp]); - expect(normalized(o.all)).toEqual([cpp, ...headers]); + expect(normalized(o.rustInputs)).toEqual(rust); + expect(o.generatedTypes).toEqual([types]); + expect(normalized(o.all)).toEqual(normalized([...cpp, ...headers, ...rust, types])); }); test.concurrent( @@ -258,15 +195,26 @@ describe("emitBindgen", () => { const bindgen = repoBindFiles(cfg); expect(bindgen).not.toBeEmpty(); - emitBindgen({ n, cfg, sources: sourceLists({ bindgen }), o, dirStamp }); + emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenInternal: [] }), o, dirStamp }); const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); - expect(await bunRun(["run", script, "--debug=ON", `--codegen-root=${cfg.codegenDir}`])).toSpawn(""); + const typesDir = resolve(String(dir), "types"); + expect( + await bunRun([ + "run", + script, + `--sources=${bindgen.join(",")}`, + `--codegen-path=${cfg.codegenDir}`, + `--types-path=${typesDir}`, + ]), + ).toSpawn(""); - // GeneratedBindings.cpp plus one header per .bind.ts, and nothing else. const written = readdirSync(cfg.codegenDir).sort(); - expect(written).toHaveLength(bindgen.length + 1); - expect(edgeOutputs(n, resolve(cfg.codegenDir, "GeneratedBindings.cpp"))).toEqual(inCodegenDir(c, written)); + const declared = edgeOutputs(n, resolve(cfg.codegenDir, "GeneratedBindings+Tables.h")); + expect(declared.filter(p => p !== n.rel(resolve(cfg.typesDir, "Bindgen.d.ts")))).toEqual( + inCodegenDir(c, written), + ); + expect(readdirSync(typesDir)).toEqual(["Bindgen.d.ts"]); }, generatorTimeout, ); diff --git a/test/internal/source-lints/host-export-callers.test.ts b/test/internal/source-lints/host-export-callers.test.ts index 9afdc32dc123..5e5934c5119f 100644 --- a/test/internal/source-lints/host-export-callers.test.ts +++ b/test/internal/source-lints/host-export-callers.test.ts @@ -24,11 +24,9 @@ import path from "path"; // - `rust`-abi markers. Their thunk is `extern "Rust"` and its consumers are // `extern "Rust" {}` blocks in other Rust crates (cycle-breaking hooks), so a // C++ search cannot see them. -// - Symbols the bindgen generators synthesize. `bindgen_*` names are built by -// `src/codegen/bindgen-lib-internal.ts` from `.bind.ts` inputs and -// `js2native_*` names by `src/codegen/generate-js2native.ts`; their callers -// live in generated C++ (`GeneratedBindings.cpp`, `GeneratedJS2Native.h`) -// that is not part of the tracked tree. +// - `js2native_*` names, which `src/codegen/generate-js2native.ts` synthesizes; +// their callers live in generated C++ (`GeneratedJS2Native.h`) that is not +// part of the tracked tree. // // Both scans go through `git grep` so they see tracked files only (a // `git stash` round-trip can leave stray files in the working directory) and @@ -42,7 +40,7 @@ const root = path.resolve(import.meta.dir, "..", "..", ".."); // the abi (`jsc` when absent). const MARKER = /^\s*\/\/\s*HOST_EXPORT\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:,\s*(jsc|c|rust))?\s*\)\s*$/; -const GENERATED_PREFIXES = ["bindgen_", "js2native_"]; +const GENERATED_PREFIXES = ["js2native_"]; // The C++ side of the boundary. Vendored C libraries (the sqlite amalgamation, // llhttp) never call back into Rust and are 10 MB of text, so they are excluded. From ea1ad165d51fe5ec21b6cf8449b9f40cb26d2031 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 02:46:25 +0000 Subject: [PATCH 02/44] Include FunctionPrototype.h in ModuleGraph.cpp and make the forged-error case fail for one reason --- src/jsc/bindings/ModuleGraph.cpp | 1 + test/internal/bindgen-compile-fail/fail/forge_error.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/ModuleGraph.cpp b/src/jsc/bindings/ModuleGraph.cpp index 5252dd598d0e..ba81752cf2eb 100644 --- a/src/jsc/bindings/ModuleGraph.cpp +++ b/src/jsc/bindings/ModuleGraph.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include diff --git a/test/internal/bindgen-compile-fail/fail/forge_error.rs b/test/internal/bindgen-compile-fail/fail/forge_error.rs index dbf81977ae93..053aaebed94e 100644 --- a/test/internal/bindgen-compile-fail/fail/forge_error.rs +++ b/test/internal/bindgen-compile-fail/fail/forge_error.rs @@ -1,4 +1,4 @@ -// E0423: an `Err` that says something was thrown when nothing was. +// E0603: an `Err` that says something was thrown when nothing was. fn f<'a>() -> bindgen::Result<'a, ()> { - Err(bindgen::Error(Default::default(), Default::default(), Default::default())) + Err(bindgen::Error(unreachable!(), Default::default(), Default::default())) } From dedec6427001b737bfdc66af7a1a35bc326178f6 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 02:49:21 +0000 Subject: [PATCH 03/44] Rename klass, values, .nullable.loose and the string and buffer types, and take type names from exports --- docs/project/bindgen.mdx | 94 ++++++------- src/codegen/bindgen-emit.ts | 49 ++++--- src/codegen/bindgen-lib-internal.ts | 132 ++++++++++-------- src/codegen/bindgen-lib.ts | 39 ++---- src/codegen/bindgen.ts | 3 +- src/jsc/NodeModuleModule.bind.ts | 2 +- src/jsc/bindgen.rs | 12 +- src/jsc/bindgen_test.bind.ts | 44 +++--- src/jsc/bindgen_test.rs | 8 +- src/jsc/bindings/Bindgen/JSBindgenObject.h | 6 +- src/jsc/fmt_jsc.bind.ts | 8 +- src/runtime/api/BunObject.bind.ts | 2 +- src/runtime/api/Glob.bind.ts | 18 +-- src/runtime/node/node_os.bind.ts | 2 +- src/runtime/socket/SSLConfig.bind.ts | 21 ++- src/runtime/socket/SocketConfig.bind.ts | 26 ++-- .../fail/value_in_payload.rs | 2 +- .../pass/methods_that_cannot_run_js.rs | 4 +- .../pass/reentrant_method.rs | 2 +- .../bindgen-compile-fail/pass/with_value.rs | 4 +- test/internal/bindgen-compile-fail/prelude.rs | 12 +- 21 files changed, 243 insertions(+), 247 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index d679fad66116..4b641c1e9793 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -104,31 +104,35 @@ that is an integer in range, and nothing else. A failed conversion throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary member. -| Schema | Rust, as an argument | Accepts | -| ----------------------------- | -------------------------- | ------------------------------------------------------------------------------------------ | -| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | -| `b.i8` … `b.u64`, `b.usize` | `i8` … `u64`, `usize` | an integer in range; see below | -| `b.f64` | `f64` | a number; `.finite` rejects NaN and infinities, `.loose` is `ToNumber` | -| `b.utf8` | `&'a [u8]` | a string or `String` object; `.loose` is `ToString` | -| `b.string` | `bun_core::String` | a string; `.loose` is `ToString` | -| `b.bytes` | `&'a [u8]` | a typed array, `DataView` or `ArrayBuffer` that is not shared; not with `capability: "js"` | -| `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | the same | -| `b.arrayBuffer`, `b.blob` | `ExternalShared<..>` | a reference-counted handle on the native object | -| `b.any` | `JsValue<'a>` | anything | -| `b.array(T)` | `Vec` | an array | -| `b.enumeration(name, values)` | a generated `enum` | one of the strings | -| `b.dictionary(name, members)` | a generated `struct` | an object that is not an array | -| `b.union(name, alternatives)` | a generated `enum` | the first alternative that fits | -| `b.class(name, ..)` | `JsWrapper<'a, T>` | an instance | +| Schema | Rust, as an argument | Accepts | +| -------------------------------------- | -------------------------- | ---------------------------------------------------------------------- | +| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | +| `b.i8` … `b.u64`, `b.usize` | `i8` … `u64`, `usize` | an integer in range; see below | +| `b.f64` | `f64` | a number; `.finite` rejects NaN and infinities, `.loose` is `ToNumber` | +| `b.string` | `bun_core::String` | a string; `.loose` is `ToString` | +| `b.string.utf8` | `&'a [u8]` | the same, and a `String` object | +| `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | +| `b.bufferSource.bytes` | `&'a [u8]` | the same; not with `capability: "js"` | +| `b.bufferSource.arrayBuffer`, `b.blob` | `ExternalShared<..>` | a reference-counted handle on the native object | +| `b.any` | `JsValue<'a>` | anything | +| `b.array(T)` | `Vec` | an array | +| `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` | the first alternative that fits | +| `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. Any type takes one of these, last: - `.optional`: `undefined` means "not given"; `Option`. - `.nullable`: `null` or `undefined` means "not given"; `Option`. +- `.falsyAsNull`: every falsy value means "not given"; `Option`. - `.default(value)`: what to use when not given; `T`. `.nullable.default(value)` also applies to `null`. - `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). -As results, `b.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything +As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that is `IntoIterator`. ### Integers @@ -140,7 +144,7 @@ that is `IntoIterator`. ### Strings -`b.utf8` is the one to reach for. When the JS string is 8-bit and ASCII the +`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. Either way nothing is allocated for a short string and nothing needs freeing. An unpaired surrogate becomes U+FFFD. @@ -152,7 +156,7 @@ one anywhere. ### Dictionaries ```ts -export const BracesOptions = b.dictionary("BracesOptions", { +export const BracesOptions = b.dictionary({ tokenize: b.bool.loose.default(false), parse: b.bool.loose.default(false), }); @@ -174,17 +178,13 @@ Returning a dictionary makes an object with a prebuilt `Structure`: one allocation and one store per member. An `.optional` member that is `None` is left out. -`b.dictionary({ name, fromJS: true }, ..)` also generates +`b.dictionary(members, { fromJS: true })` also generates `Name::from_js(cx, value)`, for code that is not a generated binding yet. ### Enumerations ```ts -export const Formatter = b.enumeration("Formatter", [ - "highlight-javascript", - "highlight-javascript-redacted", - "escape-powershell", -]); +export const Formatter = b.enumeration(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]); ``` generates `enum Formatter { HighlightJavascript, .. }`. An entry can be a list of @@ -193,9 +193,9 @@ allocates nothing after the first time. ### Buffers -`b.bytes` is only for functions that cannot run JavaScript, because JavaScript +`b.bufferSource.bytes` is only for functions that cannot run JavaScript, because JavaScript can detach, resize or transfer a buffer. A function that can takes `b.bufferSource` -and looks the bytes up when it needs them: +itself and looks the bytes up when it needs them: ```rust let before = buffer.with_bytes(&cx.heap, sum); @@ -208,16 +208,16 @@ Calling `callback.call(cx, ..)` inside the closure does not compile. ### Keeping the original value A converted argument is for use now. To hand the value to something that holds -it longer (one of a class's `values`, a `Strong`, another JS call, the result), ask +it longer (one of a class's `internalFields`, a `Strong`, another JS call, the result), ask for both: ```ts -args: { name: b.utf8.withValue }, +args: { name: b.string.utf8.withValue }, ``` ```rust fn set_name<'a>(&mut self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) { - this.name_set_cached(heap, name.value); // the JS string itself, not a copy + this.set_name_field(heap, name.value); // the JS string itself, not a copy self.name_length = name.converted.len(); } ``` @@ -233,7 +233,7 @@ task or sent to a thread. `JsValue<'a, T>` is one known to be a `JSString`, What outlives a call: -- one of a class's **`values`** (below), which the GC visits; +- one of a class's **`internalFields`** (below), which the GC visits; - a `Strong`, made with `value.to_strong(heap)` and read with `JsValue::from_strong(heap, &strong)`. It is made, read and dropped on the JS thread. A list of values goes in a `MarkedArgumentBuffer`, never a `Vec`: the GC does not @@ -241,17 +241,17 @@ look inside heap allocations. ## Classes -The keys are those of `.classes.ts`, with types. +The keys are mostly those of `.classes.ts`, with types. `klass` is `static` and `values` is `internalFields`. ```ts -export const Glob = b.class("Glob", { +export const Glob = b.class({ rustPath: "crate::api::glob::Glob", construct: { - args: { pattern: b.utf8 }, + args: { pattern: b.string.utf8 }, }, proto: { match: { - args: { str: b.utf8 }, + args: { str: b.string.utf8 }, returns: b.bool, }, }, @@ -281,12 +281,12 @@ 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. -| Key | What it declares | -| ----------- | -------------------------------------------------------------------------------------------- | -| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.to_js(heap)`. | -| `proto` | properties of the prototype | -| `klass` | functions on the constructor | -| `values` | JS values the object holds | +| Key | What it declares | +| ---------------- | -------------------------------------------------------------------------------------------- | +| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.to_js(heap)`. | +| `proto` | properties of the prototype | +| `static` | functions on the constructor | +| `internalFields` | JS values the object holds | A property of `proto` is one of: @@ -294,7 +294,7 @@ A property of `proto` is one of: | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `{ args, returns, capability, throws, .. }` | a native function; `fn` renames the trait method, `privateSymbol` keys it by a private name | | `{ getter: T, setter: true, cache: true, capability, .. }` | an accessor; `cache` computes the value once per object | -| `{ builtin, length }` | a function written in JavaScript in `src/js/builtins` | +| `{ builtin: true, length }` | the export of that name in `src/js/builtins/.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 @@ -316,7 +316,7 @@ long as nothing else can run: ```rust fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { - if let Some(listener) = this.listener_get_cached(&cx.heap).as_function() { + if let Some(listener) = this.listener_field(&cx.heap).as_function() { listener.call(cx, this.as_value(), &[])?; } Ok(this.with(&cx.payloads, |counter| counter.count)) @@ -327,12 +327,12 @@ fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result 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. -### `values` +### `internalFields` -`values: ["listener"]` generates `this.listener_get_cached(heap)` and -`this.listener_set_cached(heap, value)` on `JsWrapper<'a, T>`. A `&self` or +`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 values and not the payload. +The wrapper reaches the internal fields and not the payload. ### Staying alive diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 1a29777e1e12..76fda2d9e1e9 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -24,6 +24,7 @@ import { UnitType, WithValueType, } from "./bindgen-lib-internal.ts"; +import { cap, low } from "./helpers.ts"; export interface SchemaFile { origin: Origin; @@ -393,16 +394,14 @@ const cppExtern = (symbol: string, signature: Signature, leading: readonly strin // ───────────────────────────── C++: classes ───────────────────────────── -const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1); - /** As generate-classes.ts spells them: the role is part of the type segment. */ const symbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}__${member}`; const classSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}Class__${member}`; const protoSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}Prototype__${member}`; const methodSymbolName = (type: ClassType, method: Method) => (method.receiver === "static" ? classSymbolName : protoSymbolName)(type, method.name.replace(/^_+/, "")); -const getterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `get${capitalize(getter.name)}`); -const setterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `set${capitalize(getter.name)}`); +const getterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `get${cap(getter.name)}`); +const setterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `set${cap(getter.name)}`); const classSymbols = (type: ClassType) => [ ...[...type.methods, ...type.staticMethods].map(method => methodSymbolName(type, method)), @@ -425,8 +424,8 @@ namespace Bun::Bindgen::Generated { struct ${name}Traits { static constexpr auto className = ${asciiLiteral(name)}; static constexpr unsigned classIndex = ${type.classIndex}; - static constexpr unsigned numberOfInternalFields = ${type.internalFields.length}; - static constexpr unsigned numberOfValues = ${type.values.length}; + static constexpr unsigned numberOfInternalFields = ${type.numberOfInternalFields}; + static constexpr unsigned numberOfCachedGetters = ${type.cachedGetters.length}; static size_t payloadSize() { return ${symbolName(type, "payloadSize")}; } static void finalize(void* payload) { ${classSymbolName(type, "finalize")}(payload); } }; @@ -536,7 +535,7 @@ function cppSourceForClass(type: ClassType): string { row( builtin.name, "JSC::PropertyAttribute::Builtin", - `JSC::HashTableValue::BuiltinGeneratorType, ::WebCore::${builtin.builtin}, ${builtin.length}`, + `JSC::HashTableValue::BuiltinGeneratorType, ::WebCore::${low(name)}${cap(builtin.name)}CodeGenerator, ${builtin.length}`, ), ), ]; @@ -616,7 +615,7 @@ extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${type.origin!.stem}_$ /** sizeof(JSC::JSDestructibleObject) */ const offsetOfInternalFields = 24; -const offsetOfPayload = (type: ClassType) => offsetOfInternalFields + 8 * type.internalFields.length; +const offsetOfPayload = (type: ClassType) => offsetOfInternalFields + 8 * type.numberOfInternalFields; function cppSourceForFunction(fn: FunctionDefinition): string { const externs = fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature)).join("\n"); @@ -1045,24 +1044,24 @@ let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiv ); } - const valueDeclarations = type.values - .map(value => { - const id = snakeCase(value); - return ` fn ${id}_get_cached(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a>; - fn ${id}_set_cached(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>);`; + const fieldDeclarations = type.internalFields + .map(field => { + const id = snakeCase(field); + return ` fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a>; + fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>);`; }) .join("\n"); - const valueDefinitions = type.values - .map((value, index) => { - const id = snakeCase(value); + const fieldDefinitions = type.internalFields + .map((field, index) => { + const id = snakeCase(field); return ` #[inline(always)] - fn ${id}_get_cached(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { - // SAFETY: internal field ${index} of ${type.internalFields.length}. + fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { + // SAFETY: internal field ${index} of ${type.numberOfInternalFields}. unsafe { self.internal_field(heap, ${index}) } } #[inline(always)] - fn ${id}_set_cached(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>) { - // SAFETY: internal field ${index} of ${type.internalFields.length}. + fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>) { + // SAFETY: internal field ${index} of ${type.numberOfInternalFields}. unsafe { self.set_internal_field(heap, ${index}, value) } }`; }) @@ -1108,13 +1107,13 @@ pub unsafe extern "C" fn ${classSymbolName(type, "finalize")}(payload: *mut ${ru unsafe { core::ptr::drop_in_place(payload) } } ${ - type.values.length > 0 + type.internalFields.length > 0 ? `/// The JS values a \`${name}\` holds. -pub trait ${name}Values<'a>: Copy { -${valueDeclarations} +pub trait ${name}InternalFields<'a>: Copy { +${fieldDeclarations} } -impl<'a> ${name}Values<'a> for bindgen::JsWrapper<'a, ${rustPath}> { -${valueDefinitions} +impl<'a> ${name}InternalFields<'a> for bindgen::JsWrapper<'a, ${rustPath}> { +${fieldDefinitions} } ` : "" diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index d6281d1a0e0d..af28206e74a4 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -88,9 +88,13 @@ export abstract class Type { get nullable(): OptionalType { return new OptionalType(this, "nullable"); } + /** Every falsy value means "not provided". */ + get falsyAsNull(): OptionalType { + return new OptionalType(this, "falsyAsNull"); + } /** * Both forms: converted for use now, and the original value to hand to something that holds it - * longer (one of a class's `values`, a `Strong`, a JS call, the result). Only for arguments. + * longer (one of a class's `internalFields`, a `Strong`, a JS call, the result). Only for arguments. */ get withValue(): WithValueType { return new WithValueType(this); @@ -348,6 +352,11 @@ export class StringType extends Type { get loose(): StringType { return new StringType("::WebCore::IDLDOMString"); } + /** `&[u8]`, UTF-8, for the duration of the call, instead of an owned string. Also takes a `String` object. */ + get utf8(): Utf8Type { + const utf8 = new Utf8Type(); + return this.idlType === "::WebCore::IDLDOMString" ? utf8.loose : utf8; + } get nullRepresentation() { return "pointer" as const; } @@ -443,7 +452,7 @@ export class BytesType extends Type { out.add("Bindgen/IDLConvert.h"); } toCpp(): string { - throw new TypeError("b.bytes cannot have a default"); + throw new TypeError("b.bufferSource.bytes cannot have a default"); } layout(): Layout { return { size: 16, align: 8 }; @@ -463,6 +472,14 @@ export class BytesType extends Type { export class BufferSourceType extends Type { readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; readonly tsType = "ArrayBuffer | ArrayBufferView"; + /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ + get bytes(): BytesType { + return new BytesType(); + } + /** A reference on the `JSC::ArrayBuffer` behind it, which can be stored. */ + get arrayBuffer(): RefType { + return new RefType("::Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null); + } get holdsJSValue(): boolean { return true; } @@ -608,8 +625,8 @@ export class RefType extends Type { export class OptionalType extends Type { readonly payload: Type; - readonly kind: "optional" | "nullable" | "looseNullable"; - constructor(payload: Type, kind: "optional" | "nullable" | "looseNullable") { + readonly kind: "optional" | "nullable" | "falsyAsNull"; + constructor(payload: Type, kind: "optional" | "nullable" | "falsyAsNull") { super(); assert(!(payload instanceof OptionalType), "already optional"); assert(!(payload instanceof DefaultType), "put .default() last"); @@ -617,16 +634,11 @@ export class OptionalType extends Type { this.payload = payload; this.kind = kind; } - /** Every falsy value means "not provided". */ - get loose(): OptionalType { - assert(this.kind === "nullable", ".loose follows .nullable"); - return new OptionalType(this.payload, "looseNullable"); - } get idlType(): string { const template = { optional: "::WebCore::IDLOptional", nullable: "::WebCore::IDLNullable", - looseNullable: "::Bun::IDLLooseNullable", + falsyAsNull: "::Bun::IDLLooseNullable", }[this.kind]; return `${template}<${this.payload.idlType}>`; } @@ -742,7 +754,7 @@ export class ArrayType extends Type { } get idlType(): string { assert(!this.element.holdsJSValue, "an array argument cannot hold JS values: the GC does not scan it"); - assert(!this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string"); + assert(!this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string without .utf8"); return `::Bun::IDLArray<${this.element.idlType}>`; } get tsType(): string { @@ -785,23 +797,18 @@ export class ArrayType extends Type { // ───────────────────────────── named types ───────────────────────────── export abstract class NamedType extends Type { - readonly name: string; - /** Set when the generator finds the type among a file's exports. */ + /** The name it is exported under, and where. Set when the generator finds it among a file's exports. */ + name = ""; origin: Origin | null = null; - constructor(name: string) { - super(); - assert(/^[A-Z][A-Za-z0-9]*$/.test(name), `type names are PascalCase: ${name}`); - this.name = name; - } get cpp(): string { return `::Bun::Bindgen::Generated::${this.name}`; } getHeaders(out: Set): void { - assert(this.origin, `${this.name} is used but not exported from a .bind.ts file`); + assert(this.origin, `${this.constructor.name}: used but not exported from a .bind.ts file`); out.add(`Generated${this.origin.namespace}.h`); } protected path(scope: RustScope, name: string): string { - assert(this.origin, `${this.name} is used but not exported from a .bind.ts file`); + assert(this.origin, `${this.constructor.name}: used but not exported from a .bind.ts file`); if (this.origin.crate !== scope.crate) return `${this.origin.crate}::generated_bindgen::${this.origin.rustModule}::${name}`; if (this.origin.rustModule !== scope.rustModule) return `super::${this.origin.rustModule}::${name}`; @@ -820,14 +827,14 @@ export class EnumType extends NamedType { readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ firstEnumerationValueIndex = -1; - constructor(name: string, values: readonly (string | readonly string[])[]) { - super(name); - assert(values.length > 0, `${name} is empty`); + constructor(values: readonly (string | readonly string[])[]) { + super(); + assert(values.length > 0, "an enumeration has values"); this.spellings = values.map(value => (typeof value === "string" ? [value] : value)); const all = this.spellings.flat(); - assert(new Set(all).size === all.length, `${name} has a duplicate value`); - assert(new Set(this.variants).size === this.variants.length, `${name}: two values make the same identifier`); - for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${name}: ${variant} is not an identifier`); + assert(new Set(all).size === all.length, "duplicate value"); + assert(new Set(this.variants).size === this.variants.length, "two values make the same identifier"); + for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${variant} is not an identifier`); } get variants(): string[] { return this.spellings.map(([canonical]) => pascalCase(canonical)); @@ -889,16 +896,14 @@ export class Member { } export interface DictionaryOptions { - name: string; - /** What error messages call it. Defaults to `name`. */ + /** What error messages call it. Defaults to its name. */ userFacingName?: string; /** Also generate `Name::from_js`, for Rust that holds a JS value and no typed signature. */ fromJS?: boolean; } export class DictionaryType extends NamedType { - readonly userFacingName: string; - readonly fromJS: boolean; + readonly options: DictionaryOptions; readonly members: readonly Member[]; /** Index of the first member name in `Tables::memberNames`. Set by the generator. */ firstMemberNameIndex = -1; @@ -907,12 +912,17 @@ export class DictionaryType extends NamedType { /** Reachable from an argument or `fromJS` / from a return type. Set by the generator. */ accepted = false; returned = false; - constructor(options: string | DictionaryOptions, members: Record) { - super(typeof options === "string" ? options : options.name); - this.userFacingName = (typeof options === "string" ? undefined : options.userFacingName) ?? this.name; - this.fromJS = typeof options !== "string" && !!options.fromJS; + constructor(members: Record, options: DictionaryOptions = {}) { + super(); + this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); - assert(!this.fromJS || !this.borrowsFrame, `${this.name}: from_js outlives the frame b.utf8 borrows from`); + assert(!this.fromJS || !this.borrowsFrame, "from_js outlives the frame b.string.utf8 borrows from"); + } + get userFacingName(): string { + return this.options.userFacingName ?? this.name; + } + get fromJS(): boolean { + return !!this.options.fromJS; } get idlType(): string { return `::WebCore::IDLDictionary<${this.cpp}>`; @@ -969,13 +979,13 @@ export class DictionaryType extends NamedType { /** Alternatives are tried in order; the first whose conversion accepts the value wins. */ export class UnionType extends NamedType { readonly alternatives: readonly { name: string; variant: string; type: Type }[]; - constructor(name: string, alternatives: Record) { - super(name); + constructor(alternatives: Record) { + super(); this.alternatives = Object.entries(alternatives).map(([name, type]) => { assert(!(type instanceof DefaultType)); return { name, variant: pascalCase(name), type }; }); - assert(this.alternatives.length > 0 && this.alternatives.length <= 16, `${name}: 1 to 16 alternatives`); + assert(this.alternatives.length > 0 && this.alternatives.length <= 16, "1 to 16 alternatives"); } get idlType(): string { return `::Bun::IDLOrderedUnion<${this.alternatives.map(a => a.type.idlType).join(", ")}>`; @@ -1054,7 +1064,7 @@ export class Signature { for (const { name, type } of this.args) { assert( !type.borrowsHeap || this.capability !== "js", - `${name}: JavaScript can detach a buffer; take b.bufferSource and use with_bytes`, + `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`, ); } assert(!(this.returns instanceof DefaultType)); @@ -1090,7 +1100,7 @@ export interface FunctionField extends Omit { fn?: string; /** `&mut self` instead of `&self`. Not with `capability: "js"`, which gets the wrapper. */ mutates?: boolean; - /** Also pass the wrapper, for its `values`. `capability: "js"` always gets it. */ + /** Also pass the wrapper, for its `internalFields`. `capability: "js"` always gets it. */ passThis?: boolean; /** Key the function by this private builtin name (`$name` in src/js) instead of a string. */ privateSymbol?: string; @@ -1106,9 +1116,9 @@ export interface GetterField { throws?: boolean; } -/** A function written in JavaScript: the name of the code generator bundle-functions.ts emits. */ +/** A function written in JavaScript: the export of this name in `src/js/builtins/.ts`. */ export interface BuiltinField { - builtin: string; + builtin: true; length: number; } @@ -1122,9 +1132,9 @@ export interface ClassOptions { /** Properties of the prototype. */ proto?: Record; /** Properties of the constructor. */ - klass?: Record; - /** JS values the object holds. The GC visits them; the wrapper gets `_get_cached` / `_set_cached` for each. */ - values?: readonly string[]; + static?: Record; + /** JS values the object holds. The GC visits them; the wrapper gets `x_field` / `set_x_field` for each. */ + internalFields?: readonly string[]; /** `false`: prototype properties are non-configurable. */ configurable?: boolean; } @@ -1197,15 +1207,15 @@ export class ClassType extends NamedType { readonly methods: readonly Method[]; readonly getters: readonly Getter[]; readonly staticMethods: readonly Method[]; - readonly builtins: readonly ({ name: string } & BuiltinField)[]; - readonly values: readonly string[]; - /** `values`, then one per cached getter. */ + readonly builtins: readonly { name: string; length: number }[]; readonly internalFields: readonly string[]; + /** Each has an internal field of its own, after `internalFields`. */ + readonly cachedGetters: readonly Getter[]; readonly configurable: boolean; /** Index in `Tables::classes`. Set by the generator. */ classIndex = -1; - constructor(name: string, options: ClassOptions) { - super(name); + constructor(options: ClassOptions) { + super(); this.rustPath = options.rustPath; const own = (type: Type) => (type === selfType ? this : type); const resolve = (signature: T): T => ({ @@ -1216,34 +1226,34 @@ export class ClassType extends NamedType { this.construct = options.construct ? new Signature(resolve(options.construct)) : null; const methods: Method[] = []; const getters: Getter[] = []; - const builtins: ({ name: string } & BuiltinField)[] = []; + const builtins: { name: string; length: number }[] = []; for (const [name, field] of Object.entries(options.proto ?? {})) { - if ("builtin" in field) builtins.push({ name, ...field }); + if ("builtin" in field) builtins.push({ name, length: field.length }); else if ("getter" in field) getters.push(new Getter(name, field)); else methods.push(new Method(name, resolve(field))); } this.methods = methods; this.getters = getters; this.builtins = builtins; - this.staticMethods = Object.entries(options.klass ?? {}).map( + this.staticMethods = Object.entries(options.static ?? {}).map( ([name, field]) => new Method(name, resolve(field), true), ); - assert(this.construct || this.staticMethods.length === 0, `${name}: klass needs a constructor object`); - this.values = options.values ?? []; - const internalFields = [...this.values]; - for (const getter of getters) { - if (getter.internalFieldIndex >= 0) getter.internalFieldIndex = internalFields.push(getter.name) - 1; - } - this.internalFields = internalFields; + assert(this.construct || this.staticMethods.length === 0, "static members need a constructor object"); + this.internalFields = options.internalFields ?? []; + this.cachedGetters = getters.filter(getter => getter.internalFieldIndex >= 0); + this.cachedGetters.forEach((getter, index) => (getter.internalFieldIndex = this.internalFields.length + index)); this.configurable = options.configurable ?? true; for (const method of this.methods) { assert( method.receiver !== "mut" || !method.args.some(arg => arg.type.dependencies.includes(this) || arg.type === this), - `${name}.${method.name}: &mut self and an argument of the same class may be the same object`, + `${method.name}: &mut self and an argument of the same class may be the same object`, ); } } + get numberOfInternalFields(): number { + return this.internalFields.length + this.cachedGetters.length; + } get cpp(): string { return `::Bun::Bindgen::Generated::JS${this.name}`; } diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 126f1d42418b..2efe53615c0f 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -9,7 +9,6 @@ import { ArrayType, BoolType, BufferSourceType, - BytesType, type ClassOptions, ClassType, type DictionaryOptions, @@ -26,7 +25,6 @@ import { type Type, UnionType, UnitType, - Utf8Type, } from "./bindgen-lib-internal.ts"; export type { Capability, Type } from "./bindgen-lib-internal.ts"; @@ -44,32 +42,22 @@ export const { i8, u8, i16, u16, i32, u32, i64, u64, usize } = integers; /** Strict: only a number. `.finite`, `.loose`. */ export const f64 = new DoubleType(); -/** An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`. */ +/** + * An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`. `.utf8` for `&[u8]` for the duration + * of the call. + */ export const string = new StringType(); -/** `&[u8]`, UTF-8, for the duration of the call. Strict: a string or `String` object. `.loose` for `ToString`. */ -export const utf8 = new Utf8Type(); - /** The JS value itself, as a `JsValue<'a>`. */ export const any = new AnyType(); /** - * `&[u8]`: the bytes of a typed array, `DataView` or `ArrayBuffer`, for the duration of the call. Not shared - * ones. Not for `capability: "js"`, where JavaScript could detach them. + * A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsValue<'a, ArrayBuffer>` whose `with_bytes` + * looks the bytes up each time. `.bytes` for `&[u8]` for the duration of the call, `.arrayBuffer` for a reference on + * the `JSC::ArrayBuffer`. */ -export const bytes = new BytesType(); - -/** The same values as a `JsValue<'a, ArrayBuffer>`, whose `with_bytes` looks the bytes up each time. */ export const bufferSource = new BufferSourceType(); -/** The `JSC::ArrayBuffer` behind an `ArrayBuffer`, typed array or `DataView`. Not shared ones. */ -export const arrayBuffer = new RefType( - "::Bun::IDLArrayBufferRef", - "ArrayBuffer | ArrayBufferView", - "bun_jsc::JSCArrayBuffer", - null, -); - /** The native `Blob` behind a JS `Blob`. */ export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h"); @@ -78,18 +66,19 @@ const undefined_ = new UnitType("undefined"); export const array = (element: Type) => new ArrayType(element); +// A named type is called what it is exported as: `export const Formatter = b.enumeration([..])`. + /** An array in place of a value lists several spellings of it; the first is the one given back to JS. */ -export const enumeration = (name: string, values: readonly (string | readonly string[])[]) => - new EnumType(name, values); +export const enumeration = (values: readonly (string | readonly string[])[]) => new EnumType(values); -export const dictionary = (options: string | DictionaryOptions, members: Record) => - new DictionaryType(options, members); +export const dictionary = (members: Record, options?: DictionaryOptions) => + new DictionaryType(members, options); /** Alternatives are tried in order. */ -export const union = (name: string, alternatives: Record) => new UnionType(name, alternatives); +export const union = (alternatives: Record) => new UnionType(alternatives); /** A JS class whose state is the Rust struct at `options.rustPath`, stored inside the wrapper object. */ -const class_ = (name: string, options: ClassOptions) => new ClassType(name, options); +const class_ = (options: ClassOptions) => new ClassType(options); export { class_ as class }; /** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */ diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 542142363e82..8ec468141f42 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -66,7 +66,8 @@ function load(file: string): SchemaFile { } else if (value instanceof NamedType) { // Re-exporting an import is fine; the first file to export a type owns it. if (value.origin) continue; - assert(name === value.name, `${stem}.bind.ts: export const ${name} declares a type named ${value.name}`); + assert(/^[A-Z][A-Za-z0-9]*$/.test(name), `${stem}.bind.ts: type names are PascalCase: ${name}`); + value.name = name; value.origin = origin; result.types.push(value); } else { diff --git a/src/jsc/NodeModuleModule.bind.ts b/src/jsc/NodeModuleModule.bind.ts index 88ffd13f52d2..bb8109be4991 100644 --- a/src/jsc/NodeModuleModule.bind.ts +++ b/src/jsc/NodeModuleModule.bind.ts @@ -2,7 +2,7 @@ import * as b from "../codegen/bindgen-lib.ts"; export const _stat = b.fn({ args: { - str: b.utf8.loose, + str: b.string.utf8.loose, }, returns: b.i32, rustPath: "crate::node_module_module::stat", diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index c845bddb9f90..6f3871ae28ad 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -6,7 +6,7 @@ //! 2. A pointer into memory JavaScript can free, detach, resize or alias is only reachable inside a //! closure that borrows a field of `JsCall`, so rule 1 cannot happen while it is live. //! 3. A JS value is a [`JsValue<'a>`], tied to the host call. It cannot be stored anywhere that -//! outlives the call; a [`Strong`] and a class's `values` can hold one longer, and the GC visits +//! outlives the call; a [`Strong`] and a class's `internalFields` can hold one longer, and the GC visits //! those. //! //! `test/internal/bindgen-compile-fail` holds one rejected program per hazard. @@ -363,7 +363,7 @@ impl<'a> JsValue<'a, ArrayBuffer> { } /// An argument in both forms: converted for use now, and the value itself to hand to something that -/// holds it longer (a class's `values`, a [`Strong`], a JS call, the return value). +/// holds it longer (a class's `internalFields`, a [`Strong`], a JS call, the return value). #[derive(Clone, Copy)] pub struct WithValue<'a, T> { pub converted: T, @@ -505,14 +505,14 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { JsWrapperStrong(self.as_value().to_strong(heap), PhantomData) } - /// For the generated `_get_cached`. + /// For the generated `x_field`. /// /// # Safety - /// `index` is one of the class's `values`. + /// `index` is one of the class's `internalFields`. #[doc(hidden)] #[inline(always)] pub unsafe fn internal_field(self, _: &JsHeap<'a>, index: usize) -> JsValue<'a> { - // SAFETY: generated offset inside the cell; `values` are initialized to `undefined`. + // SAFETY: generated offset inside the cell; they are initialized to `undefined`. JsValue::wrap(unsafe { self.0 .as_ptr() @@ -523,7 +523,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { }) } - /// For the generated `_set_cached`. + /// For the generated `set_x_field`. /// /// # Safety /// As [`internal_field`](Self::internal_field). diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 33419135212b..ef9c0cc93fe2 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -23,27 +23,27 @@ export const requiredAndOptionalArg = b.fn({ returns: b.i32, }); -export const utf8Echo = b.fn({ args: { text: b.utf8 }, returns: b.utf8 }); -export const utf8EchoLoose = b.fn({ args: { text: b.utf8.loose }, returns: b.utf8 }); -export const utf8Optional = b.fn({ args: { text: b.utf8.nullable }, returns: b.utf8.nullable }); +export const utf8Echo = b.fn({ args: { text: b.string.utf8 }, returns: b.string.utf8 }); +export const utf8EchoLoose = b.fn({ args: { text: b.string.utf8.loose }, returns: b.string.utf8 }); +export const utf8Optional = b.fn({ args: { text: b.string.utf8.nullable }, returns: b.string.utf8.nullable }); export const stringEcho = b.fn({ args: { text: b.string }, returns: b.string }); -export const TestColor = b.enumeration("TestColor", ["red", ["green", "lime"], "blue"]); +export const TestColor = b.enumeration(["red", ["green", "lime"], "blue"]); export const nextColor = b.fn({ args: { color: TestColor }, returns: TestColor }); -export const TestOptions = b.dictionary("TestOptions", { +export const TestOptions = b.dictionary({ count: b.i32.default(1), flag: b.bool.loose.default(false), - name: b.utf8.optional, + name: b.string.utf8.optional, color: TestColor.default("red"), ratio: b.f64.nullable, payload: b.any, renamed: { type: b.u8.optional, altNames: ["oldName"] }, }); -export const TestOptionsEcho = b.dictionary("TestOptionsEcho", { +export const TestOptionsEcho = b.dictionary({ count: b.i32, flag: b.bool, - name: b.utf8.optional, + name: b.string.utf8.optional, color: TestColor, ratio: b.f64.nullable, payload: b.any, @@ -51,19 +51,19 @@ export const TestOptionsEcho = b.dictionary("TestOptionsEcho", { }); export const optionsEcho = b.fn({ args: { options: TestOptions }, returns: TestOptionsEcho, capability: "heap" }); -export const TestValue = b.union("TestValue", { +export const TestValue = b.union({ number: b.f64, flag: b.bool, - text: b.utf8, + text: b.string.utf8, list: b.array(b.i32), }); -export const describeValue = b.fn({ args: { value: TestValue }, returns: b.utf8 }); +export const describeValue = b.fn({ args: { value: TestValue }, returns: b.string.utf8 }); export const sumArray = b.fn({ args: { numbers: b.array(b.i32) }, returns: b.f64 }); export const range = b.fn({ args: { length: b.u32 }, returns: b.array(b.u32) }); -export const words = b.fn({ args: { text: b.utf8 }, returns: b.array(b.utf8) }); +export const words = b.fn({ args: { text: b.string.utf8 }, returns: b.array(b.string.utf8) }); -export const sumBytes = b.fn({ args: { bytes: b.bytes }, returns: b.u32 }); +export const sumBytes = b.fn({ args: { bytes: b.bufferSource.bytes }, returns: b.u32 }); /** The sum before `callback` runs, then after it, or `null` once the buffer is detached. */ export const sumBeforeAndAfterCallback = b.fn({ args: { buffer: b.bufferSource, callback: b.any }, @@ -73,18 +73,22 @@ export const sumBeforeAndAfterCallback = b.fn({ }); /** The original value if the converted text is not empty. */ -export const withValueEcho = b.fn({ args: { text: b.utf8.loose.withValue }, returns: b.any, capability: "heap" }); +export const withValueEcho = b.fn({ + args: { text: b.string.utf8.loose.withValue }, + returns: b.any, + capability: "heap", +}); -export const TestFailure = b.enumeration("TestFailure", ["type", "range", "plain", "syscall", "value"]); +export const TestFailure = b.enumeration(["type", "range", "plain", "syscall", "value"]); export const fail = b.fn({ args: { how: TestFailure, value: b.any }, capability: "heap", throws: true }); -export const TestCounter = b.class("TestCounter", { +export const TestCounter = b.class({ rustPath: "crate::bindgen_test::TestCounter", construct: { args: { start: b.i32.default(0) }, capability: "heap", throws: true }, - values: ["listener"], + internalFields: ["listener"], proto: { count: { getter: b.i32 }, - label: { getter: b.utf8, setter: true }, + label: { getter: b.string.utf8, setter: true }, tag: { getter: b.any, capability: "heap", cache: true }, increment: { args: { by: b.i32.default(1) }, returns: b.i32, mutates: true }, listen: { args: { listener: b.any }, capability: "heap", passThis: true }, @@ -93,14 +97,14 @@ export const TestCounter = b.class("TestCounter", { /** `other` may be `this`. */ plus: { args: { other: b.self }, returns: b.i32, capability: "js" }, }, - klass: { + static: { starting: { args: { at: b.i32 }, returns: b.self, capability: "heap" }, alive: { returns: b.usize }, }, }); /** Only native code makes one. */ -export const TestToken = b.class("TestToken", { +export const TestToken = b.class({ rustPath: "crate::bindgen_test::TestToken", proto: { id: { getter: b.u32 }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 497599aed0a5..0fea4aae28ce 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -7,8 +7,8 @@ use crate::bindgen::{ self, IntoError as _, JsCall, JsHeap, JsPayload as _, JsValue, JsWrapper, WithValue, }; use crate::generated_bindgen::bindgen_test::{ - TestColor, TestCounterClass, TestCounterValues as _, TestFailure, TestOptions, TestOptionsEcho, - TestTokenClass, TestValue, + TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, TestOptions, + TestOptionsEcho, TestTokenClass, TestValue, }; // This example should be kept in sync with bindgen's documentation @@ -195,11 +195,11 @@ impl TestCounterClass for TestCounter { } fn listen<'a>(&self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, listener: JsValue<'a>) { - this.listener_set_cached(heap, listener); + this.set_listener_field(heap, listener); } fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { - if let Some(listener) = this.listener_get_cached(&cx.heap).as_function() { + if let Some(listener) = this.listener_field(&cx.heap).as_function() { listener.call(cx, this.as_value(), &[])?; } Ok(this.with(&cx.payloads, |counter| counter.count)) diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h index 05b09ebf4282..35fbfa5a24c3 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -35,7 +35,7 @@ struct ClassDefinition { // // Traits, generated: // static constexpr ASCIILiteral className; -// static constexpr unsigned classIndex, numberOfInternalFields, numberOfValues; +// static constexpr unsigned classIndex, numberOfInternalFields, numberOfCachedGetters; // static size_t payloadSize(); size_of::(), a constant Rust defines // static void finalize(void* payload); drop_in_place:: template @@ -99,11 +99,11 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { JSBindgenObject(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) { - // Rust reads the schema's `values`, which start as `undefined`. The rest hold cached getters: empty until computed. + // Rust reads the schema's `internalFields`, which start as `undefined`. The rest hold cached getters: empty until computed. unsigned index = 0; for (auto& field : internalFields()) { new (NotNull, &field) JSC::WriteBarrier; - if (index++ < Traits::numberOfValues) + if (index++ < numberOfInternalFields - Traits::numberOfCachedGetters) field.setWithoutWriteBarrier(JSC::jsUndefined()); } } diff --git a/src/jsc/fmt_jsc.bind.ts b/src/jsc/fmt_jsc.bind.ts index b83c12d33cfd..2512ba3e8a3b 100644 --- a/src/jsc/fmt_jsc.bind.ts +++ b/src/jsc/fmt_jsc.bind.ts @@ -2,15 +2,11 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::fmt_jsc::js_bindings" }); -export const Formatter = b.enumeration("Formatter", [ - "highlight-javascript", - "highlight-javascript-redacted", - "escape-powershell", -]); +export const Formatter = b.enumeration(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]); export const fmtString = b.fn({ args: { - code: b.utf8.loose, + code: b.string.utf8.loose, formatter: Formatter, }, returns: b.string, diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 5f6f59600634..d71add25dd97 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -2,7 +2,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::bun_object" }); -export const BracesOptions = b.dictionary("BracesOptions", { +export const BracesOptions = b.dictionary({ tokenize: b.bool.loose.default(false), parse: b.bool.loose.default(false), }); diff --git a/src/runtime/api/Glob.bind.ts b/src/runtime/api/Glob.bind.ts index 5d00ab951ce2..fcf790cb256d 100644 --- a/src/runtime/api/Glob.bind.ts +++ b/src/runtime/api/Glob.bind.ts @@ -1,32 +1,32 @@ import * as b from "../../codegen/bindgen-lib.ts"; -export const GlobScanOptions = b.dictionary("GlobScanOptions", { +export const GlobScanOptions = b.dictionary({ onlyFiles: b.bool.loose.nullable.default(true), throwErrorOnBrokenSymlink: b.bool.loose.nullable.default(false), followSymlinks: b.bool.loose.nullable.default(false), absolute: b.bool.loose.nullable.default(false), - cwd: b.utf8.nullable, + cwd: b.string.utf8.nullable, dot: b.bool.loose.nullable.default(false), }); -export const GlobScanArgument = b.union("GlobScanArgument", { - cwd: b.utf8, +export const GlobScanArgument = b.union({ + cwd: b.string.utf8, options: GlobScanOptions, }); -export const Glob = b.class("Glob", { +export const Glob = b.class({ rustPath: "crate::api::glob::Glob", configurable: false, construct: { - args: { pattern: b.utf8 }, + args: { pattern: b.string.utf8 }, }, proto: { scan: { - builtin: "globScanCodeGenerator", + builtin: true, length: 1, }, scanSync: { - builtin: "globScanSyncCodeGenerator", + builtin: true, length: 1, }, __scan: { @@ -48,7 +48,7 @@ export const Glob = b.class("Glob", { length: 1, }, match: { - args: { str: b.utf8 }, + args: { str: b.string.utf8 }, returns: b.bool, }, }, diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 90a7889ce70a..9e8f589aedeb 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -24,7 +24,7 @@ export const release = b.fn({ returns: b.string }); export const totalmem = b.fn({ returns: b.u64 }); export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); -export const UserInfoOptions = b.dictionary("UserInfoOptions", { +export const UserInfoOptions = b.dictionary({ encoding: b.string.loose.default(""), }); export const userInfo = b.fn({ diff --git a/src/runtime/socket/SSLConfig.bind.ts b/src/runtime/socket/SSLConfig.bind.ts index 62fd21a3273e..cbf6e8f28576 100644 --- a/src/runtime/socket/SSLConfig.bind.ts +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -1,31 +1,26 @@ import * as b from "../../codegen/bindgen-lib.ts"; -export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { +export const SSLConfigSingleFile = b.union({ string: b.string, - buffer: b.arrayBuffer, + buffer: b.bufferSource.arrayBuffer, file: b.blob, }); -export const SSLConfigFile = b.union("SSLConfigFile", { +export const SSLConfigFile = b.union({ none: b.null, string: b.string, - buffer: b.arrayBuffer, + buffer: b.bufferSource.arrayBuffer, file: b.blob, array: b.array(SSLConfigSingleFile), }); -export const ALPNProtocols = b.union("ALPNProtocols", { +export const ALPNProtocols = b.union({ none: b.null, string: b.string, - buffer: b.arrayBuffer, + buffer: b.bufferSource.arrayBuffer, }); export const SSLConfig = b.dictionary( - { - name: "SSLConfig", - userFacingName: "TLSOptions", - fromJS: true, - }, { passphrase: b.string.nullable, dhParamsFile: { @@ -113,4 +108,8 @@ export const SSLConfig = b.dictionary( internalName: "ecdh_curve", }, }, + { + userFacingName: "TLSOptions", + fromJS: true, + }, ); diff --git a/src/runtime/socket/SocketConfig.bind.ts b/src/runtime/socket/SocketConfig.bind.ts index 135ee2b388f2..c05c4e8ccc3d 100644 --- a/src/runtime/socket/SocketConfig.bind.ts +++ b/src/runtime/socket/SocketConfig.bind.ts @@ -1,18 +1,13 @@ import * as b from "../../codegen/bindgen-lib.ts"; import { SSLConfig } from "./SSLConfig.bind.ts"; -export const SocketConfigBinaryType = b.enumeration("SocketConfigBinaryType", [ +export const SocketConfigBinaryType = b.enumeration([ ["arraybuffer", "ArrayBuffer"], ["buffer", "Buffer"], ["uint8array", "Uint8Array"], ]); export const SocketConfigHandlers = b.dictionary( - { - name: "SocketConfigHandlers", - userFacingName: "SocketHandler", - fromJS: true, - }, { open: { type: b.any, internalName: "onOpen" }, close: { type: b.any, internalName: "onClose" }, @@ -33,20 +28,19 @@ export const SocketConfigHandlers = b.dictionary( internalName: "binary_type", }, }, + { + userFacingName: "SocketHandler", + fromJS: true, + }, ); -export const SocketConfigTLS = b.union("SocketConfigTLS", { +export const SocketConfigTLS = b.union({ none: b.null, boolean: b.bool, object: SSLConfig, }); export const SocketConfig = b.dictionary( - { - name: "SocketConfig", - userFacingName: "SocketOptions", - fromJS: true, - }, { socket: { type: SocketConfigHandlers, @@ -59,7 +53,7 @@ export const SocketConfig = b.dictionary( internalName: "allow_half_open", }, hostname: { - type: b.string.loose.nullable.loose, + type: b.string.loose.falsyAsNull, altNames: ["host"], }, port: b.u16.loose.nullable, @@ -84,9 +78,13 @@ export const SocketConfig = b.dictionary( internalName: "pause_on_connect", }, unix: { - type: b.string.nullable.loose, + type: b.string.falsyAsNull, internalName: "unix_", // `unix` is a predefined C macro... }, fd: b.i32.optional, }, + { + userFacingName: "SocketOptions", + fromJS: true, + }, ); diff --git a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs index 3d35c6b65adf..d7f712277120 100644 --- a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs +++ b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs @@ -1,4 +1,4 @@ -// E0478: a payload that holds a value the GC would not see. Values go in the class's `values`. +// E0478: a payload that holds a value the GC would not see. Values go in the class's `internalFields`. struct Holder<'a> { callback: JsValue<'a>, } diff --git a/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs b/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs index 4e34399cee6c..b796bb85b111 100644 --- a/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs +++ b/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs @@ -16,9 +16,9 @@ impl Socket { } Ok(()) } - // `passThis`: the wrapper reaches the class's `values`, not the payload. + // `passThis`: the wrapper reaches the class's `internalFields`, not the payload. fn listen<'a>(&mut self, this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<'a>) { self.bytes_written = 0; - this.ondata_set_cached(heap, callback); + this.set_ondata_field(heap, callback); } } diff --git a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs index 5d5a928505af..6abafd054b6e 100644 --- a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs +++ b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs @@ -9,7 +9,7 @@ fn flush<'a>( this.with_mut(&mut cx.payloads, |socket| socket.pending.extend_from_slice(bytes)) }); let chunk = this.with(&cx.payloads, |socket| cx.heap.create_utf8_string(&socket.pending))?; - let Some(callback) = this.ondata_get_cached(&cx.heap).as_function() else { + let Some(callback) = this.ondata_field(&cx.heap).as_function() else { return Ok(JsValue::UNDEFINED); }; let result = callback.call(cx, this.as_value(), &[chunk.as_value()])?; diff --git a/test/internal/bindgen-compile-fail/pass/with_value.rs b/test/internal/bindgen-compile-fail/pass/with_value.rs index 82b9df1374ed..99a8ded1c477 100644 --- a/test/internal/bindgen-compile-fail/pass/with_value.rs +++ b/test/internal/bindgen-compile-fail/pass/with_value.rs @@ -1,6 +1,6 @@ // The converted form for now, the value itself for whatever holds it longer. -fn to_a_class_value<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> usize { - this.ondata_set_cached(heap, name.value); +fn to_an_internal_field<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> usize { + this.set_ondata_field(heap, name.value); name.converted.len() } fn to_a_strong<'a>(heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> (Vec, Strong) { diff --git a/test/internal/bindgen-compile-fail/prelude.rs b/test/internal/bindgen-compile-fail/prelude.rs index b1d4c97557af..3e0c71da301f 100644 --- a/test/internal/bindgen-compile-fail/prelude.rs +++ b/test/internal/bindgen-compile-fail/prelude.rs @@ -17,15 +17,15 @@ unsafe impl JsPayload for Socket { unreachable!() } } -pub trait SocketValues<'a>: Copy { - fn ondata_get_cached(self, heap: &JsHeap<'a>) -> JsValue<'a>; - fn ondata_set_cached(self, heap: &JsHeap<'a>, value: JsValue<'a, T>); +pub trait SocketInternalFields<'a>: Copy { + fn ondata_field(self, heap: &JsHeap<'a>) -> JsValue<'a>; + fn set_ondata_field(self, heap: &JsHeap<'a>, value: JsValue<'a, T>); } -impl<'a> SocketValues<'a> for JsWrapper<'a, Socket> { - fn ondata_get_cached(self, heap: &JsHeap<'a>) -> JsValue<'a> { +impl<'a> SocketInternalFields<'a> for JsWrapper<'a, Socket> { + fn ondata_field(self, heap: &JsHeap<'a>) -> JsValue<'a> { unsafe { self.internal_field(heap, 0) } } - fn ondata_set_cached(self, heap: &JsHeap<'a>, value: JsValue<'a, T>) { + fn set_ondata_field(self, heap: &JsHeap<'a>, value: JsValue<'a, T>) { unsafe { self.set_internal_field(heap, 0, value) } } } From b56193593c53848fe4102606a178552fa12bf124 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 03:06:37 +0000 Subject: [PATCH 04/44] Load schemas with import() so the generator runs under Node, and fix clippy and source lints --- src/codegen/bindgen-emit.ts | 50 +++++++++++++------ src/codegen/bindgen-lib-internal.ts | 37 ++++++++++++-- src/codegen/bindgen.ts | 10 ++-- src/jsc/bindgen.rs | 44 ++++++++-------- src/jsc/bindgen/extern_types.rs | 9 ++-- src/jsc/generated_bindgen.rs | 6 +++ src/jsc/lib.rs | 15 +----- src/runtime/api/BunObject.bind.ts | 2 +- src/runtime/api/BunObject.rs | 23 +++++---- src/runtime/api/glob.rs | 8 +-- src/runtime/generated_bindgen.rs | 6 +++ src/runtime/lib.rs | 16 +----- src/runtime/node/node_os.bind.ts | 2 +- src/runtime/node/node_os.rs | 6 +-- src/runtime/socket/Handlers.rs | 44 +++++++++------- src/runtime/socket/SSLConfig.rs | 13 ++--- .../fail/payload_without_unsafe.rs | 4 +- .../fail/value_in_payload.rs | 4 +- test/internal/bindgen-compile-fail/prelude.rs | 4 +- 19 files changed, 172 insertions(+), 131 deletions(-) create mode 100644 src/jsc/generated_bindgen.rs create mode 100644 src/runtime/generated_bindgen.rs diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 76fda2d9e1e9..948aca1b465e 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -774,7 +774,13 @@ const capabilityParameter = { }; const capabilityArgument = { none: [], heap: ["&cx.heap"], js: ["cx"] }; -const infallible = (call: string, throws: boolean) => (throws ? call : `Ok::<_, core::convert::Infallible>(${call})`); +const unit = new UnitType("undefined"); +const infallible = (call: string, throws: boolean, returns: Type | null = null) => + throws + ? call + : returns instanceof UnitType + ? `{ ${call}; Ok::<_, core::convert::Infallible>(()) }` + : `Ok::<_, core::convert::Infallible>(${call})`; const rustThunk = (symbol: string, parameters: string[], returns: string, body: string) => ` #[unsafe(no_mangle)] @@ -819,14 +825,14 @@ union Extern${type.name}Data { ${type.alternatives.map(a => ` ${rustIdent(snakeCase(a.name))}: ${a.type.rustExternType(scope)},`).join("\n")} } ${rustLayoutAssert(`Extern${type.name}`, type)} -pub enum ${type.name}${lifetime} { +${type.rustIsCopy ? "#[derive(Clone, Copy)]\n" : ""}pub enum ${type.name}${lifetime} { ${type.alternatives.map(a => (a.type instanceof UnitType ? ` ${a.variant},` : ` ${a.variant}(${a.type.rustType(scope)}),`)).join("\n")} } impl${lifetime} ${type.name}${lifetime} { /// # Safety /// \`value\` was just filled in by generated C++ and is read once. #[doc(hidden)] - pub unsafe fn from_extern(value: Extern${type.name}) -> Self { + pub unsafe fn from_extern(value: &Extern${type.name}) -> Self { // SAFETY: C++ initialized the arm \`tag\` names. unsafe { match value.tag { @@ -849,7 +855,7 @@ function rustForDictionary(type: DictionaryType, scope: RustScope): string { const lifetime = type.needsLifetime ? "<'a>" : ""; const fieldType = (t: Type) => (type.accepted ? t.rustType(scope) : t.rustResultFieldType(scope)); let text = ` -pub struct ${type.name}${lifetime} { +${type.accepted && type.rustIsCopy ? "#[derive(Clone, Copy)]\n" : ""}pub struct ${type.name}${lifetime} { ${type.members.map(member => ` pub ${member.rustName}: ${fieldType(member.type)},`).join("\n")} } `; @@ -875,7 +881,7 @@ impl${lifetime} ${type.name}${lifetime} { /// # Safety /// \`value\` was just filled in by generated C++ and is read once. #[doc(hidden)] - pub unsafe fn from_extern(value: Extern${type.name}) -> Self { + pub unsafe fn from_extern(value: &Extern${type.name}) -> Self { // SAFETY: as above, member by member. unsafe { Self { @@ -897,7 +903,7 @@ impl<'a> ${type.name}${lifetime} { // SAFETY: C++ fills \`result\` exactly when it returns true. unsafe { bindgen::extern_types::from_js(cx, |global| Bun__Bindgen__${type.name}__fromJS(global, value.to_js(), result.as_mut_ptr()))?; - Ok(Self::from_extern(result.assume_init())) + Ok(Self::from_extern(result.assume_init_ref())) } } } @@ -996,9 +1002,9 @@ bindgen::host_fn_return(cx, result, |heap, value| bindgen::to_js::any(heap, bind ].join(", ")})`; thunks += rustThunk( methodSymbolName(type, method), - [...(isStatic ? [] : ["this: *mut u8"]), ...rustExternParameters(method, scope)], + [...(isStatic ? [] : ["this: *mut core::ffi::c_void"]), ...rustExternParameters(method, scope)], "JSValue", - `${isStatic ? "" : `${takeThis}\n`}let result = ${infallible(call, method.throws)}; + `${isStatic ? "" : `${takeThis}\n`}let result = ${infallible(call, method.throws, method.returns)}; bindgen::host_fn_return(cx, result, |heap, value| ${method.returns.rustToJS("value", scope)})`, ); } @@ -1014,7 +1020,7 @@ bindgen::host_fn_return(cx, result, |heap, value| ${method.returns.rustToJS("val ); thunks += rustThunk( getterSymbolName(type, getter), - ["this: *mut u8"], + ["this: *mut core::ffi::c_void"], "JSValue", `${takeThis} let result = ${infallible(`<${rustPath} as ${trait}>::${getter.rustName}(${[...receiverArgument[receiver], ...capabilityArgument[getter.capability]].join(", ")})`, getter.throws)}; @@ -1036,10 +1042,10 @@ bindgen::host_fn_return(cx, result, |heap, value| ${getter.type.rustToJS("value" ); thunks += rustThunk( setterSymbolName(type, getter), - ["this: *mut u8", `value: ${getter.type.rustExternType(scope)}`], + ["this: *mut core::ffi::c_void", `value: ${getter.type.rustExternType(scope)}`], "bool", `${takeThis} -let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiverArgument[setterReceiver], ...capabilityArgument[getter.capability], getter.type.rustFromExtern("value", scope)].join(", ")})`, getter.throws)}; +let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiverArgument[setterReceiver], ...capabilityArgument[getter.capability], getter.type.rustFromExtern("value", scope)].join(", ")})`, getter.throws, unit)}; !bindgen::host_fn_return(cx, result, bindgen::to_js::undefined).is_empty()`, ); } @@ -1074,8 +1080,8 @@ ${declarations.map(declaration => ` ${declaration}`).join("\n")} } unsafe extern "C" { - fn ${symbolName(type, "create")}(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull; - fn ${symbolName(type, "fromJS")}(value: JSValue) -> *mut u8;${ + fn ${symbolName(type, "create")}(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull; + fn ${symbolName(type, "fromJS")}(value: JSValue) -> *mut core::ffi::c_void;${ type.construct ? ` fn ${symbolName(type, "getConstructor")}(global: *mut JSGlobalObject) -> JSValue;` @@ -1087,12 +1093,12 @@ unsafe impl bindgen::JsPayload for ${rustPath} { const INTERNAL_FIELDS_OFFSET: usize = ${offsetOfInternalFields}; const PAYLOAD_OFFSET: usize = ${offsetOfPayload(type)}; #[inline(always)] - unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull { + unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull { // SAFETY: forwarded. unsafe { ${symbolName(type, "create")}(global, structure) } } #[inline(always)] - fn cell_from_js(value: JSValue) -> *mut u8 { + fn cell_from_js(value: JSValue) -> *mut core::ffi::c_void { // SAFETY: a type check; reads the value's header. unsafe { ${symbolName(type, "fromJS")}(value) } } @@ -1137,7 +1143,7 @@ function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string { thunkSymbol(fn, signature), rustExternParameters(signature, scope), "JSValue", - `let result = ${infallible(call, signature.throws)}; + `let result = ${infallible(call, signature.throws, signature.returns)}; bindgen::host_fn_return(cx, result, |heap, value| ${signature.returns.rustToJS("value", scope)})`, ); }) @@ -1151,6 +1157,18 @@ export function rustForCrate(files: readonly SchemaFile[]): string { .map( file => ` /// \`${file.origin.stem}.bind.ts\` +#[allow( + non_snake_case, + non_upper_case_globals, + unreachable_pub, + dead_code, + unused_unsafe, + clippy::missing_safety_doc, + clippy::identity_op, + clippy::derivable_impls, + clippy::large_enum_variant, + clippy::trivially_copy_pass_by_ref +)] pub mod ${file.origin.rustModule} { use bun_jsc::{JSGlobalObject, JSValue, bindgen}; ${indent( diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index af28206e74a4..483709e70340 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -128,6 +128,10 @@ export abstract class Type { get borrowsFrame(): boolean { return this.dependencies.some(type => type.borrowsFrame); } + /** `rustType` is `Copy`. */ + get rustIsCopy(): boolean { + return this.dependencies.length > 0 && this.dependencies.every(type => type.rustIsCopy); + } get needsLifetime(): boolean { return this.holdsJSValue || this.borrowsFrame || this.borrowsHeap; } @@ -174,6 +178,9 @@ export abstract class Type { // ───────────────────────────── primitives ───────────────────────────── class PrimitiveType extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType: string; readonly tsType: string; readonly rustName: string; @@ -307,6 +314,9 @@ export class DoubleType extends PrimitiveType { /** Only as a union alternative or a return type. */ export class UnitType extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType: string; readonly tsType: string; constructor(tsType: "null" | "undefined") { @@ -389,6 +399,9 @@ export class StringType extends Type { * transcoded into the thunk's frame otherwise. */ export class Utf8Type extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType: string; readonly tsType = "string"; constructor(idlType = "::Bun::Bindgen::IDLStrictUTF8String") { @@ -440,6 +453,9 @@ export class Utf8Type extends Type { /** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ export class BytesType extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType = "::Bun::Bindgen::IDLBufferSourceBytes"; readonly tsType = "ArrayBuffer | ArrayBufferView"; get borrowsHeap(): boolean { @@ -470,6 +486,9 @@ export class BytesType extends Type { /** A typed array, `DataView` or `ArrayBuffer`, as a handle whose bytes are read through `with_bytes`. */ export class BufferSourceType extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; readonly tsType = "ArrayBuffer | ArrayBufferView"; /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ @@ -550,6 +569,9 @@ export class WithValueType extends Type { } export class AnyType extends Type { + get rustIsCopy(): boolean { + return true; + } readonly idlType = "::Bun::IDLRawAny"; readonly tsType = "any"; get holdsJSValue(): boolean { @@ -746,6 +768,9 @@ export class DefaultType extends Type { } export class ArrayType extends Type { + get rustIsCopy(): boolean { + return false; + } readonly element: Type; constructor(element: Type) { super(); @@ -823,6 +848,9 @@ export abstract class NamedType extends Type { } export class EnumType extends NamedType { + get rustIsCopy(): boolean { + return true; + } /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ @@ -969,7 +997,7 @@ export class DictionaryType extends NamedType { return this.path(scope, `Extern${this.name}`); } rustFromExtern(expr: string, scope: RustScope): string { - return `${this.path(scope, this.name)}::from_extern(${expr})`; + return `${this.path(scope, this.name)}::from_extern(&${expr})`; } rustToJS(value: string, scope: RustScope): string { return `${value}.to_js(heap)`; @@ -1018,7 +1046,7 @@ export class UnionType extends NamedType { return this.path(scope, `Extern${this.name}`); } rustFromExtern(expr: string, scope: RustScope): string { - return `${this.path(scope, this.name)}::from_extern(${expr})`; + return `${this.path(scope, this.name)}::from_extern(&${expr})`; } } @@ -1202,6 +1230,9 @@ export const selfType = new SelfType(); /** As an argument or a result: the wrapper, `JsWrapper<'a, T>`. */ export class ClassType extends NamedType { + get rustIsCopy(): boolean { + return true; + } readonly rustPath: string; readonly construct: Signature | null; readonly methods: readonly Method[]; @@ -1284,7 +1315,7 @@ export class ClassType extends NamedType { return { size: 8, align: 8 }; } rustExternType(): string { - return "*mut u8"; + return "*mut core::ffi::c_void"; } rustType(): string { return `bindgen::JsWrapper<'a, ${this.rustPath}>`; diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 8ec468141f42..206b969ce72e 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -9,8 +9,8 @@ import assert from "node:assert"; import fs from "node:fs"; -import { createRequire } from "node:module"; import path from "node:path"; +import { pathToFileURL } from "node:url"; import { analyze, cppCounts, @@ -32,8 +32,6 @@ import { } from "./bindgen-lib-internal.ts"; import { argParse, writeIfNotChanged } from "./helpers.ts"; -const require = createRequire(import.meta.url); - /** The Cargo package whose directory contains `file`. */ function crateOf(file: string): string { for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) { @@ -45,7 +43,7 @@ function crateOf(file: string): string { throw new Error(`${file} is not inside a Cargo package`); } -function load(file: string): SchemaFile { +async function load(file: string): Promise { const stem = path.basename(file, ".bind.ts"); const origin: Origin = { file, @@ -55,7 +53,7 @@ function load(file: string): SchemaFile { crate: crateOf(file), }; const result: SchemaFile = { origin, moduleDefinition: null, types: [], functions: [] }; - for (const [name, value] of Object.entries(require(file) as Record)) { + for (const [name, value] of Object.entries((await import(pathToFileURL(file).href)) as Record)) { if (value instanceof ModuleDefinition) { assert(name === "default", `${stem}.bind.ts: b.module() is the default export`); result.moduleDefinition = value; @@ -96,7 +94,7 @@ const sources = (args.sources as string) .filter(source => source.length > 0) .sort(); -const files = sources.map(load); +const files = await Promise.all(sources.map(load)); files.forEach(sortTypes); const names = files.flatMap(file => file.types.map(type => type.name)); assert(new Set(names).size === names.length, "two .bind.ts files export a type with the same name"); diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 6f3871ae28ad..a3f85667610c 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -100,7 +100,7 @@ impl<'a> JsHeap<'a> { } impl<'a> JsCall<'a> { - /// For generated thunks and event-loop dispatch. + /// For generated thunks, event-loop dispatch, and code that only has a `&JSGlobalObject`. /// /// # Safety /// `global` is the current thread's live global object, and no borrow of an outer `JsCall` is @@ -115,16 +115,6 @@ impl<'a> JsCall<'a> { f(unsafe { &mut *global.cast::>() }) } - /// For code that only has a `&JSGlobalObject`. - /// - /// # Safety - /// As [`with`](Self::with). - #[inline(always)] - pub unsafe fn from_global(global: &'a JSGlobalObject) -> &'a mut JsCall<'a> { - // SAFETY: `JsCall` is zero-sized; caller upholds exclusivity. - unsafe { &mut *global.as_mut_ptr().cast::>() } - } - /// Anything can be done with a `&JSGlobalObject`, so it costs the whole `JsCall` for as long as /// the reference lives. #[inline(always)] @@ -157,7 +147,8 @@ impl<'a> JsCall<'a> { /// Takes the pending exception off the VM. pub fn take_exception(&mut self, error: Error<'a>) -> Option> { - match error.0 { + let Error(state, ..) = error; + match state { ErrorState::ToThrow(value) => Some(JsValue::wrap(value)), ErrorState::Pending(JsError::Thrown) => { self.heap.global().try_take_exception().map(JsValue::wrap) @@ -185,7 +176,7 @@ enum ErrorState { pub struct Error<'a>(ErrorState, InvariantLifetime<'a>, NotSend); pub type Result<'a, T> = core::result::Result>; -impl Error<'_> { +impl<'a> Error<'a> { #[inline(always)] fn to_throw(value: JSValue) -> Self { Error(ErrorState::ToThrow(value), PhantomData, PhantomData) @@ -201,9 +192,8 @@ impl Error<'_> { #[doc(hidden)] #[cold] #[inline(never)] - pub fn into_host_fn_return(self, global: *mut JSGlobalObject) -> JSValue { - // SAFETY: the thunk's own global. - let global = unsafe { JSGlobalObject::opaque_ref_nn(global) }; + pub fn into_host_fn_return(self, cx: &mut JsCall<'a>) -> JSValue { + let global = cx.global(); match self.0 { ErrorState::Pending(error) => crate::host_fn::host_call_error_value(global, error), ErrorState::ToThrow(value) => { @@ -387,10 +377,10 @@ pub unsafe trait JsPayload: Sized + 'static { /// # Safety /// The caller writes the payload before anything else runs. #[doc(hidden)] - unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut c_void) -> NonNull; + unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut c_void) -> NonNull; /// `value`'s cell if it is a wrapper of `Self`, else null. #[doc(hidden)] - fn cell_from_js(value: JSValue) -> *mut u8; + fn cell_from_js(value: JSValue) -> *mut c_void; /// A new JS object that owns `self`. #[inline] @@ -411,7 +401,11 @@ pub unsafe trait JsPayload: Sized + 'static { /// The JS object that wraps a `T`. A cell pointer; the payload is reached only through /// [`JsPayloads`]. #[repr(transparent)] -pub struct JsWrapper<'a, T: JsPayload>(NonNull, InvariantLifetime<'a>, PhantomData<*const T>); +pub struct JsWrapper<'a, T: JsPayload>( + NonNull, + InvariantLifetime<'a>, + PhantomData<*const T>, +); impl Clone for JsWrapper<'_, T> { #[inline(always)] fn clone(&self) -> Self { @@ -427,7 +421,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { /// `cell` is a live wrapper of `T` belonging to the call `'a`. #[doc(hidden)] #[inline(always)] - pub unsafe fn from_extern(cell: NonNull) -> Self { + pub unsafe fn from_extern(cell: NonNull) -> Self { JsWrapper(cell, PhantomData, PhantomData) } @@ -475,7 +469,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { #[inline(always)] fn payload(self) -> *mut T { // SAFETY: generated offset inside the cell. - unsafe { self.0.as_ptr().add(T::PAYLOAD_OFFSET).cast() } + unsafe { self.0.as_ptr().byte_add(T::PAYLOAD_OFFSET).cast() } } #[inline(always)] @@ -516,7 +510,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { JsValue::wrap(unsafe { self.0 .as_ptr() - .add(T::INTERNAL_FIELDS_OFFSET) + .byte_add(T::INTERNAL_FIELDS_OFFSET) .cast::() .add(index) .read() @@ -553,7 +547,9 @@ impl JsWrapperStrong { #[inline] pub fn get<'a>(&self, _: &JsHeap<'a>) -> JsWrapper<'a, T> { // SAFETY: made from a `JsWrapper`; a cell value is its pointer. - unsafe { JsWrapper::from_extern(NonNull::new_unchecked(self.0.get().encoded() as *mut u8)) } + unsafe { + JsWrapper::from_extern(NonNull::new_unchecked(self.0.get().encoded() as *mut c_void)) + } } } @@ -670,5 +666,5 @@ pub fn host_fn_return<'a, T, E: IntoError<'a>>( }, Err(error) => error.into_error(&cx.heap), }; - error.into_host_fn_return(cx.heap.global_ptr()) + error.into_host_fn_return(cx) } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index aa0258dcc156..185b5e32f947 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -2,7 +2,7 @@ //! C++ and Rust. Everything here is plain data; ownership is taken by the generated conversion that //! reads it, exactly once. -use core::ffi::c_uint; +use core::ffi::{c_uint, c_void}; use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; @@ -36,8 +36,11 @@ union ExternOptionalData { impl ExternOptional { #[inline(always)] pub fn get(self) -> Option { + if self.tag == 0 { + return None; + } // SAFETY: C++ initialized the arm `tag` names. - (self.tag != 0).then(|| unsafe { self.data.some }) + Some(unsafe { self.data.some }) } } @@ -124,7 +127,7 @@ unsafe extern "C" { pub fn Bun__Bindgen__enumerationValueToJS(global: *mut JSGlobalObject, index: u32) -> JSValue; pub fn Bun__Bindgen__setInternalField( global: *mut JSGlobalObject, - cell: *mut u8, + cell: *mut c_void, offset: u32, value: JSValue, ); diff --git a/src/jsc/generated_bindgen.rs b/src/jsc/generated_bindgen.rs new file mode 100644 index 000000000000..ffbf61e14a52 --- /dev/null +++ b/src/jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_jsc.rs" +)); diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index a07914cc0d72..93a631b08254 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1579,16 +1579,5 @@ unsafe extern "C" { #[path = "generated.rs"] pub mod generated; -/// What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. -#[allow( - non_snake_case, - unused_unsafe, - clippy::missing_safety_doc, - clippy::identity_op -)] -pub mod generated_bindgen { - include!(concat!( - env!("BUN_CODEGEN_DIR"), - "/generated_bindgen_bun_jsc.rs" - )); -} +#[path = "generated_bindgen.rs"] +pub mod generated_bindgen; diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index d71add25dd97..33d728408a77 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -9,7 +9,7 @@ export const BracesOptions = b.dictionary({ export const braces = b.fn({ args: { - input: b.string.loose, + input: b.string.loose.utf8, options: BracesOptions.nullable.default({}), }, returns: b.any, diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 1ba707bfc74d..808f5da87df6 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -388,10 +388,10 @@ fn shell_escape(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult pub(crate) fn braces<'a>( cx: &mut JsCall<'a>, - input: BunString, + input: &'a [u8], options: BracesOptions, ) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(|global| braces_impl(global, &input, options)) + cx.with_global_value(|global| braces_impl(global, input, options)) } /// `Bun.gc(force)`: heap size after the collection. @@ -401,17 +401,15 @@ pub(crate) fn gc_from_js(cx: &mut JsCall<'_>, force: bool) -> usize { fn braces_impl( global: &JSGlobalObject, - brace_str: &BunString, + brace_slice: &[u8], opts: BracesOptions, ) -> JsResult { - let brace_slice = brace_str.to_utf8(); - let mut arena = bun_alloc::Arena::new(); let _ = &mut arena; let mut lexer_output = 'lexer_output: { - if strings::is_all_ascii(brace_slice.slice()) { - break 'lexer_output match Braces::Lexer::tokenize(brace_slice.slice()) { + if strings::is_all_ascii(brace_slice) { + break 'lexer_output match Braces::Lexer::tokenize(brace_slice) { Ok(v) => v, Err(err) => { return Err( @@ -421,7 +419,7 @@ fn braces_impl( }; } - match Braces::NewLexer::<{ Braces::StringEncoding::Wtf8 }>::tokenize(brace_slice.slice()) { + match Braces::NewLexer::<{ Braces::StringEncoding::Wtf8 }>::tokenize(brace_slice) { Ok(v) => break 'lexer_output v, Err(err) => { return Err( @@ -456,7 +454,7 @@ fn braces_impl( } if expansion_count == 0 { - return bun_string_jsc::to_js_array(global, core::slice::from_ref(brace_str)); + return bun_string_jsc::to_js_array(global, &[BunString::from_bytes(brace_slice)]); } // Hard cap before preallocation: `calculate_expanded_amount` saturates to @@ -1751,8 +1749,11 @@ fn get_archive_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValu fn get_glob_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { // SAFETY: a lazy property callback, entered from JSC with no `JsCall` borrowed. - let cx = unsafe { JsCall::from_global(global_this) }; - crate::api::glob::Glob::get_constructor(&cx.heap).to_js() + unsafe { + JsCall::with(global_this.as_mut_ptr(), |cx| { + crate::api::glob::Glob::get_constructor(&cx.heap).to_js() + }) + } } fn get_image_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { diff --git a/src/runtime/api/glob.rs b/src/runtime/api/glob.rs index 911b02546c75..d7941280f36b 100644 --- a/src/runtime/api/glob.rs +++ b/src/runtime/api/glob.rs @@ -329,12 +329,12 @@ impl JobContext for WalkTask { promise.reject_with_async_stack(global, err.to_js(global))?; return Ok(()); } - // SAFETY: a job's completion, entered from the event loop with no `JsCall` borrowed. - let heap = &unsafe { JsCall::from_global(global) }.heap; - match this.walker.matched_paths_to_js(heap) { + let settle = |cx: &mut JsCall<'_>| match this.walker.matched_paths_to_js(&cx.heap) { Ok(paths) => promise.resolve(global, paths.to_js()), Err(err) => promise.reject(global, Err(err.into_js_error(global))), - } + }; + // SAFETY: a job's completion, entered from the event loop with no `JsCall` borrowed. + unsafe { JsCall::with(global.as_mut_ptr(), settle) } } } diff --git a/src/runtime/generated_bindgen.rs b/src/runtime/generated_bindgen.rs new file mode 100644 index 000000000000..0eba402010d0 --- /dev/null +++ b/src/runtime/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_runtime.rs" +)); diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index c6174a017a45..94988537961e 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -64,21 +64,7 @@ pub(crate) mod napi; pub(crate) mod run_main; pub(crate) mod timer; -/// What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. -#[allow( - non_snake_case, - unreachable_pub, - dead_code, - unused_unsafe, - clippy::missing_safety_doc, - clippy::identity_op -)] -pub(crate) mod generated_bindgen { - include!(concat!( - env!("BUN_CODEGEN_DIR"), - "/generated_bindgen_bun_runtime.rs" - )); -} +pub(crate) mod generated_bindgen; // include!()s ${BUN_CODEGEN_DIR}/generated_bindgen_bun_runtime.rs pub(crate) mod generated_classes; // include!()s ${BUN_CODEGEN_DIR}/generated_classes.rs pub(crate) mod generated_host_exports; // include!()s ${BUN_CODEGEN_DIR}/generated_host_exports.rs pub(crate) mod generated_js2native; // include!()s ${BUN_CODEGEN_DIR}/generated_js2native.rs diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 9e8f589aedeb..1a6c2af30367 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -25,7 +25,7 @@ export const totalmem = b.fn({ returns: b.u64 }); export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); export const UserInfoOptions = b.dictionary({ - encoding: b.string.loose.default(""), + encoding: b.string.loose.utf8.default(""), }); export const userInfo = b.fn({ args: { diff --git a/src/runtime/node/node_os.rs b/src/runtime/node/node_os.rs index 98a75665de0c..cc545cfcded1 100644 --- a/src/runtime/node/node_os.rs +++ b/src/runtime/node/node_os.rs @@ -107,9 +107,9 @@ mod _impl { } pub(crate) fn user_info<'a>( cx: &mut JsCall<'a>, - options: UserInfoOptions, + options: UserInfoOptions<'a>, ) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(|global| super::user_info(global, &options)) + cx.with_global_value(|global| super::user_info(global, options)) } pub(crate) fn set_priority<'a>( cx: &mut JsCall<'a>, @@ -1391,7 +1391,7 @@ mod _impl { pub(crate) fn user_info( global_this: &JSGlobalObject, - options: &crate::generated_bindgen::node_os::UserInfoOptions, + options: crate::generated_bindgen::node_os::UserInfoOptions<'_>, ) -> JsResult { let _ = options; // TODO: diff --git a/src/runtime/socket/Handlers.rs b/src/runtime/socket/Handlers.rs index f82375736f30..484c5de62bb8 100644 --- a/src/runtime/socket/Handlers.rs +++ b/src/runtime/socket/Handlers.rs @@ -312,12 +312,14 @@ impl Handlers { opts: JSValue, mode: SocketMode, ) -> JsResult> { + let from_js = |cx: &mut JsCall<'_>| { + let opts = JsValue::new(&cx.heap, opts); + let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) + .map_err(|e| e.into_js_error(global_object))?; + Self::from_generated(global_object, &generated, mode) + }; // SAFETY: reached from a host function with no `JsCall` borrowed. - let cx = unsafe { JsCall::from_global(global_object) }; - let opts = JsValue::new(&cx.heap, opts); - let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) - .map_err(|e| e.into_js_error(global_object))?; - Self::from_generated(global_object, &generated, mode) + unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } } pub(crate) fn from_generated( @@ -412,16 +414,18 @@ impl Handlers { global_object: &JSGlobalObject, opts: JSValue, ) -> JsResult { + let from_js = |cx: &mut JsCall<'_>| { + let opts = JsValue::new(&cx.heap, opts); + let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) + .map_err(|e| e.into_js_error(global_object))?; + let callbacks = Self::validate_callbacks(global_object, &generated)?; + Ok(ReloadedHandlers { + callbacks, + binary_type: binary_type_from_generated(generated.binary_type), + }) + }; // SAFETY: reached from a host function with no `JsCall` borrowed. - let cx = unsafe { JsCall::from_global(global_object) }; - let opts = JsValue::new(&cx.heap, opts); - let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) - .map_err(|e| e.into_js_error(global_object))?; - let callbacks = Self::validate_callbacks(global_object, &generated)?; - Ok(ReloadedHandlers { - callbacks, - binary_type: binary_type_from_generated(generated.binary_type), - }) + unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } } /// Writes the validated callbacks into the existing cell, so the listener @@ -609,11 +613,13 @@ impl SocketConfig { global_object: &JSGlobalObject, mode: SocketMode, ) -> JsResult { + let from_js = |cx: &mut JsCall<'_>| { + let opts = JsValue::new(&cx.heap, opts); + let generated = GeneratedSocketConfig::from_js(cx, opts) + .map_err(|e| e.into_js_error(global_object))?; + Self::from_generated(vm, global_object, generated, mode) + }; // SAFETY: reached from a host function with no `JsCall` borrowed. - let cx = unsafe { JsCall::from_global(global_object) }; - let opts = JsValue::new(&cx.heap, opts); - let generated = - GeneratedSocketConfig::from_js(cx, opts).map_err(|e| e.into_js_error(global_object))?; - Self::from_generated(vm, global_object, generated, mode) + unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } } } diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index dd01ee90a64b..bb4da1e0fa05 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -145,13 +145,14 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { + let from_js = |cx: &mut jsc::bindgen::JsCall<'_>| { + let value = jsc::bindgen::JsValue::new(&cx.heap, value); + let generated = + generated::SSLConfig::from_js(cx, value).map_err(|e| e.into_js_error(global))?; + Self::from_generated(vm, global, &generated, is_server) + }; // SAFETY: reached from a host function with no `JsCall` borrowed. - let cx = unsafe { jsc::bindgen::JsCall::from_global(global) }; - let value = jsc::bindgen::JsValue::new(&cx.heap, value); - let generated = - generated::SSLConfig::from_js(cx, value).map_err(|e| e.into_js_error(global))?; - // `generated` dropped at scope exit - Self::from_generated(vm, global, &generated, is_server) + unsafe { jsc::bindgen::JsCall::with(global.as_mut_ptr(), from_js) } } fn from_generated( diff --git a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs index 83d7dcfb10f3..fd031b375300 100644 --- a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs +++ b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs @@ -3,10 +3,10 @@ struct Fake; impl JsPayload for Fake { const INTERNAL_FIELDS_OFFSET: usize = 0; const PAYLOAD_OFFSET: usize = 0; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut u8 { + fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { unreachable!() } } diff --git a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs index d7f712277120..00a7c007abd0 100644 --- a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs +++ b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs @@ -5,10 +5,10 @@ struct Holder<'a> { unsafe impl<'a> JsPayload for Holder<'a> { const INTERNAL_FIELDS_OFFSET: usize = 24; const PAYLOAD_OFFSET: usize = 24; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut u8 { + fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { unreachable!() } } diff --git a/test/internal/bindgen-compile-fail/prelude.rs b/test/internal/bindgen-compile-fail/prelude.rs index 3e0c71da301f..5405dffc6590 100644 --- a/test/internal/bindgen-compile-fail/prelude.rs +++ b/test/internal/bindgen-compile-fail/prelude.rs @@ -10,10 +10,10 @@ pub struct Socket { unsafe impl JsPayload for Socket { const INTERNAL_FIELDS_OFFSET: usize = 24; const PAYLOAD_OFFSET: usize = 32; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { + unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut u8 { + fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { unreachable!() } } From bb9d2d8ac85f9e5eaae15c18c89915a49b3b1472 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 03:38:47 +0000 Subject: [PATCH 05/44] Fix a use-after-free of substring bytes, read buffers and members late, restore array options and ERR_MISSING_ARGS, and delete unused surface --- .../implementing-jsc-classes-rust/SKILL.md | 2 +- .github/workflows/rust-lints.yml | 10 + docs/project/bindgen.mdx | 121 ++++++------ mordant-baseline.toml | 1 - scripts/build/codegen.ts | 15 +- src/bun_alloc/lib.rs | 20 -- src/codegen/bindgen-emit.ts | 98 +++++----- src/codegen/bindgen-lib-internal.ts | 111 +++++------ src/codegen/bindgen-lib.ts | 31 +--- src/codegen/bindgen.ts | 44 ++--- src/collections/array_list.rs | 44 ----- src/collections/lib.rs | 3 - src/js/internal-for-testing.ts | 2 + src/js/node/os.ts | 4 +- src/jsc/JSArray.rs | 16 -- src/jsc/NodeModuleModule.bind.ts | 2 +- src/jsc/bindgen.rs | 172 ++++-------------- src/jsc/bindgen/extern_types.rs | 18 +- src/jsc/bindgen_test.bind.ts | 25 ++- src/jsc/bindgen_test.rs | 44 ++++- .../bindings/Bindgen/DictionaryMemberReader.h | 38 ++-- src/jsc/bindings/Bindgen/IDLConvert.h | 3 +- src/jsc/bindings/Bindgen/IDLTypes.h | 3 +- src/jsc/bindings/Bindgen/Identifiers.h | 6 +- src/jsc/bindings/Bindgen/JSBindgenObject.h | 19 +- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 35 ++-- .../bindings/BindgenAdditionsToGlobalObject.h | 28 +++ src/jsc/bindings/BindgenIdentifiers.cpp | 29 ++- src/jsc/bindings/JSBindgenObject.cpp | 37 +++- src/jsc/bindings/ZigGlobalObject.cpp | 19 +- src/jsc/bindings/ZigGlobalObject.h | 15 +- src/jsc/bindings/bindings.cpp | 9 - src/jsc/fmt_jsc.bind.ts | 2 +- src/jsc/fmt_jsc.rs | 4 +- src/jsc/generated.rs | 6 +- src/jsc/host_fn.rs | 29 --- src/jsc/lib.rs | 2 +- src/runtime/api/BunObject.bind.ts | 13 +- src/runtime/api/BunObject.rs | 23 +-- src/runtime/api/Glob.bind.ts | 23 ++- src/runtime/node/node_os.bind.ts | 11 +- src/runtime/node/node_os.rs | 91 +++------ src/runtime/socket/Handlers.rs | 28 +-- src/runtime/socket/SSLConfig.bind.ts | 95 ++-------- src/runtime/socket/SSLConfig.rs | 9 +- src/runtime/socket/SocketConfig.bind.ts | 45 +---- test/internal/bindgen-compile-fail.test.ts | 21 ++- .../fail/payload_without_unsafe.rs | 3 - .../fail/value_between_calls.rs | 4 +- .../fail/value_in_payload.rs | 3 - .../pass/keep_past_the_call.rs | 2 +- .../pass/reentrant_method.rs | 2 +- test/internal/bindgen-compile-fail/prelude.rs | 3 - test/internal/bindgen.test.ts | 116 +++++++++--- .../build-codegen-declared-outputs.test.ts | 3 +- test/js/bun/glob/proto.test.ts | 40 ++++ test/js/bun/net/socket.test.ts | 18 ++ test/js/bun/shell/brace.test.ts | 32 ++++ test/js/node/os/os.test.js | 49 +++++ 59 files changed, 770 insertions(+), 931 deletions(-) delete mode 100644 src/collections/array_list.rs create mode 100644 src/jsc/bindings/BindgenAdditionsToGlobalObject.h diff --git a/.claude/skills/implementing-jsc-classes-rust/SKILL.md b/.claude/skills/implementing-jsc-classes-rust/SKILL.md index 6dd76be69442..890f6eab62ce 100644 --- a/.claude/skills/implementing-jsc-classes-rust/SKILL.md +++ b/.claude/skills/implementing-jsc-classes-rust/SKILL.md @@ -124,7 +124,7 @@ Never store raw `JSValue` in a struct field. Declare a slot in `.classes.ts` (`v ## Reference implementations -- `src/runtime/api/glob.rs` + `Glob.classes.ts` — constructor, methods, `hasPendingActivity`, default finalize +- `src/runtime/api/Shell.classes.ts` — constructor, methods, `hasPendingActivity` - `src/runtime/api/cron.rs` + `cron.classes.ts` — `noConstructor`, cached getter, `values: [...]`, custom finalize - `src/runtime/image/Image.rs:56` — the `pub use crate::generated_classes::js_Image as js;` one-liner - `src/jsc/host_fn.rs` — the host-fn adapters the codegen dispatches through diff --git a/.github/workflows/rust-lints.yml b/.github/workflows/rust-lints.yml index a4d3a9176c55..2834ec96307d 100644 --- a/.github/workflows/rust-lints.yml +++ b/.github/workflows/rust-lints.yml @@ -10,7 +10,10 @@ on: - "src/**/*.rs" - "src/**/Cargo.toml" - "src/**/*.classes.ts" + - "src/**/*.bind.ts" - "src/codegen/**" + - "test/internal/bindgen-compile-fail.test.ts" + - "test/internal/bindgen-compile-fail/**" - "scripts/build/**" - "scripts/build.ts" - "scripts/rust-miri.ts" @@ -73,6 +76,13 @@ jobs: BUN_CODEGEN_DIR: ${{ github.workspace }}/build/debug/codegen run: cargo check --workspace --all-targets --keep-going + - name: bindgen compile-fail cases + # Programs `bun_jsc::bindgen` has to make rustc refuse. The test lanes + # have no toolchain and skip the file. + env: + BUN_CODEGEN_DIR: ${{ github.workspace }}/build/debug/codegen + run: bun test test/internal/bindgen-compile-fail.test.ts + miri: name: cargo miri test runs-on: ubuntu-latest diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 4b641c1e9793..e9c7f34e1922 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -18,10 +18,10 @@ The generator lives in `src/codegen/bindgen*.ts`, the Rust it generates against `src/jsc/bindgen.rs`, and the C++ in `src/jsc/bindings/Bindgen/` and `src/jsc/bindings/BunIDL*.h`. -These other generators do overlapping work and are to be replaced by this one: +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. +- "JS2Native", allowing ad-hoc calls from `src/js` to native code (`$rust`, `$cpp`). - `#[bun_jsc::host_fn]` functions, which take a `CallFrame`. ## Functions @@ -57,7 +57,8 @@ pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { `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. +a function gives its full path instead. `length` sets the function's `.length`, +which is otherwise the number of required arguments. Nothing else is written by hand. If the Rust signature does not match the schema, the generated code does not compile. @@ -86,45 +87,50 @@ runs. `throws: true` makes the return type a `Result`. The error is a `bindgen::Error<'a>`, made by `heap.throw(..)`, `heap.throw_type_error(..)`, `heap.throw_range_error(..)` or `heap.throw_value(value)`, or any type that -implements `bindgen::IntoError` (`bun_sys::Error` does), which lets a +implements `bindgen::IntoError` (`bun_sys::Error` and `JsError` do), which lets a `capability: "none"` function fail too. -`cx.with_global(|global| ..)` and `cx.with_global_value(|global| ..)` run code -that takes a `&JSGlobalObject`. +`cx.global()` is the `&JSGlobalObject`, for code that takes one. It borrows all of +`cx`. Such a function can return `JsResult`, and a plain `JSValue` under +`returns: b.any`. ### Overloads `b.overloads(a, b, ..)` is one JS function with several signatures, told apart by -how many arguments were passed. Each names its own Rust function. +how many arguments were passed: the one with the most required arguments that +the call satisfies. Each names its own Rust function. ## 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 conversion throws -`ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the -dictionary member. - -| Schema | Rust, as an argument | Accepts | -| -------------------------------------- | -------------------------- | ---------------------------------------------------------------------- | -| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | -| `b.i8` … `b.u64`, `b.usize` | `i8` … `u64`, `usize` | an integer in range; see below | -| `b.f64` | `f64` | a number; `.finite` rejects NaN and infinities, `.loose` is `ToNumber` | -| `b.string` | `bun_core::String` | a string; `.loose` is `ToString` | -| `b.string.utf8` | `&'a [u8]` | the same, and a `String` object | -| `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | -| `b.bufferSource.bytes` | `&'a [u8]` | the same; not with `capability: "js"` | -| `b.bufferSource.arrayBuffer`, `b.blob` | `ExternalShared<..>` | a reference-counted handle on the native object | -| `b.any` | `JsValue<'a>` | anything | -| `b.array(T)` | `Vec` | an array | -| `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` | the first alternative that fits | -| `b.class(options)` | `JsWrapper<'a, T>` | an instance | +that is an integer in range, and nothing else. A failed strict, `.loose` or +`.validateInteger` conversion throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` +naming the argument or the dictionary member; `.wrap` and `.enforceRange` throw +what WebIDL does. Too few arguments throw `ERR_MISSING_ARGS`. + +| Schema | Rust, as an argument | Accepts | +| ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- | +| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | +| `b.u8` `b.u16` `b.u32` `b.u64` `b.usize` `b.i32` | the same | an integer in range; see below | +| `b.f64` | `f64` | a number | +| `b.string` | `bun_core::String` | a string; `.loose` is `ToString` | +| `b.string.utf8` | `&'a [u8]` | the same, and a `String` object | +| `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | +| `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) | +| `b.bufferSource.arrayBuffer` | `ExternalShared` | a typed array, `DataView` or `ArrayBuffer` | +| `b.blob` | `ExternalShared` | a `Blob` | +| `b.any` | `JsValue<'a>` | anything | +| `b.null` | `()` | `null` or `undefined`; only in a union | +| `b.array(T)` | `Vec` | an array | +| `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` | the first alternative that fits | +| `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. -Any type takes one of these, last: +A type takes one of these, last (`b.any` already accepts `undefined` and `null`, and takes only `.withValue`): - `.optional`: `undefined` means "not given"; `Option`. - `.nullable`: `null` or `undefined` means "not given"; `Option`. @@ -146,8 +152,10 @@ that is `IntoIterator`. `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. Either way nothing is allocated for a short -string and nothing needs freeing. An unpaired surrogate becomes U+FFFD. +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`. `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 @@ -162,24 +170,29 @@ export const BracesOptions = b.dictionary({ }); ``` -Members are read and converted in declaration order, each once. A member +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, internalName, altNames }` to rename its field or to accept other spellings. A dictionary whose members can all be left out implements `Default`. A member is looked up on the object and its prototype chain, **stopping before -`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 generated code reads them straight out of the object's property -table. The two paths are indistinguishable to JavaScript; +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 generated code reads the member 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 a prebuilt `Structure`: one allocation and one store per member. An `.optional` member that is `None` is -left out. +left out, and the object is then built one property at a time. -`b.dictionary(members, { fromJS: true })` also generates -`Name::from_js(cx, value)`, for code that is not a generated binding yet. +The second argument of `b.dictionary` takes: + +| Key | What it does | +| ---------------- | ---------------------------------------------------------------------------------------- | +| `userFacingName` | what error messages call it, when that is not its name | +| `allowArray` | an array is an object with none of the members, not an error | +| `fromJS` | also generates `Name::from_js(global, value, \|converted\| ..)`, for a `#[host_fn]` body | ### Enumerations @@ -193,9 +206,11 @@ allocates nothing after the first time. ### Buffers -`b.bufferSource.bytes` is only for functions that cannot run JavaScript, because JavaScript -can detach, resize or transfer a buffer. A function that can takes `b.bufferSource` -itself and looks the bytes up when it needs them: +JavaScript can detach, resize or transfer a buffer, so `b.bufferSource.bytes` is only +for an argument (not a member or an alternative) of a function that cannot run +JavaScript. The generated code reads pointer and length after every other argument +is converted, since a conversion can run JavaScript too. A function that can run +JavaScript takes `b.bufferSource` itself and looks the bytes up when it needs them: ```rust let before = buffer.with_bytes(&cx.heap, sum); @@ -203,7 +218,8 @@ callback.call(cx, JsValue::UNDEFINED, &[])?; let after = buffer.with_bytes(&cx.heap, sum); ``` -Calling `callback.call(cx, ..)` inside the closure does not compile. +Calling `callback.call(cx, ..)` inside the closure does not compile. A detached or +out-of-bounds view is empty. ### Keeping the original value @@ -234,7 +250,7 @@ task or sent to a thread. `JsValue<'a, T>` is one known to be a `JSString`, 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 `JsValue::from_strong(heap, &strong)`. It is made, read and dropped on the JS thread. +- a `Strong`, made with `value.to_strong(heap)` and read with `JsValue::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`: the GC does not look inside heap allocations. @@ -283,23 +299,26 @@ runs on the collector's threads. | Key | What it declares | | ---------------- | -------------------------------------------------------------------------------------------- | +| `rustPath` | the struct | | `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.to_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; `fn` renames the trait method, `privateSymbol` keys it by a private name | -| `{ getter: T, setter: true, cache: true, capability, .. }` | an accessor; `cache` computes the value once per object | -| `{ builtin: true, length }` | the export of that name in `src/js/builtins/.ts` | +| 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/.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 in its own signatures. `Glob::get_constructor(heap)` is the -constructor function and `Glob::from_js(value)` the wrapper, if `value` is one. +constructor function. ### The receiver @@ -342,5 +361,5 @@ thread when the work completes. ## Tests -- `test/internal/bindgen.test.ts`: every conversion, with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. -- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. +- `test/internal/bindgen.test.ts`: conversions with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. What that schema does not use is covered by the suites of the APIs that do. +- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. It needs a Rust toolchain: the `cargo clippy` job of `rust-lints.yml` runs it, the test lanes skip it. diff --git a/mordant-baseline.toml b/mordant-baseline.toml index 2e40c3742b0a..acfe84717c39 100644 --- a/mordant-baseline.toml +++ b/mordant-baseline.toml @@ -71,7 +71,6 @@ "defaulted_failure:src/jsc/ConsoleObject.rs" = 4 "generic_body_not_generic:src/jsc/ConsoleObject.rs" = 6 "generic_body_not_generic:src/jsc/JSPropertyIterator.rs" = 1 -"unused_pub:src/jsc/bindgen.rs" = 6 "unused_pub:src/jsc/host_fn.rs" = 2 "unused_pub:src/jsc/lib.rs" = 1 diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index 96e58011db70..adc8e5e80227 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -18,8 +18,8 @@ * depfile entry does that within the same run only if it names a declared * output, which ninja re-stats after the step ran. A file no edge declares * is stat'd once at startup, so a compile that includes it picks up a rerun - * of the step one build late. That is why emitBindgen declares the per-file - * Generated*.h headers and not just the .cpp files that get compiled. + * of the step one build late. That is why emitBindgen declares + * the per-file Generated*.h headers and not just the .cpp that gets compiled. * * Files nothing compiles may stay undeclared (bundle-modules' eval/ dir, * JSSink.lut.txt consumed within its own step). The .d.ts files src/js is @@ -884,22 +884,21 @@ export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { resolve(cfg.codegenDir, "GeneratedBindings+Tables.h"), resolve(cfg.codegenDir, "GeneratedBindings+Counts.h"), ]; - const rust = [...new Set(sources.bindgen.map(crateOf))].map(crate => - resolve(cfg.codegenDir, `generated_bindgen_${crate}.rs`), - ); + const crates = sources.bindgen.map(crateOf); + const rust = [...new Set(crates)].map(crate => resolve(cfg.codegenDir, `generated_bindgen_${crate}.rs`)); const types = resolve(cfg.typesDir, "Bindgen.d.ts"); n.build({ outputs: [...cpp, ...headers, ...rust, types], rule: "codegen", - inputs: [script, ...sources.bindgen, ...sources.bindgenInternal], + inputs: [...sources.bindgen, ...sources.bindgenInternal], orderOnlyInputs: [dirStamp], vars: { cwd: cfg.cwd, desc: ".bind.ts → Generated*.{cpp,h}, generated_bindgen_*.rs", args: shJoin(cfg, [ script, - `--sources=${sources.bindgen.join(",")}`, + `--sources=${sources.bindgen.map((src, i) => `${crates[i]}=${src}`).join(",")}`, `--codegen-path=${cfg.codegenDir}`, `--types-path=${cfg.typesDir}`, ]), @@ -922,7 +921,7 @@ function pascalCase(s: string): string { .join(""); } -/** Same lookup as `crateOf()` in src/codegen/bindgen.ts: the Cargo package whose directory contains `file`. */ +/** The Cargo package whose directory contains `file`. */ function crateOf(file: string): string { for (let dir = dirname(file); dir !== dirname(dir); dir = dirname(dir)) { const manifest = resolve(dir, "Cargo.toml"); diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index ac62c74fa42b..0cae99668a53 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -570,26 +570,6 @@ unsafe impl core::alloc::GlobalAlloc for Mimalloc { } } -/// Resize a mimalloc-owned buffer, taking a raw pointer for callers that -/// cannot soundly materialize a `&mut [u8]` over their buffer (e.g. it contains -/// uninitialized or padding bytes). Returns the new base pointer; -/// `min(old_size, new_size)` prefix bytes are preserved. -/// -/// # Safety -/// `ptr` must be a live allocation from the default (mimalloc) allocator with -/// alignment ≤ `MI_MAX_ALIGN_SIZE`. After return, `ptr` is invalidated. -pub unsafe fn realloc_raw( - ptr: *mut u8, - new_size: usize, -) -> core::result::Result<*mut u8, AllocError> { - // SAFETY: caller guarantees `ptr` is a mimalloc-owned block. - let new_ptr = unsafe { mimalloc::mi_realloc(ptr.cast(), new_size) }; - if new_ptr.is_null() { - return Err(AllocError); - } - Ok(new_ptr.cast::()) -} - // ────────────────────────────────────────────────────────────────────────── // Symbols hoisted DOWN into T0 so higher tiers can re-import without cycles. // ────────────────────────────────────────────────────────────────────────── diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 948aca1b465e..952a2e06a8d4 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -1,5 +1,4 @@ -// Turns the loaded schema into text: one C++ header and source per `.bind.ts` file, one table header -// for the whole program, one Rust file per crate, one `.d.ts`. +// Turns the loaded schema into text. import assert from "node:assert"; import { @@ -319,13 +318,17 @@ template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObje ctx.throwNotObject(lexicalGlobalObject, scope); return {}; } - bool isArray = JSC::isArray(globalObject, value); +${ + type.allowArray + ? "" + : ` bool isArray = JSC::isArray(globalObject, value); RETURN_IF_EXCEPTION(scope, {}); if (isArray) [[unlikely]] { ctx.throwNotObject(lexicalGlobalObject, scope); return {}; } - ::Bun::Bindgen::DictionaryMemberReader<${count}> members(vm, globalObject, object, ${type.firstMemberNameIndex}); +` +} ::Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); ${type.cpp} result; ${members.join("\n")} return result; @@ -359,13 +362,17 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str const lines: string[] = []; if (signature.requiredCount > 0) { lines.push(`if (callFrame->argumentCount() < ${signature.requiredCount}) [[unlikely]] - return JSC::throwVMError(globalObject, scope, JSC::createNotEnoughArgumentsError(globalObject));`); + return JSC::throwVMError(globalObject, scope, WebCore::createNotEnoughArgumentsErrorBun(globalObject));`); } + const rereads: string[] = []; signature.args.forEach(({ name, type }, i) => { const argument = i < signature.requiredCount ? "uncheckedArgument" : "argument"; lines.push(`::JSC::EnsureStillAliveScope argument${i} = callFrame->${argument}(${i});`); lines.push(`[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); const convert = type.cppConvert(`argument${i}.value()`, `ctx${i}`, name); + // Converting a later argument can run JavaScript that detaches or resizes the buffer. + if (type.borrowsHeap && i < signature.args.length - 1) + rereads.push(`value${i} = ${convert};`, `scope.assertNoException();`); if (type instanceof DefaultType) { const absent = type.onNull ? "isUndefinedOrNull" : "isUndefined"; lines.push( @@ -376,6 +383,7 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str } lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); + lines.push(...rereads); const args = signature.args.map(({ type }, i) => { const converted = `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::convertToExtern(::std::move(value${i}))`; return type instanceof WithValueType @@ -593,10 +601,6 @@ extern "C" JSC::JSCell* ${symbolName(type, "create")}(JSC::JSGlobalObject* globa return ${type.cpp}::create(JSC::getVM(globalObject), structure ? structure : ::Bun::Bindgen::structureForClass(globalObject, ${type.classIndex})); } -extern "C" JSC::JSCell* ${symbolName(type, "fromJS")}(JSC::EncodedJSValue value) -{ - return dynamicDowncast<${type.cpp}>(JSC::JSValue::decode(value)); -} ${ construct ? ` @@ -674,6 +678,7 @@ export function cppSource(file: SchemaFile): string { return `${banner(file.origin)}#include "root.h" #include "Generated${file.origin.namespace}.h" #include "Bindgen/DictionaryMemberReader.h" +#include "JSDOMOperation.h" #include "ZigGlobalObject.h" #include #include @@ -702,10 +707,13 @@ inline constexpr unsigned numberOfClassSubspaces = ${Math.max(tables.classes.len } export function cppTables(tables: Tables): string { - const list = (items: readonly string[]) => (items.length > 0 ? items.join(",\n ") : "{}"); - const length = (items: readonly unknown[]) => Math.max(items.length, 1); + const array = (type: string, name: string, items: readonly string[]) => + `inline constexpr std::array<${type}, ${items.length}> ${name} { { + ${items.join(",\n ")} +} };`; return `${banner(null)}#pragma once #include "Bindgen/JSBindgenObject.h" +#include #include namespace Bun::Bindgen::Generated { @@ -714,29 +722,25 @@ ${tables.classes.map(type => `extern const ClassDefinition JS${type.name}ClassDe namespace Bun::Bindgen::Tables { -inline constexpr unsigned memberNameCount = ${tables.memberNames.length}; -inline constexpr WTF::ASCIILiteral memberNames[${length(tables.memberNames)}] = { - ${list(tables.memberNames.map(asciiLiteral))} -}; +${array("WTF::ASCIILiteral", "memberNames", tables.memberNames.map(asciiLiteral))} -inline constexpr unsigned enumerationValueCount = ${tables.enumerationValues.length}; -inline constexpr WTF::ASCIILiteral enumerationValues[${length(tables.enumerationValues)}] = { - ${list(tables.enumerationValues.map(asciiLiteral))} -}; +${array("WTF::ASCIILiteral", "enumerationValues", tables.enumerationValues.map(asciiLiteral))} struct ObjectStructure { unsigned firstMemberNameIndex; unsigned memberCount; }; -inline constexpr unsigned objectStructureCount = ${tables.objectStructures.length}; -inline constexpr ObjectStructure objectStructures[${length(tables.objectStructures)}] = { - ${list(tables.objectStructures.map(structure => `{ ${structure.firstMemberNameIndex}, ${structure.memberCount} }`))} -}; +${array( + "ObjectStructure", + "objectStructures", + tables.objectStructures.map(structure => `{ ${structure.firstMemberNameIndex}, ${structure.memberCount} }`), +)} -inline constexpr unsigned classCount = ${tables.classes.length}; -inline constexpr const ClassDefinition* classes[${length(tables.classes)}] = { - ${list(tables.classes.map(type => `&Generated::JS${type.name}ClassDefinition`))} -}; +${array( + "const ClassDefinition*", + "classes", + tables.classes.map(type => `&Generated::JS${type.name}ClassDefinition`), +)} } `; @@ -896,15 +900,18 @@ ${type.members.map(member => ` ${member.rustName}: ${member.type. text += `unsafe extern "C" { fn Bun__Bindgen__${type.name}__fromJS(global: *mut JSGlobalObject, value: JSValue, result: *mut Extern${type.name}) -> bool; } -impl<'a> ${type.name}${lifetime} { - /// Reads getters and proxies, so it can run JavaScript. - pub fn from_js(cx: &mut bindgen::JsCall<'a>, value: bindgen::JsValue<'a, T>) -> bindgen::Result<'a, Self> { +impl ${type.name}${lifetime ? "<'_>" : ""} { + /// Converts \`value\` and hands it to \`f\`. + pub fn from_js( + global: &JSGlobalObject, + value: JSValue, + f: impl ${lifetime ? `for<'call> FnOnce(${type.name}<'call>)` : `FnOnce(${type.name})`} -> bun_jsc::JsResult, + ) -> bun_jsc::JsResult { let mut result = core::mem::MaybeUninit::::uninit(); // SAFETY: C++ fills \`result\` exactly when it returns true. - unsafe { - bindgen::extern_types::from_js(cx, |global| Bun__Bindgen__${type.name}__fromJS(global, value.to_js(), result.as_mut_ptr()))?; - Ok(Self::from_extern(result.assume_init_ref())) - } + bun_jsc::call_false_is_throw(global, || unsafe { Bun__Bindgen__${type.name}__fromJS(global.as_mut_ptr(), value, result.as_mut_ptr()) })?; + // SAFETY: as above; \`value\` is on the caller's stack for all of \`'call\`. + f(unsafe { ${type.name}::from_extern(result.assume_init_ref()) }) } } `; @@ -1080,8 +1087,7 @@ ${declarations.map(declaration => ` ${declaration}`).join("\n")} } unsafe extern "C" { - fn ${symbolName(type, "create")}(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull; - fn ${symbolName(type, "fromJS")}(value: JSValue) -> *mut core::ffi::c_void;${ + fn ${symbolName(type, "create")}(global: *mut JSGlobalObject, structure: *mut core::ffi::c_void) -> core::ptr::NonNull;${ type.construct ? ` fn ${symbolName(type, "getConstructor")}(global: *mut JSGlobalObject) -> JSValue;` @@ -1097,14 +1103,9 @@ unsafe impl bindgen::JsPayload for ${rustPath} { // SAFETY: forwarded. unsafe { ${symbolName(type, "create")}(global, structure) } } - #[inline(always)] - fn cell_from_js(value: JSValue) -> *mut core::ffi::c_void { - // SAFETY: a type check; reads the value's header. - unsafe { ${symbolName(type, "fromJS")}(value) } - } } -// Cells are 16-byte aligned and the payload starts at a multiple of 8. A cell fits in half a MarkedBlock. -const _: () = assert!(align_of::<${rustPath}>() <= 8 && size_of::<${rustPath}>() <= 8000); +// Cells are 16-byte aligned and the payload starts at a multiple of 8. +const _: () = assert!(align_of::<${rustPath}>() <= 8); #[unsafe(no_mangle)] pub static ${symbolName(type, "payloadSize")}: usize = size_of::<${rustPath}>(); #[unsafe(no_mangle)] @@ -1167,7 +1168,8 @@ export function rustForCrate(files: readonly SchemaFile[]): string { clippy::identity_op, clippy::derivable_impls, clippy::large_enum_variant, - clippy::trivially_copy_pass_by_ref + clippy::trivially_copy_pass_by_ref, + clippy::redundant_closure )] pub mod ${file.origin.rustModule} { use bun_jsc::{JSGlobalObject, JSValue, bindgen}; @@ -1214,13 +1216,17 @@ ${[ getter => ` ${getter.hasSetter ? "" : "readonly "}${JSON.stringify(getter.name)}: ${getter.type.tsType};`, ), ].join("\n")} - } + }${ + type.construct + ? ` interface ${type.name}Constructor { ${[ - ...(type.construct ? [` new (${parameters(type.construct)}): ${type.name};`] : []), + ` new (${parameters(type.construct)}): ${type.name};`, ` readonly prototype: ${type.name};`, ...type.staticMethods.map(method => member(method.name, false, signatureType(method))), ].join("\n")} + }` + : "" }`; }), ); diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 483709e70340..8490c2de7ff3 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1,11 +1,4 @@ -// The type model behind `bindgen-lib.ts`. Each type knows four things: -// -// idlType the C++ tag type whose `WebCore::Converter` turns a JSValue into a C++ value -// (src/jsc/bindings/BunIDL*.h, Bindgen/IDL*.h, webcore/JSDOMConvert*.h) -// rustExternType the Rust mirror of what `Bun::Bindgen::ExternTraits` lowers that C++ value to -// (src/jsc/bindings/Bindgen/Extern*.h, src/jsc/bindgen/extern_types.rs), with its C layout -// rustType the type a Rust body sees, and `rustFromExtern`, the expression that gets there -// rustToJS for types that can be returned: the expression that converts back +// The type model behind `bindgen-lib.ts`. import assert from "node:assert"; import util from "node:util"; @@ -17,8 +10,6 @@ export interface Layout { /** Where a named type or function was declared. */ export interface Origin { - /** Absolute path of the `.bind.ts` file. */ - file: string; /** `node_os` for `node_os.bind.ts`. */ stem: string; /** `NodeOs`: the C++ namespace of the file's functions, and `Generated.h`. */ @@ -92,10 +83,7 @@ export abstract class Type { get falsyAsNull(): OptionalType { return new OptionalType(this, "falsyAsNull"); } - /** - * Both forms: converted for use now, and the original value to hand to something that holds it - * longer (one of a class's `internalFields`, a `Strong`, a JS call, the result). Only for arguments. - */ + /** Converted for use now, and the value itself for whatever holds it longer. Only for arguments. */ get withValue(): WithValueType { return new WithValueType(this); } @@ -104,12 +92,13 @@ export abstract class Type { return new DefaultType(this, value); } + /** The C++ tag type whose `WebCore::Converter` turns a JSValue into a C++ value. */ abstract readonly idlType: string; /** TypeScript spelling. */ abstract readonly tsType: string; /** How `IDLNullable::ImplementationType` says "null". */ - get nullRepresentation(): "optional" | "pointer" | "undefined" { + get nullRepresentation(): "optional" | "pointer" { return "optional"; } /** Converting `undefined` succeeds, so a dictionary member of this type is not required. */ @@ -154,7 +143,9 @@ export abstract class Type { } abstract layout(): Layout; + /** The Rust mirror of what `Bun::Bindgen::ExternTraits` lowers the C++ value to. */ abstract rustExternType(scope: RustScope): string; + /** What a Rust body sees. */ abstract rustType(scope: RustScope): string; /** A Rust expression (inside `unsafe`) that consumes `expr`, a `rustExternType`. */ rustFromExtern(expr: string, scope: RustScope): string { @@ -287,28 +278,17 @@ const integer = (rust: string, cpp: string, size: number, webidl: string, min: n new IntegerType({ rust, cpp: `::std::${cpp}`, size, webidl, min, max }); export const integers = { - i8: integer("i8", "int8_t", 1, "IDLByte", -128, 127), u8: integer("u8", "uint8_t", 1, "IDLOctet", 0, 255), - i16: integer("i16", "int16_t", 2, "IDLShort", -32768, 32767), u16: integer("u16", "uint16_t", 2, "IDLUnsignedShort", 0, 65535), i32: integer("i32", "int32_t", 4, "IDLLong", -2147483648, 2147483647), u32: integer("u32", "uint32_t", 4, "IDLUnsignedLong", 0, 4294967295), - i64: integer("i64", "int64_t", 8, "IDLLongLong", Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), u64: integer("u64", "uint64_t", 8, "IDLUnsignedLongLong", 0, Number.MAX_SAFE_INTEGER), usize: integer("usize", "uint64_t", 8, "IDLUnsignedLongLong", 0, Number.MAX_SAFE_INTEGER), }; export class DoubleType extends PrimitiveType { - constructor(idlType = "::Bun::IDLStrictDouble") { - super(idlType, "number", "f64", 8); - } - /** Rejects NaN and the infinities. */ - get finite(): DoubleType { - return new DoubleType("::Bun::IDLFiniteDouble"); - } - /** `ToNumber`. */ - get loose(): DoubleType { - return new DoubleType("::WebCore::IDLUnrestrictedDouble"); + constructor() { + super("::Bun::IDLStrictDouble", "number", "f64", 8); } } @@ -343,7 +323,7 @@ export class UnitType extends Type { return "()"; } rustToJS(value: string): string { - assert(this.tsType === "undefined", "return b.undefined, or a nullable type"); + assert(this.tsType === "undefined", "leave `returns` out, or return a nullable type"); return `bindgen::to_js::undefined(heap, ${value})`; } } @@ -364,8 +344,8 @@ export class StringType extends Type { } /** `&[u8]`, UTF-8, for the duration of the call, instead of an owned string. Also takes a `String` object. */ get utf8(): Utf8Type { - const utf8 = new Utf8Type(); - return this.idlType === "::WebCore::IDLDOMString" ? utf8.loose : utf8; + const isLoose = this.idlType === "::WebCore::IDLDOMString"; + return new Utf8Type(`::Bun::Bindgen::IDL${isLoose ? "Loose" : "Strict"}UTF8String`); } get nullRepresentation() { return "pointer" as const; @@ -394,33 +374,23 @@ export class StringType extends Type { } } -/** - * UTF-8 bytes for the duration of the call: borrowed from the JS string when it is 8-bit ASCII, - * transcoded into the thunk's frame otherwise. - */ +/** UTF-8 bytes for the duration of the call: `Bun::Bindgen::JSStringUTF8View`. */ export class Utf8Type extends Type { get rustIsCopy(): boolean { return true; } readonly idlType: string; readonly tsType = "string"; - constructor(idlType = "::Bun::Bindgen::IDLStrictUTF8String") { + constructor(idlType: string) { super(); this.idlType = idlType; } - /** `ToString`: any value is accepted. */ - get loose(): Utf8Type { - return new Utf8Type("::Bun::Bindgen::IDLLooseUTF8String"); - } get borrowsFrame(): boolean { return true; } get nullRepresentation() { return "pointer" as const; } - getHeaders(out: Set): void { - out.add("Bindgen/IDLConvert.h"); - } toCpp(value: unknown): string { assert(typeof value === "string"); return `::Bun::Bindgen::JSStringUTF8View { ${asciiLiteral(value)} }`; @@ -464,9 +434,6 @@ export class BytesType extends Type { get nullRepresentation() { return "pointer" as const; } - getHeaders(out: Set): void { - out.add("Bindgen/IDLConvert.h"); - } toCpp(): string { throw new TypeError("b.bufferSource.bytes cannot have a default"); } @@ -502,9 +469,6 @@ export class BufferSourceType extends Type { get holdsJSValue(): boolean { return true; } - getHeaders(out: Set): void { - out.add("Bindgen/IDLConvert.h"); - } toCpp(): string { throw new TypeError("b.bufferSource cannot have a default"); } @@ -530,7 +494,8 @@ export class WithValueType extends Type { readonly inner: Type; constructor(inner: Type) { super(); - assert(!(inner instanceof DefaultType || inner instanceof WithValueType)); + assert(!(inner instanceof DefaultType), ".withValue cannot be combined with .default()"); + assert(!(inner instanceof WithValueType), "already .withValue"); this.inner = inner; } get idlType(): string { @@ -549,7 +514,7 @@ export class WithValueType extends Type { return [this.inner]; } toCpp(): string { - throw new TypeError("put .withValue last"); + throw new TypeError(".withValue cannot be combined with .default()"); } cppConvert(value: string, ctx: string, name: string): string { return this.inner.cppConvert(value, ctx, name); @@ -580,9 +545,6 @@ export class AnyType extends Type { get permitsUndefined(): boolean { return true; } - get nullRepresentation() { - return "undefined" as const; - } toCpp(): string { throw new TypeError("b.any cannot have a default"); } @@ -653,6 +615,7 @@ export class OptionalType extends Type { assert(!(payload instanceof OptionalType), "already optional"); assert(!(payload instanceof DefaultType), "put .default() last"); assert(!(payload instanceof AnyType), "b.any already accepts undefined and null"); + assert(!(payload instanceof WithValueType), "put .withValue last"); this.payload = payload; this.kind = kind; } @@ -775,6 +738,10 @@ export class ArrayType extends Type { constructor(element: Type) { super(); assert(!(element instanceof DefaultType)); + assert( + !element.borrowsHeap, + "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + ); this.element = element; } get idlType(): string { @@ -898,7 +865,6 @@ export class EnumType extends NamedType { export interface MemberOptions { type: Type; - default?: unknown; /** Name of the C++ field and, in snake_case, of the Rust one. Defaults to the JS name. */ internalName?: string; /** Other JS names, tried in order while the value is `undefined`. */ @@ -912,8 +878,10 @@ export class Member { constructor(name: string, spec: Type | MemberOptions) { const options: MemberOptions = spec instanceof Type ? { type: spec } : spec; this.names = [name, ...(options.altNames ?? [])]; + for (const name of this.names) + assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a member is named by an identifier`); this.internalName = options.internalName ?? name; - this.type = Object.hasOwn(options, "default") ? options.type.default(options.default) : options.type; + this.type = options.type; } get name(): string { return this.names[0]; @@ -928,6 +896,8 @@ export interface DictionaryOptions { userFacingName?: string; /** Also generate `Name::from_js`, for Rust that holds a JS value and no typed signature. */ fromJS?: boolean; + /** An array is an object with none of the members, not an error. */ + allowArray?: boolean; } export class DictionaryType extends NamedType { @@ -944,6 +914,11 @@ export class DictionaryType extends NamedType { super(); this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); + assert(this.members.length > 0, "a dictionary has members"); + assert( + !this.borrowsHeap, + "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + ); assert(!this.fromJS || !this.borrowsFrame, "from_js outlives the frame b.string.utf8 borrows from"); } get userFacingName(): string { @@ -952,6 +927,9 @@ export class DictionaryType extends NamedType { get fromJS(): boolean { return !!this.options.fromJS; } + get allowArray(): boolean { + return !!this.options.allowArray; + } get idlType(): string { return `::WebCore::IDLDictionary<${this.cpp}>`; } @@ -1013,7 +991,11 @@ export class UnionType extends NamedType { assert(!(type instanceof DefaultType)); return { name, variant: pascalCase(name), type }; }); - assert(this.alternatives.length > 0 && this.alternatives.length <= 16, "1 to 16 alternatives"); + assert(this.alternatives.length > 0 && this.alternatives.length <= 16, "1 to 16 alternatives (Bindgen/Macros.h)"); + assert( + !this.borrowsHeap, + "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", + ); } get idlType(): string { return `::Bun::IDLOrderedUnion<${this.alternatives.map(a => a.type.idlType).join(", ")}>`; @@ -1052,10 +1034,7 @@ export class UnionType extends NamedType { // ───────────────────────────── functions ───────────────────────────── -/** - * What the Rust body is handed, and so what it is able to do: - * `"none"` computes; `"heap"` (`&JsHeap`) also allocates JS values; `"js"` (`&mut JsCall`) also runs JavaScript. - */ +/** What the Rust body is handed: nothing, `&JsHeap` (allocates JS values) or `&mut JsCall` (runs JavaScript). */ export type Capability = "none" | "heap" | "js"; export interface SignatureOptions { @@ -1104,13 +1083,15 @@ export class Signature { } export class FunctionDefinition { - /** More than one: picked by argument count, most arguments first. */ + /** More than one: picked by argument count, most required arguments first. */ readonly signatures: readonly Signature[]; name = ""; origin: Origin | null = null; constructor(signatures: readonly SignatureOptions[]) { assert(signatures.length > 0); - this.signatures = signatures.map(options => new Signature(options)).sort((a, b) => b.args.length - a.args.length); + this.signatures = signatures + .map(options => new Signature(options)) + .sort((a, b) => b.requiredCount - a.requiredCount); const counts = this.signatures.map(signature => signature.requiredCount); assert(new Set(counts).size === counts.length, "overloads must differ in their number of required arguments"); } @@ -1124,8 +1105,6 @@ export class FunctionDefinition { /** A native function. */ export interface FunctionField extends Omit { - /** Name of the method in the generated trait. Defaults to the property's name in snake_case. */ - fn?: string; /** `&mut self` instead of `&self`. Not with `capability: "js"`, which gets the wrapper. */ mutates?: boolean; /** Also pass the wrapper, for its `internalFields`. `capability: "js"` always gets it. */ @@ -1176,7 +1155,7 @@ export class Method extends Signature { constructor(name: string, options: FunctionField, isStatic = false) { super(options); this.name = name; - this.rustName = options.fn ?? rustIdent(snakeCase(name)); + this.rustName = rustIdent(snakeCase(name.replace(/^_+/, ""))); assert( !(options.mutates && this.capability === "js"), `${name}: a method that can run JavaScript gets the wrapper, not &mut self`, diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 2efe53615c0f..920f81c437ac 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -1,8 +1,4 @@ -// What a `.bind.ts` file imports: -// -// import * as b from "../../codegen/bindgen-lib.ts"; -// -// See docs/project/bindgen.mdx. +// What a `.bind.ts` file imports as `b`: docs/project/bindgen.mdx import { AnyType, @@ -28,41 +24,30 @@ import { } from "./bindgen-lib-internal.ts"; export type { Capability, Type } from "./bindgen-lib-internal.ts"; -export { null_ as null, undefined_ as undefined }; +export { null_ as null }; /** Strict: only a boolean. `.loose` for `ToBoolean`. */ export const bool = new BoolType(); -/** - * Strict: a number (or BigInt) that is an integer in range. `.loose`, `.wrap`, `.enforceRange()`, - * `.validateInteger()`. - */ -export const { i8, u8, i16, u16, i32, u32, i64, u64, usize } = integers; +/** Strict: a number (or BigInt) that is an integer in range. `.loose`, `.wrap`, `.enforceRange()`, `.validateInteger()`. */ +export const { u8, u16, i32, u32, u64, usize } = integers; -/** Strict: only a number. `.finite`, `.loose`. */ +/** Strict: only a number. */ export const f64 = new DoubleType(); -/** - * An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`. `.utf8` for `&[u8]` for the duration - * of the call. - */ +/** An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`, then `.utf8` for `&[u8]`. */ export const string = new StringType(); /** The JS value itself, as a `JsValue<'a>`. */ export const any = new AnyType(); -/** - * A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsValue<'a, ArrayBuffer>` whose `with_bytes` - * looks the bytes up each time. `.bytes` for `&[u8]` for the duration of the call, `.arrayBuffer` for a reference on - * the `JSC::ArrayBuffer`. - */ +/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsValue<'a, ArrayBuffer>`. `.bytes`, `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h"); const null_ = new UnitType("null"); -const undefined_ = new UnitType("undefined"); export const array = (element: Type) => new ArrayType(element); @@ -89,5 +74,5 @@ export const fn = (signature: SignatureOptions) => new FunctionDefinition([signa /** One JS function, several signatures, told apart by how many arguments were passed. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); -/** `export default b.module({ rustPath: "crate::node::node_os" })`: where the file's Rust functions are. */ +/** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ export const module = (options: { rustPath: string }) => new ModuleDefinition(options); diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 206b969ce72e..6733149a390e 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1,14 +1,7 @@ #!/usr/bin/env bun -// Usage: bindgen.ts --sources= --codegen-path= --types-path= -// -// Writes, and scripts/build/codegen.ts declares: -// /Generated.{h,cpp} per source -// /GeneratedBindings+Tables.h, GeneratedBindings+Counts.h -// /generated_bindgen_.rs per crate that owns a source -// /Bindgen.d.ts +// Usage: bindgen.ts --sources==,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) import assert from "node:assert"; -import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { @@ -32,26 +25,11 @@ import { } from "./bindgen-lib-internal.ts"; import { argParse, writeIfNotChanged } from "./helpers.ts"; -/** The Cargo package whose directory contains `file`. */ -function crateOf(file: string): string { - for (let dir = path.dirname(file); dir !== path.dirname(dir); dir = path.dirname(dir)) { - const manifest = path.join(dir, "Cargo.toml"); - if (!fs.existsSync(manifest)) continue; - const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(fs.readFileSync(manifest, "utf8")); - if (name) return name[1]; - } - throw new Error(`${file} is not inside a Cargo package`); -} - -async function load(file: string): Promise { +async function load(source: string): Promise { + const crate = source.slice(0, source.indexOf("=")); + const file = source.slice(crate.length + 1); const stem = path.basename(file, ".bind.ts"); - const origin: Origin = { - file, - stem, - namespace: pascalCase(stem), - rustModule: snakeCase(stem), - crate: crateOf(file), - }; + const origin: Origin = { stem, namespace: pascalCase(stem), rustModule: snakeCase(stem), crate }; const result: SchemaFile = { origin, moduleDefinition: null, types: [], functions: [] }; for (const [name, value] of Object.entries((await import(pathToFileURL(file).href)) as Record)) { if (value instanceof ModuleDefinition) { @@ -94,10 +72,16 @@ const sources = (args.sources as string) .filter(source => source.length > 0) .sort(); -const files = await Promise.all(sources.map(load)); +const files: SchemaFile[] = []; +for (const source of sources) files.push(await load(source)); files.forEach(sortTypes); -const names = files.flatMap(file => file.types.map(type => type.name)); -assert(new Set(names).size === names.length, "two .bind.ts files export a type with the same name"); +const owners = new Map(); +for (const file of files) { + for (const { name } of file.types) { + assert(!owners.has(name), `${owners.get(name)}.bind.ts and ${file.origin.stem}.bind.ts both export a type ${name}`); + owners.set(name, file.origin.stem); + } +} const tables = analyze(files); for (const file of files) { diff --git a/src/collections/array_list.rs b/src/collections/array_list.rs deleted file mode 100644 index 7f4bde27b7d4..000000000000 --- a/src/collections/array_list.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![forbid(unsafe_code)] -//! Managed `ArrayList` wrappers. - -use bun_alloc::AllocError; - -/// Managed `ArrayList` using the default allocator. No overhead compared to an unmanaged -/// `ArrayList`. -pub type ArrayListDefault = ArrayListAlignedIn; - -/// Managed `ArrayList` wrapper around `Vec`. -/// -/// NOTE: dropping this type runs `Drop` on each of the items. -#[derive(Default)] -pub struct ArrayListAlignedIn { - unmanaged: Unmanaged, -} - -pub(crate) type Unmanaged = Vec; - -impl ArrayListAlignedIn { - pub fn init() -> Self { - Self { - unmanaged: Vec::new(), - } - } - - pub fn init_capacity(num: usize) -> Result { - // Vec::with_capacity aborts on OOM rather than returning Err. Could swap to - // `Vec::try_with_capacity` (nightly) or a fallible wrapper if OOM recovery matters. - Ok(Self { - unmanaged: Vec::with_capacity(num), - }) - } - - /// The contents of `unmanaged` must have been allocated by the global allocator. - /// This function takes ownership of `unmanaged`. - pub fn from_unmanaged(unmanaged: Unmanaged) -> Self { - Self { unmanaged } - } - - pub fn append_assume_capacity(&mut self, item: T) { - self.unmanaged.push(item); - } -} diff --git a/src/collections/lib.rs b/src/collections/lib.rs index 57539df10913..66f8901f10ff 100644 --- a/src/collections/lib.rs +++ b/src/collections/lib.rs @@ -467,6 +467,3 @@ pub mod hash_map { /// re-exported from `array_hash_map` rather than duplicated. pub use crate::array_hash_map::KV; } - -pub mod array_list; -pub use array_list::ArrayListDefault; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 353f5fa529cf..a7e3329ee775 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -245,10 +245,12 @@ export const bindgen = { range: $bindgenFn("bindgen_test.bind.ts", "range"), words: $bindgenFn("bindgen_test.bind.ts", "words"), sumBytes: $bindgenFn("bindgen_test.bind.ts", "sumBytes"), + sumBytesThenText: $bindgenFn("bindgen_test.bind.ts", "sumBytesThenText"), sumBeforeAndAfterCallback: $bindgenFn("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), withValueEcho: $bindgenFn("bindgen_test.bind.ts", "withValueEcho"), fail: $bindgenFn("bindgen_test.bind.ts", "fail"), TestCounter: $bindgenFn("bindgen_test.bind.ts", "TestCounter"), + TestHolder: $bindgenFn("bindgen_test.bind.ts", "TestHolder"), makeToken: $bindgenFn("bindgen_test.bind.ts", "makeToken"), }; diff --git a/src/js/node/os.ts b/src/js/node/os.ts index d2c90632d36c..a3f090100c34 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -128,7 +128,9 @@ function bound(binding) { : $bundleError("TODO: type"); }, uptime: binding.uptime, - userInfo: binding.userInfo, + userInfo: function userInfo(options) { + return binding.userInfo(typeof options === "object" ? options : undefined); + }, version: binding.version, machine: function () { // TODO: linux arm64 should also return "aarch64" (Node/uname compat) — diff --git a/src/jsc/JSArray.rs b/src/jsc/JSArray.rs index 02b3eca99096..016d1a5964c4 100644 --- a/src/jsc/JSArray.rs +++ b/src/jsc/JSArray.rs @@ -6,26 +6,10 @@ bun_opaque::opaque_ffi! { } unsafe extern "C" { - // TODO: this can throw - fn JSArray__constructArray( - global: *const JSGlobalObject, - items: *const JSValue, - len: usize, - ) -> JSValue; safe fn JSArray__constructEmptyArray(global: &JSGlobalObject, len: usize) -> JSValue; } impl JSArray { - #[track_caller] - pub fn create(global: &JSGlobalObject, items: &[JSValue]) -> JsResult { - // `from_js_host_call` runs the closure, then checks the VM for a - // pending exception. - crate::from_js_host_call(global, || unsafe { - // SAFETY: items.ptr/len are a valid contiguous slice; global is a live &JSGlobalObject. - JSArray__constructArray(global, items.as_ptr(), items.len()) - }) - } - #[track_caller] pub fn create_empty(global: &JSGlobalObject, len: usize) -> JsResult { crate::from_js_host_call(global, || JSArray__constructEmptyArray(global, len)) diff --git a/src/jsc/NodeModuleModule.bind.ts b/src/jsc/NodeModuleModule.bind.ts index bb8109be4991..cce88d018b08 100644 --- a/src/jsc/NodeModuleModule.bind.ts +++ b/src/jsc/NodeModuleModule.bind.ts @@ -2,7 +2,7 @@ import * as b from "../codegen/bindgen-lib.ts"; export const _stat = b.fn({ args: { - str: b.string.utf8.loose, + str: b.string.loose.utf8, }, returns: b.i32, rustPath: "crate::node_module_module::stat", diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index a3f85667610c..f3a7668a3182 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -1,15 +1,4 @@ -//! What code generated by `src/codegen/bindgen.ts` hands to a binding's Rust body. -//! -//! Three rules, all enforced by the borrow checker: -//! -//! 1. Running JavaScript, or anything that can call back into Rust, takes `&mut JsCall`. -//! 2. A pointer into memory JavaScript can free, detach, resize or alias is only reachable inside a -//! closure that borrows a field of `JsCall`, so rule 1 cannot happen while it is live. -//! 3. A JS value is a [`JsValue<'a>`], tied to the host call. It cannot be stored anywhere that -//! outlives the call; a [`Strong`] and a class's `internalFields` can hold one longer, and the GC visits -//! those. -//! -//! `test/internal/bindgen-compile-fail` holds one rejected program per hazard. +//! What code generated by `src/codegen/bindgen.ts` hands to a binding's Rust body: `docs/project/bindgen.mdx`. use core::cell::UnsafeCell; use core::ffi::c_void; @@ -27,14 +16,12 @@ pub mod extern_types; /// Makes `'a` invariant, so two calls' lifetimes never unify. type InvariantLifetime<'a> = PhantomData &'a ()>; type NotSend = PhantomData<*mut ()>; -/// As `bun_opaque::opaque_ffi!`: zero-sized, `!Freeze`, `!Unpin`, so a reference to it is a bare -/// pointer to LLVM (no `noalias`). +/// As `bun_opaque::opaque_ffi!`: a reference to it is a bare pointer to LLVM (no `noalias`). type OpaqueFfi = (UnsafeCell<[u8; 0]>, PhantomPinned); // ───────────────────────────── capabilities ───────────────────────────── -/// The JS heap is alive and this is its thread: allocate, read JS-owned memory, describe an error -/// to throw. Cannot run JavaScript. +/// Allocate, read JS-owned memory, describe an error to throw. Cannot run JavaScript. #[repr(C)] pub struct JsHeap<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); @@ -42,9 +29,7 @@ pub struct JsHeap<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); #[repr(C)] pub struct JsPayloads<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); -/// A call into Rust on the JS thread. `&mut JsCall` is what runs JavaScript. It is the -/// `JSGlobalObject*` the host function received: both fields are zero-sized at offset 0, and public -/// so a borrow of one leaves the other usable. +/// `&mut JsCall` is what runs JavaScript. It is the `JSGlobalObject*`: both fields are zero-sized at offset 0. #[repr(C)] pub struct JsCall<'a> { pub heap: JsHeap<'a>, @@ -58,7 +43,6 @@ impl<'a> JsHeap<'a> { unsafe { JSGlobalObject::opaque_ref_nn(core::ptr::from_ref(self).cast()) } } - /// For generated code. #[doc(hidden)] #[inline(always)] pub fn global_ptr(&self) -> *mut JSGlobalObject { @@ -100,12 +84,8 @@ impl<'a> JsHeap<'a> { } impl<'a> JsCall<'a> { - /// For generated thunks, event-loop dispatch, and code that only has a `&JSGlobalObject`. - /// /// # Safety - /// `global` is the current thread's live global object, and no borrow of an outer `JsCall` is - /// in use. That holds wherever the engine or the event loop calls into Rust: getting there took - /// the outer `&mut JsCall`. + /// `global` is this thread's live global object and the caller was entered by the engine or the event loop. #[inline(always)] pub unsafe fn with( global: *mut JSGlobalObject, @@ -115,8 +95,7 @@ impl<'a> JsCall<'a> { f(unsafe { &mut *global.cast::>() }) } - /// Anything can be done with a `&JSGlobalObject`, so it costs the whole `JsCall` for as long as - /// the reference lives. + /// Anything can be done with a `&JSGlobalObject`, so it borrows the whole `JsCall`. #[inline(always)] pub fn global(&mut self) -> &JSGlobalObject { self.heap.global() @@ -126,51 +105,18 @@ impl<'a> JsCall<'a> { pub fn js_thread(&mut self) -> JsThread<'_> { self.heap.global().js_thread_of_caller_no_frame() } - - /// Runs `f`, which takes a `&JSGlobalObject`. - #[inline] - pub fn with_global( - &mut self, - f: impl FnOnce(&JSGlobalObject) -> crate::JsResult, - ) -> Result<'a, R> { - f(self.heap.global()).map_err(Error::pending) - } - - /// [`with_global`](Self::with_global) for an `f` that returns a JS value. - #[inline] - pub fn with_global_value( - &mut self, - f: impl FnOnce(&JSGlobalObject) -> crate::JsResult, - ) -> Result<'a, JsValue<'a>> { - JsValue::from_js_result(f(self.heap.global())) - } - - /// Takes the pending exception off the VM. - pub fn take_exception(&mut self, error: Error<'a>) -> Option> { - let Error(state, ..) = error; - match state { - ErrorState::ToThrow(value) => Some(JsValue::wrap(value)), - ErrorState::Pending(JsError::Thrown) => { - self.heap.global().try_take_exception().map(JsValue::wrap) - } - ErrorState::Pending(_) => None, - } - } } // ───────────────────────────── errors ───────────────────────────── #[derive(Debug)] enum ErrorState { - /// Already pending on the VM (or, for the other `JsError`s, what the thunk must do). Pending(JsError), - /// Made under `&JsHeap`; the thunk throws it once the body has returned. `VM::throwException` - /// notifies the debugger, which can run code, so it must not happen while a body holds borrows. + /// Thrown once the body has returned: `VM::throwException` notifies the debugger, which can run code. ToThrow(JSValue), } -/// The call failed with a JS exception. Made only by this module; tied to the call, so it cannot be -/// kept and replayed. +/// The call failed with a JS exception. #[derive(Debug)] #[must_use = "dropping this leaves an exception pending (or never throws it)"] pub struct Error<'a>(ErrorState, InvariantLifetime<'a>, NotSend); @@ -187,8 +133,6 @@ impl<'a> Error<'a> { Error(ErrorState::Pending(error), PhantomData, PhantomData) } - /// For generated thunks: leaves the exception pending and returns what the host function - /// returns to JSC. #[doc(hidden)] #[cold] #[inline(never)] @@ -242,9 +186,7 @@ impl<'a> IntoError<'a> for core::convert::Infallible { // ───────────────────────────── values ───────────────────────────── -/// A [`JSValue`] held by a host call, known to be a `T` ([`JSString`], [`JSObject`], [`JSFunction`], -/// [`ArrayBuffer`]; `JSValue` is "anything"). `Copy`, one register, no rooting: JSC scans the stack. -/// Keep lists of them in a [`MarkedArgumentBuffer`]; the GC does not look inside a `Vec`. +/// A [`JSValue`] held by a host call, known to be a `T`. Lists of them go in a [`MarkedArgumentBuffer`]. #[repr(transparent)] pub struct JsValue<'a, T = JSValue>(JSValue, InvariantLifetime<'a>, PhantomData<*const T>); impl Clone for JsValue<'_, T> { @@ -266,8 +208,6 @@ impl<'a, T> JsValue<'a, T> { result.map(Self::wrap).map_err(Error::pending) } - /// For generated thunks. - /// /// # Safety /// `value` is a live, non-empty `T` belonging to the call `'a`. #[doc(hidden)] @@ -303,19 +243,10 @@ impl<'a> JsValue<'a> { Self::wrap(value) } - #[inline] - pub fn from_strong(_: &JsHeap<'a>, strong: &Strong) -> Self { - Self::wrap(strong.get()) - } - #[inline(always)] pub fn is_undefined(self) -> bool { self.0.is_undefined() } - #[inline(always)] - pub fn is_undefined_or_null(self) -> bool { - self.0.is_undefined_or_null() - } #[inline] pub fn as_function(self) -> Option> { self.0.is_callable().then(|| JsValue::wrap(self.0)) @@ -337,23 +268,17 @@ impl<'a> JsValue<'a, JSFunction> { } impl<'a> JsValue<'a, ArrayBuffer> { - /// The bytes of a typed array, `DataView` or `ArrayBuffer`, or `None` if it is detached. - /// Pointer and length are read on every call: JavaScript may have run since the last one. - /// Nothing that could detach, resize or re-home the storage can run inside `f`. + /// The bytes as they are now. A detached or out-of-bounds view is empty. #[inline] - pub fn with_bytes(self, heap: &JsHeap<'a>, f: impl FnOnce(&[u8]) -> R) -> Option { - let array_buffer = self.0.as_array_buffer(heap.global())?; - if array_buffer.shared || array_buffer.ptr.is_null() && array_buffer.byte_len != 0 { - return None; - } - let result = f(array_buffer.byte_slice()); + pub fn with_bytes(self, heap: &JsHeap<'a>, f: impl FnOnce(&[u8]) -> R) -> R { + let array_buffer = self.0.as_array_buffer(heap.global()); + let result = f(array_buffer.as_ref().map_or(&[], ArrayBuffer::byte_slice)); self.0.ensure_still_alive(); - Some(result) + result } } -/// An argument in both forms: converted for use now, and the value itself to hand to something that -/// holds it longer (a class's `internalFields`, a [`Strong`], a JS call, the return value). +/// An argument converted for use now, and the value itself for whatever holds it longer. #[derive(Clone, Copy)] pub struct WithValue<'a, T> { pub converted: T, @@ -362,8 +287,7 @@ pub struct WithValue<'a, T> { // ───────────────────────────── classes ───────────────────────────── -/// The Rust struct of a generated class, stored inside its JS wrapper. `'static`, so it cannot hold a -/// [`JsValue`]. The sweeper drops it with no capability in scope, so `Drop` cannot touch the JS heap. +/// The Rust struct of a generated class, stored inside its JS wrapper and dropped by the sweeper. /// /// # Safety /// Implemented by generated code only: the offsets describe the generated C++ cell. @@ -372,15 +296,10 @@ pub unsafe trait JsPayload: Sized + 'static { const PAYLOAD_OFFSET: usize; #[doc(hidden)] const INTERNAL_FIELDS_OFFSET: usize; - /// A new cell whose payload is uninitialized. A null `structure` is the class's own. - /// /// # Safety - /// The caller writes the payload before anything else runs. + /// The caller writes the payload before anything else runs. A null `structure` is the class's own. #[doc(hidden)] unsafe fn create_cell(global: *mut JSGlobalObject, structure: *mut c_void) -> NonNull; - /// `value`'s cell if it is a wrapper of `Self`, else null. - #[doc(hidden)] - fn cell_from_js(value: JSValue) -> *mut c_void; /// A new JS object that owns `self`. #[inline] @@ -388,18 +307,9 @@ pub unsafe trait JsPayload: Sized + 'static { // SAFETY: a null structure is always valid. unsafe { JsWrapper::create_with_structure(heap, core::ptr::null_mut(), self) } } - - /// `value` as a wrapper of `Self`, if it is one. - #[inline] - fn from_js<'a, T>(value: JsValue<'a, T>) -> Option> { - // SAFETY: C++ checked the class. - NonNull::new(Self::cell_from_js(value.0)) - .map(|cell| unsafe { JsWrapper::from_extern(cell) }) - } } -/// The JS object that wraps a `T`. A cell pointer; the payload is reached only through -/// [`JsPayloads`]. +/// The JS object that wraps a `T`. The payload is reached only through [`JsPayloads`]. #[repr(transparent)] pub struct JsWrapper<'a, T: JsPayload>( NonNull, @@ -415,8 +325,6 @@ impl Clone for JsWrapper<'_, T> { impl Copy for JsWrapper<'_, T> {} impl<'a, T: JsPayload> JsWrapper<'a, T> { - /// For generated thunks. - /// /// # Safety /// `cell` is a live wrapper of `T` belonging to the call `'a`. #[doc(hidden)] @@ -425,8 +333,6 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { JsWrapper(cell, PhantomData, PhantomData) } - /// For generated constructors. - /// /// # Safety /// `structure` is null or a Structure of `T`'s cell class. #[doc(hidden)] @@ -444,8 +350,6 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { } } - /// For the generated thunk of a method that cannot run JavaScript. - /// /// # Safety /// Nothing can reach the payload mutably for `'b`, and the caller's frame holds the cell. #[doc(hidden)] @@ -455,8 +359,6 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { unsafe { &*self.payload() } } - /// As [`payload_ref`](Self::payload_ref). - /// /// # Safety /// Nothing else can reach the payload for `'b`, and the caller's frame holds the cell. #[doc(hidden)] @@ -499,8 +401,6 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { JsWrapperStrong(self.as_value().to_strong(heap), PhantomData) } - /// For the generated `x_field`. - /// /// # Safety /// `index` is one of the class's `internalFields`. #[doc(hidden)] @@ -517,8 +417,6 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { }) } - /// For the generated `set_x_field`. - /// /// # Safety /// As [`internal_field`](Self::internal_field). #[doc(hidden)] @@ -534,7 +432,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { extern_types::Bun__Bindgen__setInternalField( heap.global_ptr(), self.0.as_ptr(), - (T::INTERNAL_FIELDS_OFFSET + index * 8) as u32, + (T::INTERNAL_FIELDS_OFFSET + index * size_of::()) as u32, value.0, ) } @@ -555,8 +453,7 @@ impl JsWrapperStrong { // ───────────────────────────── results ───────────────────────────── -/// Rust to JS, one function per schema type. The schema picks the function, not the Rust type: a -/// `Vec` is a string under `b.utf8` and an array of numbers under `b.array(b.u8)`. +/// Rust to JS, one function per schema type: a `Vec` is a string or an array of numbers. pub mod to_js { use super::*; @@ -570,13 +467,10 @@ pub mod to_js { } number! { bool: bool => JSValue::from; - i8: i8 => |n| JSValue::js_number_from_int32(i32::from(n)); u8: u8 => |n| JSValue::js_number_from_int32(i32::from(n)); - i16: i16 => |n| JSValue::js_number_from_int32(i32::from(n)); u16: u16 => |n| JSValue::js_number_from_int32(i32::from(n)); i32: i32 => JSValue::js_number_from_int32; u32: u32 => |n| JSValue::js_number(f64::from(n)); - i64: i64 => JSValue::js_number_from_int64; u64: u64 => JSValue::js_number_from_uint64; usize: usize => |n| JSValue::js_number_from_uint64(n as u64); f64: f64 => JSValue::js_number; @@ -587,9 +481,26 @@ pub mod to_js { Ok(JsValue::UNDEFINED) } + /// A body may also return a plain [`JSValue`]: it goes straight back to JSC. + pub trait Any<'a> { + fn into_value(self, heap: &JsHeap<'a>) -> JsValue<'a>; + } + impl<'a, T> Any<'a> for JsValue<'a, T> { + #[inline(always)] + fn into_value(self, _: &JsHeap<'a>) -> JsValue<'a> { + self.as_value() + } + } + impl<'a> Any<'a> for JSValue { + #[inline(always)] + fn into_value(self, heap: &JsHeap<'a>) -> JsValue<'a> { + JsValue::new(heap, self) + } + } + #[inline(always)] - pub fn any<'a, T>(_: &JsHeap<'a>, value: JsValue<'a, T>) -> Result<'a, JsValue<'a>> { - Ok(value.as_value()) + pub fn any<'a>(heap: &JsHeap<'a>, value: impl Any<'a>) -> Result<'a, JsValue<'a>> { + Ok(value.into_value(heap)) } #[inline] @@ -620,8 +531,7 @@ pub mod to_js { }) } - /// For generated dictionaries: an object with the generated Structure at `structure_index`. An - /// empty value is a member that is absent. + /// An empty value is a member that is absent. #[doc(hidden)] #[inline] pub fn object_with_structure<'a>( @@ -640,7 +550,6 @@ pub mod to_js { })) } - /// For generated enums. #[doc(hidden)] #[inline] pub fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsValue<'a>> { @@ -651,7 +560,6 @@ pub mod to_js { } } -/// For generated thunks: a body's result as what the host function returns to JSC. #[doc(hidden)] #[inline(always)] pub fn host_fn_return<'a, T, E: IntoError<'a>>( diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 185b5e32f947..6dc8479d1ac7 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -1,6 +1,4 @@ -//! Rust side of `src/jsc/bindings/Bindgen/Extern*.h`: the C layouts generated code passes between -//! C++ and Rust. Everything here is plain data; ownership is taken by the generated conversion that -//! reads it, exactly once. +//! Rust side of `src/jsc/bindings/Bindgen/Extern*.h`. The generated conversion that reads a value takes ownership. use core::ffi::{c_uint, c_void}; @@ -9,8 +7,7 @@ use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; /// `WTF::StringImpl*` carrying one ref, or null. pub type ExternString = *mut bun_core::WTFStringImplStruct; -/// An empty `StringImpl` becomes `String::EMPTY`, as `Bun::toString` does: a WTF-backed `String` is -/// never empty. +/// A WTF-backed `String` is never empty, as in `Bun::toString`. #[inline] pub fn adopt_string(ptr: ExternString) -> bun_core::String { let string = bun_core::String::adopt_wtf_impl(ptr); @@ -83,8 +80,6 @@ impl ExternBytes { // SAFETY: C++ never passes a null pointer for a present value. unsafe { core::slice::from_raw_parts(self.data, self.length) } } - /// A null pointer is "absent". - /// /// # Safety /// As [`get`](Self::get). #[inline(always)] @@ -104,15 +99,6 @@ pub struct ExternWithValue { pub value: JSValue, } -/// Runs a generated `Bun__Bindgen__*__fromJS`, which returns false exactly when it threw. -#[inline] -pub fn from_js<'a>( - cx: &mut super::JsCall<'a>, - convert: impl FnOnce(*mut JSGlobalObject) -> bool, -) -> super::Result<'a, ()> { - cx.with_global(|global| crate::call_false_is_throw(global, || convert(global.as_mut_ptr()))) -} - unsafe extern "C" { pub fn Bun__Bindgen__constructArray( global: *mut JSGlobalObject, diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index ef9c0cc93fe2..14cb44c5c9dc 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -24,7 +24,7 @@ export const requiredAndOptionalArg = b.fn({ }); export const utf8Echo = b.fn({ args: { text: b.string.utf8 }, returns: b.string.utf8 }); -export const utf8EchoLoose = b.fn({ args: { text: b.string.utf8.loose }, returns: b.string.utf8 }); +export const utf8EchoLoose = b.fn({ args: { text: b.string.loose.utf8 }, returns: b.string.utf8 }); export const utf8Optional = b.fn({ args: { text: b.string.utf8.nullable }, returns: b.string.utf8.nullable }); export const stringEcho = b.fn({ args: { text: b.string }, returns: b.string }); @@ -64,17 +64,22 @@ export const range = b.fn({ args: { length: b.u32 }, returns: b.array(b.u32) }); export const words = b.fn({ args: { text: b.string.utf8 }, returns: b.array(b.string.utf8) }); export const sumBytes = b.fn({ args: { bytes: b.bufferSource.bytes }, returns: b.u32 }); -/** The sum before `callback` runs, then after it, or `null` once the buffer is detached. */ +/** Converting `text` can run JavaScript. */ +export const sumBytesThenText = b.fn({ + args: { bytes: b.bufferSource.bytes, text: b.string.loose.utf8 }, + returns: b.u32, +}); +/** The sum before `callback` runs, then after it. */ export const sumBeforeAndAfterCallback = b.fn({ args: { buffer: b.bufferSource, callback: b.any }, - returns: b.array(b.u32.nullable), + returns: b.array(b.u32), capability: "js", throws: true, }); /** The original value if the converted text is not empty. */ export const withValueEcho = b.fn({ - args: { text: b.string.utf8.loose.withValue }, + args: { text: b.string.loose.utf8.withValue }, returns: b.any, capability: "heap", }); @@ -89,7 +94,7 @@ export const TestCounter = b.class({ proto: { count: { getter: b.i32 }, label: { getter: b.string.utf8, setter: true }, - tag: { getter: b.any, capability: "heap", cache: true }, + tag: { getter: b.any, capability: "heap", cache: true, throws: true }, increment: { args: { by: b.i32.default(1) }, returns: b.i32, mutates: true }, listen: { args: { listener: b.any }, capability: "heap", passThis: true }, /** Calls the listener, which may call back into this object, then returns the count. */ @@ -103,6 +108,16 @@ export const TestCounter = b.class({ }, }); +/** Keeps a `TestCounter` alive from Rust. */ +export const TestHolder = b.class({ + rustPath: "crate::bindgen_test::TestHolder", + construct: { args: { counter: TestCounter }, capability: "heap" }, + proto: { + counter: { getter: TestCounter, capability: "heap" }, + incrementCounter: { returns: b.i32, capability: "js" }, + }, +}); + /** Only native code makes one. */ export const TestToken = b.class({ rustPath: "crate::bindgen_test::TestToken", diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 0fea4aae28ce..f00090a21d3f 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -4,11 +4,12 @@ use core::sync::atomic::{AtomicUsize, Ordering}; use crate::ArrayBuffer; use crate::bindgen::{ - self, IntoError as _, JsCall, JsHeap, JsPayload as _, JsValue, JsWrapper, WithValue, + self, IntoError as _, JsCall, JsHeap, JsPayload as _, JsValue, JsWrapper, JsWrapperStrong, + WithValue, }; use crate::generated_bindgen::bindgen_test::{ - TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, TestOptions, - TestOptionsEcho, TestTokenClass, TestValue, + TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, TestHolderClass, + TestOptions, TestOptionsEcho, TestTokenClass, TestValue, }; // This example should be kept in sync with bindgen's documentation @@ -102,11 +103,15 @@ pub fn sum_bytes(bytes: &[u8]) -> u32 { sum(bytes) } +pub fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { + sum(bytes) + sum(text) +} + pub fn sum_before_and_after_callback<'a>( cx: &mut JsCall<'a>, buffer: JsValue<'a, ArrayBuffer>, callback: JsValue<'a>, -) -> bindgen::Result<'a, [Option; 2]> { +) -> bindgen::Result<'a, [u32; 2]> { let before = buffer.with_bytes(&cx.heap, sum); let Some(callback) = callback.as_function() else { return Err(cx @@ -149,7 +154,7 @@ pub struct TestCounter { impl TestCounter { fn new(count: i32) -> Self { - ALIVE.fetch_add(1, Ordering::Relaxed); + ALIVE.fetch_add(1, Ordering::SeqCst); TestCounter { count, label: Vec::new(), @@ -159,7 +164,7 @@ impl TestCounter { impl Drop for TestCounter { fn drop(&mut self) { - ALIVE.fetch_sub(1, Ordering::Relaxed); + ALIVE.fetch_sub(1, Ordering::SeqCst); } } @@ -185,8 +190,8 @@ impl TestCounterClass for TestCounter { self.label = value.to_vec(); } - fn tag<'a>(&self, heap: &JsHeap<'a>) -> JsValue<'a> { - bindgen::to_js::array(heap, [self.count], bindgen::to_js::i32).unwrap_or(JsValue::NULL) + fn tag<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a>> { + bindgen::to_js::array(heap, [self.count], bindgen::to_js::i32) } fn increment(&mut self, by: i32) -> i32 { @@ -215,7 +220,28 @@ impl TestCounterClass for TestCounter { } fn alive() -> usize { - ALIVE.load(Ordering::Relaxed) + ALIVE.load(Ordering::SeqCst) + } +} + +pub struct TestHolder { + counter: JsWrapperStrong, +} + +impl TestHolderClass for TestHolder { + fn constructor<'a>(heap: &JsHeap<'a>, counter: JsWrapper<'a, TestCounter>) -> Self { + TestHolder { + counter: counter.to_strong(heap), + } + } + + fn counter<'a>(&self, heap: &JsHeap<'a>) -> JsWrapper<'a, TestCounter> { + self.counter.get(heap) + } + + fn increment_counter<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> i32 { + let counter = this.with(&cx.payloads, |holder| holder.counter.get(&cx.heap)); + counter.with_mut(&mut cx.payloads, |counter| counter.increment(1)) } } diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 0aeb4857bb51..bd56523c6c85 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -8,33 +8,33 @@ namespace Bun::Bindgen { // Reads the members of a dictionary, by index into its run of Tables::memberNames. -// -// Every lookup follows one rule: own properties, then the prototype chain up to but not including -// Object.prototype. For a plain object whose prototype is Object.prototype or null, the own property -// table is therefore the whole answer, and it is read directly: no PropertySlot, no walk. Reading -// data properties is unobservable, so they are all read up front. -template class DictionaryMemberReader { public: - DictionaryMemberReader(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, unsigned firstMemberNameIndex) + DictionaryMemberReader(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, unsigned firstMemberNameIndex, unsigned memberNameCount) : m_vm(vm) , m_globalObject(globalObject) , m_object(object) , m_memberNames(identifiers(vm).identifiersAt(firstMemberNameIndex, memberNameCount)) - , m_isPlainObject(readPlainObject()) { } // May throw. ALWAYS_INLINE JSC::JSValue get(unsigned index) { - if (m_isPlainObject) [[likely]] - return m_values[index]; + if (ownPropertiesAreTheWholeAnswer()) [[likely]] { + unsigned attributes = 0; + JSC::PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); + if (offset == JSC::invalidOffset) + return JSC::jsUndefined(); + if (!(attributes & (JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::CustomAccessorOrValue))) [[likely]] + return m_object->getDirect(offset); + } return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); } private: - bool readPlainObject() + // The lookup below stops before Object.prototype. Asked per member: converting one can run JavaScript. + ALWAYS_INLINE bool ownPropertiesAreTheWholeAnswer() const { if (m_object->type() != JSC::FinalObjectType) return false; @@ -42,27 +42,13 @@ class DictionaryMemberReader { if (structure->hasPolyProto()) return false; JSC::JSValue prototype = structure->storedPrototype(); - if (prototype != m_globalObject->objectPrototype() && !prototype.isNull()) - return false; - for (unsigned i = 0; i < memberNameCount; ++i) { - unsigned attributes = 0; - JSC::PropertyOffset offset = structure->get(m_vm, m_memberNames[i], attributes); - if (offset == JSC::invalidOffset) - m_values[i] = JSC::jsUndefined(); - else if (attributes & (JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::CustomAccessorOrValue)) [[unlikely]] - return false; - else - m_values[i] = m_object->getDirect(offset); - } - return true; + return prototype == m_globalObject->objectPrototype() || prototype.isNull(); } JSC::VM& m_vm; JSC::JSGlobalObject* m_globalObject; JSC::JSObject* m_object; const JSC::Identifier* m_memberNames; - JSC::JSValue m_values[memberNameCount]; - bool m_isPlainObject; }; } diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 4a878e7b2858..8e590a2892b5 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -17,8 +17,7 @@ template<> struct IDLHumanReadableName : IDLHumanRe namespace Bindgen { -// std::nullopt for anything else, a shared buffer included: another thread may be writing to it. A -// detached or out-of-bounds view is empty. +// std::nullopt for anything else, a shared buffer included. A detached or out-of-bounds view is empty. inline std::optional> unsharedBufferSourceSpan(JSC::JSValue value) { if (auto* view = dynamicDowncast(value)) { diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index c82d3824eac4..793d89b1682c 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -25,8 +25,7 @@ struct IDLStrictUTF8String : WebCore::IDLType { // `ToString`. struct IDLLooseUTF8String : IDLStrictUTF8String {}; -// The bytes of a BufferSource that is not shared. Valid while the argument is alive and no JavaScript -// runs; the generator refuses this type to a function that can run any. +// The bytes of a BufferSource that is not shared. Valid while the argument is alive and no JavaScript runs. struct IDLBufferSourceBytes : WebCore::IDLType> { using SequenceStorageType = void; using NullableType = std::span; diff --git a/src/jsc/bindings/Bindgen/Identifiers.h b/src/jsc/bindings/Bindgen/Identifiers.h index b7ef047863a5..28a9699672ad 100644 --- a/src/jsc/bindings/Bindgen/Identifiers.h +++ b/src/jsc/bindings/Bindgen/Identifiers.h @@ -5,8 +5,7 @@ namespace Bun::Bindgen { -// Identifiers for dictionary member names and JSStrings for enumeration values, each made on first -// use. Atom tables are per thread, so this lives in JSVMClientData, never in a process global. +// Identifiers for dictionary member names and JSStrings for enumeration values, each made on first use. class Identifiers { WTF_MAKE_NONCOPYABLE(Identifiers); WTF_MAKE_NONMOVABLE(Identifiers); @@ -45,8 +44,7 @@ class Identifiers { Identifiers& identifiers(JSC::VM&); -// The Structures of returned dictionaries, by index into Tables::objectStructures. -unsigned objectStructureCount(); +// The Structure of a returned dictionary, by index into Tables::objectStructures. JSC::Structure* createObjectStructure(JSC::VM&, JSC::JSGlobalObject*, unsigned index); } diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h index 35fbfa5a24c3..1be17e8321f5 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -11,7 +11,7 @@ namespace Bun::Bindgen { -// What is the same for every generated class, as data. One per class, defined by generated code. +// One per class, defined by generated code. struct ClassDefinition { WTF::ASCIILiteral className; const JSC::ClassInfo* info; @@ -25,19 +25,7 @@ struct ClassDefinition { unsigned constructorLength; }; -// The wrapper of a generated class: -// -// JSDestructibleObject | WriteBarrier internalFields[numberOfInternalFields] | the Rust payload -// -// The payload is stored in the cell, so a wrapper and its state have one lifetime, and a pointer -// into the payload keeps the wrapper alive: the conservative scan maps interior pointers to their -// cell. The collector reads the internal fields and nothing else; no Rust runs on its threads. -// -// Traits, generated: -// static constexpr ASCIILiteral className; -// static constexpr unsigned classIndex, numberOfInternalFields, numberOfCachedGetters; -// static size_t payloadSize(); size_of::(), a constant Rust defines -// static void finalize(void* payload); drop_in_place:: +// JSDestructibleObject | WriteBarrier internalFields[numberOfInternalFields] | the Rust payload template class JSBindgenObject final : public JSC::JSDestructibleObject { public: @@ -59,6 +47,7 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { auto& clientData = *downcast(vm.clientData); if (auto* space = clientData.clientSubspaces().m_clientSubspaceForBindgenClass[Traits::classIndex]) [[likely]] return space; + RELEASE_ASSERT(allocationSize() <= JSC::MarkedSpace::largeCutoff); return WebCore::subspaceForImplSlow( vm, WebCore::SubspaceForInit { static_cast(allocationSize()), numberOfLowerTierPreciseCells, WebCore::SubspaceForInit::CellType::Destructible, false }, @@ -156,8 +145,6 @@ class JSBindgenConstructor final : public JSC::InternalFunction { }; // By index into Tables::classes (GeneratedBindings+Tables.h). -unsigned classCount(); -void setupClassStructure(JSC::LazyClassStructure::Initializer&, unsigned classIndex); JSC::Structure* structureForClass(JSC::JSGlobalObject*, unsigned classIndex); JSC::JSObject* constructorForClass(JSC::JSGlobalObject*, unsigned classIndex); // The class's own, or one for `new.target` (a subclass, Reflect.construct). May throw. diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 2461dc7a1740..2a756222c20e 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -1,16 +1,13 @@ #pragma once #include "root.h" +#include #include #include #include namespace Bun::Bindgen { -// Bun::UTF8View for an argument of a host call: the JS string's own bytes when it is 8-bit ASCII, a -// converted copy otherwise, which for a short string needs no allocation. Lives in the host -// function's frame. Holding the JSString until destruction is what keeps borrowed bytes valid: if JSC -// swaps the string's impl for an atom, it keeps the old one for as long as the conservative scan -// still finds the JSString. +// Bun::UTF8View for an argument of a host call. Lives in the host function's frame. class JSStringUTF8View { WTF_MAKE_NONCOPYABLE(JSStringUTF8View); @@ -19,26 +16,26 @@ class JSStringUTF8View { JSStringUTF8View(JSStringUTF8View&&) = default; JSStringUTF8View& operator=(JSStringUTF8View&&) = default; explicit JSStringUTF8View(WTF::ASCIILiteral literal) - : m_borrowed(literal.span8()) + : m_view(nullptr, literal) , m_isNull(false) { } - ~JSStringUTF8View() { JSC::ensureStillAliveHere(m_string); } - // Resolving a rope can throw. + // Resolving a rope and converting can throw. static JSStringUTF8View create(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); JSStringUTF8View result; - auto view = string->view(&globalObject); - RETURN_IF_EXCEPTION(scope, result); - result.m_string = string; + result.m_view = string->view(&globalObject); + RETURN_IF_EXCEPTION(scope, {}); result.m_isNull = false; - if (view->is8Bit() && WTF::charactersAreAllASCII(view->span8())) [[likely]] - result.m_borrowed = view->span8(); - else - result.convert(view); + if (result.m_view->is8Bit() && WTF::charactersAreAllASCII(result.m_view->span8())) [[likely]] + return result; + if (!result.convert()) [[unlikely]] { + JSC::throwOutOfMemoryError(&globalObject, scope); + return {}; + } return result; } @@ -49,17 +46,17 @@ class JSStringUTF8View { { if (m_isNull) return {}; - auto span = m_isConverted ? m_converted.span() : byteCast(m_borrowed); + auto span = m_isConverted ? m_converted.span() : byteCast(m_view->span8()); if (!span.data()) return { reinterpret_cast(""), 0 }; return span; } private: - void convert(WTF::StringView); + bool convert(); - JSC::JSString* m_string { nullptr }; - std::span m_borrowed; + // The owner is not always the string: the characters of a substring are its base's. + JSC::GCOwnedDataScope m_view; WTF::Vector m_converted; bool m_isConverted { false }; bool m_isNull { true }; diff --git a/src/jsc/bindings/BindgenAdditionsToGlobalObject.h b/src/jsc/bindings/BindgenAdditionsToGlobalObject.h new file mode 100644 index 000000000000..98b57211fa3d --- /dev/null +++ b/src/jsc/bindings/BindgenAdditionsToGlobalObject.h @@ -0,0 +1,28 @@ +#pragma once +#include "root.h" +#include +#include +#include + +namespace Bun { + +// What src/codegen/bindgen.ts generates has per global object, by index into Bindgen::Tables. +struct BindgenAdditionsToGlobalObject { + BindgenAdditionsToGlobalObject(); + void initialize(); + + template + void visit(Visitor& visitor) + { + for (auto& structure : m_objectStructures) + structure.visit(visitor); + for (auto& lazyClass : m_classes) + lazyClass.visit(visitor); + } + + // Sized by the constructor, so the marker never sees them change. + WTF::FixedVector> m_objectStructures; + WTF::FixedVector m_classes; +}; + +} diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/BindgenIdentifiers.cpp index 728fcec402ff..4e9ce6e052ef 100644 --- a/src/jsc/bindings/BindgenIdentifiers.cpp +++ b/src/jsc/bindings/BindgenIdentifiers.cpp @@ -14,8 +14,8 @@ using namespace JSC; Identifiers::Identifiers(VM& vm) : m_vm(vm) - , m_memberNames(std::make_unique(std::size(Tables::memberNames))) - , m_enumerationValues(std::make_unique(std::size(Tables::enumerationValues))) + , m_memberNames(std::make_unique(Tables::memberNames.size())) + , m_enumerationValues(std::make_unique(Tables::enumerationValues.size())) { } @@ -35,7 +35,7 @@ void Identifiers::initializeEnumerationValue(unsigned index) template void Identifiers::visit(Visitor& visitor) { - for (unsigned i = 0; i < Tables::enumerationValueCount; ++i) { + for (unsigned i = 0; i < Tables::enumerationValues.size(); ++i) { if (auto* string = m_enumerationValues[i]) visitor.appendUnbarriered(string); } @@ -48,14 +48,10 @@ Identifiers& identifiers(VM& vm) return static_cast(vm.clientData)->bindgenIdentifiers; } -unsigned objectStructureCount() -{ - return Tables::objectStructureCount; -} - Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) { auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; + RELEASE_ASSERT(memberCount <= JSFinalObject::maxInlineCapacity); const Identifier* memberNames = identifiers(vm).identifiersAt(firstMemberNameIndex, memberCount); Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); for (unsigned i = 0; i < memberCount; ++i) { @@ -66,26 +62,29 @@ Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned return structure; } -void JSStringUTF8View::convert(StringView view) +bool JSStringUTF8View::convert() { + StringView view = m_view; m_isConverted = true; if (view.is8Bit()) { auto span = view.span8(); - m_converted.grow(span.size() * 2); + if (!m_converted.tryGrow(span.size() * 2)) + return false; m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span.data()), span.size(), byteCast(m_converted.mutableSpan().data()))); - return; + return true; } auto span = view.span16(); - m_converted.grow(span.size() * 3); + if (!m_converted.tryGrow(span.size() * 3)) + return false; size_t length = simdutf::convert_utf16le_to_utf8(span.data(), span.size(), byteCast(m_converted.mutableSpan().data())); if (length) [[likely]] { m_converted.shrink(length); - return; + return true; } // simdutf refuses unpaired surrogates; each becomes U+FFFD. auto lenient = view.utf8(WTF::ConversionMode::StrictConversionReplacingUnpairedSurrogatesWithFFFD); m_converted.shrink(0); - m_converted.append(byteCast(lenient.span())); + return m_converted.tryAppend(byteCast(lenient.span())); } } @@ -101,7 +100,7 @@ extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObjec for (size_t i = 0; i < count; ++i) hasEveryMember &= !!values[i]; if (hasEveryMember) [[likely]] { - JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->bindgenObjectStructure(structureIndex)); + JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->m_bindgenAdditions.m_objectStructures[structureIndex].getInitializedOnMainThread(globalObject)); for (size_t i = 0; i < count; ++i) object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); return JSValue::encode(object); diff --git a/src/jsc/bindings/JSBindgenObject.cpp b/src/jsc/bindings/JSBindgenObject.cpp index 95e07e84ed2e..11201aa7deca 100644 --- a/src/jsc/bindings/JSBindgenObject.cpp +++ b/src/jsc/bindings/JSBindgenObject.cpp @@ -34,12 +34,7 @@ JSBindgenConstructor* JSBindgenConstructor::create(VM& vm, JSGlobalObject* globa return constructor; } -unsigned classCount() -{ - return Tables::classCount; -} - -void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIndex) +static void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIndex) { const ClassDefinition& definition = *Tables::classes[classIndex]; VM& vm = init.vm; @@ -59,13 +54,13 @@ void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIn Structure* structureForClass(JSGlobalObject* globalObject, unsigned classIndex) { auto* global = defaultGlobalObject(globalObject); - return global->m_bindgenLazyClasses[classIndex].getInitializedOnMainThread(global); + return global->m_bindgenAdditions.m_classes[classIndex].getInitializedOnMainThread(global); } JSObject* constructorForClass(JSGlobalObject* globalObject, unsigned classIndex) { auto* global = defaultGlobalObject(globalObject); - return global->m_bindgenLazyClasses[classIndex].constructorInitializedOnMainThread(global); + return global->m_bindgenAdditions.m_classes[classIndex].constructorInitializedOnMainThread(global); } Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFrame, unsigned classIndex) @@ -83,6 +78,32 @@ Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFr } +namespace Bun { + +BindgenAdditionsToGlobalObject::BindgenAdditionsToGlobalObject() + : m_objectStructures(Bindgen::Tables::objectStructures.size()) + , m_classes(Bindgen::Tables::classes.size()) +{ +} + +void BindgenAdditionsToGlobalObject::initialize() +{ + for (auto& structure : m_objectStructures) { + structure.initLater([](const JSC::LazyProperty::Initializer& init) { + auto& additions = defaultGlobalObject(init.owner)->m_bindgenAdditions; + init.set(Bindgen::createObjectStructure(init.vm, init.owner, &init.property - additions.m_objectStructures.begin())); + }); + } + for (auto& lazyClass : m_classes) { + lazyClass.initLater([](JSC::LazyClassStructure::Initializer& init) { + auto& additions = defaultGlobalObject(init.global)->m_bindgenAdditions; + Bindgen::setupClassStructure(init, &init.classStructure - additions.m_classes.begin()); + }); + } +} + +} + extern "C" void Bun__Bindgen__setInternalField(JSC::JSGlobalObject* globalObject, JSC::JSCell* cell, uint32_t offset, JSC::EncodedJSValue value) { auto* field = reinterpret_cast*>(reinterpret_cast(cell) + offset); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 06dbb4562083..456dee5bb449 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -70,7 +70,6 @@ #include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" -#include "Bindgen/JSBindgenObject.h" #include "BunPlugin.h" #include "BunProcess.h" #include "BunSecureContextCache.h" @@ -2140,6 +2139,7 @@ void GlobalObject::finishCreation(VM& vm) setEvalEnabled(false, Bun::codeGenerationFromStringsDisallowedMessage); m_bakeAdditions.initialize(); + m_bindgenAdditions.initialize(); m_markdownTagStrings.initialize(); static const LazyClassStructureInit lazyClassStructureInits[] = { @@ -2753,19 +2753,6 @@ void GlobalObject::finishCreation(VM& vm) this->initGeneratedLazyClasses(); - for (auto& lazyClass : m_bindgenLazyClasses) { - lazyClass.initLater([](LazyClassStructure::Initializer& init) { - auto* globalObject = uncheckedDowncast(init.global); - Bun::Bindgen::setupClassStructure(init, &init.classStructure - globalObject->m_bindgenLazyClasses.begin()); - }); - } - for (auto& structure : m_bindgenObjectStructures) { - structure.initLater([](const Initializer& init) { - auto* globalObject = uncheckedDowncast(init.owner); - init.set(Bun::Bindgen::createObjectStructure(init.vm, globalObject, &init.property - globalObject->m_bindgenObjectStructures.begin())); - }); - } - m_napiTypeTags.initLater([](const JSC::LazyProperty::Initializer& init) { init.set(JSC::JSWeakMap::create(init.vm, init.owner->weakMapStructure())); }); @@ -3337,10 +3324,6 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) WebCore::clientData(visitor.collector().heap().vm())->httpHeaderIdentifiers().template visit(visitor); thisObject->visitGeneratedLazyClasses(thisObject, visitor); - for (auto& structure : thisObject->m_bindgenObjectStructures) - structure.visit(visitor); - for (auto& lazyClass : thisObject->m_bindgenLazyClasses) - lazyClass.visit(visitor); thisObject->visitAdditionalChildrenInGCThread(visitor); } diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index de6bd286e572..75350e13b005 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -71,9 +71,9 @@ struct node_module; #include #include #include "BakeAdditionsToGlobalObject.h" +#include "BindgenAdditionsToGlobalObject.h" #include "NativeModuleList.h" #include "streams/JSStreamsRuntime.h" -#include namespace Bun { class JSCommonJSExtensions; @@ -98,11 +98,6 @@ extern "C" bool Bun__VirtualMachine__isShuttingDown(void* /* BunVM */); extern "C" uv_loop_t* Bun__ZigGlobalObject__uvLoop(void* /* BunVM */); #endif -namespace Bun::Bindgen { -unsigned objectStructureCount(); -unsigned classCount(); -} - namespace Zig { class JSCStackTrace; @@ -505,6 +500,7 @@ class GlobalObject : public Bun::GlobalScope { #define FOR_EACH_GLOBALOBJECT_GC_MEMBER(V) \ V(public, Bun::BakeAdditionsToGlobalObject, m_bakeAdditions) \ + V(public, Bun::BindgenAdditionsToGlobalObject, m_bindgenAdditions) \ \ /* TODO: these should use LazyProperty */ \ \ @@ -790,13 +786,6 @@ class GlobalObject : public Bun::GlobalScope { Bun::MarkdownTagStrings& markdownTagStrings() { return m_markdownTagStrings; } #include "ZigGeneratedClasses+lazyStructureHeader.h" - // Structures of the dictionaries src/codegen/bindgen.ts returns, by index into Bun::Bindgen::Tables::objectStructures. - // Sized in the member initializer, so the marker never sees it change. - JSC::Structure* bindgenObjectStructure(unsigned index) const { return m_bindgenObjectStructures[index].getInitializedOnMainThread(this); } - WTF::FixedVector> m_bindgenObjectStructures { Bun::Bindgen::objectStructureCount() }; - // The classes it generates, by index into Bun::Bindgen::Tables::classes. - WTF::FixedVector m_bindgenLazyClasses { Bun::Bindgen::classCount() }; - void finishCreation(JSC::VM&); private: diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index b0f54b83996e..497dc818c113 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6432,15 +6432,6 @@ extern "C" EncodedJSValue JSFunction__createFromZig( nullptr)); } -extern "C" EncodedJSValue JSArray__constructArray( - JSC::JSGlobalObject* global, - const JSValue* values, - size_t values_len) -{ - return JSValue::encode( - JSC::constructArray(global, (ArrayAllocationProfile*)nullptr, values, values_len)); -} - extern "C" EncodedJSValue JSArray__constructEmptyArray( JSC::JSGlobalObject* global, size_t len) diff --git a/src/jsc/fmt_jsc.bind.ts b/src/jsc/fmt_jsc.bind.ts index 2512ba3e8a3b..d0a7bcb9f9b3 100644 --- a/src/jsc/fmt_jsc.bind.ts +++ b/src/jsc/fmt_jsc.bind.ts @@ -6,7 +6,7 @@ export const Formatter = b.enumeration(["highlight-javascript", "highlight-javas export const fmtString = b.fn({ args: { - code: b.string.utf8.loose, + code: b.string.loose.utf8, formatter: Formatter, }, returns: b.string, diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index a17362872d9a..5285f16bc18f 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -1,6 +1,6 @@ //! Bindgen target for `fmt_jsc.bind.ts`. The actual formatters live in -//! `src/bun_core/fmt.rs`; only the JS-facing wrapper lives here so `bun_core/` -//! stays JSC-free. +//! `src/bun_core/fmt.rs`; only the JS-facing wrapper +//! lives here so `bun_core/` stays JSC-free. use std::io::Write as _; diff --git a/src/jsc/generated.rs b/src/jsc/generated.rs index 142fc5efb9cf..85b889c120cb 100644 --- a/src/jsc/generated.rs +++ b/src/jsc/generated.rs @@ -1,10 +1,6 @@ //! Hand-written Rust surface for the `.classes.ts` codegen. //! -//! Each `JS${Type}` module exposes `from_js` / `from_js_direct` / `to_js` / `get_constructor`, -//! thin-wrapping the `${Type}__fromJS` / `__fromJSDirect` / `__create` / `__getConstructor` C++ -//! exports of `src/codegen/generate-classes.ts`, plus one `${name}_get_cached` / -//! `${name}_set_cached` pair per `cache: true` property, for the classes downstream crates name -//! directly (`bun_jsc::generated::{JSTimeout, JSImmediate, JSBlob, JSResponse, JSRequest}`). +//! One module per class downstream crates name directly, wrapping its `generate-classes.ts` C++ exports. //! //! Symbol-naming contract (kept in sync with generate-classes.ts): //! `${T}Prototype__${name}GetCachedValue(JSValue) -> JSValue` diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index 3bd9f1e56c6f..b1af2a730822 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -774,15 +774,6 @@ mod private { data: *mut c_void, ) -> JSValue; - pub(super) safe fn Bun__CreateFFIFunctionValue( - global_object: &JSGlobalObject, - symbol_name: Option<&EncodedSlice>, - arg_count: u32, - function: JsHostFn, - add_ptr_field: bool, - input_function_ptr: *mut c_void, // ?*anyopaque - ) -> JSValue; - // safe: `JSValue` is a by-value tagged i64; `data` is an opaque // round-trip pointer the C++ side stores in the JSFunction's private // slot without dereferencing it as Rust data. @@ -791,26 +782,6 @@ mod private { } } -#[track_caller] -pub fn new_runtime_function( - global_object: &JSGlobalObject, - symbol_name: Option<&EncodedSlice>, - arg_count: u32, - function_pointer: JsHostFn, - add_ptr_property: bool, - input_function_ptr: Option<*mut c_void>, -) -> JSValue { - jsc::mark_binding(); - private::Bun__CreateFFIFunctionValue( - global_object, - symbol_name, - arg_count, - function_pointer, - add_ptr_property, - input_function_ptr.unwrap_or(core::ptr::null_mut()), - ) -} - #[track_caller] pub fn get_function_data(function: JSValue) -> Option<*mut c_void> { jsc::mark_binding(); diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 93a631b08254..f2514f897504 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1575,7 +1575,7 @@ unsafe extern "C" { } // Hand-stubbed in `generated.rs` until `src/codegen/generate-classes.ts` -// grows a `.rs` backend (same arrangement as `r#gen` below). +// grows a `.rs` backend. #[path = "generated.rs"] pub mod generated; diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 33d728408a77..c3a335af3e27 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -2,14 +2,17 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::bun_object" }); -export const BracesOptions = b.dictionary({ - tokenize: b.bool.loose.default(false), - parse: b.bool.loose.default(false), -}); +export const BracesOptions = b.dictionary( + { + tokenize: b.bool.loose.default(false), + parse: b.bool.loose.default(false), + }, + { allowArray: true }, +); export const braces = b.fn({ args: { - input: b.string.loose.utf8, + input: b.string.loose, options: BracesOptions.nullable.default({}), }, returns: b.any, diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 808f5da87df6..00e7a23ab17c 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -94,7 +94,7 @@ use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::BracesOptions; use crate::generated_bindgen::glob::GlobClass as _; -use bun_jsc::bindgen::{self, JsCall, JsValue}; +use bun_jsc::bindgen::JsCall; // ─── wrap_static_method adapters ─────────────────────────────────────────── // `#[bun_jsc::host_fn(static)]` is not yet emitted, @@ -386,24 +386,21 @@ fn shell_escape(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult Ok(jsval) } -pub(crate) fn braces<'a>( - cx: &mut JsCall<'a>, - input: &'a [u8], - options: BracesOptions, -) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(|global| braces_impl(global, input, options)) -} - /// `Bun.gc(force)`: heap size after the collection. pub(crate) fn gc_from_js(cx: &mut JsCall<'_>, force: bool) -> usize { cx.global().bun_vm().as_mut().garbage_collect_from_js(force) } -fn braces_impl( - global: &JSGlobalObject, - brace_slice: &[u8], +#[allow(clippy::needless_pass_by_value)] // the generated thunk hands over the string it owns +pub(crate) fn braces( + cx: &mut JsCall<'_>, + brace_str: BunString, opts: BracesOptions, ) -> JsResult { + let global = cx.global(); + let brace_utf8 = brace_str.to_utf8(); + let brace_slice = brace_utf8.slice(); + let mut arena = bun_alloc::Arena::new(); let _ = &mut arena; @@ -454,7 +451,7 @@ fn braces_impl( } if expansion_count == 0 { - return bun_string_jsc::to_js_array(global, &[BunString::from_bytes(brace_slice)]); + return bun_string_jsc::to_js_array(global, core::slice::from_ref(&brace_str)); } // Hard cap before preallocation: `calculate_expanded_amount` saturates to diff --git a/src/runtime/api/Glob.bind.ts b/src/runtime/api/Glob.bind.ts index fcf790cb256d..e22c004cc378 100644 --- a/src/runtime/api/Glob.bind.ts +++ b/src/runtime/api/Glob.bind.ts @@ -1,13 +1,16 @@ import * as b from "../../codegen/bindgen-lib.ts"; -export const GlobScanOptions = b.dictionary({ - onlyFiles: b.bool.loose.nullable.default(true), - throwErrorOnBrokenSymlink: b.bool.loose.nullable.default(false), - followSymlinks: b.bool.loose.nullable.default(false), - absolute: b.bool.loose.nullable.default(false), - cwd: b.string.utf8.nullable, - dot: b.bool.loose.nullable.default(false), -}); +export const GlobScanOptions = b.dictionary( + { + onlyFiles: b.bool.loose.nullable.default(true), + throwErrorOnBrokenSymlink: b.bool.loose.nullable.default(false), + followSymlinks: b.bool.loose.nullable.default(false), + absolute: b.bool.loose.nullable.default(false), + cwd: b.string.utf8.nullable, + dot: b.bool.loose.nullable.default(false), + }, + { allowArray: true }, +); export const GlobScanArgument = b.union({ cwd: b.string.utf8, @@ -30,22 +33,18 @@ export const Glob = b.class({ length: 1, }, __scan: { - fn: "scan", privateSymbol: "pull", args: { options: GlobScanArgument.nullable }, returns: b.any, capability: "js", throws: true, - length: 1, }, __scanSync: { - fn: "scan_sync", privateSymbol: "resolveSync", args: { options: GlobScanArgument.nullable }, returns: b.any, capability: "heap", throws: true, - length: 1, }, match: { args: { str: b.string.utf8 }, diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 1a6c2af30367..c40cb26d931f 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -2,7 +2,7 @@ // The entrypoint for node:os is `src/js/node/os.ts` import * as b from "../../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::node::os::js_bindings" }); +export default b.module({ rustPath: "crate::node::os" }); export const hostCpuCount = b.fn({ returns: b.i32 }); export const cpus = b.fn({ returns: b.any, capability: "js", throws: true }); @@ -24,9 +24,7 @@ export const release = b.fn({ returns: b.string }); export const totalmem = b.fn({ returns: b.u64 }); export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); -export const UserInfoOptions = b.dictionary({ - encoding: b.string.loose.utf8.default(""), -}); +export const UserInfoOptions = b.dictionary({ encoding: b.string.loose.utf8.default("") }, { allowArray: true }); export const userInfo = b.fn({ args: { options: UserInfoOptions.nullable.default({}), @@ -34,7 +32,6 @@ export const userInfo = b.fn({ returns: b.any, capability: "js", throws: true, - length: 1, }); export const version = b.fn({ returns: b.string, throws: true }); @@ -48,7 +45,7 @@ export const setPriority = b.overloads( }, capability: "js", throws: true, - rustPath: "crate::node::os::js_bindings::set_priority", + rustPath: "crate::node::os::set_priority", length: 2, }, { @@ -57,6 +54,6 @@ export const setPriority = b.overloads( }, capability: "js", throws: true, - rustPath: "crate::node::os::js_bindings::set_priority_of_current_process", + rustPath: "crate::node::os::set_priority_of_current_process", }, ); diff --git a/src/runtime/node/node_os.rs b/src/runtime/node/node_os.rs index cc545cfcded1..ef6ea8cbd342 100644 --- a/src/runtime/node/node_os.rs +++ b/src/runtime/node/node_os.rs @@ -3,6 +3,7 @@ use core::ffi::c_int; use core::ffi::{c_char, c_uint, c_void}; use bun_core::String as BunString; +use bun_jsc::bindgen::JsCall; use bun_jsc::bun_string_jsc; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; @@ -73,57 +74,8 @@ mod _impl { // safe upper bound for the stack buffer on every platform. const HOST_NAME_MAX: usize = 256; - /// What `node_os.bind.ts` binds. - pub(crate) mod js_bindings { - use super::BunString; - use crate::generated_bindgen::node_os::UserInfoOptions; - use bun_jsc::bindgen::{self, JsCall, JsValue}; - - pub(crate) use super::super::freemem; - pub(crate) use super::{loadavg, release, totalmem, version}; - - pub(crate) fn host_cpu_count() -> i32 { - super::bun_sysconf__SC_NPROCESSORS_ONLN().max(1) - } - pub(crate) fn cpus<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(super::cpus) - } - pub(crate) fn get_priority<'a>(cx: &mut JsCall<'a>, pid: i32) -> bindgen::Result<'a, i32> { - cx.with_global(|global| super::get_priority(global, pid)) - } - pub(crate) fn homedir<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, BunString> { - cx.with_global(super::homedir) - } - pub(crate) fn hostname<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(super::hostname) - } - pub(crate) fn network_interfaces<'a>( - cx: &mut JsCall<'a>, - ) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(super::network_interfaces) - } - pub(crate) fn uptime<'a>(cx: &mut JsCall<'a>) -> bindgen::Result<'a, f64> { - cx.with_global(super::uptime) - } - pub(crate) fn user_info<'a>( - cx: &mut JsCall<'a>, - options: UserInfoOptions<'a>, - ) -> bindgen::Result<'a, JsValue<'a>> { - cx.with_global_value(|global| super::user_info(global, options)) - } - pub(crate) fn set_priority<'a>( - cx: &mut JsCall<'a>, - pid: i32, - priority: i32, - ) -> bindgen::Result<'a, ()> { - cx.with_global(|global| super::set_priority(global, pid, priority)) - } - pub(crate) fn set_priority_of_current_process<'a>( - cx: &mut JsCall<'a>, - priority: i32, - ) -> bindgen::Result<'a, ()> { - cx.with_global(|global| super::set_priority(global, 0, priority)) - } + pub(crate) fn host_cpu_count() -> i32 { + bun_sysconf__SC_NPROCESSORS_ONLN().max(1) } impl CPUTimes { @@ -158,7 +110,8 @@ mod _impl { } } - pub(crate) fn cpus(global: &JSGlobalObject) -> JsResult { + pub(crate) fn cpus(cx: &mut JsCall<'_>) -> JsResult { + let global = cx.global(); #[cfg(any(target_os = "linux", target_os = "android"))] let result = cpus_impl_linux(global); #[cfg(target_os = "macos")] @@ -577,7 +530,8 @@ mod _impl { safe fn get_process_priority(pid: i32) -> i32; } - pub(crate) fn get_priority(global: &JSGlobalObject, pid: i32) -> JsResult { + pub(crate) fn get_priority(cx: &mut JsCall<'_>, pid: i32) -> JsResult { + let global = cx.global(); let result = get_process_priority(pid); if result == i32::MAX { let err = SystemError { @@ -595,7 +549,8 @@ mod _impl { Ok(result) } - pub(crate) fn homedir(global: &JSGlobalObject) -> JsResult { + pub(crate) fn homedir(cx: &mut JsCall<'_>) -> JsResult { + let global = cx.global(); // In Node.js, this is a wrapper around uv_os_homedir. #[cfg(windows)] { @@ -698,7 +653,8 @@ mod _impl { } } - pub(crate) fn hostname(global: &JSGlobalObject) -> JsResult { + pub(crate) fn hostname(cx: &mut JsCall<'_>) -> JsResult { + let global = cx.global(); #[cfg(windows)] { let mut name_buffer: [u16; 130] = [0; 130]; // [129:0]u16 → 130 u16s with NUL at [129] @@ -782,7 +738,8 @@ mod _impl { pub(crate) use network_interfaces_windows as network_interfaces; #[cfg(unix)] - pub(crate) fn network_interfaces_posix(global_this: &JSGlobalObject) -> JsResult { + pub(crate) fn network_interfaces_posix(cx: &mut JsCall<'_>) -> JsResult { + let global_this = cx.global(); // getifaddrs sets a pointer to a linked list let mut interface_start: *mut libc::ifaddrs = core::ptr::null_mut(); // SAFETY: valid out-pointer @@ -1075,7 +1032,8 @@ mod _impl { } #[cfg(windows)] - pub(crate) fn network_interfaces_windows(global_this: &JSGlobalObject) -> JsResult { + pub(crate) fn network_interfaces_windows(cx: &mut JsCall<'_>) -> JsResult { + let global_this = cx.global(); let mut ifaces: *mut libuv::uv_interface_address_t = core::ptr::null_mut(); let mut count: c_int = 0; // SAFETY: valid out-pointers @@ -1307,7 +1265,15 @@ mod _impl { return bun_sys::get_errno(code); } - pub(crate) fn set_priority(global: &JSGlobalObject, pid: i32, priority: i32) -> JsResult<()> { + pub(crate) fn set_priority_of_current_process( + cx: &mut JsCall<'_>, + priority: i32, + ) -> JsResult<()> { + set_priority(cx, 0, priority) + } + + pub(crate) fn set_priority(cx: &mut JsCall<'_>, pid: i32, priority: i32) -> JsResult<()> { + let global = cx.global(); let errno = set_process_priority_impl(pid, priority); if errno == bun_sys::E::SUCCESS { return Ok(()); @@ -1352,7 +1318,8 @@ mod _impl { } } - pub(crate) fn uptime(global: &JSGlobalObject) -> JsResult { + pub(crate) fn uptime(cx: &mut JsCall<'_>) -> JsResult { + let global = cx.global(); #[cfg(windows)] { let mut uptime_value: f64 = 0.0; @@ -1390,15 +1357,15 @@ mod _impl { } pub(crate) fn user_info( - global_this: &JSGlobalObject, + cx: &mut JsCall<'_>, options: crate::generated_bindgen::node_os::UserInfoOptions<'_>, ) -> JsResult { let _ = options; // TODO: + let home = homedir(cx)?; + let global_this = cx.global(); let result = JSValue::create_empty_object(global_this, 5); - let home = homedir(global_this)?; - result.put(global_this, b"homedir", home.into_js(global_this)?); #[cfg(windows)] diff --git a/src/runtime/socket/Handlers.rs b/src/runtime/socket/Handlers.rs index 484c5de62bb8..46b24ac68a93 100644 --- a/src/runtime/socket/Handlers.rs +++ b/src/runtime/socket/Handlers.rs @@ -8,7 +8,6 @@ use crate::generated_bindgen::socket_config::{ }; use bun_core::Utf8Bytes; use bun_jsc::array_buffer::BinaryType; -use bun_jsc::bindgen::{JsCall, JsValue}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{GlobalRef, JSGlobalObject, JSValue, JsResult, Strong}; use bun_sys::Fd; @@ -312,14 +311,9 @@ impl Handlers { opts: JSValue, mode: SocketMode, ) -> JsResult> { - let from_js = |cx: &mut JsCall<'_>| { - let opts = JsValue::new(&cx.heap, opts); - let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) - .map_err(|e| e.into_js_error(global_object))?; + GeneratedSocketConfigHandlers::from_js(global_object, opts, |generated| { Self::from_generated(global_object, &generated, mode) - }; - // SAFETY: reached from a host function with no `JsCall` borrowed. - unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } + }) } pub(crate) fn from_generated( @@ -414,18 +408,13 @@ impl Handlers { global_object: &JSGlobalObject, opts: JSValue, ) -> JsResult { - let from_js = |cx: &mut JsCall<'_>| { - let opts = JsValue::new(&cx.heap, opts); - let generated = GeneratedSocketConfigHandlers::from_js(cx, opts) - .map_err(|e| e.into_js_error(global_object))?; + GeneratedSocketConfigHandlers::from_js(global_object, opts, |generated| { let callbacks = Self::validate_callbacks(global_object, &generated)?; Ok(ReloadedHandlers { callbacks, binary_type: binary_type_from_generated(generated.binary_type), }) - }; - // SAFETY: reached from a host function with no `JsCall` borrowed. - unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } + }) } /// Writes the validated callbacks into the existing cell, so the listener @@ -613,13 +602,8 @@ impl SocketConfig { global_object: &JSGlobalObject, mode: SocketMode, ) -> JsResult { - let from_js = |cx: &mut JsCall<'_>| { - let opts = JsValue::new(&cx.heap, opts); - let generated = GeneratedSocketConfig::from_js(cx, opts) - .map_err(|e| e.into_js_error(global_object))?; + GeneratedSocketConfig::from_js(global_object, opts, |generated| { Self::from_generated(vm, global_object, generated, mode) - }; - // SAFETY: reached from a host function with no `JsCall` borrowed. - unsafe { JsCall::with(global_object.as_mut_ptr(), from_js) } + }) } } diff --git a/src/runtime/socket/SSLConfig.bind.ts b/src/runtime/socket/SSLConfig.bind.ts index cbf6e8f28576..f08e95d1f059 100644 --- a/src/runtime/socket/SSLConfig.bind.ts +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -23,90 +23,29 @@ export const ALPNProtocols = b.union({ export const SSLConfig = b.dictionary( { passphrase: b.string.nullable, - dhParamsFile: { - type: b.string.nullable, - internalName: "dh_params_file", - }, - serverName: { - type: b.string.nullable, - internalName: "server_name", - altNames: ["servername"], - }, - lowMemoryMode: { - type: b.bool, - default: false, - internalName: "low_memory_mode", - }, - rejectUnauthorized: { - type: b.bool.nullable, - internalName: "reject_unauthorized", - }, - requestCert: { - type: b.bool, - default: false, - internalName: "request_cert", - }, + dhParamsFile: b.string.nullable, + serverName: { type: b.string.nullable, altNames: ["servername"] }, + lowMemoryMode: b.bool.default(false), + rejectUnauthorized: b.bool.nullable, + requestCert: b.bool.default(false), ca: SSLConfigFile, cert: SSLConfigFile, key: SSLConfigFile, - secureOptions: { - type: b.u32, - default: 0, - internalName: "secure_options", - }, - minVersion: { - type: b.i32, - default: 0, - internalName: "ssl_min_version", - }, - maxVersion: { - type: b.i32, - default: 0, - internalName: "ssl_max_version", - }, - keyFile: { - type: b.string.nullable, - internalName: "key_file", - }, - certFile: { - type: b.string.nullable, - internalName: "cert_file", - }, - caFile: { - type: b.string.nullable, - internalName: "ca_file", - }, - ALPNProtocols: { - type: ALPNProtocols, - internalName: "alpn_protocols", - }, + secureOptions: b.u32.default(0), + minVersion: { type: b.i32.default(0), internalName: "ssl_min_version" }, + maxVersion: { type: b.i32.default(0), internalName: "ssl_max_version" }, + keyFile: b.string.nullable, + certFile: b.string.nullable, + caFile: b.string.nullable, + ALPNProtocols: { type: ALPNProtocols, internalName: "alpn_protocols" }, ciphers: b.string.nullable, - clientRenegotiationLimit: { - type: b.u32, - default: 0, - internalName: "client_renegotiation_limit", - }, - clientRenegotiationWindow: { - type: b.u32, - default: 0, - internalName: "client_renegotiation_window", - }, + clientRenegotiationLimit: b.u32.default(0), + clientRenegotiationWindow: b.u32.default(0), crl: SSLConfigFile, - allowPartialTrustChain: { - type: b.bool, - default: false, - internalName: "allow_partial_trust_chain", - }, - sessionTimeout: { - type: b.i32, - default: 0, - internalName: "session_timeout", - }, + allowPartialTrustChain: b.bool.default(false), + sessionTimeout: b.i32.default(0), sigalgs: b.string.nullable, - ecdhCurve: { - type: b.string.nullable, - internalName: "ecdh_curve", - }, + ecdhCurve: b.string.nullable, }, { userFacingName: "TLSOptions", diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index bb4da1e0fa05..30bdb5a47c9e 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -145,14 +145,9 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { - let from_js = |cx: &mut jsc::bindgen::JsCall<'_>| { - let value = jsc::bindgen::JsValue::new(&cx.heap, value); - let generated = - generated::SSLConfig::from_js(cx, value).map_err(|e| e.into_js_error(global))?; + generated::SSLConfig::from_js(global, value, |generated| { Self::from_generated(vm, global, &generated, is_server) - }; - // SAFETY: reached from a host function with no `JsCall` borrowed. - unsafe { jsc::bindgen::JsCall::with(global.as_mut_ptr(), from_js) } + }) } fn from_generated( diff --git a/src/runtime/socket/SocketConfig.bind.ts b/src/runtime/socket/SocketConfig.bind.ts index c05c4e8ccc3d..9e6020667d5c 100644 --- a/src/runtime/socket/SocketConfig.bind.ts +++ b/src/runtime/socket/SocketConfig.bind.ts @@ -22,11 +22,7 @@ export const SocketConfigHandlers = b.dictionary( keylog: { type: b.any, internalName: "onKeylog" }, serverName: { type: b.any, internalName: "onServerName" }, alpnCallback: { type: b.any, internalName: "onALPNCallback" }, - binaryType: { - type: SocketConfigBinaryType, - default: "buffer", - internalName: "binary_type", - }, + binaryType: SocketConfigBinaryType.default("buffer"), }, { userFacingName: "SocketHandler", @@ -42,41 +38,16 @@ export const SocketConfigTLS = b.union({ export const SocketConfig = b.dictionary( { - socket: { - type: SocketConfigHandlers, - internalName: "handlers", - }, + socket: { type: SocketConfigHandlers, internalName: "handlers" }, data: b.any, - allowHalfOpen: { - type: b.bool, - default: false, - internalName: "allow_half_open", - }, - hostname: { - type: b.string.loose.falsyAsNull, - altNames: ["host"], - }, + allowHalfOpen: b.bool.default(false), + hostname: { type: b.string.loose.falsyAsNull, altNames: ["host"] }, port: b.u16.loose.nullable, tls: SocketConfigTLS, - exclusive: { - type: b.bool, - default: false, - }, - reusePort: { - type: b.bool, - default: false, - internalName: "reuse_port", - }, - ipv6Only: { - type: b.bool, - default: false, - internalName: "ipv6_only", - }, - pauseOnConnect: { - type: b.bool.loose, - default: false, - internalName: "pause_on_connect", - }, + exclusive: b.bool.default(false), + reusePort: b.bool.default(false), + ipv6Only: b.bool.default(false), + pauseOnConnect: b.bool.loose.default(false), unix: { type: b.string.falsyAsNull, internalName: "unix_", // `unix` is a predefined C macro... diff --git a/test/internal/bindgen-compile-fail.test.ts b/test/internal/bindgen-compile-fail.test.ts index 8c243706676d..25752922fe35 100644 --- a/test/internal/bindgen-compile-fail.test.ts +++ b/test/internal/bindgen-compile-fail.test.ts @@ -5,7 +5,7 @@ // The cases are type-checked against the metadata `cargo check` leaves behind; nothing is linked. import { which } from "bun"; import { beforeAll, describe, expect, test } from "bun:test"; -import { tempDir } from "harness"; +import { bunExe, tempDir } from "harness"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -13,8 +13,9 @@ const cargo = which("cargo"); const rustc = which("rustc"); const repoRoot = join(import.meta.dir, "..", ".."); const cases = join(import.meta.dir, "bindgen-compile-fail"); -const codegenDir = join(repoRoot, "build", "debug", "codegen"); -// Test-only CI lanes run a prebuilt binary: no toolchain, no vendored crates, no generated code. +const codegenDir = process.env.BUN_CODEGEN_DIR ?? join(dirname(bunExe()), "codegen"); +// The test lanes run a prebuilt binary: no toolchain, no vendored crates, no generated code. The `cargo clippy` job of +// .github/workflows/rust-lints.yml has all three and runs this file. const canCheck = !!cargo && !!rustc && @@ -38,7 +39,7 @@ describe.skipIf(!canCheck)("bun_jsc::bindgen", () => { }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stderr, exitCode }).toMatchObject({ exitCode: 0 }); - for (const line of stdout.split("\n")) { + for (const line of stdout.split(/\r?\n/)) { if (!line.startsWith("{")) continue; const message = JSON.parse(line); if (message.reason !== "compiler-artifact") continue; @@ -50,7 +51,7 @@ describe.skipIf(!canCheck)("bun_jsc::bindgen", () => { }, 600_000); /** The distinct errors rustc reports for the case: a code, or "lifetime" for the one that has none. */ - async function check(kind: string, file: string): Promise<{ codes: string[]; stderr: string }> { + async function check(kind: string, file: string): Promise<{ codes: string[]; ok: boolean; stderr: string }> { using dir = tempDir("bindgen-compile-fail", { [file]: prelude + readFileSync(join(cases, kind, file), "utf8") }); const source = join(String(dir), file); await using proc = Bun.spawn({ @@ -72,25 +73,25 @@ describe.skipIf(!canCheck)("bun_jsc::bindgen", () => { stdout: "ignore", stderr: "pipe", }); - const [stderr] = await Promise.all([proc.stderr.text(), proc.exited]); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); const errors = stderr - .split("\n") + .split(/\r?\n/) .filter(line => /\berror\b/.test(line) && !line.startsWith("error: aborting due to")); const code = (line: string) => /error\[(E\d+)\]/.exec(line)?.[1] ?? (line.includes("lifetime may not live long enough") ? "lifetime" : line); - return { codes: [...new Set(errors.map(code))], stderr }; + return { codes: [...new Set(errors.map(code))], ok: exitCode === 0, stderr }; } describe("pass", () => { test.concurrent.each(readdirSync(join(cases, "pass")))("%s", async file => { - expect(await check("pass", file)).toMatchObject({ codes: [] }); + expect(await check("pass", file)).toMatchObject({ codes: [], ok: true }); }); }); describe("fail", () => { test.concurrent.each(readdirSync(join(cases, "fail")))("%s", async file => { const expected = /^\/\/ (E\d+|lifetime): /.exec(readFileSync(join(cases, "fail", file), "utf8"))![1]; - expect(await check("fail", file)).toMatchObject({ codes: [expected] }); + expect(await check("fail", file)).toMatchObject({ codes: [expected], ok: false }); }); }); }); diff --git a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs index fd031b375300..3bc1f9b3fa99 100644 --- a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs +++ b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs @@ -6,7 +6,4 @@ impl JsPayload for Fake { unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { - unreachable!() - } } diff --git a/test/internal/bindgen-compile-fail/fail/value_between_calls.rs b/test/internal/bindgen-compile-fail/fail/value_between_calls.rs index c82fba9d57de..74e2275ee8f6 100644 --- a/test/internal/bindgen-compile-fail/fail/value_between_calls.rs +++ b/test/internal/bindgen-compile-fail/fail/value_between_calls.rs @@ -2,8 +2,8 @@ fn f(global: *mut JSGlobalObject) { unsafe { JsCall::with(global, |outer| { - let error = outer.heap.throw_value(JsValue::NULL); - JsCall::with(global, |inner| drop(inner.take_exception(error))); + let value = JsValue::new(&outer.heap, JSValue::NULL); + JsCall::with(global, |inner| drop(inner.heap.throw_value(value))); }) }; } diff --git a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs index 00a7c007abd0..448fbaf8f7c1 100644 --- a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs +++ b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs @@ -8,7 +8,4 @@ unsafe impl<'a> JsPayload for Holder<'a> { unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { - unreachable!() - } } diff --git a/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs b/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs index ce31d78ee53f..ce2eafd12369 100644 --- a/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs +++ b/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs @@ -9,7 +9,7 @@ fn start<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<' fn finish<'a>(pending: Pending, cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { let socket = pending.socket.get(&cx.heap); socket.with_mut(&mut cx.payloads, |socket| socket.pending.clear()); - let Some(callback) = JsValue::from_strong(&cx.heap, &pending.callback).as_function() else { + let Some(callback) = JsValue::new(&cx.heap, pending.callback.get()).as_function() else { return Ok(JsValue::UNDEFINED); }; callback.call(cx, socket.as_value(), &[]) diff --git a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs index 6abafd054b6e..fe428bb35acf 100644 --- a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs +++ b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs @@ -14,7 +14,7 @@ fn flush<'a>( }; let result = callback.call(cx, this.as_value(), &[chunk.as_value()])?; // Afterwards everything is looked up again. A `JsValue` stays usable: the stack is scanned. - let written = extra.with_bytes(&cx.heap, <[u8]>::len).unwrap_or(0); + let written = extra.with_bytes(&cx.heap, <[u8]>::len); this.with_mut(&mut cx.payloads, |socket| { socket.pending.clear(); socket.bytes_written += written as u64; diff --git a/test/internal/bindgen-compile-fail/prelude.rs b/test/internal/bindgen-compile-fail/prelude.rs index 5405dffc6590..9b5695bb3bb4 100644 --- a/test/internal/bindgen-compile-fail/prelude.rs +++ b/test/internal/bindgen-compile-fail/prelude.rs @@ -13,9 +13,6 @@ unsafe impl JsPayload for Socket { unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { unreachable!() } - fn cell_from_js(_: JSValue) -> *mut core::ffi::c_void { - unreachable!() - } } pub trait SocketInternalFields<'a>: Copy { fn ondata_field(self, heap: &JsHeap<'a>) -> JsValue<'a>; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index f7f06ace8c98..dab4ea88ef52 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -94,10 +94,10 @@ describe("strings", () => { "ascii", "latin1 éÿ", "utf16 你好 \u{1f600}", - rope("a".repeat(40), "b".repeat(40)), - rope("é".repeat(40), "你".repeat(40)), - "x".repeat(1000), - "é".repeat(1000), + rope(Buffer.alloc(40, "a").toString(), Buffer.alloc(40, "b").toString()), + rope(Buffer.alloc(80, "é").toString(), Buffer.alloc(120, "你").toString()), + Buffer.alloc(1000, "x").toString(), + Buffer.alloc(2000, "é").toString(), "nul \0 inside", ]; @@ -146,7 +146,34 @@ describe("strings", () => { }); it("checks the argument count", () => { - expect(() => bindgen.utf8Echo()).toThrow("Not enough arguments"); + expect(() => bindgen.utf8Echo()).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), + ); + }); + + it("keeps the characters of a substring while its base is swapped for an atom", () => { + const fill = (length: number, char: string) => Buffer.alloc(length, char).toString("latin1"); + const overwriteStack = (depth: number): number => (depth > 0 ? overwriteStack(depth - 1) + depth : 0); + const keys: Record = {}; + const strings = { base: fill(300, "q"), equal: fill(300, "q") }; + keys[strings.equal] = 1; + const options = { + name: strings.base.substring(10, 290), + // Read after `name` is converted. + get renamed() { + // The base takes the atom's characters; two more lookups push its own out of the VM's last-atomized slot. + keys[strings.base]; + keys[fill(33, "q")]; + keys[fill(34, "q")]; + overwriteStack(200); + Bun.gc(true); + const reuse = Array.from({ length: 5000 }, () => fill(300, "Z")); + Bun.gc(true); + return reuse.length ? 1 : 0; + }, + }; + overwriteStack(300); + expect(bindgen.optionsEcho(options).name).toBe(fill(280, "q")); }); }); @@ -402,6 +429,26 @@ describe("dictionaries", () => { }; expect(bindgen.optionsEcho(options)).toStrictEqual({ ...defaults, name: "added" }); }); + + it("a conversion that changes the object is seen by the members after it", () => { + const make = () => { + const options: any = { payload: 0, color: "red", renamed: 1 }; + options.name = Object.assign(new String("x"), { + toString() { + options.color = "blue"; + options.renamed = 9; + delete options.payload; + options.payload = "late"; + return "n"; + }, + }); + return options; + }; + const expected = { ...defaults, name: "n", color: "blue", payload: "late", renamed: 9 }; + expect(bindgen.optionsEcho(make())).toStrictEqual(expected); + expect(bindgen.optionsEcho(Object.setPrototypeOf(make(), {}))).toStrictEqual(expected); + expect(bindgen.optionsEcho(Object.defineProperty(make(), "count", { get: () => 1 }))).toStrictEqual(expected); + }); }); describe("unions", () => { @@ -477,6 +524,21 @@ describe("buffers", () => { } }); + it("reads the bytes after every other argument is converted", () => { + const transferred = new Uint8Array([1, 2, 3]); + const transfers = { toString: () => (transferred.buffer.transfer(), "\x01") }; + expect(bindgen.sumBytesThenText(transferred, transfers)).toBe(1); + + const resizable = new ArrayBuffer(1 << 20, { maxByteLength: 1 << 20 }); + const view = new Uint8Array(resizable).fill(1); + const shrinks = { toString: () => (resizable.resize(2), "") }; + expect(bindgen.sumBytesThenText(view, shrinks)).toBe(2); + + expect(() => bindgen.sumBytesThenText("no", { toString: () => expect.unreachable() })).toThrow( + "bytes must be an ArrayBuffer or ArrayBufferView that is not shared", + ); + }); + it("looks the bytes up again after JavaScript ran", () => { const same = new Uint8Array([1, 2, 3]); expect(bindgen.sumBeforeAndAfterCallback(same, () => {})).toEqual([6, 6]); @@ -533,9 +595,9 @@ describe("errors", () => { }); describe("classes", () => { - const { TestCounter } = bindgen as any; + const { TestCounter, TestHolder } = bindgen as any; - it("has the shape of a class", () => { + it("looks like a class", () => { expect(typeof TestCounter).toBe("function"); expect(TestCounter.name).toBe("TestCounter"); expect(TestCounter.length).toBe(0); @@ -561,11 +623,11 @@ describe("classes", () => { }); it("a constructor that throws makes nothing", () => { - Bun.gc(true); const before = TestCounter.alive(); expect(() => new TestCounter(-1)).toThrow(new RangeError("start must not be negative")); expect(() => new TestCounter("1")).toThrow(TypeError); - expect(TestCounter.alive()).toBe(before); + // A collection in between can only lower it. + expect(TestCounter.alive()).toBeLessThanOrEqual(before); }); it("setters convert like arguments", () => { @@ -643,7 +705,7 @@ describe("classes", () => { expect(made.plus(made)).toBe(80); }); - it("holds a value in a slot and lets JavaScript re-enter", () => { + it("holds a value in an internal field and lets JavaScript re-enter", () => { const counter = new TestCounter(1); expect(counter.notify()).toBe(1); const receivers: unknown[] = []; @@ -673,19 +735,31 @@ describe("classes", () => { expect(Object.getPrototypeOf(bindgen.makeToken(8))).toBe(prototype); }); + it("a payload keeps another object alive with a Strong", () => { + const holder = new TestHolder(new TestCounter(5)); + Bun.gc(true); + expect(holder.counter).toBeInstanceOf(TestCounter); + expect(holder.counter).toBe(holder.counter); + expect(holder.incrementCounter()).toBe(6); + expect(holder.counter.count).toBe(6); + expect(() => new TestHolder({})).toThrow("counter must be an instance of TestCounter"); + }); + it("drops the Rust value when the object is collected", () => { + const count = 1000; + let counters: unknown[] | null = Array.from({ length: count }, (_, i) => { + const counter = new TestCounter(i); + counter.label = "label " + i; + counter.listen(() => counter); + counter.tag; + return new TestHolder(counter); + }); + Bun.gc(true); + expect(TestCounter.alive()).toBeGreaterThanOrEqual(counters.length); + counters = null; + // The first collection drops the holders and their Strongs, the second the counters. Bun.gc(true); - const before = TestCounter.alive(); - (function make() { - for (let i = 0; i < 1000; i++) { - const counter = new TestCounter(i); - counter.label = "label " + i; - counter.listen(() => counter); - counter.tag; - } - })(); - expect(TestCounter.alive()).toBeGreaterThan(before); Bun.gc(true); - expect(TestCounter.alive()).toBeLessThan(before + 100); + expect(TestCounter.alive()).toBeLessThan(count); }); }); diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index a7297f49fd60..d4796ef79740 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -199,11 +199,12 @@ describe("emitBindgen", () => { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); const typesDir = resolve(String(dir), "types"); + const sources = /--sources=[^\s'"]+/.exec(n.toString().replaceAll("$:", ":"))![0]; expect( await bunRun([ "run", script, - `--sources=${bindgen.join(",")}`, + sources, `--codegen-path=${cfg.codegenDir}`, `--types-path=${typesDir}`, ]), diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index f6ed1bff81e8..2a26b0a70f20 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -44,3 +44,43 @@ test("Object prototype followSymlinks", async () => { }); expect([...third].map(a => a.replaceAll("\\", "/"))).toEqual(["def/file.txt"]); }); + +test("arguments of the wrong type throw a TypeError", () => { + const invalid = (message: string) => + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message }); + const missing = expect.objectContaining({ + name: "TypeError", + code: "ERR_MISSING_ARGS", + message: "Not enough arguments", + }); + const glob = new Bun.Glob("*"); + expect(() => new (Bun.Glob as any)()).toThrow(missing); + expect(() => new Bun.Glob(5 as any)).toThrow(invalid("pattern must be a string")); + expect(() => (glob.match as any)()).toThrow(missing); + expect(() => glob.match(5 as any)).toThrow(invalid("str must be a string")); + expect(() => glob.scanSync(5 as any)).toThrow(invalid("GlobScanOptions must be an object")); + expect(() => glob.scan(5 as any)).toThrow(invalid("GlobScanOptions must be an object")); + expect(() => glob.scanSync({ cwd: 5 as any })).toThrow(invalid("GlobScanOptions.cwd must be a string")); + expect(Bun.Glob.length).toBe(1); +}); + +test("scan options", () => { + using dir = tempDir("glob-options", { "a.txt": "", ".dot": "", "sub/b.txt": "" }); + const cwd = String(dir); + const scan = (pattern: string, options: any) => [...new Bun.Glob(pattern).scanSync(options)].sort(); + + // Booleans are ToBoolean. + expect(scan("*", { cwd, dot: 1 })).toEqual([".dot", "a.txt"]); + expect(scan("*", { cwd, dot: 0 })).toEqual(["a.txt"]); + expect(scan("*", { cwd, onlyFiles: 1 })).toEqual(["a.txt"]); + expect(scan("*", { cwd, onlyFiles: "" })).toEqual(["a.txt", "sub"]); + expect(scan("*", { cwd, onlyFiles: null })).toEqual(["a.txt"]); + + // A String object is a cwd, as the argument and as the option. + expect(scan("*", new String(path.join(cwd, "sub")))).toEqual(["b.txt"]); + expect(scan("*", { cwd: new String(path.join(cwd, "sub")) })).toEqual(["b.txt"]); + + // An array is an object with none of the options. + expect(scan("package.json", [])).toEqual(scan("package.json", {})); + expect(scan("*", Object.assign([], { cwd, dot: true }))).toEqual([".dot", "a.txt"]); +}); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 8bfba77a0ac2..e2155a33c534 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6456,3 +6456,21 @@ describe.concurrent("Bun.listen() with tls.secureContext and the client certific ); }); }); + +it("socket and tls options are not read from Object.prototype", async () => { + const polluted: Record = { + port: 1, + hostname: "256.256.256.256", + unix: "/nonexistent/socket", + tls: true, + }; + Object.assign(Object.prototype, polluted); + try { + expect(() => Bun.listen({ socket: { data() {} } } as any)).toThrow(); + using server = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } }); + expect(server.port).toBeGreaterThan(1); + expect(server.hostname).toBe("127.0.0.1"); + } finally { + for (const name of Object.keys(polluted)) delete (Object.prototype as any)[name]; + } +}); diff --git a/test/js/bun/shell/brace.test.ts b/test/js/bun/shell/brace.test.ts index c4c29140c79e..b6b5e1339e80 100644 --- a/test/js/bun/shell/brace.test.ts +++ b/test/js/bun/shell/brace.test.ts @@ -333,3 +333,35 @@ describe("comma-less brace group is literal (bash 5.2)", () => { }); }); }); + +describe("$.braces arguments", () => { + test("input is ToString", () => { + expect($.braces(12 as any)).toEqual(["12"]); + expect($.braces({ toString: () => "a{b,c}" } as any)).toEqual(["ab", "ac"]); + }); + + test("a word with nothing to expand comes back as it was", () => { + expect($.braces("a\ud800b")).toEqual(["a\ud800b"]); + }); + + test("options are an object, null or undefined", () => { + for (const options of [undefined, null, {}, [], () => {}]) { + expect($.braces("a{b,c}", options as any)).toEqual(["ab", "ac"]); + } + for (const options of [5, "s", true, Symbol("s")]) { + expect(() => $.braces("a{b,c}", options as any)).toThrow( + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: "BracesOptions must be an object", + }), + ); + } + }); + + test("without arguments throws ERR_MISSING_ARGS", () => { + expect(() => ($.braces as any)()).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), + ); + }); +}); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index 007f92805a64..ecfd54f1caa2 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -309,3 +309,52 @@ it("setPriority throws ESRCH for a nonexistent pid", () => { }), ); }); + +it("functions have Node's length", () => { + const lengths = Object.fromEntries( + [ + "cpus", + "freemem", + "getPriority", + "homedir", + "hostname", + "loadavg", + "networkInterfaces", + "release", + "setPriority", + "totalmem", + "uptime", + "userInfo", + "version", + ].map(name => [name, os[name].length]), + ); + // node -p 'os..length', v26.7.0 + expect(lengths).toEqual({ + cpus: 0, + freemem: 0, + getPriority: 1, + homedir: 0, + hostname: 0, + loadavg: 0, + networkInterfaces: 0, + release: 0, + setPriority: 2, + totalmem: 0, + uptime: 0, + userInfo: 1, + version: 0, + }); +}); + +it("userInfo ignores options that are not an object, like Node", () => { + const info = os.userInfo(); + for (const options of [undefined, null, 5, "utf8", true, Symbol("s"), [], ["utf8"], () => {}]) { + expect(os.userInfo(options)).toEqual(info); + } +}); + +it("setPriority without arguments throws ERR_MISSING_ARGS", () => { + expect(() => os.setPriority()).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), + ); +}); From 50fe67125379a7a0dfbe2b50ff3acb55edc4acc7 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 03:39:27 +0000 Subject: [PATCH 06/44] Name both .bind.ts files when two of them generate the same header --- scripts/build/codegen.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index adc8e5e80227..e9f9cff2b91b 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -878,6 +878,11 @@ export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { // What bindgen.ts writes follows from the names and places of its inputs alone. const stems = sources.bindgen.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); + stems.forEach((stem, i) => { + const first = stems.indexOf(stem); + if (first !== i) + throw new BuildError(`${sources.bindgen[first]} and ${sources.bindgen[i]} both generate ${stem}.h`); + }); const cpp = stems.map(stem => resolve(cfg.codegenDir, `${stem}.cpp`)); const headers = [ ...stems.map(stem => resolve(cfg.codegenDir, `${stem}.h`)), From 81a7bf32ef408a08a392d1119df7ffd3d9d816cb Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 03:41:05 +0000 Subject: [PATCH 07/44] Test that native node:os functions are not constructors and that Glob reads every option before reporting a long cwd --- test/js/bun/glob/proto.test.ts | 16 ++++++++++++++++ test/js/node/os/os.test.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index 2a26b0a70f20..4aa0ed0403d5 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -84,3 +84,19 @@ test("scan options", () => { expect(scan("package.json", [])).toEqual(scan("package.json", {})); expect(scan("*", Object.assign([], { cwd, dot: true }))).toEqual([".dot", "a.txt"]); }); + +test("every option is read before a cwd that is too long is reported", () => { + const read: string[] = []; + const options = { + get cwd() { + read.push("cwd"); + return Buffer.alloc(5000, "a").toString(); + }, + get dot() { + read.push("dot"); + return false; + }, + }; + expect(() => new Bun.Glob("*").scanSync(options)).toThrow("scanSync: invalid `cwd`, longer than"); + expect(read).toEqual(["cwd", "dot"]); +}); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index ecfd54f1caa2..5cf33dc349c8 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -358,3 +358,18 @@ it("setPriority without arguments throws ERR_MISSING_ARGS", () => { expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), ); }); + +it("native functions are not constructors", () => { + for (const name of [ + "freemem", + "getPriority", + "homedir", + "hostname", + "loadavg", + "release", + "setPriority", + "totalmem", + ]) { + expect(() => new os[name]()).toThrow(TypeError); + } +}); From f555dfb0cec95df5b79974931a8e9fbac9261a2b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 03:41:54 +0000 Subject: [PATCH 08/44] Stop naming Glob in the .classes.ts skill's example --- .../skills/implementing-jsc-classes-rust/SKILL.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/skills/implementing-jsc-classes-rust/SKILL.md b/.claude/skills/implementing-jsc-classes-rust/SKILL.md index 890f6eab62ce..087c4b3b9f8c 100644 --- a/.claude/skills/implementing-jsc-classes-rust/SKILL.md +++ b/.claude/skills/implementing-jsc-classes-rust/SKILL.md @@ -18,7 +18,7 @@ Bridge JavaScript and Rust through `.classes.ts` definitions and Rust implementa ```typescript export default [ define({ - name: "Glob", + name: "Matcher", construct: true, finalize: true, hasPendingActivity: true, @@ -45,16 +45,16 @@ use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; use std::sync::atomic::{AtomicUsize, Ordering}; #[bun_jsc::JsClass] -pub struct Glob { +pub struct Matcher { pattern: Box<[u8]>, has_pending_activity: AtomicUsize, } -impl Glob { - pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { +impl Matcher { + pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult> { let arg = frame.argument(0); let pattern = bun_core::String::from_js(arg, global)?.to_utf8_bytes().into(); - Ok(Box::new(Glob { pattern, has_pending_activity: AtomicUsize::new(0) })) + Ok(Box::new(Matcher { pattern, has_pending_activity: AtomicUsize::new(0) })) } #[bun_jsc::host_fn(method)] @@ -88,7 +88,7 @@ A missing or mis-typed hook is a **compile error** in `cargo check -p bun_runtim The codegen also emits a `js_$T` module with the cached-value accessors. Re-export it when you need `*_set_cached` / `*_get_cached` or `detach_ptr`: ```rust -pub use crate::generated_classes::js_Glob as js; +pub use crate::generated_classes::js_Image as js; // or bun_jsc::impl_js_class_via_generated!(Archive => crate::generated_classes::js_Archive); ``` From 937eed11f9b17480c8478f4677da264bb9c8d9e8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:44:27 +0000 Subject: [PATCH 09/44] [autofix.ci] apply automated fixes --- test/internal/build-codegen-declared-outputs.test.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index d4796ef79740..528bd528aa68 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -201,13 +201,7 @@ describe("emitBindgen", () => { const typesDir = resolve(String(dir), "types"); const sources = /--sources=[^\s'"]+/.exec(n.toString().replaceAll("$:", ":"))![0]; expect( - await bunRun([ - "run", - script, - sources, - `--codegen-path=${cfg.codegenDir}`, - `--types-path=${typesDir}`, - ]), + await bunRun(["run", script, sources, `--codegen-path=${cfg.codegenDir}`, `--types-path=${typesDir}`]), ).toSpawn(""); const written = readdirSync(cfg.codegenDir).sort(); From d2849b2f266cb9127fd04b8aea2b12be8cd1a34e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 04:01:17 +0000 Subject: [PATCH 10/44] Use a cwd over every platform's path limit in the Glob option-order test --- test/js/bun/glob/proto.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index 4aa0ed0403d5..e8b40a048c95 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -90,7 +90,8 @@ test("every option is read before a cwd that is too long is reported", () => { const options = { get cwd() { read.push("cwd"); - return Buffer.alloc(5000, "a").toString(); + // Over MAX_PATH_BYTES everywhere: Windows has the largest, 98302. + return Buffer.alloc(100_000, "a").toString(); }, get dot() { read.push("dot"); From 6c64a49a32dfdfa2e757c81643676a279f48354d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 04:05:49 +0000 Subject: [PATCH 11/44] Generate compiling Rust for nullable byte views, assert the exact error in the prototype-pollution test, and make the compile-fail suite opt-in --- docs/project/bindgen.mdx | 2 +- src/codegen/bindgen-lib-internal.ts | 2 +- src/js/internal-for-testing.ts | 1 + src/jsc/bindgen_test.bind.ts | 1 + src/jsc/bindgen_test.rs | 4 ++++ test/internal/bindgen-compile-fail.test.ts | 9 +++++---- test/internal/bindgen.test.ts | 11 +++++++++++ test/js/bun/net/socket.test.ts | 4 +++- 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index e9c7f34e1922..73f4ff5572dd 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -362,4 +362,4 @@ thread when the work completes. ## Tests - `test/internal/bindgen.test.ts`: conversions with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. What that schema does not use is covered by the suites of the APIs that do. -- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. It needs a Rust toolchain: the `cargo clippy` job of `rust-lints.yml` runs it, the test lanes skip it. +- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. It runs `cargo check`, so it is skipped unless `BUN_CODEGEN_DIR` is set (`BUN_CODEGEN_DIR=$PWD/build/debug/codegen bun bd test bindgen-compile-fail`). The `cargo clippy` job of `rust-lints.yml` runs it. diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 8490c2de7ff3..e31a1cb04e0c 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -660,7 +660,7 @@ export class OptionalType extends Type { const inner = this.payload.rustFromExtern("value", scope); return inner === "value" ? `${expr}.get()` : `${expr}.get().map(|value| ${inner})`; } - if (this.payload instanceof Utf8Type) return `${expr}.get_optional()`; + if (this.payload instanceof Utf8Type || this.payload instanceof BytesType) return `${expr}.get_optional()`; return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.payload.rustFromExtern("value", scope)}) } }`; } rustResultFieldType(scope: RustScope): string { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index a7e3329ee775..d508c7b81215 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -245,6 +245,7 @@ export const bindgen = { range: $bindgenFn("bindgen_test.bind.ts", "range"), words: $bindgenFn("bindgen_test.bind.ts", "words"), sumBytes: $bindgenFn("bindgen_test.bind.ts", "sumBytes"), + sumBytesOptional: $bindgenFn("bindgen_test.bind.ts", "sumBytesOptional"), sumBytesThenText: $bindgenFn("bindgen_test.bind.ts", "sumBytesThenText"), sumBeforeAndAfterCallback: $bindgenFn("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), withValueEcho: $bindgenFn("bindgen_test.bind.ts", "withValueEcho"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 14cb44c5c9dc..df4d0f838bb5 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -64,6 +64,7 @@ export const range = b.fn({ args: { length: b.u32 }, returns: b.array(b.u32) }); export const words = b.fn({ args: { text: b.string.utf8 }, returns: b.array(b.string.utf8) }); export const sumBytes = b.fn({ args: { bytes: b.bufferSource.bytes }, returns: b.u32 }); +export const sumBytesOptional = b.fn({ args: { bytes: b.bufferSource.bytes.nullable }, returns: b.u32.nullable }); /** Converting `text` can run JavaScript. */ export const sumBytesThenText = b.fn({ args: { bytes: b.bufferSource.bytes, text: b.string.loose.utf8 }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index f00090a21d3f..21bde535c0fe 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -103,6 +103,10 @@ pub fn sum_bytes(bytes: &[u8]) -> u32 { sum(bytes) } +pub fn sum_bytes_optional(bytes: Option<&[u8]>) -> Option { + bytes.map(sum) +} + pub fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { sum(bytes) + sum(text) } diff --git a/test/internal/bindgen-compile-fail.test.ts b/test/internal/bindgen-compile-fail.test.ts index 25752922fe35..1e3a729c432d 100644 --- a/test/internal/bindgen-compile-fail.test.ts +++ b/test/internal/bindgen-compile-fail.test.ts @@ -5,7 +5,7 @@ // The cases are type-checked against the metadata `cargo check` leaves behind; nothing is linked. import { which } from "bun"; import { beforeAll, describe, expect, test } from "bun:test"; -import { bunExe, tempDir } from "harness"; +import { tempDir } from "harness"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -13,10 +13,11 @@ const cargo = which("cargo"); const rustc = which("rustc"); const repoRoot = join(import.meta.dir, "..", ".."); const cases = join(import.meta.dir, "bindgen-compile-fail"); -const codegenDir = process.env.BUN_CODEGEN_DIR ?? join(dirname(bunExe()), "codegen"); -// The test lanes run a prebuilt binary: no toolchain, no vendored crates, no generated code. The `cargo clippy` job of -// .github/workflows/rust-lints.yml has all three and runs this file. +// Opt-in, as for `cargo check`: BUN_CODEGEN_DIR=$PWD/build/debug/codegen. The first run compiles `bun_jsc`'s dependencies. +// The `cargo clippy` job of .github/workflows/rust-lints.yml runs this file. +const codegenDir = process.env.BUN_CODEGEN_DIR ?? ""; const canCheck = + !!codegenDir && !!cargo && !!rustc && existsSync(join(repoRoot, "vendor", "lolhtml", "Cargo.toml")) && diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index dab4ea88ef52..3138d097c3dd 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -524,6 +524,17 @@ describe("buffers", () => { } }); + it("tells an absent buffer from an empty one", () => { + expect(bindgen.sumBytesOptional(null)).toBe(null); + expect(bindgen.sumBytesOptional(undefined)).toBe(null); + expect(bindgen.sumBytesOptional()).toBe(null); + expect(bindgen.sumBytesOptional(new Uint8Array(0))).toBe(0); + expect(bindgen.sumBytesOptional(new Uint8Array([1, 2]))).toBe(3); + const detached = new Uint8Array([1, 2]); + detached.buffer.transfer(); + expect(bindgen.sumBytesOptional(detached)).toBe(0); + }); + it("reads the bytes after every other argument is converted", () => { const transferred = new Uint8Array([1, 2, 3]); const transfers = { toString: () => (transferred.buffer.transfer(), "\x01") }; diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index e2155a33c534..0c8983cfae14 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6466,7 +6466,9 @@ it("socket and tls options are not read from Object.prototype", async () => { }; Object.assign(Object.prototype, polluted); try { - expect(() => Bun.listen({ socket: { data() {} } } as any)).toThrow(); + expect(() => Bun.listen({ socket: { data() {} } } as any)).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", message: 'Expected either "hostname" or "unix"' }), + ); using server = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } }); expect(server.port).toBeGreaterThan(1); expect(server.hostname).toBe("127.0.0.1"); From acbac086984e6b25ad880ed985fea4f48658b9e4 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 04:34:59 +0000 Subject: [PATCH 12/44] Build string views in place, make small arrays in one call, encode large integers directly, and hide private-symbol methods from stack traces --- src/codegen/bindgen-emit.ts | 2 +- src/jsc/JSValue.rs | 4 ++-- src/jsc/bindgen.rs | 22 +++++++++++++++++++ src/jsc/bindgen/extern_types.rs | 5 +++++ src/jsc/bindings/Bindgen/IDLConvert.h | 9 ++++++++ src/jsc/bindings/Bindgen/JSStringUTF8View.h | 8 +++---- src/jsc/bindings/BindgenIdentifiers.cpp | 5 +++++ test/internal/bindgen.test.ts | 5 +++++ test/js/bun/glob/proto.test.ts | 24 ++++++++++++++++++++- 9 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 952a2e06a8d4..55f5b863d393 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -577,7 +577,7 @@ ${table(`JS${name}PrototypeTableValues`, prototypeRows)}${table(`JS${name}Constr ${privateMethods .map( method => - ` prototype->putDirect(vm, names.${method.privateSymbol}PrivateName(), JSC::JSFunction::create(vm, globalObject, ${method.length ?? method.requiredCount}, ${asciiLiteral(method.name)}, ${methodHostFunctionName(type, method)}, JSC::ImplementationVisibility::Public), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | 0);`, + ` prototype->putDirect(vm, names.${method.privateSymbol}PrivateName(), JSC::JSFunction::create(vm, globalObject, ${method.length ?? method.requiredCount}, ${asciiLiteral(method.name)}, ${methodHostFunctionName(type, method)}, JSC::ImplementationVisibility::Private), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | 0);`, ) .join("\n")} } diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 99857aead1a1..39de42b9d283 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -473,7 +473,7 @@ impl JSValue { if i <= i32::MAX as u64 { Self::js_number_from_int32(i as i32) } else { - Self::js_number(i as f64) + Self::js_double_number(i as f64) } } /// `JSValue.jsNumberFromInt64` — int32 fast-path, @@ -482,7 +482,7 @@ impl JSValue { if i <= i32::MAX as i64 && i >= i32::MIN as i64 { Self::js_number_from_int32(i as i32) } else { - Self::js_number(i as f64) + Self::js_double_number(i as f64) } } pub fn js_number(n: f64) -> JSValue { diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index f3a7668a3182..242cc35fea49 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -520,7 +520,29 @@ pub mod to_js { items: I, mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsValue<'a>>, ) -> Result<'a, JsValue<'a>> { + // As many as a `MarkedArgumentBuffer` holds inline, on this stack, where the GC sees them just the same. + let mut inline = [JSValue::ZERO; 8]; + let mut items = items.into_iter(); + for count in 0..inline.len() { + let Some(item) = items.next() else { + // SAFETY: `count` initialized values. + return JsValue::from_js_result(crate::call_zero_is_throw( + heap.global(), + || unsafe { + extern_types::Bun__Bindgen__constructArrayFromValues( + heap.global_ptr(), + inline.as_ptr(), + count, + ) + }, + )); + }; + inline[count] = convert(heap, item)?.0; + } MarkedArgumentBuffer::new(|values| { + for value in inline { + values.append(value); + } for item in items { values.append(convert(heap, item)?.0); } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 6dc8479d1ac7..96234650d198 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -104,6 +104,11 @@ unsafe extern "C" { global: *mut JSGlobalObject, values: *mut MarkedArgumentBuffer, ) -> JSValue; + pub fn Bun__Bindgen__constructArrayFromValues( + global: *mut JSGlobalObject, + values: *const JSValue, + count: usize, + ) -> JSValue; pub fn Bun__Bindgen__createObject( global: *mut JSGlobalObject, structure_index: u32, diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 8e590a2892b5..173847a170a2 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -37,6 +37,15 @@ inline std::optional> unsharedBufferSourceSpan(JSC::JSV } template<> struct WebCore::Converter : Bun::DefaultTryConverter { + // Not through tryConvert's std::optional: the view is large, and this way it is never moved. + template + static Bun::Bindgen::JSStringUTF8View convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isString()) [[likely]] + return Bun::Bindgen::JSStringUTF8View::create(globalObject, JSC::asString(value)); + return Bun::DefaultTryConverter::convert(globalObject, value, ctx); + } + template static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) { diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 2a756222c20e..0bff7a53a770 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -21,21 +21,19 @@ class JSStringUTF8View { { } - // Resolving a rope and converting can throw. + // Resolving a rope and converting can throw. Every path returns `result`, so it is built in the caller's slot. static JSStringUTF8View create(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); JSStringUTF8View result; result.m_view = string->view(&globalObject); - RETURN_IF_EXCEPTION(scope, {}); + RETURN_IF_EXCEPTION(scope, result); result.m_isNull = false; if (result.m_view->is8Bit() && WTF::charactersAreAllASCII(result.m_view->span8())) [[likely]] return result; - if (!result.convert()) [[unlikely]] { + if (!result.convert()) [[unlikely]] JSC::throwOutOfMemoryError(&globalObject, scope); - return {}; - } return result; } diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/BindgenIdentifiers.cpp index 4e9ce6e052ef..a6b84f564266 100644 --- a/src/jsc/bindings/BindgenIdentifiers.cpp +++ b/src/jsc/bindings/BindgenIdentifiers.cpp @@ -119,6 +119,11 @@ extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* glo return JSValue::encode(Bun::Bindgen::identifiers(getVM(globalObject)).stringAt(index)); } +extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) +{ + return JSValue::encode(constructArray(globalObject, static_cast(nullptr), values, count)); +} + extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) { VM& vm = getVM(globalObject); diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 3138d097c3dd..2484c5e664d5 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -482,6 +482,11 @@ describe("arrays", () => { expect(bindgen.range(0)).toEqual([]); expect(bindgen.range(4)).toEqual([0, 1, 2, 3]); expect(bindgen.range(5000)).toEqual(Array.from({ length: 5000 }, (_, i) => i)); + for (const length of [7, 8, 9]) { + expect(bindgen.range(length)).toEqual(Array.from({ length }, (_, i) => i)); + const words = Array.from({ length }, (_, i) => "word" + i); + expect(bindgen.words(words.join(" "))).toEqual(words); + } expect(bindgen.words("one twö 三")).toEqual(["one", "twö", "三"]); expect(Array.isArray(bindgen.range(1))).toBe(true); }); diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index e8b40a048c95..311bb9367b9b 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test"; import { symlink } from "fs/promises"; -import { tempDir } from "harness"; +import { isDebug, tempDir } from "harness"; import path from "path"; test("Object prototype followSymlinks", async () => { @@ -101,3 +101,25 @@ test("every option is read before a cwd that is too long is reported", () => { expect(() => new Bun.Glob("*").scanSync(options)).toThrow("scanSync: invalid `cwd`, longer than"); expect(read).toEqual(["cwd", "dot"]); }); + +// Debug builds turn on showPrivateScriptsInStackTraces. +test.skipIf(isDebug)("the native half of scan and scanSync is not in a stack trace", async () => { + const cwd = Buffer.alloc(100_000, "a").toString(); + const glob = new Bun.Glob("*"); + function scanSync() { + return [...glob.scanSync({ cwd })]; + } + async function scan() { + return await Array.fromAsync(glob.scan({ cwd })); + } + const frames = (error: Error) => error.stack!.split("\n").filter(line => line.includes("__scan")); + let thrown: Error | undefined; + try { + scanSync(); + } catch (error) { + thrown = error as Error; + } + expect(frames(thrown!)).toEqual([]); + expect(thrown!.stack).toContain("at scanSync"); + expect(frames(await scan().catch(error => error))).toEqual([]); +}); From 54828132bc1f3d08182d9503a9efaf5d270280a1 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 04:36:53 +0000 Subject: [PATCH 13/44] Delete the compile-fail tests for bun_jsc::bindgen --- .github/workflows/rust-lints.yml | 9 -- docs/project/bindgen.mdx | 1 - test/internal/bindgen-compile-fail.test.ts | 98 ------------------- .../fail/bytes_across_js.rs | 7 -- .../bindgen-compile-fail/fail/bytes_escape.rs | 6 -- .../fail/error_escapes_call.rs | 5 - .../fail/error_to_thread.rs | 4 - .../fail/forge_capability.rs | 4 - .../bindgen-compile-fail/fail/forge_error.rs | 4 - .../bindgen-compile-fail/fail/forge_value.rs | 4 - .../fail/global_across_payload.rs | 7 -- .../fail/heap_only_runs_js.rs | 4 - .../fail/internal_field_without_unsafe.rs | 4 - .../fail/pass_this_reaches_payload.rs | 6 -- .../fail/payload_mut_across_js.rs | 7 -- .../fail/payload_ref_across_js.rs | 7 -- .../fail/payload_ref_and_mut.rs | 6 -- .../fail/payload_ref_escapes.rs | 6 -- .../fail/payload_without_unsafe.rs | 9 -- .../fail/shared_call_runs_js.rs | 4 - .../fail/string_arg_escapes.rs | 9 -- .../fail/strong_to_thread.rs | 4 - .../fail/two_payload_muts.rs | 6 -- .../fail/value_between_calls.rs | 9 -- .../fail/value_escapes_call.rs | 5 - .../fail/value_in_payload.rs | 11 --- .../fail/value_in_static_task.rs | 5 - .../fail/value_to_thread.rs | 4 - .../fail/with_value_converted_stored.rs | 9 -- .../pass/keep_past_the_call.rs | 16 --- .../pass/lists_of_values.rs | 12 --- .../pass/methods_that_cannot_run_js.rs | 24 ----- .../pass/reentrant_method.rs | 23 ----- .../bindgen-compile-fail/pass/with_value.rs | 18 ---- test/internal/bindgen-compile-fail/prelude.rs | 29 ------ 35 files changed, 386 deletions(-) delete mode 100644 test/internal/bindgen-compile-fail.test.ts delete mode 100644 test/internal/bindgen-compile-fail/fail/bytes_across_js.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/bytes_escape.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/error_escapes_call.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/error_to_thread.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/forge_capability.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/forge_error.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/forge_value.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/global_across_payload.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/strong_to_thread.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/two_payload_muts.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/value_between_calls.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/value_escapes_call.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/value_in_payload.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/value_in_static_task.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/value_to_thread.rs delete mode 100644 test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs delete mode 100644 test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs delete mode 100644 test/internal/bindgen-compile-fail/pass/lists_of_values.rs delete mode 100644 test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs delete mode 100644 test/internal/bindgen-compile-fail/pass/reentrant_method.rs delete mode 100644 test/internal/bindgen-compile-fail/pass/with_value.rs delete mode 100644 test/internal/bindgen-compile-fail/prelude.rs diff --git a/.github/workflows/rust-lints.yml b/.github/workflows/rust-lints.yml index 2834ec96307d..2a9f21326dc7 100644 --- a/.github/workflows/rust-lints.yml +++ b/.github/workflows/rust-lints.yml @@ -12,8 +12,6 @@ on: - "src/**/*.classes.ts" - "src/**/*.bind.ts" - "src/codegen/**" - - "test/internal/bindgen-compile-fail.test.ts" - - "test/internal/bindgen-compile-fail/**" - "scripts/build/**" - "scripts/build.ts" - "scripts/rust-miri.ts" @@ -76,13 +74,6 @@ jobs: BUN_CODEGEN_DIR: ${{ github.workspace }}/build/debug/codegen run: cargo check --workspace --all-targets --keep-going - - name: bindgen compile-fail cases - # Programs `bun_jsc::bindgen` has to make rustc refuse. The test lanes - # have no toolchain and skip the file. - env: - BUN_CODEGEN_DIR: ${{ github.workspace }}/build/debug/codegen - run: bun test test/internal/bindgen-compile-fail.test.ts - miri: name: cargo miri test runs-on: ubuntu-latest diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 73f4ff5572dd..f6dab500cc4d 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -362,4 +362,3 @@ thread when the work completes. ## Tests - `test/internal/bindgen.test.ts`: conversions with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. What that schema does not use is covered by the suites of the APIs that do. -- `test/internal/bindgen-compile-fail.test.ts`: programs rustc must reject, one per hazard, and the right way to write each. Add a case when adding to `bindgen.rs`. It runs `cargo check`, so it is skipped unless `BUN_CODEGEN_DIR` is set (`BUN_CODEGEN_DIR=$PWD/build/debug/codegen bun bd test bindgen-compile-fail`). The `cargo clippy` job of `rust-lints.yml` runs it. diff --git a/test/internal/bindgen-compile-fail.test.ts b/test/internal/bindgen-compile-fail.test.ts deleted file mode 100644 index 1e3a729c432d..000000000000 --- a/test/internal/bindgen-compile-fail.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -// `bun_jsc::bindgen` makes rustc enforce how native code may touch the JS heap. Each file in -// bindgen-compile-fail/fail is a mistake it must refuse, for the reason on the file's first line and no -// other; each file in bindgen-compile-fail/pass is the right way to do the same work. -// -// The cases are type-checked against the metadata `cargo check` leaves behind; nothing is linked. -import { which } from "bun"; -import { beforeAll, describe, expect, test } from "bun:test"; -import { tempDir } from "harness"; -import { existsSync, readdirSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; - -const cargo = which("cargo"); -const rustc = which("rustc"); -const repoRoot = join(import.meta.dir, "..", ".."); -const cases = join(import.meta.dir, "bindgen-compile-fail"); -// Opt-in, as for `cargo check`: BUN_CODEGEN_DIR=$PWD/build/debug/codegen. The first run compiles `bun_jsc`'s dependencies. -// The `cargo clippy` job of .github/workflows/rust-lints.yml runs this file. -const codegenDir = process.env.BUN_CODEGEN_DIR ?? ""; -const canCheck = - !!codegenDir && - !!cargo && - !!rustc && - existsSync(join(repoRoot, "vendor", "lolhtml", "Cargo.toml")) && - existsSync(join(codegenDir, "generated_bindgen_bun_jsc.rs")); - -const env = { ...process.env, CARGO_TERM_COLOR: "never", BUN_CODEGEN_DIR: codegenDir }; - -describe.skipIf(!canCheck)("bun_jsc::bindgen", () => { - let metadata = ""; - const searchPaths = new Set(); - const prelude = readFileSync(join(cases, "prelude.rs"), "utf8"); - - beforeAll(async () => { - await using proc = Bun.spawn({ - cmd: [cargo!, "check", "--locked", "-p", "bun_jsc", "--message-format=json"], - cwd: repoRoot, - env, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stderr, exitCode }).toMatchObject({ exitCode: 0 }); - for (const line of stdout.split(/\r?\n/)) { - if (!line.startsWith("{")) continue; - const message = JSON.parse(line); - if (message.reason !== "compiler-artifact") continue; - for (const name of message.filenames as string[]) searchPaths.add(dirname(name)); - if (message.target.name === "bun_jsc" && message.target.kind.includes("lib")) - metadata = message.filenames.find((name: string) => name.endsWith(".rmeta")); - } - expect(metadata).toEndWith(".rmeta"); - }, 600_000); - - /** The distinct errors rustc reports for the case: a code, or "lifetime" for the one that has none. */ - async function check(kind: string, file: string): Promise<{ codes: string[]; ok: boolean; stderr: string }> { - using dir = tempDir("bindgen-compile-fail", { [file]: prelude + readFileSync(join(cases, kind, file), "utf8") }); - const source = join(String(dir), file); - await using proc = Bun.spawn({ - cmd: [ - rustc!, - "--edition=2024", - "--crate-type=bin", - "--emit=metadata", - "--error-format=short", - `--out-dir=${String(dir)}`, - "-Cpanic=abort", - ...Array.from(searchPaths, path => `-Ldependency=${path}`), - "--extern", - `bun_jsc=${metadata}`, - source, - ], - cwd: repoRoot, - env, - stdout: "ignore", - stderr: "pipe", - }); - const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); - const errors = stderr - .split(/\r?\n/) - .filter(line => /\berror\b/.test(line) && !line.startsWith("error: aborting due to")); - const code = (line: string) => - /error\[(E\d+)\]/.exec(line)?.[1] ?? (line.includes("lifetime may not live long enough") ? "lifetime" : line); - return { codes: [...new Set(errors.map(code))], ok: exitCode === 0, stderr }; - } - - describe("pass", () => { - test.concurrent.each(readdirSync(join(cases, "pass")))("%s", async file => { - expect(await check("pass", file)).toMatchObject({ codes: [], ok: true }); - }); - }); - - describe("fail", () => { - test.concurrent.each(readdirSync(join(cases, "fail")))("%s", async file => { - const expected = /^\/\/ (E\d+|lifetime): /.exec(readFileSync(join(cases, "fail", file), "utf8"))![1]; - expect(await check("fail", file)).toMatchObject({ codes: [expected], ok: false }); - }); - }); -}); diff --git a/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs b/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs deleted file mode 100644 index 493c342bb454..000000000000 --- a/test/internal/bindgen-compile-fail/fail/bytes_across_js.rs +++ /dev/null @@ -1,7 +0,0 @@ -// E0500: ArrayBuffer bytes in use while JavaScript runs (it can detach, resize or transfer them). -fn f<'a>(cx: &mut JsCall<'a>, buffer: JsValue<'a, ArrayBuffer>, callback: JsValue<'a, JSFunction>) { - buffer.with_bytes(&cx.heap, |bytes| { - let _ = callback.call(cx, JsValue::UNDEFINED, &[]); - bytes[0] - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/bytes_escape.rs b/test/internal/bindgen-compile-fail/fail/bytes_escape.rs deleted file mode 100644 index f95c1b67f4b2..000000000000 --- a/test/internal/bindgen-compile-fail/fail/bytes_escape.rs +++ /dev/null @@ -1,6 +0,0 @@ -// E0521: ArrayBuffer bytes carried out of the closure that bounds them. -fn f<'a>(cx: &mut JsCall<'a>, buffer: JsValue<'a, ArrayBuffer>) -> u8 { - let mut escaped: &[u8] = &[]; - buffer.with_bytes(&cx.heap, |bytes| escaped = bytes); - escaped[0] -} diff --git a/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs b/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs deleted file mode 100644 index d6584a87deb0..000000000000 --- a/test/internal/bindgen-compile-fail/fail/error_escapes_call.rs +++ /dev/null @@ -1,5 +0,0 @@ -// E0521: a pending exception carried out of the call it belongs to. -fn f(global: *mut JSGlobalObject) { - let mut escaped = None; - unsafe { JsCall::with(global, |cx| escaped = Some(cx.heap.throw_out_of_memory())) }; -} diff --git a/test/internal/bindgen-compile-fail/fail/error_to_thread.rs b/test/internal/bindgen-compile-fail/fail/error_to_thread.rs deleted file mode 100644 index 93a80b7479d4..000000000000 --- a/test/internal/bindgen-compile-fail/fail/error_to_thread.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0277: a pending exception sent to another thread. -fn f(error: bindgen::Error<'static>) { - std::thread::spawn(move || drop(error)); -} diff --git a/test/internal/bindgen-compile-fail/fail/forge_capability.rs b/test/internal/bindgen-compile-fail/fail/forge_capability.rs deleted file mode 100644 index b714b58b40d8..000000000000 --- a/test/internal/bindgen-compile-fail/fail/forge_capability.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0423: making a capability out of nothing. -fn f<'a>() -> JsHeap<'a> { - JsHeap(Default::default(), Default::default(), Default::default()) -} diff --git a/test/internal/bindgen-compile-fail/fail/forge_error.rs b/test/internal/bindgen-compile-fail/fail/forge_error.rs deleted file mode 100644 index 053aaebed94e..000000000000 --- a/test/internal/bindgen-compile-fail/fail/forge_error.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0603: an `Err` that says something was thrown when nothing was. -fn f<'a>() -> bindgen::Result<'a, ()> { - Err(bindgen::Error(unreachable!(), Default::default(), Default::default())) -} diff --git a/test/internal/bindgen-compile-fail/fail/forge_value.rs b/test/internal/bindgen-compile-fail/fail/forge_value.rs deleted file mode 100644 index 9261218d5d71..000000000000 --- a/test/internal/bindgen-compile-fail/fail/forge_value.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0423: a value made without a capability. -fn f<'a>() -> JsValue<'a> { - JsValue(JSValue::UNDEFINED, Default::default(), Default::default()) -} diff --git a/test/internal/bindgen-compile-fail/fail/global_across_payload.rs b/test/internal/bindgen-compile-fail/fail/global_across_payload.rs deleted file mode 100644 index 9a8718ac64d3..000000000000 --- a/test/internal/bindgen-compile-fail/fail/global_across_payload.rs +++ /dev/null @@ -1,7 +0,0 @@ -// E0502: a `&JSGlobalObject`, with which anything can run, held while a wrapper's payload is borrowed. -fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { - let global = cx.global(); - this.with(&cx.payloads, |socket| { - let _ = (global, socket); - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs b/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs deleted file mode 100644 index a221845be49c..000000000000 --- a/test/internal/bindgen-compile-fail/fail/heap_only_runs_js.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0308: running JavaScript with the capability to allocate only. -fn f<'a>(heap: &JsHeap<'a>, callback: JsValue<'a, JSFunction>) { - let _ = callback.call(heap, JsValue::UNDEFINED, &[]); -} diff --git a/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs deleted file mode 100644 index d41a0392b285..000000000000 --- a/test/internal/bindgen-compile-fail/fail/internal_field_without_unsafe.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0133: reading an internal field by number; only the generated accessors know how many there are. -fn f<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>) -> JsValue<'a> { - this.internal_field(heap, 7) -} diff --git a/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs b/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs deleted file mode 100644 index 98979c2afbc9..000000000000 --- a/test/internal/bindgen-compile-fail/fail/pass_this_reaches_payload.rs +++ /dev/null @@ -1,6 +0,0 @@ -// E0609: a `passThis` method with `&mut self` aliasing itself. `JsHeap` leads to no `JsPayloads`. -impl Socket { - fn f<'a>(&mut self, this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>) { - self.bytes_written = this.with(&heap.payloads, |socket| socket.bytes_written); - } -} diff --git a/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs b/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs deleted file mode 100644 index c2b46a64b6e6..000000000000 --- a/test/internal/bindgen-compile-fail/fail/payload_mut_across_js.rs +++ /dev/null @@ -1,7 +0,0 @@ -// E0500: `&mut T` into a wrapper's payload in use while JavaScript re-enters. -fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>, callback: JsValue<'a, JSFunction>) { - this.with_mut(&mut cx.payloads, |socket| { - let _ = callback.call(cx, JsValue::UNDEFINED, &[]); - socket.bytes_written += 1; - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs deleted file mode 100644 index 613615cd5832..000000000000 --- a/test/internal/bindgen-compile-fail/fail/payload_ref_across_js.rs +++ /dev/null @@ -1,7 +0,0 @@ -// E0500: `&T` into a wrapper's payload in use while JavaScript re-enters and may mutate it. -fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>, callback: JsValue<'a, JSFunction>) { - this.with(&cx.payloads, |socket| { - let _ = callback.call(cx, JsValue::UNDEFINED, &[]); - socket.bytes_written - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs deleted file mode 100644 index 7f6c759aa1ef..000000000000 --- a/test/internal/bindgen-compile-fail/fail/payload_ref_and_mut.rs +++ /dev/null @@ -1,6 +0,0 @@ -// E0500: `&T` and `&mut T`; the two wrappers may be the same object. -fn f<'a>(a: JsWrapper<'a, Socket>, b: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { - a.with(&cx.payloads, |a| { - b.with_mut(&mut cx.payloads, |b| b.bytes_written = a.bytes_written); - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs b/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs deleted file mode 100644 index 852591aed17f..000000000000 --- a/test/internal/bindgen-compile-fail/fail/payload_ref_escapes.rs +++ /dev/null @@ -1,6 +0,0 @@ -// E0521: a reference into a wrapper's payload carried out of the closure that bounds it. -fn f<'a>(this: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) -> usize { - let mut escaped: &[u8] = &[]; - this.with(&cx.payloads, |socket| escaped = &socket.pending); - escaped.len() -} diff --git a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs b/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs deleted file mode 100644 index 3bc1f9b3fa99..000000000000 --- a/test/internal/bindgen-compile-fail/fail/payload_without_unsafe.rs +++ /dev/null @@ -1,9 +0,0 @@ -// E0200: claiming a cell layout; only generated code knows it. -struct Fake; -impl JsPayload for Fake { - const INTERNAL_FIELDS_OFFSET: usize = 0; - const PAYLOAD_OFFSET: usize = 0; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { - unreachable!() - } -} diff --git a/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs b/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs deleted file mode 100644 index 3d025f238fa2..000000000000 --- a/test/internal/bindgen-compile-fail/fail/shared_call_runs_js.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0308: running JavaScript through a shared reference to the capability. -fn f<'a>(cx: &JsCall<'a>, callback: JsValue<'a, JSFunction>) { - let _ = callback.call(cx, JsValue::UNDEFINED, &[]); -} diff --git a/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs b/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs deleted file mode 100644 index 1980c35c8084..000000000000 --- a/test/internal/bindgen-compile-fail/fail/string_arg_escapes.rs +++ /dev/null @@ -1,9 +0,0 @@ -// lifetime: the UTF-8 view of a string argument stored past the call. -struct Route { - path: &'static [u8], -} -impl Route { - fn configure<'a>(&mut self, path: &'a [u8]) { - self.path = path; - } -} diff --git a/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs b/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs deleted file mode 100644 index 4c2309ef8ad0..000000000000 --- a/test/internal/bindgen-compile-fail/fail/strong_to_thread.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0277: a wrapper's `Strong` sent to another thread; it is made, read and dropped on the JS thread. -fn f(strong: JsWrapperStrong) { - std::thread::spawn(move || drop(strong)); -} diff --git a/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs b/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs deleted file mode 100644 index c3bc03e0e605..000000000000 --- a/test/internal/bindgen-compile-fail/fail/two_payload_muts.rs +++ /dev/null @@ -1,6 +0,0 @@ -// E0500: two `&mut T`; the two wrappers may be the same object. -fn f<'a>(a: JsWrapper<'a, Socket>, b: JsWrapper<'a, Socket>, cx: &mut JsCall<'a>) { - a.with_mut(&mut cx.payloads, |a| { - b.with_mut(&mut cx.payloads, |b| core::mem::swap(a, b)); - }); -} diff --git a/test/internal/bindgen-compile-fail/fail/value_between_calls.rs b/test/internal/bindgen-compile-fail/fail/value_between_calls.rs deleted file mode 100644 index 74e2275ee8f6..000000000000 --- a/test/internal/bindgen-compile-fail/fail/value_between_calls.rs +++ /dev/null @@ -1,9 +0,0 @@ -// E0521: a value of one call used in another. -fn f(global: *mut JSGlobalObject) { - unsafe { - JsCall::with(global, |outer| { - let value = JsValue::new(&outer.heap, JSValue::NULL); - JsCall::with(global, |inner| drop(inner.heap.throw_value(value))); - }) - }; -} diff --git a/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs b/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs deleted file mode 100644 index e1f2b9f0089d..000000000000 --- a/test/internal/bindgen-compile-fail/fail/value_escapes_call.rs +++ /dev/null @@ -1,5 +0,0 @@ -// E0521: a value carried out of the call it belongs to. -fn f(global: *mut JSGlobalObject) { - let mut escaped = None; - unsafe { JsCall::with(global, |cx| escaped = cx.heap.create_utf8_string(b"text").ok()) }; -} diff --git a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs b/test/internal/bindgen-compile-fail/fail/value_in_payload.rs deleted file mode 100644 index 448fbaf8f7c1..000000000000 --- a/test/internal/bindgen-compile-fail/fail/value_in_payload.rs +++ /dev/null @@ -1,11 +0,0 @@ -// E0478: a payload that holds a value the GC would not see. Values go in the class's `internalFields`. -struct Holder<'a> { - callback: JsValue<'a>, -} -unsafe impl<'a> JsPayload for Holder<'a> { - const INTERNAL_FIELDS_OFFSET: usize = 24; - const PAYLOAD_OFFSET: usize = 24; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { - unreachable!() - } -} diff --git a/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs b/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs deleted file mode 100644 index de2592a5f879..000000000000 --- a/test/internal/bindgen-compile-fail/fail/value_in_static_task.rs +++ /dev/null @@ -1,5 +0,0 @@ -// lifetime: a value captured by a task that runs later. A `Strong` is what outlives the call. -fn queue(_: Box) {} -fn f<'a>(cx: &mut JsCall<'a>, value: JsValue<'a>) { - queue(Box::new(move || drop(value))); -} diff --git a/test/internal/bindgen-compile-fail/fail/value_to_thread.rs b/test/internal/bindgen-compile-fail/fail/value_to_thread.rs deleted file mode 100644 index 4e3f2308edc9..000000000000 --- a/test/internal/bindgen-compile-fail/fail/value_to_thread.rs +++ /dev/null @@ -1,4 +0,0 @@ -// E0277: a value sent to another thread. -fn f(value: JsValue<'static>) { - std::thread::spawn(move || drop(value)); -} diff --git a/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs b/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs deleted file mode 100644 index 91c0149417c5..000000000000 --- a/test/internal/bindgen-compile-fail/fail/with_value_converted_stored.rs +++ /dev/null @@ -1,9 +0,0 @@ -// lifetime: of a `withValue` argument, the converted form stored instead of the value. -struct Route { - path: &'static [u8], -} -impl Route { - fn configure<'a>(&mut self, path: WithValue<'a, &'a [u8]>) { - self.path = path.converted; - } -} diff --git a/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs b/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs deleted file mode 100644 index ce2eafd12369..000000000000 --- a/test/internal/bindgen-compile-fail/pass/keep_past_the_call.rs +++ /dev/null @@ -1,16 +0,0 @@ -// What outlives a call is a `Strong`; a capability turns it back into a value. -struct Pending { - socket: JsWrapperStrong, - callback: Strong, -} -fn start<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<'a, JSFunction>) -> Pending { - Pending { socket: this.to_strong(heap), callback: callback.to_strong(heap) } -} -fn finish<'a>(pending: Pending, cx: &mut JsCall<'a>) -> bindgen::Result<'a, JsValue<'a>> { - let socket = pending.socket.get(&cx.heap); - socket.with_mut(&mut cx.payloads, |socket| socket.pending.clear()); - let Some(callback) = JsValue::new(&cx.heap, pending.callback.get()).as_function() else { - return Ok(JsValue::UNDEFINED); - }; - callback.call(cx, socket.as_value(), &[]) -} diff --git a/test/internal/bindgen-compile-fail/pass/lists_of_values.rs b/test/internal/bindgen-compile-fail/pass/lists_of_values.rs deleted file mode 100644 index 36732d145777..000000000000 --- a/test/internal/bindgen-compile-fail/pass/lists_of_values.rs +++ /dev/null @@ -1,12 +0,0 @@ -// A list of values the GC can see. -fn pair<'a>(heap: &JsHeap<'a>, a: JsValue<'a>, b: JsValue<'a>) -> bindgen::Result<'a, JsValue<'a>> { - to_js::array(heap, [a, b], to_js::any) -} -fn collect<'a>(heap: &JsHeap<'a>, names: &[&[u8]]) -> bindgen::Result<'a, ()> { - MarkedArgumentBuffer::new(|values| { - for name in names { - values.append(heap.create_utf8_string(name)?.to_js()); - } - Ok(()) - }) -} diff --git a/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs b/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs deleted file mode 100644 index b796bb85b111..000000000000 --- a/test/internal/bindgen-compile-fail/pass/methods_that_cannot_run_js.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Methods that cannot run JavaScript get plain references. -impl Socket { - fn bytes_written(&self) -> u64 { - self.bytes_written - } - fn write<'a>(&mut self, data: &'a [u8]) -> usize { - self.pending.extend_from_slice(data); - data.len() - } - fn pending<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a, JSString>> { - heap.create_utf8_string(&self.pending) - } - fn check<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { - if self.pending.is_empty() { - return Err(heap.throw_type_error(format_args!("nothing is pending"))); - } - Ok(()) - } - // `passThis`: the wrapper reaches the class's `internalFields`, not the payload. - fn listen<'a>(&mut self, this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, callback: JsValue<'a>) { - self.bytes_written = 0; - this.set_ondata_field(heap, callback); - } -} diff --git a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs b/test/internal/bindgen-compile-fail/pass/reentrant_method.rs deleted file mode 100644 index fe428bb35acf..000000000000 --- a/test/internal/bindgen-compile-fail/pass/reentrant_method.rs +++ /dev/null @@ -1,23 +0,0 @@ -// A method that can run JavaScript: the wrapper, its payload borrowed for as long as nothing else can run. -fn flush<'a>( - this: JsWrapper<'a, Socket>, - cx: &mut JsCall<'a>, - extra: JsValue<'a, ArrayBuffer>, -) -> bindgen::Result<'a, JsValue<'a>> { - // Payload borrowed mutably and bytes viewed at once: disjoint fields of `cx`. - extra.with_bytes(&cx.heap, |bytes| { - this.with_mut(&mut cx.payloads, |socket| socket.pending.extend_from_slice(bytes)) - }); - let chunk = this.with(&cx.payloads, |socket| cx.heap.create_utf8_string(&socket.pending))?; - let Some(callback) = this.ondata_field(&cx.heap).as_function() else { - return Ok(JsValue::UNDEFINED); - }; - let result = callback.call(cx, this.as_value(), &[chunk.as_value()])?; - // Afterwards everything is looked up again. A `JsValue` stays usable: the stack is scanned. - let written = extra.with_bytes(&cx.heap, <[u8]>::len); - this.with_mut(&mut cx.payloads, |socket| { - socket.pending.clear(); - socket.bytes_written += written as u64; - }); - Ok(result) -} diff --git a/test/internal/bindgen-compile-fail/pass/with_value.rs b/test/internal/bindgen-compile-fail/pass/with_value.rs deleted file mode 100644 index 99a8ded1c477..000000000000 --- a/test/internal/bindgen-compile-fail/pass/with_value.rs +++ /dev/null @@ -1,18 +0,0 @@ -// The converted form for now, the value itself for whatever holds it longer. -fn to_an_internal_field<'a>(this: JsWrapper<'a, Socket>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> usize { - this.set_ondata_field(heap, name.value); - name.converted.len() -} -fn to_a_strong<'a>(heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) -> (Vec, Strong) { - (name.converted.to_vec(), name.value.to_strong(heap)) -} -fn to_a_call<'a>( - cx: &mut JsCall<'a>, - callback: JsValue<'a, JSFunction>, - name: WithValue<'a, &'a [u8]>, -) -> bindgen::Result<'a, JsValue<'a>> { - if name.converted.is_empty() { - return Ok(name.value); - } - callback.call(cx, JsValue::UNDEFINED, &[name.value]) -} diff --git a/test/internal/bindgen-compile-fail/prelude.rs b/test/internal/bindgen-compile-fail/prelude.rs deleted file mode 100644 index 9b5695bb3bb4..000000000000 --- a/test/internal/bindgen-compile-fail/prelude.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Shared by every case: a class as its author writes it, and what the generator adds. -#![allow(unused)] -use bun_jsc::bindgen::{self, *}; -use bun_jsc::{ArrayBuffer, JSFunction, JSGlobalObject, JSString, JSValue, MarkedArgumentBuffer, Strong}; - -pub struct Socket { - pub pending: Vec, - pub bytes_written: u64, -} -unsafe impl JsPayload for Socket { - const INTERNAL_FIELDS_OFFSET: usize = 24; - const PAYLOAD_OFFSET: usize = 32; - unsafe fn create_cell(_: *mut JSGlobalObject, _: *mut core::ffi::c_void) -> core::ptr::NonNull { - unreachable!() - } -} -pub trait SocketInternalFields<'a>: Copy { - fn ondata_field(self, heap: &JsHeap<'a>) -> JsValue<'a>; - fn set_ondata_field(self, heap: &JsHeap<'a>, value: JsValue<'a, T>); -} -impl<'a> SocketInternalFields<'a> for JsWrapper<'a, Socket> { - fn ondata_field(self, heap: &JsHeap<'a>) -> JsValue<'a> { - unsafe { self.internal_field(heap, 0) } - } - fn set_ondata_field(self, heap: &JsHeap<'a>, value: JsValue<'a, T>) { - unsafe { self.set_internal_field(heap, 0, value) } - } -} -fn main() {} From 7eaf4674fb1004481612dc57a13ccb04b0e80306 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 04:55:28 +0000 Subject: [PATCH 14/44] Share one dictionary member read, let a dictionary ignore non-objects, adopt arrays that need no conversion, and keep JS strings only for returned enumerations --- docs/project/bindgen.mdx | 11 +++++---- src/codegen/bindgen-emit.ts | 23 ++++++++++++++----- src/codegen/bindgen-lib-internal.ts | 10 +++++++- src/js/node/os.ts | 4 +--- src/jsc/bindgen/extern_types.rs | 10 ++++++++ .../bindings/Bindgen/DictionaryMemberReader.h | 17 ++------------ src/jsc/bindings/BindgenIdentifiers.cpp | 16 +++++++++++++ src/runtime/node/node_os.bind.ts | 6 ++++- test/js/node/os/os.test.js | 1 + 9 files changed, 67 insertions(+), 31 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index f6dab500cc4d..ca34ba92784b 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -188,11 +188,12 @@ left out, and the object is then built one property at a time. The second argument of `b.dictionary` takes: -| Key | What it does | -| ---------------- | ---------------------------------------------------------------------------------------- | -| `userFacingName` | what error messages call it, when that is not its name | -| `allowArray` | an array is an object with none of the members, not an error | -| `fromJS` | also generates `Name::from_js(global, value, \|converted\| ..)`, for a `#[host_fn]` body | +| Key | What it does | +| ---------------- | -------------------------------------------------------------------------------------------------------------- | +| `userFacingName` | what error messages call it, when that is not its name | +| `allowNonObject` | so is a value whose `typeof` is not `"object"`, as in Node's `if (typeof options !== "object") options = null` | +| `allowArray` | an array is an object with none of the members, not an error | +| `fromJS` | also generates `Name::from_js(global, value, \|converted\| ..)`, for a `#[host_fn]` body | ### Enumerations diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 55f5b863d393..b43e4c6d7b28 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -67,7 +67,8 @@ function walk(type: Type, visit: (type: Type) => void, seen = new Set()): export function analyze(files: readonly SchemaFile[]): Tables { const tables: Tables = { memberNames: [], enumerationValues: [], objectStructures: [], classes: [] }; const markAccepted = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.accepted = true))); - const markReturned = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.returned = true))); + const markReturned = (type: Type) => + walk(type, t => void ((t instanceof DictionaryType || t instanceof EnumType) && (t.returned = true))); for (const file of files) { for (const type of file.types) { if (type instanceof DictionaryType && type.fromJS) markAccepted(type); @@ -98,6 +99,7 @@ export function analyze(files: readonly SchemaFile[]): Tables { for (const file of files) { for (const type of file.types) { if (type instanceof EnumType) { + if (!type.returned) continue; type.firstEnumerationValueIndex = tables.enumerationValues.length; tables.enumerationValues.push(...type.spellings.map(([canonical]) => canonical)); } else if (type instanceof DictionaryType) { @@ -312,12 +314,17 @@ template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObje auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; + [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; auto* object = value.getObject(); - if (!object) [[unlikely]] { +${ + type.allowNonObject + ? ` if (!object || object->isCallable()) + return ${type.toCpp({})};` + : ` if (!object) [[unlikely]] { ctx.throwNotObject(lexicalGlobalObject, scope); return {}; - } + }` +} ${ type.allowArray ? "" @@ -805,13 +812,17 @@ function rustForEnum(type: EnumType): string { pub enum ${type.name} { ${type.variants.map((variant, i) => ` /// \`${JSON.stringify(type.spellings[i][0])}\`\n ${variant} = ${i},`).join("\n")} } -impl ${type.name} { +${ + type.returned + ? `impl ${type.name} { #[inline] pub fn to_js<'a>(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { bindgen::to_js::enumeration_value(heap, ${type.firstEnumerationValueIndex} + self as u32) } } -`; +` + : "" +}`; } function rustForUnion(type: UnionType, scope: RustScope): string { diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index e31a1cb04e0c..e0386075610c 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -773,7 +773,8 @@ export class ArrayType extends Type { return `Vec<${this.element.rustType(scope)}>`; } rustFromExtern(expr: string, scope: RustScope): string { - return `${expr}.into_vec(|value| ${this.element.rustFromExtern("value", scope)})`; + const element = this.element.rustFromExtern("value", scope); + return element === "value" ? `${expr}.into_vec_unconverted()` : `${expr}.into_vec(|value| ${element})`; } rustResultFieldType(scope: RustScope): string { return `Vec<${this.element.rustResultFieldType(scope)}>`; @@ -822,6 +823,8 @@ export class EnumType extends NamedType { readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ firstEnumerationValueIndex = -1; + /** Reachable from a return type. Set by the generator. */ + returned = false; constructor(values: readonly (string | readonly string[])[]) { super(); assert(values.length > 0, "an enumeration has values"); @@ -898,6 +901,8 @@ export interface DictionaryOptions { fromJS?: boolean; /** An array is an object with none of the members, not an error. */ allowArray?: boolean; + /** So is a value whose `typeof` is not "object": Node's `if (typeof options !== "object") options = null`. */ + allowNonObject?: boolean; } export class DictionaryType extends NamedType { @@ -930,6 +935,9 @@ export class DictionaryType extends NamedType { get allowArray(): boolean { return !!this.options.allowArray; } + get allowNonObject(): boolean { + return !!this.options.allowNonObject; + } get idlType(): string { return `::WebCore::IDLDictionary<${this.cpp}>`; } diff --git a/src/js/node/os.ts b/src/js/node/os.ts index a3f090100c34..d2c90632d36c 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -128,9 +128,7 @@ function bound(binding) { : $bundleError("TODO: type"); }, uptime: binding.uptime, - userInfo: function userInfo(options) { - return binding.userInfo(typeof options === "object" ? options : undefined); - }, + userInfo: binding.userInfo, version: binding.version, machine: function () { // TODO: linux arm64 should also return "aarch64" (Node/uname compat) — diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 96234650d198..8ef3f5064632 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -50,6 +50,16 @@ pub struct ExternVector { capacity: c_uint, } impl ExternVector { + /// The buffer becomes the `Vec`, where the global allocator is the one C++ used. + #[inline] + pub fn into_vec_unconverted(self) -> Vec { + if !bun_alloc::USE_MIMALLOC || self.data.is_null() { + return self.into_vec(|item| item); + } + // SAFETY: a mimalloc buffer of `capacity` elements, `length` of them initialized; ownership was transferred. + unsafe { Vec::from_raw_parts(self.data, self.length as usize, self.capacity as usize) } + } + /// Converts every element and frees the buffer. #[inline] pub fn into_vec(self, mut convert: impl FnMut(T) -> U) -> Vec { diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index bd56523c6c85..8eacdbacf3e3 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -1,9 +1,7 @@ #pragma once #include "root.h" #include "Bindgen/Identifiers.h" -#include "ObjectBindings.h" #include -#include namespace Bun::Bindgen { @@ -18,19 +16,8 @@ class DictionaryMemberReader { { } - // May throw. - ALWAYS_INLINE JSC::JSValue get(unsigned index) - { - if (ownPropertiesAreTheWholeAnswer()) [[likely]] { - unsigned attributes = 0; - JSC::PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); - if (offset == JSC::invalidOffset) - return JSC::jsUndefined(); - if (!(attributes & (JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::CustomAccessorOrValue))) [[likely]] - return m_object->getDirect(offset); - } - return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); - } + // May throw. Out of line: inlined, it is most of the size of every generated convertDictionary. + JSC::JSValue get(unsigned index); private: // The lookup below stops before Object.prototype. Asked per member: converting one can run JavaScript. diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/BindgenIdentifiers.cpp index a6b84f564266..3254715006b2 100644 --- a/src/jsc/bindings/BindgenIdentifiers.cpp +++ b/src/jsc/bindings/BindgenIdentifiers.cpp @@ -1,12 +1,15 @@ #include "root.h" +#include "Bindgen/DictionaryMemberReader.h" #include "Bindgen/Identifiers.h" #include "Bindgen/JSStringUTF8View.h" #include "GeneratedBindings+Tables.h" #include "BunClientData.h" +#include "ObjectBindings.h" #include "ZigGlobalObject.h" #include #include #include +#include #include namespace Bun::Bindgen { @@ -62,6 +65,19 @@ Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned return structure; } +NEVER_INLINE JSValue DictionaryMemberReader::get(unsigned index) +{ + if (ownPropertiesAreTheWholeAnswer()) [[likely]] { + unsigned attributes = 0; + PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); + if (offset == invalidOffset) + return jsUndefined(); + if (!(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) [[likely]] + return m_object->getDirect(offset); + } + return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); +} + bool JSStringUTF8View::convert() { StringView view = m_view; diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index c40cb26d931f..37fda4a2d070 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -24,7 +24,10 @@ export const release = b.fn({ returns: b.string }); export const totalmem = b.fn({ returns: b.u64 }); export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); -export const UserInfoOptions = b.dictionary({ encoding: b.string.loose.utf8.default("") }, { allowArray: true }); +export const UserInfoOptions = b.dictionary( + { encoding: b.string.loose.utf8.default("") }, + { allowArray: true, allowNonObject: true }, +); export const userInfo = b.fn({ args: { options: UserInfoOptions.nullable.default({}), @@ -32,6 +35,7 @@ export const userInfo = b.fn({ returns: b.any, capability: "js", throws: true, + length: 1, }); export const version = b.fn({ returns: b.string, throws: true }); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index 5cf33dc349c8..f993347b7f4d 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -369,6 +369,7 @@ it("native functions are not constructors", () => { "release", "setPriority", "totalmem", + "userInfo", ]) { expect(() => new os[name]()).toThrow(TypeError); } From db99ca22b224587664f58162b4da30167c26836d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 05:05:42 +0000 Subject: [PATCH 15/44] Describe allowArray and allowNonObject correctly and refuse allowNonObject on a dictionary with required members --- docs/project/bindgen.mdx | 12 ++++++------ src/codegen/bindgen-lib-internal.ts | 5 +++-- test/js/bun/glob/proto.test.ts | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index ca34ba92784b..86b173b692cd 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -188,12 +188,12 @@ left out, and the object is then built one property at a time. The second argument of `b.dictionary` takes: -| Key | What it does | -| ---------------- | -------------------------------------------------------------------------------------------------------------- | -| `userFacingName` | what error messages call it, when that is not its name | -| `allowNonObject` | so is a value whose `typeof` is not `"object"`, as in Node's `if (typeof options !== "object") options = null` | -| `allowArray` | an array is an object with none of the members, not an error | -| `fromJS` | also generates `Name::from_js(global, value, \|converted\| ..)`, for a `#[host_fn]` body | +| Key | What it does | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `userFacingName` | what error messages call it, when that is not its name | +| `allowArray` | an array is read like any other object, not refused | +| `allowNonObject` | a value whose `typeof` is not `"object"` gives every member its default, 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, \|converted\| ..)`, for a `#[host_fn]` body | ### Enumerations diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index e0386075610c..aec38d1dba7a 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -899,9 +899,9 @@ export interface DictionaryOptions { userFacingName?: string; /** Also generate `Name::from_js`, for Rust that holds a JS value and no typed signature. */ fromJS?: boolean; - /** An array is an object with none of the members, not an error. */ + /** An array is read like any other object, not refused. */ allowArray?: boolean; - /** So is a value whose `typeof` is not "object": Node's `if (typeof options !== "object") options = null`. */ + /** A value whose `typeof` is not "object" gives every member its default: Node's `if (typeof options !== "object") options = null`. */ allowNonObject?: boolean; } @@ -920,6 +920,7 @@ export class DictionaryType extends NamedType { this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); assert(this.members.length > 0, "a dictionary has members"); + assert(!this.allowNonObject || this.hasDefault, "allowNonObject: every member must be optional or have a default"); assert( !this.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index 311bb9367b9b..8a6b2ad6f463 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -80,7 +80,7 @@ test("scan options", () => { expect(scan("*", new String(path.join(cwd, "sub")))).toEqual(["b.txt"]); expect(scan("*", { cwd: new String(path.join(cwd, "sub")) })).toEqual(["b.txt"]); - // An array is an object with none of the options. + // An array is read like any other object. expect(scan("package.json", [])).toEqual(scan("package.json", {})); expect(scan("*", Object.assign([], { cwd, dot: true }))).toEqual([".dot", "a.txt"]); }); From 8bde319639324feed1949be5d1253963c2c57e08 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 05:31:48 +0000 Subject: [PATCH 16/44] Check the size of returned dictionaries at compile time against JSC's inline capacity --- src/jsc/bindings/BindgenIdentifiers.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/BindgenIdentifiers.cpp index 3254715006b2..35e18656ed20 100644 --- a/src/jsc/bindings/BindgenIdentifiers.cpp +++ b/src/jsc/bindings/BindgenIdentifiers.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Bun::Bindgen { @@ -51,10 +52,11 @@ Identifiers& identifiers(VM& vm) return static_cast(vm.clientData)->bindgenIdentifiers; } +static_assert(std::ranges::all_of(Tables::objectStructures, [](const auto& structure) { return structure.memberCount <= JSFinalObject::maxInlineCapacity; }), "a returned dictionary has more members than an object holds inline"); + Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) { auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; - RELEASE_ASSERT(memberCount <= JSFinalObject::maxInlineCapacity); const Identifier* memberNames = identifiers(vm).identifiersAt(firstMemberNameIndex, memberCount); Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); for (unsigned i = 0; i < memberCount; ++i) { From 69080b7c424f020b7659123874ffde79caab55b8 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 06:15:22 +0000 Subject: [PATCH 17/44] Leave bindgen_test.bind.ts out of builds without assertions --- docs/project/bindgen.mdx | 2 +- scripts/build/codegen.ts | 17 +++++++++++++++-- src/codegen/replacements.ts | 6 ++++++ src/js/internal-for-testing.ts | 3 ++- src/jsc/lib.rs | 1 + test/internal/bindgen.test.ts | 8 +++++++- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 86b173b692cd..4b755441e843 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -362,4 +362,4 @@ thread when the work completes. ## Tests -- `test/internal/bindgen.test.ts`: conversions with hostile inputs, through `src/jsc/bindgen_test.bind.ts`. What that schema does not use is covered by the suites of the APIs that do. +- `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; `$bindgenFn` 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. diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index e9f9cff2b91b..712c23f02758 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -821,7 +821,14 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void { desc: "JS modules (bundle-modules)", // Note: arg is BUILD_PATH (buildDir), not CODEGEN_PATH. The script // derives CODEGEN_DIR = join(BUILD_PATH, "codegen") internally. - args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir]), + args: shJoin(cfg, [ + "run", + script, + debugFlag(cfg), + cfg.buildDir, + cfg.typesDir, + `--without-bindings=${sources.bindgen.filter(src => !hasSchema(cfg, src)).map(src => basename(src))}`, + ]), }, }); @@ -872,9 +879,15 @@ function emitBakeCodegen({ n, cfg, sources, o, dirStamp }: Ctx): void { o.rustInputs.push(...outputs); } +/** A `*_test.bind.ts` binds what exists for tests. Only a build with assertions has it, and none of those ships. */ +function hasSchema(cfg: CodegenFields, src: string): boolean { + return cfg.assertions || !src.endsWith("_test.bind.ts"); +} + /** Exported for test/internal/build-codegen-declared-outputs.test.ts. */ -export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { +export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); + const sources = { ...allSources, bindgen: allSources.bindgen.filter(src => hasSchema(cfg, src)) }; // What bindgen.ts writes follows from the names and places of its inputs alone. const stems = sources.bindgen.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index 3aaf4f3132c0..b5ce5e764aff 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -129,6 +129,10 @@ export const enums = { // These are passed to --define to the bundler const debug = process.argv[2] === "--debug=ON"; +/** Schemas this build leaves out (`hasSchema` in scripts/build/codegen.ts). `$bindgenFn` of one is `void 0`, which the minifier folds. */ +const withoutBindings = (process.argv.find(arg => arg.startsWith("--without-bindings=")) ?? "=") + .split("=")[1] + .split(","); export const define: Record = { "process.env.NODE_ENV": JSON.stringify(debug ? "development" : "production"), "IS_BUN_DEVELOPMENT": String(debug), @@ -286,6 +290,8 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } + if (withoutBindings.includes(args[0])) return [slice.slice(0, match.index) + "void 0", inner.rest, true]; + const id = registerNativeCall("bind", args[0], args[1], null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index d508c7b81215..d56c5e6d5d7c 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -231,7 +231,8 @@ export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFramework }; }; -export const bindgen = { +// Only in a build with assertions: `hasSchema` in scripts/build/codegen.ts. +export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { add: $bindgenFn("bindgen_test.bind.ts", "add"), requiredAndOptionalArg: $bindgenFn("bindgen_test.bind.ts", "requiredAndOptionalArg"), utf8Echo: $bindgenFn("bindgen_test.bind.ts", "utf8Echo"), diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index f2514f897504..907fc49d520b 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -458,6 +458,7 @@ pub mod zig_stack_trace; pub mod async_module; #[path = "bindgen.rs"] pub mod bindgen; +#[cfg(debug_assertions)] #[path = "bindgen_test.rs"] pub mod bindgen_test; #[path = "BunCPUProfiler.rs"] diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 2484c5e664d5..b9b53642b5e0 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -1,4 +1,10 @@ -import { bindgen } from "bun:internal-for-testing"; +import { bindgen as bindings } from "bun:internal-for-testing"; +import { describe as describeAlways, expect, it as itAlways } from "bun:test"; + +// bindgen_test.bind.ts is only in a build with assertions: a debug build, CI's ASAN lane. +const bindgen = bindings ?? ({} as NonNullable); +const describe = describeAlways.skipIf(!bindings); +const it = itAlways.skipIf(!bindings); it("bindgen add example", () => { // Simple cases From 511afb84ba6fa96a3776884bf2f7a1df2091bc16 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 06:40:29 +0000 Subject: [PATCH 18/44] Describe every schema in Bindgen.d.ts whatever the profile, and run the TLS range test on every build --- scripts/build/codegen.ts | 19 +++++++++---------- src/codegen/bindgen.ts | 15 +++++++++------ test/internal/bindgen.test.ts | 3 ++- .../build-codegen-declared-outputs.test.ts | 13 +++++++++++-- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index a75133da7d36..f1d9cc82e0d8 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -870,14 +870,7 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void { desc: "JS modules (bundle-modules)", // Note: arg is BUILD_PATH (buildDir), not CODEGEN_PATH. The script // derives CODEGEN_DIR = join(BUILD_PATH, "codegen") internally. - args: shJoin(cfg, [ - "run", - script, - debugFlag(cfg), - cfg.buildDir, - cfg.typesDir, - `--without-bindings=${sources.bindgen.filter(src => !hasSchema(cfg, src)).map(src => basename(src))}`, - ]), + args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, withoutBindings(cfg, sources)]), }, }); @@ -933,6 +926,10 @@ function hasSchema(cfg: CodegenFields, src: string): boolean { return cfg.assertions || !src.endsWith("_test.bind.ts"); } +function withoutBindings(cfg: CodegenFields, sources: Sources): string { + return `--without-bindings=${sources.bindgen.filter(src => !hasSchema(cfg, src)).map(src => basename(src))}`; +} + /** Exported for test/internal/build-codegen-declared-outputs.test.ts. */ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); @@ -958,14 +955,16 @@ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): n.build({ outputs: [...cpp, ...headers, ...rust, types], rule: "codegen", - inputs: [...sources.bindgen, ...sources.bindgenInternal], + inputs: [...allSources.bindgen, ...sources.bindgenInternal], orderOnlyInputs: [dirStamp], vars: { cwd: cfg.cwd, desc: ".bind.ts → Generated*.{cpp,h}, generated_bindgen_*.rs", args: shJoin(cfg, [ script, - `--sources=${sources.bindgen.map((src, i) => `${crates[i]}=${src}`).join(",")}`, + // Every schema: build/types is shared by every profile, so Bindgen.d.ts describes them all. + `--sources=${allSources.bindgen.map(src => `${crateOf(src)}=${src}`).join(",")}`, + withoutBindings(cfg, allSources), `--codegen-path=${cfg.codegenDir}`, `--types-path=${cfg.typesDir}`, ]), diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 6733149a390e..2302a5f721a8 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1,5 +1,5 @@ #!/usr/bin/env bun -// Usage: bindgen.ts --sources==,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) +// Usage: bindgen.ts --sources==,.. --without-bindings=,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) import assert from "node:assert"; import path from "node:path"; @@ -65,16 +65,19 @@ function sortTypes(file: SchemaFile): void { file.types = sorted; } -const args = argParse(["sources", "codegen-path", "types-path"]); +const args = argParse(["sources", "without-bindings", "codegen-path", "types-path"]); const codegenPath: string = args["codegen-path"]; const sources = (args.sources as string) .split(",") .filter(source => source.length > 0) .sort(); -const files: SchemaFile[] = []; -for (const source of sources) files.push(await load(source)); -files.forEach(sortTypes); +const allFiles: SchemaFile[] = []; +for (const source of sources) allFiles.push(await load(source)); +allFiles.forEach(sortTypes); +/** What this build compiles. The types describe every schema. */ +const without = ((args["without-bindings"] as string | undefined) ?? "").split(","); +const files = allFiles.filter(file => !without.includes(`${file.origin.stem}.bind.ts`)); const owners = new Map(); for (const file of files) { for (const { name } of file.types) { @@ -93,4 +96,4 @@ writeIfNotChanged(path.join(codegenPath, "GeneratedBindings+Counts.h"), cppCount for (const [crate, crateFiles] of Map.groupBy(files, file => file.origin.crate)) { writeIfNotChanged(path.join(codegenPath, `generated_bindgen_${crate}.rs`), rustForCrate(crateFiles)); } -writeIfNotChanged(path.join(args["types-path"], "Bindgen.d.ts"), dts(files)); +writeIfNotChanged(path.join(args["types-path"], "Bindgen.d.ts"), dts(allFiles)); diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index ae45a275afbc..7aa1f5c6cd28 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -75,7 +75,8 @@ it("custom enforceRange boundaries", () => { expect(bindgen.requiredAndOptionalArg(false, 0, 0)).toBe(0); }); -it("unsigned integer dictionary fields reject negative values", () => { +// Not through the test schema, so on every build. +itAlways("unsigned integer dictionary fields reject negative values", () => { const listen = (tls: Record) => Bun.listen({ hostname: "127.0.0.1", port: 0, tls, socket: { data() {} } }); diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index 528bd528aa68..f33822ece9cc 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -199,9 +199,18 @@ describe("emitBindgen", () => { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); const typesDir = resolve(String(dir), "types"); - const sources = /--sources=[^\s'"]+/.exec(n.toString().replaceAll("$:", ":"))![0]; + const schemas = n + .toString() + .replaceAll("$:", ":") + .match(/--(sources|without-bindings)=[^\s'"]*/g)!; expect( - await bunRun(["run", script, sources, `--codegen-path=${cfg.codegenDir}`, `--types-path=${typesDir}`]), + await bunRun([ + "run", + script, + ...new Set(schemas), + `--codegen-path=${cfg.codegenDir}`, + `--types-path=${typesDir}`, + ]), ).toSpawn(""); const written = readdirSync(cfg.codegenDir).sort(); From b3bb7e087ff1682c3d81fd25b34f63911dab69c3 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 13 Oct 2026 07:07:35 +0000 Subject: [PATCH 19/44] Refuse views of shared memory in b.bufferSource.arrayBuffer and keep the test schema wherever Rust has debug_assertions --- scripts/build/codegen.ts | 4 ++-- src/jsc/bindings/BunIDLConvert.h | 6 +++--- test/js/bun/net/socket.test.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index f1d9cc82e0d8..49ce41d5dd5a 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -921,9 +921,9 @@ function emitBakeCodegen({ n, cfg, sources, o, dirStamp }: Ctx): void { o.rustInputs.push(...outputs); } -/** A `*_test.bind.ts` binds what exists for tests. Only a build with assertions has it, and none of those ships. */ +/** A `*_test.bind.ts` binds what exists for tests. Only a build where Rust has `debug_assertions` has it, and none ships. */ function hasSchema(cfg: CodegenFields, src: string): boolean { - return cfg.assertions || !src.endsWith("_test.bind.ts"); + return cfg.debug || cfg.assertions || !src.endsWith("_test.bind.ts"); } function withoutBindings(cfg: CodegenFields, sources: Sources): string { diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 7ac1d80c88b8..6ef23b887b9f 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -189,11 +189,11 @@ template<> struct WebCore::Converter return jsBuffer; } if (auto* jsView = dynamicDowncast(value)) { + if (jsView->isShared()) { + return std::nullopt; + } return jsView->unsharedBuffer(); } - if (auto* jsDataView = dynamicDowncast(value)) { - return jsDataView->unsharedBuffer(); - } return std::nullopt; } diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 0c8983cfae14..73b0c8ced837 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6476,3 +6476,15 @@ it("socket and tls options are not read from Object.prototype", async () => { for (const name of Object.keys(polluted)) delete (Object.prototype as any)[name]; } }); + +it("a view of shared memory as a tls option is refused", () => { + const shared = new SharedArrayBuffer(8); + const values = [shared, new Uint8Array(shared), new DataView(shared), Buffer.from(shared), [new Uint8Array(shared)]]; + for (const name of ["key", "cert", "ca", "crl", "ALPNProtocols"]) { + for (const value of values) { + expect(() => + Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} }, tls: { [name]: value } as any }), + ).toThrow(expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE" })); + } + } +}); From 511f86bde416b339d2c4ddc5d05bd6212be33f2e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 09:38:33 +0000 Subject: [PATCH 20/44] Choose a union's alternative and an overload by the kind of the value with one decision tree, convert in place, and check a receiver from the cell alone --- docs/project/bindgen.mdx | 53 ++++- src/codegen/bindgen-emit.ts | 225 +++++++++++++++--- src/codegen/bindgen-lib-internal.ts | 244 +++++++++++++++++++- src/codegen/bindgen-lib.ts | 8 +- src/js/internal-for-testing.ts | 2 + src/jsc/bindgen_test.bind.ts | 42 ++++ src/jsc/bindgen_test.rs | 59 ++++- src/jsc/bindings/Bindgen/ExternTraits.h | 31 ++- src/jsc/bindings/Bindgen/ExternUnion.h | 106 +++------ src/jsc/bindings/Bindgen/IDLConvert.h | 51 ++-- src/jsc/bindings/Bindgen/JSBindgenObject.h | 19 +- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 18 +- src/jsc/bindings/Bindgen/Macros.h | 36 --- src/jsc/bindings/BunIDLConvert.h | 98 -------- src/jsc/bindings/JSDOMWrapper.h | 1 + test/internal/bindgen.test.ts | 138 +++++++++++ 16 files changed, 808 insertions(+), 323 deletions(-) delete mode 100644 src/jsc/bindings/Bindgen/Macros.h diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index b7b3e8e852a4..798a7fb48e38 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -96,9 +96,26 @@ implements `bindgen::IntoError` (`bun_sys::Error` and `JsError` do), which lets ### Overloads -`b.overloads(a, b, ..)` is one JS function with several signatures, told apart by -how many arguments were passed: the one with the most required arguments that -the call satisfies. Each names its own Rust function. +`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.bufferSource, 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 candidates are the signatures that do not require more arguments than were +passed. Then, argument by argument, those that take a value of its +[kind](#unions-and-kinds) stay; a signature that has no such argument ignores it. +If several are left when the arguments run out, or none takes the kind, the call goes +to the one that requires the most arguments, then to the one written first, and its +conversion reports what is wrong. Choosing converts nothing and runs no JavaScript. A +signature that no call can reach is an error when the schema is loaded. ## Types @@ -125,7 +142,7 @@ what WebIDL does. Too few arguments throw `ERR_MISSING_ARGS`. | `b.array(T)` | `Vec` | an array | | `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` | the first alternative that fits | +| `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 @@ -210,6 +227,34 @@ generates `enum Formatter { HighlightJavascript, .. }`. An entry can be a list o 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 in its row of the table above. A type that coerces or +reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, +`.validateInteger`, `b.any`, an enumeration, 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. 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. The generated C++ tests whether the value is a cell once, loads the type byte +once, makes no test twice, and converts into the place the result ends up in. + ### Buffers JavaScript can detach, resize or transfer a buffer, so `b.bufferSource.bytes` is only diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index b43e4c6d7b28..f358a709fc67 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -9,14 +9,16 @@ import { EnumType, type FunctionDefinition, type Getter, + type Kind, type Method, type ModuleDefinition, NamedType, OptionalType, type Origin, + type OverloadChoice, rustIdent, type RustScope, - type Signature, + Signature, snakeCase, type Type, UnionType, @@ -141,6 +143,73 @@ const externType = (type: Type): string => ? `::Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` : `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::ExternType`; +/** True of a value that is not a cell. */ +const valueTests: Partial string>> = { + undefined: value => `${value}.isUndefined()`, + null: value => `${value}.isNull()`, + boolean: value => `${value}.isBoolean()`, + number: value => `${value}.isNumber()`, + bigint: value => `::Bun::Bindgen::isBigInt32(${value})`, +}; +/** True of `cell`, whose type byte is `type`. */ +const cellTests: Partial> = { + bigint: "type == JSC::HeapBigIntType", + string: "type == JSC::StringType", + stringObject: "type == JSC::StringObjectType || type == JSC::DerivedStringObjectType", + array: "type == JSC::ArrayType || type == JSC::DerivedArrayType", + arrayBuffer: "type == JSC::ArrayBufferType", + arrayBufferView: "JSC::isTypedArrayTypeIncludingDataView(type)", +}; + +/** + * C++ that declares `choice`, an index into the `choices` it returns: the distinct values of `byKind`. One test of whether + * `value` is a cell, then the cheaper tests first, none twice, none that can run JavaScript, and none for a kind that is + * treated as "other" is. + */ +function cppChooseByKind(value: string, byKind: ReadonlyMap, key: (choice: T) => string) { + const choices: T[] = []; + const indexOf = (choice: T) => { + const index = choices.findIndex(other => key(other) === key(choice)); + return index >= 0 ? index : choices.push(choice) - 1; + }; + const other = indexOf(byKind.get("other")!); + const ofValue = new Map(); + const ofType = new Map(); + const ofClass = new Map(); + const add = (tests: Map, index: number, test: string | undefined) => { + if (test !== undefined && index !== other) tests.set(index, [...(tests.get(index) ?? []), test]); + }; + for (const [kind, choice] of byKind) { + if (kind === "other") continue; + const index = indexOf(choice); + if (typeof kind === "object") { + add(ofClass, index, `JSC::JSCastingHelpers::inherits<${kind.cpp}>(cell)`); + } else { + add(ofValue, index, valueTests[kind]?.(value)); + add(ofType, index, cellTests[kind]); + } + } + const chain = (...tests: Map[]) => + tests + .flatMap(map => [...map]) + .map(([index, conditions]) => { + const nullish = [`${value}.isUndefined()`, `${value}.isNull()`]; + const merged = nullish.every(test => conditions.includes(test)) + ? [`${value}.isUndefinedOrNull()`, ...conditions.filter(test => !nullish.includes(test))] + : conditions; + return `if (${merged.join(" || ")})\n choice = ${index};`; + }) + .join("\nelse "); + const cell = `${ofClass.size > 0 ? "auto* cell" : "JSC::JSType type"} = ${value}.asCell()${ofClass.size > 0 ? "" : "->type()"}; +${ofClass.size > 0 && ofType.size > 0 ? "JSC::JSType type = cell->type();\n" : ""}${chain(ofType, ofClass)}`; + const hasCellTests = ofType.size + ofClass.size > 0; + let tree = ""; + if (hasCellTests && ofValue.size > 0) tree = `if (${value}.isCell()) {\n${indent(cell, 4)}\n} else ${chain(ofValue)}`; + else if (hasCellTests) tree = `if (${value}.isCell()) {\n${indent(cell, 4)}\n}`; + else if (ofValue.size > 0) tree = chain(ofValue); + return { choices, code: `unsigned choice = ${other};${tree && "\n"}${tree}` }; +} + function layoutAssert(cppType: string, type: Type): string { const { size, align } = type.layout(); return `static_assert(sizeof(${cppType}) == ${size} && alignof(${cppType}) == ${align});`; @@ -152,6 +221,62 @@ function humanReadableEnum(type: EnumType): string { return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; } +function cppHeaderForUnion(type: UnionType): string { + const { name, alternatives } = type; + const idlTypes = alternatives.map(a => a.type.idlType).join(", "); + const { choices, code } = cppChooseByKind("value", type.alternativesByKind, choice => choice.join()); + const cases = choices.map((choice, index) => { + if (choice.length === 0) return ""; + const attempts = choice.map(i => { + const alternative = alternatives[i].type; + const slot = `result.template emplace<${i}>`; + if (alternative.kinds !== "any" && alternative.refusesSomeOfItsKinds) + return `if (auto converted = ::Bun::tryConvertIDL<${alternative.idlType}>(lexicalGlobalObject, value, ctx)) { + ${slot}(::std::move(*converted)); + return; +}`; + const convert = alternative.convertsInPlace + ? alternative.cppConvertInto(`${slot}()`, "value", "ctx", "") + : `${slot}(::Bun::convertIDL<${alternative.idlType}>(lexicalGlobalObject, value, ctx));`; + return `scope.release();\n${convert}\nreturn;`; + }); + const refused = attempts.at(-1)!.endsWith("return;") ? "" : "\n break;"; + return `case ${index}: {\n${indent(attempts.join("\n"), 4)}${refused}\n}`; + }); + const canMatchNothing = cases.some(text => text.length === 0 || text.endsWith("break;\n}")); + return ` +namespace Bun { +namespace Bindgen::Generated { +struct IDL${name} : ::Bun::IDLOrderedUnion<${idlTypes}> {}; +using ${name} = IDL${name}::ImplementationType; +${layoutAssert(`ExternTraits<${name}>::ExternType`, type)} +} +template<> struct IDLHumanReadableName : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; +} +template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextConverter<${type.idlType}> { + template<::Bun::IDLConversionContext Ctx> + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + ReturnType result; + convertInto(globalObject, value, ctx, result); + return result; + } + + template<::Bun::IDLConversionContext Ctx> + static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) + { + [[maybe_unused]] auto* globalObject = &lexicalGlobalObject; + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); +${indent(code, 8)} + switch (choice) { +${indent(cases.filter(text => text.length > 0).join("\n"), 8)} + }${canMatchNothing ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(lexicalGlobalObject, scope);` : ""} + } +}; +`; +} + function cppHeaderForType(type: NamedType): string { const { name } = type; if (type instanceof ClassType) return cppHeaderForClass(type); @@ -174,14 +299,7 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue); `; } - if (type instanceof UnionType) { - return ` -namespace Bun::Bindgen::Generated { -using ${name} = ${implementationType(type)}; -${layoutAssert(`ExternTraits<${name}>::ExternType`, type)} -} -`; - } + if (type instanceof UnionType) return cppHeaderForUnion(type); assert(type instanceof DictionaryType); if (!type.accepted) return ""; return ` @@ -195,6 +313,7 @@ struct Extern${name} { ${type.members.map(member => ` ${externType(member.type)} ${member.internalName};`).join("\n")} }; ${layoutAssert(`Extern${name}`, type)} +void convert${name}(JSC::JSGlobalObject&, JSC::JSValue, ${name}& result); } template<> struct ExternTraits { using ExternType = Generated::Extern${name}; @@ -276,27 +395,27 @@ function cppSourceForDictionary(type: DictionaryType): string { let nameIndex = 0; const members = type.members.map((member, i) => { const reads = member.names.map((_, alt) => { - const read = `value${i} = members.get(${nameIndex++});\n RETURN_IF_EXCEPTION(scope, {});`; + const read = `value${i} = members.get(${nameIndex++});\n RETURN_IF_EXCEPTION(scope, void());`; return alt === 0 ? read : `if (value${i}.isUndefined()) {\n ${read.replace("\n", "\n ")}\n }`; }); const qualified = `${type.userFacingName}.${member.name}`; - const convert = `result.${member.internalName} = ${member.type.cppConvert(`value${i}`, `ctx${i}`, qualified)};\n RETURN_IF_EXCEPTION(scope, {});`; + const convert = `${indent(member.type.cppConvertInto(`result.${member.internalName}`, `value${i}`, `ctx${i}`, qualified), 8).trimStart()}\n RETURN_IF_EXCEPTION(scope, void());`; let body: string; if (member.type instanceof DefaultType) { const absent = member.type.onNull ? "isUndefinedOrNull" : "isUndefined"; body = `if (value${i}.${absent}()) { result.${member.internalName} = ${member.type.toCpp(member.type.value)}; } else { - ${convert.replace("\n", "\n ")} + ${convert.replaceAll("\n", "\n ")} }`; } else if (member.type.permitsUndefined) { body = convert; } else { body = `if (value${i}.isUndefined()) [[unlikely]] { ctx${i}.throwRequired(lexicalGlobalObject, scope); - return {}; + return; } ${convert}`; } @@ -309,7 +428,7 @@ function cppSourceForDictionary(type: DictionaryType): string { }); const count = type.names.length; return ` -template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) +void Bun::Bindgen::Generated::convert${type.name}(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) { auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); @@ -318,26 +437,33 @@ template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObje auto* object = value.getObject(); ${ type.allowNonObject - ? ` if (!object || object->isCallable()) - return ${type.toCpp({})};` + ? ` if (!object || object->isCallable()) { + result = ${type.toCpp({})}; + return; + }` : ` if (!object) [[unlikely]] { ctx.throwNotObject(lexicalGlobalObject, scope); - return {}; + return; }` } ${ type.allowArray ? "" : ` bool isArray = JSC::isArray(globalObject, value); - RETURN_IF_EXCEPTION(scope, {}); + RETURN_IF_EXCEPTION(scope, void()); if (isArray) [[unlikely]] { ctx.throwNotObject(lexicalGlobalObject, scope); - return {}; + return; } ` } ::Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); - ${type.cpp} result; ${members.join("\n")} +} + +template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) +{ + ${type.cpp} result; + ::Bun::Bindgen::Generated::convert${type.name}(globalObject, value, result); return result; } ${ @@ -347,7 +473,8 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto converted = WebCore::convertDictionary<${type.cpp}>(*globalObject, JSC::JSValue::decode(value)); + ${type.cpp} converted; + ::Bun::Bindgen::Generated::convert${type.name}(*globalObject, JSC::JSValue::decode(value), converted); RETURN_IF_EXCEPTION(scope, false); *result = ::Bun::Bindgen::ExternTraits<${type.cpp}>::convertToExtern(::std::move(converted)); return true; @@ -357,13 +484,32 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb }`; } -/** Told apart by argument count, so named by it. */ const overloadSuffix = (fn: FunctionDefinition, signature: Signature) => - fn.signatures.length > 1 ? `With${signature.args.length}Arguments` : ""; + fn.signatures.length > 1 ? `${fn.signatures.indexOf(signature) + 1}` : ""; const thunkSymbol = (fn: FunctionDefinition, signature: Signature) => `Bun__Bindgen__${fn.origin!.namespace}__${fn.name.replace(/^_+/, "")}${overloadSuffix(fn, signature)}`; +/** Lines that declare `variable` as what `value` converts to. */ +function cppDeclareConverted(type: Type, variable: string, value: string, ctx: string, name: string): string[] { + const declaration = `${implementationType(type)} ${variable}`; + const absent = type instanceof DefaultType && `${value}.${type.onNull ? "isUndefinedOrNull" : "isUndefined"}()`; + if (!type.convertsInPlace) { + const convert = type.cppConvert(value, ctx, name); + return [`${declaration} = ${absent ? `${absent} ? ${type.toCpp(type.value)} : ${convert}` : convert};`]; + } + const into = type.cppConvertInto(variable, value, ctx, name).split("\n"); + if (!absent) return [`${declaration};`, ...into]; + return [ + `${declaration};`, + `if (${absent})`, + ` ${variable} = ${type.toCpp(type.value)};`, + "else {", + ...into.map(line => ` ${line}`), + "}", + ]; +} + /** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */ function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string { const lines: string[] = []; @@ -380,14 +526,7 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str // Converting a later argument can run JavaScript that detaches or resizes the buffer. if (type.borrowsHeap && i < signature.args.length - 1) rereads.push(`value${i} = ${convert};`, `scope.assertNoException();`); - if (type instanceof DefaultType) { - const absent = type.onNull ? "isUndefinedOrNull" : "isUndefined"; - lines.push( - `${implementationType(type)} value${i} = argument${i}.value().${absent}() ? ${type.toCpp(type.value)} : ${convert};`, - ); - } else { - lines.push(`${implementationType(type)} value${i} = ${convert};`); - } + lines.push(...cppDeclareConverted(type, `value${i}`, `argument${i}.value()`, `ctx${i}`, name)); lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); lines.push(...rereads); @@ -517,7 +656,7 @@ ${cppPrologue} ::JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; ::JSC::EnsureStillAliveScope ensureValueStillAlive { JSC::JSValue::decode(encodedValue) }; [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }; - ${implementationType(getter.type)} value = ${getter.type.cppConvert("ensureValueStillAlive.value()", "ctx", getter.name)}; + ${cppDeclareConverted(getter.type, "value", "ensureValueStillAlive.value()", "ctx", getter.name).join("\n ")} RETURN_IF_EXCEPTION(scope, false); RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, thisObject, ::Bun::Bindgen::ExternTraits<${implementationType(getter.type)}>::convertToExtern(::std::move(value)))); } @@ -646,11 +785,23 @@ ${cppPrologue} ${cppCallBody(signature, thunkSymbol(fn, signature))} }`, ); - const dispatch = fn.signatures.map((signature, index) => - index === fn.signatures.length - 1 - ? ` RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, signature)}(globalObject, callFrame));` - : ` if (callFrame->argumentCount() >= ${signature.requiredCount}) - RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, signature)}(globalObject, callFrame));`, + const ids = new Map(); + const choose = (choice: OverloadChoice): string => { + if (choice instanceof Signature) + return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, choice)}(globalObject, callFrame));`; + const { choices, code } = cppChooseByKind(`argument${choice.argument}`, choice.byKind, next => + String(ids.get(next) ?? ids.set(next, ids.size).size - 1), + ); + return `JSC::JSValue argument${choice.argument} = callFrame->argument(${choice.argument}); +${code} +switch (choice) { +${choices.map((next, index) => `${index === choices.length - 1 ? "default" : `case ${index}`}: {\n${indent(choose(next), 4)}\n}`).join("\n")} +}`; + }; + const dispatch = fn.choices.map(({ atLeast, choice }, index) => + index === fn.choices.length - 1 + ? indent(choose(choice), 4) + : ` if (callFrame->argumentCount() >= ${atLeast}) {\n${indent(choose(choice), 8)}\n }`, ); definition = `${overloads.join("\n")} JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 0b64d3750533..ad62e8add6f6 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -70,6 +70,49 @@ export function asciiLiteral(value: string): string { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"_s`; } +/** + * JavaScript values, divided by what tells them apart: the bits of the value, then the type byte in a cell's header, + * then, for the instances of a class, its ClassInfo. A value of none of these kinds is "other". + */ +export type Kind = + | "undefined" + | "null" + | "boolean" + | "number" + | "bigint" + | "string" + | "stringObject" + | "array" + | "arrayBuffer" + | "arrayBufferView" + | JSClass; + +/** The instances of a C++ `JSCell` class. */ +export interface JSClass { + readonly cpp: string; +} + +const takes = (type: Type, kind: Kind | "other") => type.kinds === "any" || type.kinds.includes(kind as Kind); + +const kindsToTellApart = (types: readonly Type[]): (Kind | "other")[] => [ + ...new Set(types.flatMap(type => (type.kinds === "any" ? [] : type.kinds))), + "other", +]; + +/** + * Which of `alternatives` get a value of each kind: the first, in written order, that takes the kind, preceded by those + * that take it but refuse some of its values. + */ +export function alternativesByKind(alternatives: readonly Type[]): Map { + return new Map( + kindsToTellApart(alternatives).map(kind => { + const takers = alternatives.flatMap((type, i) => (takes(type, kind) ? [i] : [])); + const last = takers.findIndex(i => alternatives[i].kinds === "any" || !alternatives[i].refusesSomeOfItsKinds); + return [kind, last < 0 ? takers : takers.slice(0, last + 1)]; + }), + ); +} + export abstract class Type { /** `undefined` means "not provided". */ get optional(): OptionalType { @@ -105,6 +148,14 @@ export abstract class Type { get permitsUndefined(): boolean { return false; } + /** The kinds of value it takes where there is a choice. "any": its own conversion decides, by coercing or by throwing. */ + get kinds(): readonly Kind[] | "any" { + return "any"; + } + /** Such as a shared buffer, or a Blob that was detached. In a union the next alternative gets them. */ + get refusesSomeOfItsKinds(): boolean { + return false; + } /** Holds a JS value: the Rust type is branded with the call. */ get holdsJSValue(): boolean { return this.dependencies.some(type => type.holdsJSValue); @@ -141,6 +192,14 @@ export abstract class Type { cppConvert(value: string, ctx: string, name: string): string { return `::Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; } + /** Large, or holds something large: built where it ends up, not returned and moved there. */ + get convertsInPlace(): boolean { + return false; + } + /** C++ statements that convert `value` into `target`, which was just default-constructed. */ + cppConvertInto(target: string, value: string, ctx: string, name: string): string { + return `${target} = ${this.cppConvert(value, ctx, name)};`; + } abstract layout(): Layout; /** The Rust mirror of what `Bun::Bindgen::ExternTraits` lowers the C++ value to. */ @@ -217,6 +276,9 @@ export class BoolType extends PrimitiveType { get loose(): BoolType { return new BoolType("::WebCore::IDLBoolean"); } + get kinds() { + return this.idlType === "::Bun::IDLStrictBoolean" ? (["boolean"] as const) : "any"; + } } interface IntegerInfo { @@ -235,6 +297,9 @@ export class IntegerType extends PrimitiveType { super(idlType, "number", info.rust, info.size); this.info = info; } + get kinds() { + return this.idlType.startsWith("::Bun::IDLStrictInteger<") ? (["number", "bigint"] as const) : "any"; + } /** `ToNumeric` first, then as strict as the default. */ get loose(): IntegerType { return new IntegerType(this.info, `::Bun::IDLLooseInteger<${this.info.cpp}>`); @@ -290,6 +355,9 @@ export class DoubleType extends PrimitiveType { constructor() { super("::Bun::IDLStrictDouble", "number", "f64", 8); } + get kinds() { + return ["number"] as const; + } } /** Only as a union alternative or a return type. */ @@ -304,6 +372,9 @@ export class UnitType extends Type { this.tsType = tsType; this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : "::Bun::IDLStrictUndefined"; } + get kinds(): readonly Kind[] { + return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; + } get permitsUndefined(): boolean { return true; } @@ -338,6 +409,10 @@ export class StringType extends Type { super(); this.idlType = idlType; } + get kinds(): readonly Kind[] | "any" { + if (this.idlType === "::Bun::IDLStrictString") return ["string"]; + return this.idlType === "::Bun::Bindgen::IDLStrictStringOrStringObject" ? ["string", "stringObject"] : "any"; + } /** `ToString`: any value is accepted. */ get loose(): StringType { return new StringType("::WebCore::IDLDOMString"); @@ -406,12 +481,21 @@ export class Utf8Type extends Type { const isLoose = this.idlType === "::Bun::Bindgen::IDLLooseUTF8String"; return new OwnedUtf8Type(isLoose ? "::WebCore::IDLDOMString" : "::Bun::Bindgen::IDLStrictStringOrStringObject"); } + get kinds(): readonly Kind[] | "any" { + return this.idlType === "::Bun::Bindgen::IDLStrictUTF8String" ? ["string", "stringObject"] : "any"; + } get borrowsFrame(): boolean { return true; } get nullRepresentation() { return "pointer" as const; } + get convertsInPlace(): boolean { + return true; + } + cppConvertInto(target: string, value: string, ctx: string): string { + return `::WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; + } toCpp(value: unknown): string { assert(typeof value === "string"); return `::Bun::Bindgen::JSStringUTF8View { ${asciiLiteral(value)} }`; @@ -442,6 +526,8 @@ export class Utf8Type extends Type { } } +const bufferSourceKinds = ["arrayBuffer", "arrayBufferView"] as const; + /** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ export class BytesType extends Type { get rustIsCopy(): boolean { @@ -449,6 +535,12 @@ export class BytesType extends Type { } readonly idlType = "::Bun::Bindgen::IDLBufferSourceBytes"; readonly tsType = "ArrayBuffer | ArrayBufferView"; + get kinds() { + return bufferSourceKinds; + } + get refusesSomeOfItsKinds(): boolean { + return true; + } get borrowsHeap(): boolean { return true; } @@ -479,13 +571,19 @@ export class BufferSourceType extends Type { } readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; readonly tsType = "ArrayBuffer | ArrayBufferView"; + get kinds() { + return bufferSourceKinds; + } + get refusesSomeOfItsKinds(): boolean { + return true; + } /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ get bytes(): BytesType { return new BytesType(); } /** A reference on the `JSC::ArrayBuffer` behind it, which can be stored. */ get arrayBuffer(): RefType { - return new RefType("::Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null); + return new RefType("::Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null, bufferSourceKinds); } get holdsJSValue(): boolean { return true; @@ -528,12 +626,21 @@ export class WithValueType extends Type { get permitsUndefined(): boolean { return this.inner.permitsUndefined; } + get kinds() { + return this.inner.kinds; + } get holdsJSValue(): boolean { return true; } get dependencies(): readonly Type[] { return [this.inner]; } + get convertsInPlace(): boolean { + return this.inner.convertsInPlace; + } + cppConvertInto(target: string, value: string, ctx: string, name: string): string { + return this.inner.cppConvertInto(target, value, ctx, name); + } toCpp(): string { throw new TypeError(".withValue cannot be combined with .default()"); } @@ -595,12 +702,20 @@ export class RefType extends Type { readonly tsType: string; readonly pointee: string; readonly header: string | null; - constructor(idlType: string, tsType: string, pointee: string, header: string | null) { + readonly wrapperKinds: readonly Kind[]; + constructor(idlType: string, tsType: string, pointee: string, header: string | null, wrapperKinds: readonly Kind[]) { super(); this.idlType = idlType; this.tsType = tsType; this.pointee = pointee; this.header = header; + this.wrapperKinds = wrapperKinds; + } + get kinds() { + return this.wrapperKinds; + } + get refusesSomeOfItsKinds(): boolean { + return true; } get nullRepresentation() { return "pointer" as const; @@ -654,9 +769,26 @@ export class OptionalType extends Type { get permitsUndefined(): boolean { return true; } + get kinds(): readonly Kind[] | "any" { + if (this.payload.kinds === "any" || this.kind === "falsyAsNull") return "any"; + return [...this.payload.kinds, "undefined", ...(this.kind === "nullable" ? (["null"] as const) : [])]; + } get dependencies(): readonly Type[] { return [this.payload]; } + get convertsInPlace(): boolean { + return this.payload.convertsInPlace; + } + /** A default-constructed `target` is the null value. */ + cppConvertInto(target: string, value: string, ctx: string, name: string): string { + const provided = { + optional: `!${value}.isUndefined()`, + nullable: `!${value}.isUndefinedOrNull()`, + falsyAsNull: `${value}.toBoolean(globalObject)`, + }[this.kind]; + const slot = this.payload.nullRepresentation === "optional" ? `${target}.emplace()` : target; + return `if (${provided}) {\n ${this.payload.cppConvertInto(slot, value, ctx, name).replaceAll("\n", "\n ")}\n}`; + } toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); } @@ -734,9 +866,20 @@ export class DefaultType extends Type { toRust(value: unknown, scope: RustScope): string { return this.payload.toRust(value, scope); } + get kinds(): readonly Kind[] | "any" { + if (this.payload.kinds === "any") return "any"; + return [...this.payload.kinds, "undefined", ...(this.onNull ? (["null"] as const) : [])]; + } cppConvert(value: string, ctx: string, name: string): string { return this.payload.cppConvert(value, ctx, name); } + get convertsInPlace(): boolean { + return this.payload.convertsInPlace; + } + /** Of a value that is provided. */ + cppConvertInto(target: string, value: string, ctx: string, name: string): string { + return this.payload.cppConvertInto(target, value, ctx, name); + } layout(): Layout { return this.payload.layout(); } @@ -773,6 +916,9 @@ export class ArrayType extends Type { get tsType(): string { return `(${this.element.tsType})[]`; } + get kinds() { + return ["array"] as const; + } get dependencies(): readonly Type[] { return [this.element]; } @@ -966,6 +1112,12 @@ export class DictionaryType extends NamedType { get dependencies(): readonly Type[] { return this.members.map(member => member.type); } + get convertsInPlace(): boolean { + return true; + } + cppConvertInto(target: string, value: string): string { + return `::Bun::Bindgen::Generated::convert${this.name}(*globalObject, ${value}, ${target});`; + } /** Every JS name read, in order. */ get names(): string[] { return this.members.flatMap(member => member.names); @@ -1012,7 +1164,7 @@ export class DictionaryType extends NamedType { } } -/** Alternatives are tried in order; the first whose conversion accepts the value wins. */ +/** A value goes to the first alternative, in written order, that takes its kind. */ export class UnionType extends NamedType { readonly alternatives: readonly { name: string; variant: string; type: Type }[]; constructor(alternatives: Record) { @@ -1021,14 +1173,35 @@ export class UnionType extends NamedType { assert(!(type instanceof DefaultType)); return { name, variant: pascalCase(name), type }; }); - assert(this.alternatives.length > 0 && this.alternatives.length <= 16, "1 to 16 alternatives (Bindgen/Macros.h)"); + assert(this.alternatives.length > 0); assert( !this.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", ); + for (const { name, type } of this.alternatives) { + assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); + assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); + } + const chosen = new Set([...this.alternativesByKind.values()].flat()); + this.alternatives.forEach(({ name }, i) => + assert(chosen.has(i), `${name} is never chosen: the alternatives before it take every value it would`), + ); + } + get alternativesByKind() { + return alternativesByKind(this.alternatives.map(a => a.type)); + } + get kinds(): readonly Kind[] | "any" { + const kinds = this.alternatives.map(a => a.type.kinds); + return kinds.includes("any") ? "any" : [...new Set(kinds.flat() as Kind[])]; } get idlType(): string { - return `::Bun::IDLOrderedUnion<${this.alternatives.map(a => a.type.idlType).join(", ")}>`; + return `::Bun::Bindgen::Generated::IDL${this.name}`; + } + get convertsInPlace(): boolean { + return true; + } + cppConvertInto(target: string, value: string, ctx: string): string { + return `::WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; } get tsType(): string { return this.alternatives.map(a => a.type.tsType).join(" | "); @@ -1112,18 +1285,64 @@ export class Signature { } } +/** Call a signature, or look at the kind of one more argument. */ +export type OverloadChoice = Signature | { argument: number; byKind: Map }; + export class FunctionDefinition { - /** More than one: picked by argument count, most required arguments first. */ readonly signatures: readonly Signature[]; + /** How the signature is chosen when at least this many arguments were passed. Most arguments first; the last is for any number. */ + readonly choices: readonly { atLeast: number; choice: OverloadChoice }[]; name = ""; origin: Origin | null = null; constructor(signatures: readonly SignatureOptions[]) { assert(signatures.length > 0); - this.signatures = signatures - .map(options => new Signature(options)) - .sort((a, b) => b.requiredCount - a.requiredCount); - const counts = this.signatures.map(signature => signature.requiredCount); - assert(new Set(counts).size === counts.length, "overloads must differ in their number of required arguments"); + this.signatures = signatures.map(options => new Signature(options)); + const counts = [...new Set(this.signatures.map(signature => signature.requiredCount))].sort((a, b) => b - a); + const made = new Map(); + this.choices = counts.map(atLeast => ({ + atLeast, + choice: this.choose( + this.signatures.filter(signature => signature.requiredCount <= atLeast), + 0, + made, + ), + })); + const chosen = new Set(made.values()); + this.signatures.forEach((signature, i) => + assert(chosen.has(signature), `overload ${i + 1} is never chosen: nothing tells it from the others`), + ); + } + /** + * Argument by argument, keeps the candidates that take a value of its kind; one that has no such argument ignores it. + * What is left when the arguments run out, or when no candidate takes the kind, goes to the one that requires the most + * arguments, then to the one written first: its conversion reports what is wrong. + */ + private choose( + candidates: readonly Signature[], + argument: number, + made: Map, + ): OverloadChoice { + const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; + const existing = made.get(key); + if (existing) return existing; + const best = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); + let choice: OverloadChoice = best; + if (candidates.length > 1 && candidates.some(signature => argument < signature.args.length)) { + const typeOf = (signature: Signature) => signature.args[argument]?.type; + choice = { + argument, + byKind: new Map( + kindsToTellApart(candidates.flatMap(signature => typeOf(signature) ?? [])).map(kind => { + const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); + return [kind, this.choose(takers.length > 0 ? takers : [best], argument + 1, made)]; + }), + ), + }; + const outcomes = new Set(choice.byKind.values()); + if (outcomes.size === 1) [choice] = outcomes; + } + made.set(key, choice); + return choice; } get length(): number { const given = this.signatures.find(signature => signature.length !== undefined); @@ -1300,6 +1519,9 @@ export class ClassType extends NamedType { get idlType(): string { return `::Bun::Bindgen::IDLRawInterface<${this.cpp}>`; } + get kinds(): readonly Kind[] { + return [this]; + } get holdsJSValue(): boolean { return true; } diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 920f81c437ac..c4102bb23eab 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -45,7 +45,9 @@ export const any = new AnyType(); export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ -export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h"); +export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h", [ + { cpp: "::WebCore::JSBlob" }, +]); const null_ = new UnitType("null"); @@ -59,7 +61,7 @@ export const enumeration = (values: readonly (string | readonly string[])[]) => export const dictionary = (members: Record, options?: DictionaryOptions) => new DictionaryType(members, options); -/** Alternatives are tried in order. */ +/** A value goes to the first alternative, in written order, that takes its kind. */ export const union = (alternatives: Record) => new UnionType(alternatives); /** A JS class whose state is the Rust struct at `options.rustPath`, stored inside the wrapper object. */ @@ -71,7 +73,7 @@ export { selfType as self } from "./bindgen-lib-internal.ts"; export const fn = (signature: SignatureOptions) => new FunctionDefinition([signature]); -/** One JS function, several signatures, told apart by how many arguments were passed. */ +/** One JS function, several signatures, told apart by how many arguments were passed and by their kinds. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index ee53c9adf454..b853e8940394 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -254,6 +254,8 @@ export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { nextColor: $bindgenFn("bindgen_test.bind.ts", "nextColor"), optionsEcho: $bindgenFn("bindgen_test.bind.ts", "optionsEcho"), describeValue: $bindgenFn("bindgen_test.bind.ts", "describeValue"), + describeAnything: $bindgenFn("bindgen_test.bind.ts", "describeAnything"), + overloaded: $bindgenFn("bindgen_test.bind.ts", "overloaded"), sumArray: $bindgenFn("bindgen_test.bind.ts", "sumArray"), range: $bindgenFn("bindgen_test.bind.ts", "range"), words: $bindgenFn("bindgen_test.bind.ts", "words"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 51e4a8e5f562..6548cf879ce7 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -128,3 +128,45 @@ export const TestToken = b.class({ }, }); export const makeToken = b.fn({ args: { id: b.u32 }, returns: TestToken, capability: "heap" }); + +/** An alternative for each kind of value. What `bytes` and `blob` refuse, and everything else, goes to `options`. */ +export const TestAnything = b.union({ + nothing: b.null, + flag: b.bool, + integer: b.i32, + text: b.string, + bytes: b.bufferSource.arrayBuffer, + blob: b.blob, + counter: TestCounter, + token: TestToken, + list: b.array(b.f64), + options: TestOptions, +}); +export const describeAnything = b.fn({ args: { value: TestAnything }, returns: b.string.utf8, capability: "js" }); + +/** Told apart by the number of arguments, by the first and by the second. */ +export const overloaded = b.overloads( + { + args: { text: b.string.utf8, times: b.u32.default(1) }, + returns: b.string.utf8, + rustPath: "crate::bindgen_test::overloaded_text_times", + }, + { + args: { text: b.string.utf8, upper: b.bool }, + returns: b.string.utf8, + rustPath: "crate::bindgen_test::overloaded_text_upper", + }, + { + args: { a: b.f64, b: b.f64 }, + returns: b.string.utf8, + rustPath: "crate::bindgen_test::overloaded_two_numbers", + }, + { args: { number: b.f64 }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_number" }, + { args: { list: b.array(b.f64) }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_list" }, + { + args: { counter: TestCounter }, + returns: b.string.utf8, + capability: "js", + rustPath: "crate::bindgen_test::overloaded_counter", + }, +); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index a1a71f9ce83e..bf9c85e58365 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -8,8 +8,8 @@ use crate::bindgen::{ WithValue, }; use crate::generated_bindgen::bindgen_test::{ - TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, TestHolderClass, - TestOptions, TestOptionsEcho, TestTokenClass, TestValue, + TestAnything, TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, + TestHolderClass, TestOptions, TestOptionsEcho, TestTokenClass, TestValue, }; // This example should be kept in sync with bindgen's documentation @@ -85,6 +85,61 @@ pub fn describe_value(value: TestValue<'_>) -> Vec { } } +pub fn describe_anything<'a>(cx: &mut JsCall<'a>, value: TestAnything<'a>) -> Vec { + match value { + TestAnything::Nothing => b"nothing".to_vec(), + TestAnything::Flag(flag) => format!("flag {flag}").into_bytes(), + TestAnything::Integer(integer) => format!("integer {integer}").into_bytes(), + TestAnything::Text(text) => format!("text {text}").into_bytes(), + TestAnything::Bytes(buffer) => { + // SAFETY: `buffer` holds a ref on it, and nothing else borrows it here. + let buffer: ArrayBuffer = unsafe { (*buffer.get()).as_array_buffer() }; + format!("bytes {}", buffer.byte_slice().len()).into_bytes() + } + TestAnything::Blob(_) => b"blob".to_vec(), + TestAnything::Counter(counter) => { + let count = counter.with(&cx.payloads, |counter| counter.count); + format!("counter {count}").into_bytes() + } + TestAnything::Token(token) => { + let id = token.with(&cx.payloads, |token| token.id); + format!("token {id}").into_bytes() + } + TestAnything::List(list) => format!("list {list:?}").into_bytes(), + TestAnything::Options(options) => format!("options {}", options.count).into_bytes(), + } +} + +pub fn overloaded_text_times(text: &[u8], times: u32) -> Vec { + [b"text times ", text.repeat(times as usize).as_slice()].concat() +} + +pub fn overloaded_text_upper(text: &[u8], upper: bool) -> Vec { + let text = if upper { + text.to_ascii_uppercase() + } else { + text.to_vec() + }; + [b"text upper ", text.as_slice()].concat() +} + +pub fn overloaded_two_numbers(a: f64, b: f64) -> Vec { + format!("two numbers {a} {b}").into_bytes() +} + +pub fn overloaded_number(number: f64) -> Vec { + format!("number {number}").into_bytes() +} + +pub fn overloaded_list(list: Vec) -> Vec { + format!("list {:?}", list.into_boxed_slice()).into_bytes() +} + +pub fn overloaded_counter<'a>(cx: &mut JsCall<'a>, counter: JsWrapper<'a, TestCounter>) -> Vec { + let count = counter.with(&cx.payloads, |counter| counter.count); + format!("counter {count}").into_bytes() +} + pub fn sum_array(numbers: Vec) -> f64 { numbers .into_iter() diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 84ada1d82ea5..023917258cd9 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -68,12 +68,7 @@ struct ExternVariant { static_assert(sizeof...(Args) > 0); static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); - - explicit ExternVariant(std::variant&& variant) - : data(std::move(variant)) - , tag(static_cast(variant.index())) - { - } + static_assert(std::is_trivially_copyable_v>); }; template @@ -82,12 +77,12 @@ struct ExternTraits> { static ExternType convertToExtern(std::variant&& cppValue) { - using VariantOfExtern = std::variant::ExternType...>; - return ExternType { std::visit([](auto&& arg) -> VariantOfExtern { - using ArgType = std::decay_t; - return { ExternTraits::convertToExtern(std::move(arg)) }; - }, - std::move(cppValue)) }; + ExternType result; + result.tag = static_cast(cppValue.index()); + [&](std::index_sequence) { + static_cast(((cppValue.index() == indices && (result.data.template set(ExternTraits::convertToExtern(std::move(*std::get_if(&cppValue)))), true)) || ...)); + }(std::index_sequence_for {}); + return result; } }; @@ -97,11 +92,13 @@ struct ExternTraits> { static ExternType convertToExtern(std::optional&& cppValue) { - using StdVariant = std::variant::ExternType>; - if (!cppValue) { - return ExternType { StdVariant { ExternNullPtr {} } }; - } - return ExternType { StdVariant { ExternTraits::convertToExtern(std::move(*cppValue)) } }; + ExternType result; + result.tag = cppValue.has_value(); + if (cppValue) + result.data.template set<1>(ExternTraits::convertToExtern(std::move(*cppValue))); + else + result.data.template set<0>(ExternNullPtr {}); + return result; } }; diff --git a/src/jsc/bindings/Bindgen/ExternUnion.h b/src/jsc/bindings/Bindgen/ExternUnion.h index f7552605fa71..9b8aba8045df 100644 --- a/src/jsc/bindings/Bindgen/ExternUnion.h +++ b/src/jsc/bindings/Bindgen/ExternUnion.h @@ -1,88 +1,38 @@ #pragma once #include -#include #include -#include -#include "Macros.h" - -#define BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, ...) \ - template \ - union ExternUnion { \ - BUN_BINDGEN_DETAIL_FOREACH( \ - BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ - T0 __VA_OPT__(, ) __VA_ARGS__) \ - ExternUnion(std::variant&& variant) \ - { \ - using This = std::decay_t; \ - static_assert(std::is_trivially_copyable_v); \ - const std::size_t index = variant.index(); \ - std::visit([this, index](auto&& arg) { \ - using Arg = std::decay_t; \ - BUN_BINDGEN_DETAIL_FOREACH( \ - BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT, \ - T0 __VA_OPT__(, ) __VA_ARGS__) \ - }, \ - std::move(variant)); \ - } \ - } - -#define BUN_BINDGEN_DETAIL_EXTERN_UNION_TEMPLATE_PARAM(Type) , typename Type -#define BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD(Type) Type alternative##Type; -#define BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT(Type) \ - if constexpr (std::is_same_v) { \ - if (index == ::Bun::Bindgen::Detail::indexOf##Type) { \ - alternative##Type = std::move(arg); \ - return; \ - } \ - } namespace Bun::Bindgen { -namespace Detail { -// For use in macros. -static constexpr std::size_t indexOfT0 = 0; -static constexpr std::size_t indexOfT1 = 1; -static constexpr std::size_t indexOfT2 = 2; -static constexpr std::size_t indexOfT3 = 3; -static constexpr std::size_t indexOfT4 = 4; -static constexpr std::size_t indexOfT5 = 5; -static constexpr std::size_t indexOfT6 = 6; -static constexpr std::size_t indexOfT7 = 7; -static constexpr std::size_t indexOfT8 = 8; -static constexpr std::size_t indexOfT9 = 9; -static constexpr std::size_t indexOfT10 = 10; -static constexpr std::size_t indexOfT11 = 11; -static constexpr std::size_t indexOfT12 = 12; -static constexpr std::size_t indexOfT13 = 13; -static constexpr std::size_t indexOfT14 = 14; -static constexpr std::size_t indexOfT15 = 15; -} +// `union { Args... }`, which is how it is passed to Rust. template union ExternUnion; -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); -BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( - T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); +template +union ExternUnion { + First first; + + template + void set(First value) + { + static_assert(!index); + first = value; + } +}; + +template +union ExternUnion { + First first; + ExternUnion rest; + + template + void set(auto value) + { + if constexpr (index) + rest.template set(value); + else + first = value; + } +}; + } diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index e9d0fb2f6c9b..09607204436a 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -23,6 +23,17 @@ inline bool isStringObject(JSC::JSValue value) return value.isCell() && (value.asCell()->type() == JSC::StringObjectType || value.asCell()->type() == JSC::DerivedStringObjectType); } +// A BigInt that is not a cell. +inline bool isBigInt32(JSC::JSValue value) +{ +#if USE(BIGINT32) + return value.isBigInt32(); +#else + UNUSED_PARAM(value); + return false; +#endif +} + // std::nullopt for anything else, a shared buffer included. A detached or out-of-bounds view is empty. inline std::optional> unsharedBufferSourceSpan(JSC::JSValue value) { @@ -42,34 +53,21 @@ inline std::optional> unsharedBufferSourceSpan(JSC::JSV } } -template<> struct WebCore::Converter : Bun::DefaultTryConverter { - // Not through tryConvert's std::optional: the view is large, and this way it is never moved. - template - static Bun::Bindgen::JSStringUTF8View convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) - { - if (value.isString()) [[likely]] - return Bun::Bindgen::JSStringUTF8View::create(globalObject, JSC::asString(value)); - return Bun::DefaultTryConverter::convert(globalObject, value, ctx); - } - +template<> struct WebCore::Converter : Bun::DefaultContextConverter { template - static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) + static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) { if (value.isString()) [[likely]] - return Bun::Bindgen::JSStringUTF8View::create(globalObject, JSC::asString(value)); - if (!Bun::Bindgen::isStringObject(value)) - return std::nullopt; + return result.initialize(globalObject, JSC::asString(value)); auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); + if (!Bun::Bindgen::isStringObject(value)) { + ctx.throwNotString(globalObject, scope); + return; + } JSC::JSString* string = value.toString(&globalObject); - RETURN_IF_EXCEPTION(scope, Bun::Bindgen::JSStringUTF8View {}); - RELEASE_AND_RETURN(scope, Bun::Bindgen::JSStringUTF8View::create(globalObject, string)); - } - - template - static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) - { - ctx.throwNotString(globalObject, scope); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, result.initialize(globalObject, string)); } }; @@ -89,14 +87,15 @@ template<> struct WebCore::Converter struct WebCore::Converter : WebCore::DefaultConverter { - static Bun::Bindgen::JSStringUTF8View convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) +template<> struct WebCore::Converter : Bun::DefaultContextConverter { + template + static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&, Bun::Bindgen::JSStringUTF8View& result) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); JSC::JSString* string = value.toString(&globalObject); - RETURN_IF_EXCEPTION(scope, {}); - RELEASE_AND_RETURN(scope, Bun::Bindgen::JSStringUTF8View::create(globalObject, string)); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, result.initialize(globalObject, string)); } }; diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h index 1be17e8321f5..ff391fd1e977 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -4,11 +4,28 @@ #include "Bindgen/ExternTraits.h" #include "Bindgen/IDLConvert.h" #include "ErrorCode.h" +#include "JSDOMWrapper.h" #include "ZigGlobalObject.h" #include #include #include +namespace Bun::Bindgen { +template class JSBindgenObject; +} + +// Reads the cell and nothing else: one of JSDOMWrapperType is a JSDestructibleObject, which holds its ClassInfo. +template +struct JSC::JSCastingHelpers::InheritsTraits> { + static constexpr std::optional typeRange { std::nullopt }; + template + static inline bool inherits(From* from) + { + return from->type() == static_cast(WebCore::JSDOMWrapperType) + && static_cast(static_cast(from))->classInfo() == Bun::Bindgen::JSBindgenObject::info(); + } +}; + namespace Bun::Bindgen { // One per class, defined by generated code. @@ -57,7 +74,7 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { - return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(static_cast(WebCore::JSDOMWrapperType), StructureFlags), info()); } // The payload is uninitialized: the caller writes it before anything else can run. diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 0bff7a53a770..18ddb99e02e8 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -21,20 +21,18 @@ class JSStringUTF8View { { } - // Resolving a rope and converting can throw. Every path returns `result`, so it is built in the caller's slot. - static JSStringUTF8View create(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + // Of a view that is null. It is large, so it is filled in where it lives. Resolving a rope and converting can throw. + void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - JSStringUTF8View result; - result.m_view = string->view(&globalObject); - RETURN_IF_EXCEPTION(scope, result); - result.m_isNull = false; - if (result.m_view->is8Bit() && WTF::charactersAreAllASCII(result.m_view->span8())) [[likely]] - return result; - if (!result.convert()) [[unlikely]] + m_view = string->view(&globalObject); + RETURN_IF_EXCEPTION(scope, void()); + m_isNull = false; + if (m_view->is8Bit() && WTF::charactersAreAllASCII(m_view->span8())) [[likely]] + return; + if (!convert()) [[unlikely]] JSC::throwOutOfMemoryError(&globalObject, scope); - return result; } bool isNull() const { return m_isNull; } diff --git a/src/jsc/bindings/Bindgen/Macros.h b/src/jsc/bindings/Bindgen/Macros.h deleted file mode 100644 index f70d2d8120db..000000000000 --- a/src/jsc/bindings/Bindgen/Macros.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include -#include - -#define BUN_BINDGEN_DETAIL_FOREACH(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH2(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH2(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH3(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH3(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH4(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH4(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH5(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH5(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH6(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH6(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH7(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH7(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH8(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH8(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH9(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH9(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH10(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH10(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH11(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH11(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH12(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH12(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH13(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH13(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH14(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH14(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH15(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH15(macro, arg, ...) macro(arg) \ - __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH16(macro, __VA_ARGS__)) -#define BUN_BINDGEN_DETAIL_FOREACH16(macro, arg, ...) macro(arg) \ - __VA_OPT__(static_assert(false, "Bindgen/Macros.h: too many items")) diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 6ef23b887b9f..3469e14b6906 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -206,101 +206,3 @@ template<> struct WebCore::Converter ctx.throwNotBufferSource(globalObject, scope); } }; - -template -struct WebCore::Converter> - : Bun::DefaultTryConverter> { -private: - using Base = Bun::DefaultTryConverter>; - -public: - using typename Base::ReturnType; - - static constexpr bool conversionHasSideEffects - = (WebCore::Converter::conversionHasSideEffects || ...); - - template - static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) - { - using Last = std::tuple_element_t>; - if constexpr (requires { - WebCore::Converter::tryConvert(globalObject, value, ctx); - }) { - return Base::convert(globalObject, value, ctx); - } else { - return convertWithInfallibleLast( - globalObject, - value, - ctx, - std::make_index_sequence {}); - } - } - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - auto& vm = JSC::getVM(&globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - std::optional result; - auto tryAlternative = [&]() -> bool { - auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); - RETURN_IF_EXCEPTION(scope, true); - if (!alternativeResult.has_value()) { - return false; - } - result = ReturnType { std::move(*alternativeResult) }; - return true; - }; - (tryAlternative.template operator()() || ...); - return result; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.template throwNoMatchInUnion(globalObject, scope); - } - -private: - template - static ReturnType convertWithInfallibleLast( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx, - std::index_sequence) - { - auto& vm = JSC::getVM(&globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - std::optional result; - auto tryAlternative = [&]() -> bool { - using T = std::tuple_element_t>; - if constexpr (index == sizeof...(IDL) - 1) { - auto alternativeResult = Bun::convertIDL(globalObject, value, ctx); - RETURN_IF_EXCEPTION(scope, true); - result = ReturnType { std::move(alternativeResult) }; - return true; - } else { - auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); - RETURN_IF_EXCEPTION(scope, true); - if (!alternativeResult.has_value()) { - return false; - } - result = ReturnType { std::move(*alternativeResult) }; - return true; - } - }; - bool done = (tryAlternative.template operator()() || ...); - ASSERT(done); - if (!result.has_value()) { - // Exception - return {}; - } - return std::move(*result); - } -}; diff --git a/src/jsc/bindings/JSDOMWrapper.h b/src/jsc/bindings/JSDOMWrapper.h index 31a2b19edf27..c5a30afa7a49 100644 --- a/src/jsc/bindings/JSDOMWrapper.h +++ b/src/jsc/bindings/JSDOMWrapper.h @@ -45,6 +45,7 @@ class ScriptExecutionContext; // offset | 7 | 6 | 5 | 4 | 3 2 1 0 | // value | 1 | 1 | 1 | 1 | NodeType | +// Only for a JSDestructibleObject: Bun::Bindgen::JSBindgenObject's cast reads classInfo() from any cell of this type. inline constexpr uint8_t JSDOMWrapperType = 0b11101110; inline constexpr uint8_t JSEventType = 0b11101111; inline constexpr uint8_t JSNodeType = 0b11110000; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 7aa1f5c6cd28..cb272b35b5a8 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -1,5 +1,6 @@ import { bindgen as bindings } from "bun:internal-for-testing"; import { describe as describeAlways, expect, it as itAlways } from "bun:test"; +import * as b from "../../src/codegen/bindgen-lib.ts"; // bindgen_test.bind.ts is only in a build with assertions: a debug build, CI's ASAN lane. const bindgen = bindings ?? ({} as NonNullable); @@ -478,6 +479,138 @@ describe("unions", () => { } expect(() => bindgen.describeValue(["not a number"])).toThrow(TypeError); }); + + it("sends each kind of value to the alternative that takes it", () => { + const { TestCounter } = bindgen as any; + class SubCounter extends TestCounter {} + class SubArray extends Array {} + const cases: [unknown, string][] = [ + [undefined, "nothing"], + [null, "nothing"], + [false, "flag false"], + [5, "integer 5"], + [-0, "integer 0"], + [7n, "integer 7"], + ["", "text "], + ["téxt", "text téxt"], + [new ArrayBuffer(3), "bytes 3"], + [new Uint8Array(4), "bytes 4"], + [new Float64Array(2), "bytes 16"], + [new DataView(new ArrayBuffer(5)), "bytes 5"], + [Buffer.alloc(6).buffer.slice(0, 6), "bytes 6"], + [new Blob(["x"]), "blob"], + [Bun.file("/nonexistent"), "blob"], + [new TestCounter(3), "counter 3"], + [new SubCounter(4), "counter 4"], + [bindgen.makeToken(9), "token 9"], + [[], "list []"], + [[1, 2.5], "list [1.0, 2.5]"], + [SubArray.of(1), "list [1.0]"], + [{ count: 8 }, "options 8"], + ]; + for (const [value, expected] of cases) expect(bindgen.describeAnything(value)).toBe(expected); + }); + + it("the last alternative gets what no other takes, and what another refuses", () => { + const shared = new SharedArrayBuffer(2); + for (const value of [ + shared, + new Uint8Array(shared), + new String("boxed"), + new Number(1), + function () {}, + new Date(0), + new Map(), + new Bun.Glob("x"), + new URL("http://localhost/"), + new Proxy({}, {}), + ]) { + expect(bindgen.describeAnything(value)).toBe("options 1"); + } + for (const value of [Symbol("s"), new Proxy([], {})]) { + expect(() => bindgen.describeAnything(value)).toThrow("TestOptions must be an object"); + } + }); + + it("the alternative that was chosen reports what is wrong with the value", () => { + expect(() => bindgen.describeAnything(1.5)).toThrow("value must be an integer (received 1.5)"); + expect(() => bindgen.describeAnything(2n ** 40n)).toThrow(RangeError); + expect(() => bindgen.describeAnything([1, "x"])).toThrow("element of value must be a number"); + expect(() => bindgen.describeAnything({ count: "x" })).toThrow("TestOptions.count must be a number"); + }); + + it("choosing runs no JavaScript", () => { + const calls: string[] = []; + const trap = + (name: string) => + (...args: unknown[]) => (calls.push(name), (Reflect as any)[name](...args)); + const handler = Object.fromEntries(Reflect.ownKeys(Reflect).map(name => [name, trap(name as string)])); + expect(() => bindgen.describeValue(new Proxy({}, handler))).toThrow("value must be of type"); + expect(() => + bindgen.describeValue({ toString: () => calls.push("toString"), valueOf: () => calls.push("valueOf") }), + ).toThrow("value must be of type"); + expect(calls).toEqual([]); + }); + + itAlways("an alternative that cannot be chosen is an error in the schema", () => { + expect(() => b.union({ anything: b.any, text: b.string })).toThrow("text is never chosen"); + expect(() => b.union({ text: b.string.loose, flag: b.bool })).toThrow("flag is never chosen"); + expect(() => b.union({ text: b.string.utf8, other: b.string })).toThrow("other is never chosen"); + expect(() => b.union({ integer: b.i32, number: b.f64 })).toThrow("number is never chosen"); + expect(() => b.union({ none: b.null, inner: b.union({ text: b.string }) })).toThrow("list the alternatives"); + expect(() => b.union({ text: b.string.nullable, flag: b.bool })).toThrow("add b.null as an alternative"); + // What one refuses, the next can take. + b.union({ bytes: b.bufferSource, reference: b.bufferSource.arrayBuffer }); + b.union({ text: b.string, boxed: b.string.utf8 }); + }); +}); + +describe("overloads", () => { + it("are told apart by the number of arguments and by their kinds", () => { + const { TestCounter } = bindgen as any; + const cases: [unknown[], string][] = [ + [["ab"], "text times ab"], + [["ab", 2], "text times abab"], + [["ab", 2n], "text times abab"], + [["ab", undefined], "text times ab"], + [["ab", true], "text upper AB"], + [[new String("ab"), false], "text upper ab"], + [[1], "number 1"], + [[1, 2], "two numbers 1 2"], + [[1, 2, 3], "two numbers 1 2"], + [[[1]], "list [1.0]"], + [[new TestCounter(6)], "counter 6"], + ]; + for (const [args, expected] of cases) expect(bindgen.overloaded(...args)).toBe(expected); + expect(bindgen.overloaded.length).toBe(1); + }); + + it("a signature ignores the arguments it does not have", () => { + expect(bindgen.overloaded(1, "x")).toBe("number 1"); + expect(bindgen.overloaded([1], 5)).toBe("list [1.0]"); + }); + + it("what fits none is reported by the one that requires the most arguments", () => { + expect(() => bindgen.overloaded()).toThrow(expect.objectContaining({ code: "ERR_MISSING_ARGS" })); + expect(() => bindgen.overloaded({})).toThrow("text must be a string"); + expect(() => bindgen.overloaded(null, 1)).toThrow("text must be a string"); + expect(() => bindgen.overloaded("ab", "x")).toThrow("upper must be a boolean"); + }); + + itAlways("a signature that cannot be chosen is an error in the schema", () => { + expect(() => b.overloads({ args: { a: b.string } }, { args: { b: b.string } })).toThrow( + "overload 2 is never chosen", + ); + expect(() => b.overloads({ args: { a: b.string.loose } }, { args: { b: b.bool } })).toThrow( + "overload 2 is never chosen", + ); + expect(() => b.overloads({ args: { a: b.i32, b: b.bool } }, { args: { a: b.f64, b: b.bool } })).toThrow( + "overload 2 is never chosen", + ); + b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } }); + b.overloads({ args: { a: b.string, b: b.f64 } }, { args: { a: b.string, b: b.bool } }); + b.overloads({ args: { a: b.bool } }, { args: { a: b.any } }); + }); }); describe("arrays", () => { @@ -696,6 +829,11 @@ describe("classes", () => { TestCounter.prototype, Object.create(new TestCounter()), new Bun.Glob("*"), + bindgen.makeToken(1), + new Blob([]), + new URL("http://localhost/"), + new Headers(), + 10n ** 30n, ]) { expect(() => increment.call(receiver)).toThrow(TypeError); expect(() => count.call(receiver)).toThrow(TypeError); From 670c522ab78f2cee5a24e43488feb12b5e0b62fb Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 09:46:17 +0000 Subject: [PATCH 21/44] Hold a union as a struct with a member for each alternative instead of a std::variant --- src/codegen/bindgen-emit.ts | 64 ++++++++++++++++----- src/codegen/bindgen-lib-internal.ts | 8 +-- src/jsc/bindgen/extern_types.rs | 2 +- src/jsc/bindings/Bindgen/ExternTraits.h | 36 +++--------- src/jsc/bindings/Bindgen/ExternUnion.h | 38 ------------ src/jsc/bindings/Bindgen/JSStringUTF8View.h | 3 +- 6 files changed, 67 insertions(+), 84 deletions(-) delete mode 100644 src/jsc/bindings/Bindgen/ExternUnion.h diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index f358a709fc67..7090f526a26e 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -221,6 +221,9 @@ function humanReadableEnum(type: EnumType): string { return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; } +/** Value-initialization zeroes a struct whose constructor is implicit, and these hold the buffers of string views. */ +const cppDefaultConstructor = (name: string) => ` ${name}() {}`; + function cppHeaderForUnion(type: UnionType): string { const { name, alternatives } = type; const idlTypes = alternatives.map(a => a.type.idlType).join(", "); @@ -228,28 +231,62 @@ function cppHeaderForUnion(type: UnionType): string { const cases = choices.map((choice, index) => { if (choice.length === 0) return ""; const attempts = choice.map(i => { - const alternative = alternatives[i].type; - const slot = `result.template emplace<${i}>`; - if (alternative.kinds !== "any" && alternative.refusesSomeOfItsKinds) - return `if (auto converted = ::Bun::tryConvertIDL<${alternative.idlType}>(lexicalGlobalObject, value, ctx)) { - ${slot}(::std::move(*converted)); + const alternative = alternatives[i]; + const member = `result.${alternative.name}`; + if (alternative.type.kinds !== "any" && alternative.type.refusesSomeOfItsKinds) + return `if (auto converted = ::Bun::tryConvertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx)) { + ${member} = ::std::move(*converted); + result.tag = ${i}; return; }`; - const convert = alternative.convertsInPlace - ? alternative.cppConvertInto(`${slot}()`, "value", "ctx", "") - : `${slot}(::Bun::convertIDL<${alternative.idlType}>(lexicalGlobalObject, value, ctx));`; - return `scope.release();\n${convert}\nreturn;`; + if (alternative.type instanceof UnitType) return `result.tag = ${i};\nreturn;`; + const convert = alternative.type.convertsInPlace + ? alternative.type.cppConvertInto(member, "value", "ctx", "") + : `${member} = ::Bun::convertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx);`; + return `result.tag = ${i};\nscope.release();\n${convert}\nreturn;`; }); const refused = attempts.at(-1)!.endsWith("return;") ? "" : "\n break;"; return `case ${index}: {\n${indent(attempts.join("\n"), 4)}${refused}\n}`; }); const canMatchNothing = cases.some(text => text.length === 0 || text.endsWith("break;\n}")); + const held = alternatives.filter(a => !(a.type instanceof UnitType)); return ` namespace Bun { -namespace Bindgen::Generated { -struct IDL${name} : ::Bun::IDLOrderedUnion<${idlTypes}> {}; -using ${name} = IDL${name}::ImplementationType; -${layoutAssert(`ExternTraits<${name}>::ExternType`, type)} +namespace Bindgen { +namespace Generated { +// A member for each alternative, so that the one \`tag\` names is converted where it stays. +struct ${name} { +${cppDefaultConstructor(name)} +${held.map(a => ` ${implementationType(a.type)} ${a.name};`).join("\n")} + ::std::uint8_t tag; +}; +struct Extern${name} { + union { +${alternatives.map(a => ` ${externType(a.type)} ${a.name};`).join("\n")} + } data; + ::std::uint8_t tag; +}; +${layoutAssert(`Extern${name}`, type)} +struct IDL${name} : ::WebCore::IDLType<${name}> {}; +} +template<> struct ExternTraits { + using ExternType = Generated::Extern${name}; + static ExternType convertToExtern(Generated::${name}&& value) + { + ExternType result; + result.tag = value.tag; + switch (value.tag) { +${held + .map( + a => ` case ${alternatives.indexOf(a)}: + result.data.${a.name} = ExternTraits<${implementationType(a.type)}>::convertToExtern(::std::move(value.${a.name})); + break;`, + ) + .join("\n")} + } + return result; + } +}; } template<> struct IDLHumanReadableName : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; } @@ -307,6 +344,7 @@ namespace Bun { namespace Bindgen { namespace Generated { struct ${name} { +${cppDefaultConstructor(name)} ${type.members.map(member => ` ${implementationType(member.type)} ${member.internalName};`).join("\n")} }; struct Extern${name} { diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index ad62e8add6f6..9ceb3b2e8035 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -35,7 +35,7 @@ export function structLayout(fields: readonly Layout[]): Layout { return { size: roundUp(size, align), align }; } -/** `ExternVariant<...>`: `{ union data; uint8_t tag; }`. */ +/** `{ union data; uint8_t tag; }`. */ export function variantLayout(alternatives: readonly Layout[]): Layout { const align = Math.max(...alternatives.map(a => a.align)); const union = { size: roundUp(Math.max(...alternatives.map(a => a.size)), align), align }; @@ -1135,9 +1135,9 @@ export class DictionaryType extends NamedType { if (Object.hasOwn(given, member.name)) field = given[member.name]; else if (member.type instanceof DefaultType) field = member.type.value; else assert(member.type.permitsUndefined, `missing key: ${member.name}`); - return `.${member.internalName} = ${member.type.toCpp(field)}`; + return `value.${member.internalName} = ${member.type.toCpp(field)};`; }); - return `${this.cpp} { ${fields.join(", ")} }`; + return `[] { ${this.cpp} value; ${fields.join(" ")} return value; }()`; } /** Every member can be left out. */ get hasDefault(): boolean { @@ -1215,7 +1215,7 @@ export class UnionType extends NamedType { toCpp(value: unknown): string { const unit = this.alternatives.find(a => a.type instanceof UnitType && a.type.tsType === String(value)); assert(unit, `${this.name}: only null or undefined can be a default`); - return `${this.idlType}::ImplementationType { ${unit.type.toCpp(value)} }`; + return `[] { ${this.cpp} value; value.tag = ${this.alternatives.indexOf(unit)}; return value; }()`; } toRust(value: unknown, scope: RustScope): string { const unit = this.alternatives.find( diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 8ef3f5064632..7076f169b3cf 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -17,7 +17,7 @@ pub fn adopt_string(ptr: ExternString) -> bun_core::String { string } -/// `ExternVariant`, which is what `std::optional` lowers to. +/// What `std::optional` lowers to. #[repr(C)] #[derive(Clone, Copy)] pub struct ExternOptional { diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 023917258cd9..8922595e15db 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -1,7 +1,6 @@ #pragma once #include #include -#include #include #include #include @@ -11,7 +10,6 @@ #include #include #include -#include "ExternUnion.h" #include "JSStringUTF8View.h" namespace Bun::Bindgen { @@ -61,43 +59,27 @@ template<> struct ExternTraits { } }; -template -struct ExternVariant { - ExternUnion data; +template +struct ExternOptional { + union { + ExternNullPtr none; + T some; + } data; std::uint8_t tag; - - static_assert(sizeof...(Args) > 0); - static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); - static_assert(std::is_trivially_copyable_v>); -}; - -template -struct ExternTraits> { - using ExternType = ExternVariant::ExternType...>; - - static ExternType convertToExtern(std::variant&& cppValue) - { - ExternType result; - result.tag = static_cast(cppValue.index()); - [&](std::index_sequence) { - static_cast(((cppValue.index() == indices && (result.data.template set(ExternTraits::convertToExtern(std::move(*std::get_if(&cppValue)))), true)) || ...)); - }(std::index_sequence_for {}); - return result; - } }; template struct ExternTraits> { - using ExternType = ExternVariant::ExternType>; + using ExternType = ExternOptional::ExternType>; static ExternType convertToExtern(std::optional&& cppValue) { ExternType result; result.tag = cppValue.has_value(); if (cppValue) - result.data.template set<1>(ExternTraits::convertToExtern(std::move(*cppValue))); + result.data.some = ExternTraits::convertToExtern(std::move(*cppValue)); else - result.data.template set<0>(ExternNullPtr {}); + result.data.none = ExternNullPtr {}; return result; } }; diff --git a/src/jsc/bindings/Bindgen/ExternUnion.h b/src/jsc/bindings/Bindgen/ExternUnion.h deleted file mode 100644 index 9b8aba8045df..000000000000 --- a/src/jsc/bindings/Bindgen/ExternUnion.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include -#include - -namespace Bun::Bindgen { - -// `union { Args... }`, which is how it is passed to Rust. -template -union ExternUnion; - -template -union ExternUnion { - First first; - - template - void set(First value) - { - static_assert(!index); - first = value; - } -}; - -template -union ExternUnion { - First first; - ExternUnion rest; - - template - void set(auto value) - { - if constexpr (index) - rest.template set(value); - else - first = value; - } -}; - -} diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 18ddb99e02e8..ac1568b40982 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -12,7 +12,8 @@ class JSStringUTF8View { WTF_MAKE_NONCOPYABLE(JSStringUTF8View); public: - JSStringUTF8View() = default; + // Not `= default`: value-initialization would zero the buffer first. + JSStringUTF8View() {} JSStringUTF8View(JSStringUTF8View&&) = default; JSStringUTF8View& operator=(JSStringUTF8View&&) = default; explicit JSStringUTF8View(WTF::ASCIILiteral literal) From 6febce3a9e9274b25b002fffbd2308f45fe39134 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 09:55:28 +0000 Subject: [PATCH 22/44] Remove the rules that the conservative stack scanner is never the cause --- CLAUDE.md | 1 - REVIEW.md | 1 - 2 files changed, 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2ff91567b42..6cf3ce55dad5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -204,7 +204,6 @@ Several situational sections live in `.claude/docs/landing-prs.md` — read the 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. 13. **If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code.**. 14. After every code comment you write, ask yourself, "Is this information the next Claude would spend multiple tool calls trying to understand?". If the answer isn't clearly yes, the code comment is noise - delete it. -15. **CRITICAL**: **The conservative stack scanner is never the cause of the bug you are debugging.** If you land on "it is the conservative stack scanner", you have not found the root cause yet. Keep going until you have the exact one. JSC runs `sanitizeStack` at every interpreter entrance and again before each collection scans the stack. An object that survives GC is held by a real reference (a `WriteBarrier`, a `Strong`, a `protect()`, a missing `deref`, pending activity, a closure). Find it. Another PR, branch, or issue that says the same bug is the stack scanner is not evidence. Those claims are usually wrong. If you still believe it is the stack scanner, rule out every other retainer first, then prove it yourself, with two artifacts in the PR: a heap snapshot from `require("bun:jsc").generateHeapSnapshotForDebugging()`, taken while the object is retained, in which the retained cell has no incoming edge and no `roots` entry (every reported retainer is ruled out; conservative roots are marked through `SlotVisitor::appendJSCellOrAuxiliary`, which reports nothing to the snapshot), and a debugger session that shows the stack word holding that cell's address and the frame that owns it. Without both, the bug is somewhere in the code you are debugging and you have not found it yet. Changing a test so the GC runs from a different stack depth, more times, or after a sleep is blaming the scanner without the proof. **ONLY** push up changes after running `bun bd test ` and ensuring your tests pass. diff --git a/REVIEW.md b/REVIEW.md index 909abbbc5229..6ecfe5a92d49 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -38,7 +38,6 @@ Several situational sections live in `.claude/docs/landing-prs.md` — read the - **Treat every refactor as guilty until proven behavior-preserving.** Diff the old path's complete behavior — out-parameter writes, error-path side effects, condition polarity, defaults, unconditional operations becoming conditional. Test status flags with bitwise-AND, not equality. Audit every hit of bulk find-and-replace. Before deleting odd-looking code, git-blame why it was written — it is usually load-bearing. If neighboring code does something differently than you're about to, find out why. - **One source of truth; update every consumer atomically.** When a fact lives in two places (mirrored tables, encode/decode pairs), derive one from the other. New enum variant or struct field → audit every switch on the discriminant, every constructor/clone site, every hasher/serialization pair. Signature changes and renames → grep the whole repo including cfg-gated code and generated-binding inputs; stale call sites compile fine and silently miss the new behavior. - **Cache keys cover every input that shapes the output** (target OS/arch, config, registry of origin; for pooled connections, establishment-time TLS mode/SNI/credentials). A false hit is far worse than a false miss. Completion markers are written only after the last mutation. Any change to cached/serialized output bumps the format version constant. File-keyed caches include mtime/size or content hash. -- **It's never a conservative stack scanner bug** - JSC runs sanitizeStack at every interpreter entrance site. Never blame the conservative stack scanner. It's always a bug in YOUR code. ## Error handling From ba8ebabe6b4f1f51c0dbc83e240faadf344434a0 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 09:58:59 +0000 Subject: [PATCH 23/44] Restore the conservative stack scanner rules in CLAUDE.md and REVIEW.md --- CLAUDE.md | 1 + REVIEW.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 6cf3ce55dad5..b2ff91567b42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -204,6 +204,7 @@ Several situational sections live in `.claude/docs/landing-prs.md` — read the 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. 13. **If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code.**. 14. After every code comment you write, ask yourself, "Is this information the next Claude would spend multiple tool calls trying to understand?". If the answer isn't clearly yes, the code comment is noise - delete it. +15. **CRITICAL**: **The conservative stack scanner is never the cause of the bug you are debugging.** If you land on "it is the conservative stack scanner", you have not found the root cause yet. Keep going until you have the exact one. JSC runs `sanitizeStack` at every interpreter entrance and again before each collection scans the stack. An object that survives GC is held by a real reference (a `WriteBarrier`, a `Strong`, a `protect()`, a missing `deref`, pending activity, a closure). Find it. Another PR, branch, or issue that says the same bug is the stack scanner is not evidence. Those claims are usually wrong. If you still believe it is the stack scanner, rule out every other retainer first, then prove it yourself, with two artifacts in the PR: a heap snapshot from `require("bun:jsc").generateHeapSnapshotForDebugging()`, taken while the object is retained, in which the retained cell has no incoming edge and no `roots` entry (every reported retainer is ruled out; conservative roots are marked through `SlotVisitor::appendJSCellOrAuxiliary`, which reports nothing to the snapshot), and a debugger session that shows the stack word holding that cell's address and the frame that owns it. Without both, the bug is somewhere in the code you are debugging and you have not found it yet. Changing a test so the GC runs from a different stack depth, more times, or after a sleep is blaming the scanner without the proof. **ONLY** push up changes after running `bun bd test ` and ensuring your tests pass. diff --git a/REVIEW.md b/REVIEW.md index 6ecfe5a92d49..909abbbc5229 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -38,6 +38,7 @@ Several situational sections live in `.claude/docs/landing-prs.md` — read the - **Treat every refactor as guilty until proven behavior-preserving.** Diff the old path's complete behavior — out-parameter writes, error-path side effects, condition polarity, defaults, unconditional operations becoming conditional. Test status flags with bitwise-AND, not equality. Audit every hit of bulk find-and-replace. Before deleting odd-looking code, git-blame why it was written — it is usually load-bearing. If neighboring code does something differently than you're about to, find out why. - **One source of truth; update every consumer atomically.** When a fact lives in two places (mirrored tables, encode/decode pairs), derive one from the other. New enum variant or struct field → audit every switch on the discriminant, every constructor/clone site, every hasher/serialization pair. Signature changes and renames → grep the whole repo including cfg-gated code and generated-binding inputs; stale call sites compile fine and silently miss the new behavior. - **Cache keys cover every input that shapes the output** (target OS/arch, config, registry of origin; for pooled connections, establishment-time TLS mode/SNI/credentials). A false hit is far worse than a false miss. Completion markers are written only after the last mutation. Any change to cached/serialized output bumps the format version constant. File-keyed caches include mtime/size or content hash. +- **It's never a conservative stack scanner bug** - JSC runs sanitizeStack at every interpreter entrance site. Never blame the conservative stack scanner. It's always a bug in YOUR code. ## Error handling From 3137a34f1598a88c318016deb532f3b1aedbc917 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 10:16:15 +0000 Subject: [PATCH 24/44] Inline the string test of both UTF-8 conversions, make a union's large alternatives when chosen, and assign defaults in place --- src/codegen/bindgen-emit.ts | 21 ++++++++------ src/codegen/bindgen-lib-internal.ts | 31 +++++++++++++++++++-- src/jsc/bindings/Bindgen/IDLConvert.h | 19 +++++++++++-- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 6 ++++ 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 7090f526a26e..4d9bc8c10784 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -241,7 +241,7 @@ function cppHeaderForUnion(type: UnionType): string { }`; if (alternative.type instanceof UnitType) return `result.tag = ${i};\nreturn;`; const convert = alternative.type.convertsInPlace - ? alternative.type.cppConvertInto(member, "value", "ctx", "") + ? alternative.type.cppConvertInto(`${member}.emplace()`, "value", "ctx", "") : `${member} = ::Bun::convertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx);`; return `result.tag = ${i};\nscope.release();\n${convert}\nreturn;`; }); @@ -254,10 +254,10 @@ function cppHeaderForUnion(type: UnionType): string { namespace Bun { namespace Bindgen { namespace Generated { -// A member for each alternative, so that the one \`tag\` names is converted where it stays. +// A member for each alternative, so that the one \`tag\` names is converted where it stays. A large one is made when chosen. struct ${name} { ${cppDefaultConstructor(name)} -${held.map(a => ` ${implementationType(a.type)} ${a.name};`).join("\n")} +${held.map(a => ` ${a.type.convertsInPlace ? `::std::optional<${implementationType(a.type)}>` : implementationType(a.type)} ${a.name};`).join("\n")} ::std::uint8_t tag; }; struct Extern${name} { @@ -279,7 +279,7 @@ template<> struct ExternTraits { ${held .map( a => ` case ${alternatives.indexOf(a)}: - result.data.${a.name} = ExternTraits<${implementationType(a.type)}>::convertToExtern(::std::move(value.${a.name})); + result.data.${a.name} = ExternTraits<${implementationType(a.type)}>::convertToExtern(::std::move(${a.type.convertsInPlace ? "*" : ""}value.${a.name})); break;`, ) .join("\n")} @@ -444,7 +444,7 @@ function cppSourceForDictionary(type: DictionaryType): string { if (member.type instanceof DefaultType) { const absent = member.type.onNull ? "isUndefinedOrNull" : "isUndefined"; body = `if (value${i}.${absent}()) { - result.${member.internalName} = ${member.type.toCpp(member.type.value)}; + ${member.type.cppAssign(`result.${member.internalName}`, member.type.value).replaceAll("\n", "\n ")} } else { ${convert.replaceAll("\n", "\n ")} }`; @@ -476,7 +476,7 @@ void Bun::Bindgen::Generated::convert${type.name}(JSC::JSGlobalObject& lexicalGl ${ type.allowNonObject ? ` if (!object || object->isCallable()) { - result = ${type.toCpp({})}; + ${type.cppAssign("result", {}).replaceAll("\n", "\n ")} return; }` : ` if (!object) [[unlikely]] { @@ -540,9 +540,12 @@ function cppDeclareConverted(type: Type, variable: string, value: string, ctx: s if (!absent) return [`${declaration};`, ...into]; return [ `${declaration};`, - `if (${absent})`, - ` ${variable} = ${type.toCpp(type.value)};`, - "else {", + `if (${absent}) {`, + ...type + .cppAssign(variable, type.value) + .split("\n") + .map(line => ` ${line}`), + "} else {", ...into.map(line => ` ${line}`), "}", ]; diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 9ceb3b2e8035..ac2ecb290e20 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -184,6 +184,10 @@ export abstract class Type { /** A C++ expression of `idlType::ImplementationType` for a default value written in the schema. */ abstract toCpp(value: unknown): string; + /** C++ statements that give that value to `target`, which was just default-constructed. */ + cppAssign(target: string, value: unknown): string { + return `${target} = ${this.toCpp(value)};`; + } /** The same value as a Rust expression of `rustType`. */ toRust(value: unknown, scope: RustScope): string { throw new TypeError(`${this.tsType} cannot have a default`); @@ -500,6 +504,10 @@ export class Utf8Type extends Type { assert(typeof value === "string"); return `::Bun::Bindgen::JSStringUTF8View { ${asciiLiteral(value)} }`; } + cppAssign(target: string, value: unknown): string { + assert(typeof value === "string"); + return `${target}.initialize(${asciiLiteral(value)});`; + } toRust(value: unknown): string { return `b${JSON.stringify(value)}`; } @@ -792,6 +800,14 @@ export class OptionalType extends Type { toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); } + cppAssign(target: string, value: unknown): string { + if (!this.convertsInPlace) return super.cppAssign(target, value); + if (value == null) return ""; + return this.payload.cppAssign( + this.payload.nullRepresentation === "optional" ? `${target}.emplace()` : target, + value, + ); + } toRust(value: unknown, scope: RustScope): string { return value == null ? "None" : `Some(${this.payload.toRust(value, scope)})`; } @@ -880,6 +896,9 @@ export class DefaultType extends Type { cppConvertInto(target: string, value: string, ctx: string, name: string): string { return this.payload.cppConvertInto(target, value, ctx, name); } + cppAssign(target: string, value: unknown): string { + return this.payload.cppAssign(target, value); + } layout(): Layout { return this.payload.layout(); } @@ -1123,6 +1142,9 @@ export class DictionaryType extends NamedType { return this.members.flatMap(member => member.names); } toCpp(value: unknown): string { + return `[] { ${this.cpp} value; ${this.cppAssign("value", value).replaceAll("\n", " ")} return value; }()`; + } + cppAssign(target: string, value: unknown): string { assert(typeof value === "object" && value !== null); const given = value as Record; for (const key of Object.keys(given)) @@ -1135,9 +1157,9 @@ export class DictionaryType extends NamedType { if (Object.hasOwn(given, member.name)) field = given[member.name]; else if (member.type instanceof DefaultType) field = member.type.value; else assert(member.type.permitsUndefined, `missing key: ${member.name}`); - return `value.${member.internalName} = ${member.type.toCpp(field)};`; + return member.type.cppAssign(`${target}.${member.internalName}`, field); }); - return `[] { ${this.cpp} value; ${fields.join(" ")} return value; }()`; + return fields.filter(text => text.length > 0).join("\n"); } /** Every member can be left out. */ get hasDefault(): boolean { @@ -1213,9 +1235,12 @@ export class UnionType extends NamedType { return this.alternatives.map(a => a.type); } toCpp(value: unknown): string { + return `[] { ${this.cpp} value; ${this.cppAssign("value", value)} return value; }()`; + } + cppAssign(target: string, value: unknown): string { const unit = this.alternatives.find(a => a.type instanceof UnitType && a.type.tsType === String(value)); assert(unit, `${this.name}: only null or undefined can be a default`); - return `[] { ${this.cpp} value; value.tag = ${this.alternatives.indexOf(unit)}; return value; }()`; + return `${target}.tag = ${this.alternatives.indexOf(unit)};`; } toRust(value: unknown, scope: RustScope): string { const unit = this.alternatives.find( diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 09607204436a..d3c679e6fa6f 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -55,10 +55,17 @@ inline std::optional> unsharedBufferSourceSpan(JSC::JSV template<> struct WebCore::Converter : Bun::DefaultContextConverter { template - static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) + ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) { if (value.isString()) [[likely]] return result.initialize(globalObject, JSC::asString(value)); + convertNonStringInto(globalObject, value, ctx, result); + } + +private: + template + NEVER_INLINE static void convertNonStringInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) + { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); if (!Bun::Bindgen::isStringObject(value)) { @@ -89,7 +96,15 @@ template<> struct WebCore::Converter struct WebCore::Converter : Bun::DefaultContextConverter { template - static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&, Bun::Bindgen::JSStringUTF8View& result) + ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&, Bun::Bindgen::JSStringUTF8View& result) + { + if (value.isString()) [[likely]] + return result.initialize(globalObject, JSC::asString(value)); + convertNonStringInto(globalObject, value, result); + } + +private: + NEVER_INLINE static void convertNonStringInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Bun::Bindgen::JSStringUTF8View& result) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index ac1568b40982..5da4a2acf588 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -36,6 +36,12 @@ class JSStringUTF8View { JSC::throwOutOfMemoryError(&globalObject, scope); } + void initialize(WTF::ASCIILiteral literal) + { + m_view = { nullptr, literal }; + m_isNull = false; + } + bool isNull() const { return m_isNull; } // Null data only when isNull(). From 15c2e6b2e9457e211b37d186c48d68e2c144c94a Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 10:49:06 +0000 Subject: [PATCH 25/44] Inline the fast path of every strict conversion, and give a nested optional dictionary, a b.any member and a union member their defaults --- src/codegen/bindgen-emit.ts | 3 ++- src/codegen/bindgen-lib-internal.ts | 32 +++++++++++++++---------- src/js/internal-for-testing.ts | 1 + src/jsc/bindgen_test.bind.ts | 17 +++++++++++++ src/jsc/bindgen_test.rs | 25 +++++++++++++++++-- src/jsc/bindings/BunIDLConvertBase.h | 21 +++++++++++----- src/jsc/bindings/BunIDLConvertNumbers.h | 16 ++++++++++++- test/internal/bindgen.test.ts | 12 ++++++++++ 8 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 4d9bc8c10784..1898cbbe5526 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -4,6 +4,7 @@ import assert from "node:assert"; import { asciiLiteral, ClassType, + cppEmplace, DefaultType, DictionaryType, EnumType, @@ -241,7 +242,7 @@ function cppHeaderForUnion(type: UnionType): string { }`; if (alternative.type instanceof UnitType) return `result.tag = ${i};\nreturn;`; const convert = alternative.type.convertsInPlace - ? alternative.type.cppConvertInto(`${member}.emplace()`, "value", "ctx", "") + ? cppEmplace(member, target => alternative.type.cppConvertInto(target, "value", "ctx", "")) : `${member} = ::Bun::convertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx);`; return `result.tag = ${i};\nscope.release();\n${convert}\nreturn;`; }); diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index ac2ecb290e20..c9a277ca7e05 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -113,6 +113,12 @@ export function alternativesByKind(alternatives: readonly Type[]): Map string): string { + const target = `${optional.replace(/\W+/g, "_")}_value`; + return `auto& ${target} = ${optional}.emplace();\n${fill(target)}`; +} + export abstract class Type { /** `undefined` means "not provided". */ get optional(): OptionalType { @@ -184,7 +190,7 @@ export abstract class Type { /** A C++ expression of `idlType::ImplementationType` for a default value written in the schema. */ abstract toCpp(value: unknown): string; - /** C++ statements that give that value to `target`, which was just default-constructed. */ + /** C++ statements that give that value to `target`, which names an object that was just default-constructed. */ cppAssign(target: string, value: unknown): string { return `${target} = ${this.toCpp(value)};`; } @@ -200,7 +206,7 @@ export abstract class Type { get convertsInPlace(): boolean { return false; } - /** C++ statements that convert `value` into `target`, which was just default-constructed. */ + /** C++ statements that convert `value` into `target`, which names an object that was just default-constructed. */ cppConvertInto(target: string, value: string, ctx: string, name: string): string { return `${target} = ${this.cppConvert(value, ctx, name)};`; } @@ -681,8 +687,9 @@ export class AnyType extends Type { get permitsUndefined(): boolean { return true; } - toCpp(): string { - throw new TypeError("b.any cannot have a default"); + toCpp(value: unknown): string { + assert(value === undefined, "b.any cannot have a default"); + return "::JSC::jsUndefined()"; } toRust(): string { return "bindgen::JsValue::UNDEFINED"; @@ -794,8 +801,9 @@ export class OptionalType extends Type { nullable: `!${value}.isUndefinedOrNull()`, falsyAsNull: `${value}.toBoolean(globalObject)`, }[this.kind]; - const slot = this.payload.nullRepresentation === "optional" ? `${target}.emplace()` : target; - return `if (${provided}) {\n ${this.payload.cppConvertInto(slot, value, ctx, name).replaceAll("\n", "\n ")}\n}`; + const convert = (slot: string) => this.payload.cppConvertInto(slot, value, ctx, name); + const body = this.payload.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); + return `if (${provided}) {\n ${body.replaceAll("\n", "\n ")}\n}`; } toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); @@ -803,10 +811,10 @@ export class OptionalType extends Type { cppAssign(target: string, value: unknown): string { if (!this.convertsInPlace) return super.cppAssign(target, value); if (value == null) return ""; - return this.payload.cppAssign( - this.payload.nullRepresentation === "optional" ? `${target}.emplace()` : target, - value, - ); + const assign = (slot: string) => this.payload.cppAssign(slot, value); + return this.payload.nullRepresentation === "optional" + ? `{\n ${cppEmplace(target, assign).replaceAll("\n", "\n ")}\n}` + : assign(target); } toRust(value: unknown, scope: RustScope): string { return value == null ? "None" : `Some(${this.payload.toRust(value, scope)})`; @@ -1238,8 +1246,8 @@ export class UnionType extends NamedType { return `[] { ${this.cpp} value; ${this.cppAssign("value", value)} return value; }()`; } cppAssign(target: string, value: unknown): string { - const unit = this.alternatives.find(a => a.type instanceof UnitType && a.type.tsType === String(value)); - assert(unit, `${this.name}: only null or undefined can be a default`); + const unit = value == null && this.alternatives.find(a => a.type instanceof UnitType && takes(a.type, `${value}`)); + assert(unit, `${this.name}: only null or undefined can be a default, and only for an alternative that takes it`); return `${target}.tag = ${this.alternatives.indexOf(unit)};`; } toRust(value: unknown, scope: RustScope): string { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index b853e8940394..bb7570c981aa 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -256,6 +256,7 @@ export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { describeValue: $bindgenFn("bindgen_test.bind.ts", "describeValue"), describeAnything: $bindgenFn("bindgen_test.bind.ts", "describeAnything"), overloaded: $bindgenFn("bindgen_test.bind.ts", "overloaded"), + describeDefaults: $bindgenFn("bindgen_test.bind.ts", "describeDefaults"), sumArray: $bindgenFn("bindgen_test.bind.ts", "sumArray"), range: $bindgenFn("bindgen_test.bind.ts", "range"), words: $bindgenFn("bindgen_test.bind.ts", "words"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 6548cf879ce7..d78083ada2da 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -170,3 +170,20 @@ export const overloaded = b.overloads( rustPath: "crate::bindgen_test::overloaded_counter", }, ); + +export const TestInner = b.dictionary({ + a: b.bool.default(true), + b: b.i32.default(7), + name: b.string.utf8.default("inner"), +}); +export const TestChoice = b.union({ none: b.null, flag: b.bool }); +export const TestOuter = b.dictionary({ + inner: TestInner.nullable, + value: b.any, + choice: TestChoice, +}); +/** What a default written in the schema gives each member, the nested ones included. */ +export const describeDefaults = b.fn({ + args: { options: TestOuter.default({ inner: {} }) }, + returns: b.string.utf8, +}); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index bf9c85e58365..c75f150e1188 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -8,8 +8,9 @@ use crate::bindgen::{ WithValue, }; use crate::generated_bindgen::bindgen_test::{ - TestAnything, TestColor, TestCounterClass, TestCounterInternalFields as _, TestFailure, - TestHolderClass, TestOptions, TestOptionsEcho, TestTokenClass, TestValue, + TestAnything, TestChoice, TestColor, TestCounterClass, TestCounterInternalFields as _, + TestFailure, TestHolderClass, TestOptions, TestOptionsEcho, TestOuter, TestTokenClass, + TestValue, }; // This example should be kept in sync with bindgen's documentation @@ -110,6 +111,26 @@ pub fn describe_anything<'a>(cx: &mut JsCall<'a>, value: TestAnything<'a>) -> Ve } } +pub fn describe_defaults(options: TestOuter<'_>) -> Vec { + let inner = match options.inner { + Some(inner) => { + let name = bstr::BStr::new(inner.name); + format!("a={} b={} name={name}", inner.a, inner.b) + } + None => "none".to_owned(), + }; + let value = if options.value.is_undefined() { + "undefined" + } else { + "given" + }; + let choice = match options.choice { + TestChoice::None => "none".to_owned(), + TestChoice::Flag(flag) => flag.to_string(), + }; + format!("inner: {inner}; value: {value}; choice: {choice}").into_bytes() +} + pub fn overloaded_text_times(text: &[u8], times: u32) -> Vec { [b"text times ", text.repeat(times as usize).as_slice()].concat() } diff --git a/src/jsc/bindings/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index 7d53546df067..5e3721263acf 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -9,7 +9,7 @@ namespace Bun { template -typename WebCore::Converter::ReturnType convertIDL( +ALWAYS_INLINE typename WebCore::Converter::ReturnType convertIDL( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -22,7 +22,7 @@ typename WebCore::Converter::ReturnType convertIDL( } template -std::optional::ReturnType> tryConvertIDL( +ALWAYS_INLINE std::optional::ReturnType> tryConvertIDL( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -51,17 +51,19 @@ template struct DefaultTryConverter : DefaultContextConverter { using typename DefaultContextConverter::ReturnType; + // Inlined so that a value of the right type costs its test and nothing else. template - static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + ALWAYS_INLINE static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); - RETURN_IF_EXCEPTION(scope, {}); - if (result.has_value()) { + if (result.has_value()) [[likely]] { + scope.release(); return std::move(*result); } - WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + RETURN_IF_EXCEPTION(scope, {}); + throwConversionFailed(globalObject, scope, ctx); return ReturnType {}; } @@ -72,6 +74,13 @@ struct DefaultTryConverter : DefaultContextConverter { auto ctx = DefaultConversionContext {}; return WebCore::Converter::tryConvert(globalObject, value, ctx); } + +private: + template + NEVER_INLINE static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + } }; } diff --git a/src/jsc/bindings/BunIDLConvertNumbers.h b/src/jsc/bindings/BunIDLConvertNumbers.h index 9f32f7928fd4..6748bb57e0db 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/BunIDLConvertNumbers.h @@ -43,7 +43,20 @@ struct WebCore::Converter> static constexpr bool conversionHasSideEffects = false; template - static std::optional tryConvert( + ALWAYS_INLINE static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isInt32() && std::in_range(value.asInt32())) [[likely]] { + return static_cast(value.asInt32()); + } + return tryConvertSlow(globalObject, value, ctx); + } + +private: + template + NEVER_INLINE static std::optional tryConvertSlow( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -104,6 +117,7 @@ struct WebCore::Converter> return {}; } +public: template static void throwConversionFailed( JSC::JSGlobalObject& globalObject, diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index cb272b35b5a8..37b05b59a23f 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -237,6 +237,18 @@ describe("dictionaries", () => { expect(Object.keys(seen)).toEqual(["count", "flag", "name", "color", "ratio", "payload", "renamed"]); }); + it("a default written in the schema reaches every member, the nested ones included", () => { + expect(bindgen.describeDefaults()).toBe("inner: a=true b=7 name=inner; value: undefined; choice: none"); + expect(bindgen.describeDefaults(undefined)).toBe("inner: a=true b=7 name=inner; value: undefined; choice: none"); + expect(bindgen.describeDefaults({})).toBe("inner: none; value: undefined; choice: none"); + expect(bindgen.describeDefaults({ inner: {} })).toBe( + "inner: a=true b=7 name=inner; value: undefined; choice: none", + ); + expect(bindgen.describeDefaults({ inner: { a: false, name: "n" }, value: null, choice: true })).toBe( + "inner: a=false b=7 name=n; value: given; choice: true", + ); + }); + it("returns ordinary objects", () => { const seen = bindgen.optionsEcho({ name: "a", renamed: 1 }); expect(Object.getPrototypeOf(seen)).toBe(Object.prototype); From ab0568693afc9d349742ce3750efa9b260280c59 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 10:53:40 +0000 Subject: [PATCH 26/44] Always assign a nullable member that does not convert in place --- src/codegen/bindgen-lib-internal.ts | 3 ++- src/jsc/bindgen_test.bind.ts | 1 + src/jsc/bindgen_test.rs | 3 ++- test/internal/bindgen.test.ts | 31 ++++++++++++++++++++--------- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index c9a277ca7e05..72c284f0960c 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -794,8 +794,9 @@ export class OptionalType extends Type { get convertsInPlace(): boolean { return this.payload.convertsInPlace; } - /** A default-constructed `target` is the null value. */ + /** What converts in place is null when default-constructed. Not everything else is: a pointer to a class instance. */ cppConvertInto(target: string, value: string, ctx: string, name: string): string { + if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name); const provided = { optional: `!${value}.isUndefined()`, nullable: `!${value}.isUndefinedOrNull()`, diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index d78083ada2da..bdddf60670ed 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -181,6 +181,7 @@ export const TestOuter = b.dictionary({ inner: TestInner.nullable, value: b.any, choice: TestChoice, + counter: TestCounter.nullable, }); /** What a default written in the schema gives each member, the nested ones included. */ export const describeDefaults = b.fn({ diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index c75f150e1188..f990dc3c2f68 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -128,7 +128,8 @@ pub fn describe_defaults(options: TestOuter<'_>) -> Vec { TestChoice::None => "none".to_owned(), TestChoice::Flag(flag) => flag.to_string(), }; - format!("inner: {inner}; value: {value}; choice: {choice}").into_bytes() + let counter = options.counter.is_some(); + format!("inner: {inner}; value: {value}; choice: {choice}; counter: {counter}").into_bytes() } pub fn overloaded_text_times(text: &[u8], times: u32) -> Vec { diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 37b05b59a23f..a5489c5c1e41 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -238,15 +238,28 @@ describe("dictionaries", () => { }); it("a default written in the schema reaches every member, the nested ones included", () => { - expect(bindgen.describeDefaults()).toBe("inner: a=true b=7 name=inner; value: undefined; choice: none"); - expect(bindgen.describeDefaults(undefined)).toBe("inner: a=true b=7 name=inner; value: undefined; choice: none"); - expect(bindgen.describeDefaults({})).toBe("inner: none; value: undefined; choice: none"); - expect(bindgen.describeDefaults({ inner: {} })).toBe( - "inner: a=true b=7 name=inner; value: undefined; choice: none", - ); - expect(bindgen.describeDefaults({ inner: { a: false, name: "n" }, value: null, choice: true })).toBe( - "inner: a=false b=7 name=n; value: given; choice: true", - ); + const defaults = "inner: a=true b=7 name=inner; value: undefined; choice: none; counter: false"; + expect(bindgen.describeDefaults()).toBe(defaults); + expect(bindgen.describeDefaults(undefined)).toBe(defaults); + expect(bindgen.describeDefaults({ inner: {} })).toBe(defaults); + expect(bindgen.describeDefaults({})).toBe("inner: none; value: undefined; choice: none; counter: false"); + expect( + bindgen.describeDefaults({ + inner: { a: false, name: "n" }, + value: null, + choice: true, + counter: new (bindgen as any).TestCounter(), + }), + ).toBe("inner: a=false b=7 name=n; value: given; choice: true; counter: true"); + }); + + it("a member that is absent is absent, whatever was on the stack before", () => { + const counter = new (bindgen as any).TestCounter(); + for (let i = 0; i < 100; i++) { + expect(bindgen.describeDefaults({ counter })).toEndWith("counter: true"); + expect(bindgen.describeDefaults({})).toEndWith("counter: false"); + expect(bindgen.describeDefaults({ counter: null })).toEndWith("counter: false"); + } }); it("returns ordinary objects", () => { From da328dbcd381bfc4d747013d9bc589ae946d1b46 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 10:57:43 +0000 Subject: [PATCH 27/44] Delete what the generic union converter left behind, make the UTF-8 view immovable, and emit by-value converters only for what an array can hold --- src/codegen/bindgen-emit.ts | 39 ++++++---- src/codegen/bindgen-lib-internal.ts | 36 ++------- .../{BindgenIdentifiers.cpp => Bindgen.cpp} | 2 +- src/jsc/bindings/Bindgen.h | 11 --- .../bindings/Bindgen/DictionaryMemberReader.h | 4 +- src/jsc/bindings/Bindgen/ExternTraits.h | 24 ------ src/jsc/bindings/Bindgen/ExternVectorTraits.h | 48 ++---------- src/jsc/bindings/Bindgen/IDLConvert.h | 75 ++++++------------- src/jsc/bindings/Bindgen/IDLTypes.h | 8 +- src/jsc/bindings/Bindgen/JSBindgenObject.h | 5 +- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 12 +-- src/jsc/bindings/BindgenCustomEnforceRange.h | 47 ++++-------- src/jsc/bindings/BunIDLConvert.h | 68 +---------------- src/jsc/bindings/BunIDLConvertBase.h | 25 +------ src/jsc/bindings/BunIDLConvertContext.h | 28 ------- src/jsc/bindings/BunIDLConvertNumbers.h | 59 +++------------ src/jsc/bindings/BunIDLHumanReadable.h | 5 -- src/jsc/bindings/BunIDLTypes.h | 19 +---- src/jsc/bindings/JSBindgenObject.cpp | 2 +- .../bindings/webcore/JSDOMConvertDictionary.h | 10 --- .../webcore/JSDOMConvertEnumeration.h | 13 ---- .../bindings/webcore/JSDOMConvertNullable.h | 11 --- .../bindings/webcore/JSDOMConvertOptional.h | 15 ---- 23 files changed, 100 insertions(+), 466 deletions(-) rename src/jsc/bindings/{BindgenIdentifiers.cpp => Bindgen.cpp} (100%) delete mode 100644 src/jsc/bindings/Bindgen.h diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 1898cbbe5526..c5c780402296 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -150,7 +150,7 @@ const valueTests: Partial string>> = { null: value => `${value}.isNull()`, boolean: value => `${value}.isBoolean()`, number: value => `${value}.isNumber()`, - bigint: value => `::Bun::Bindgen::isBigInt32(${value})`, + bigint: value => `${value}.isBigInt32()`, }; /** True of `cell`, whose type byte is `type`. */ const cellTests: Partial> = { @@ -184,7 +184,7 @@ function cppChooseByKind(value: string, byKind: ReadonlyMap(cell)`); + add(ofClass, index, `cell->inherits<${kind.cpp}>()`); } else { add(ofValue, index, valueTests[kind]?.(value)); add(ofType, index, cellTests[kind]); @@ -291,7 +291,10 @@ ${held } template<> struct IDLHumanReadableName : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; } -template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextConverter<${type.idlType}> { +template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextConverter<${type.idlType}> {${ + type.needsLifetime + ? "" + : ` template<::Bun::IDLConversionContext Ctx> static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) { @@ -299,7 +302,8 @@ template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextCon convertInto(globalObject, value, ctx, result); return result; } - +` + } template<::Bun::IDLConversionContext Ctx> static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) { @@ -374,10 +378,10 @@ template<> struct IDLHumanReadableName<::WebCore::IDLDictionary struct WebCore::IDLDictionary<${type.cpp}> : ::Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};\n` - : "" -}template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue); + type.needsLifetime + ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : ::Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` + : `template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue);` +} `; } @@ -388,7 +392,7 @@ export function cppHeader(file: SchemaFile): string { headers.delete(`Generated${file.origin.namespace}.h`); return `${banner(file.origin)}#pragma once #include "root.h" -#include "Bindgen.h" +#include "Bindgen/ExternTraits.h" #include "Bindgen/IDLConvert.h" #include "JSDOMConvertDictionary.h" #include "JSDOMConvertEnumeration.h" @@ -498,16 +502,20 @@ ${ } ::Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); ${members.join("\n")} } - +${ + type.needsLifetime + ? "" + : ` template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { ${type.cpp} result; ::Bun::Bindgen::Generated::convert${type.name}(globalObject, value, result); return result; } -${ - type.fromJS - ? ` +` +}${ + type.fromJS + ? ` extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value, ::Bun::Bindgen::Generated::Extern${type.name}* result) { auto& vm = JSC::getVM(globalObject); @@ -519,8 +527,8 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb return true; } ` - : "" -}`; + : "" + }`; } const overloadSuffix = (fn: FunctionDefinition, signature: Signature) => @@ -773,7 +781,6 @@ ${privateMethods : "" } const ::Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { - .className = ${asciiLiteral(name)}, .info = ${type.cpp}::info(), .createStructure = ${type.cpp}::createStructure, .prototypeTableValues = ${span(`JS${name}PrototypeTableValues`, prototypeRows)}, diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 72c284f0960c..827fd90c4e1f 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -189,7 +189,9 @@ export abstract class Type { } /** A C++ expression of `idlType::ImplementationType` for a default value written in the schema. */ - abstract toCpp(value: unknown): string; + toCpp(value: unknown): string { + throw new TypeError(`${this.tsType} cannot have a default`); + } /** C++ statements that give that value to `target`, which names an object that was just default-constructed. */ cppAssign(target: string, value: unknown): string { return `${target} = ${this.toCpp(value)};`; @@ -380,7 +382,7 @@ export class UnitType extends Type { constructor(tsType: "null" | "undefined") { super(); this.tsType = tsType; - this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : "::Bun::IDLStrictUndefined"; + this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : ""; } get kinds(): readonly Kind[] { return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; @@ -388,9 +390,6 @@ export class UnitType extends Type { get permitsUndefined(): boolean { return true; } - toCpp(): string { - return this.tsType === "null" ? "nullptr" : "::std::monostate {}"; - } layout(): Layout { return { size: 1, align: 1 }; } @@ -506,10 +505,6 @@ export class Utf8Type extends Type { cppConvertInto(target: string, value: string, ctx: string): string { return `::WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; } - toCpp(value: unknown): string { - assert(typeof value === "string"); - return `::Bun::Bindgen::JSStringUTF8View { ${asciiLiteral(value)} }`; - } cppAssign(target: string, value: unknown): string { assert(typeof value === "string"); return `${target}.initialize(${asciiLiteral(value)});`; @@ -561,9 +556,6 @@ export class BytesType extends Type { get nullRepresentation() { return "pointer" as const; } - toCpp(): string { - throw new TypeError("b.bufferSource.bytes cannot have a default"); - } layout(): Layout { return { size: 16, align: 8 }; } @@ -602,9 +594,6 @@ export class BufferSourceType extends Type { get holdsJSValue(): boolean { return true; } - toCpp(): string { - throw new TypeError("b.bufferSource cannot have a default"); - } layout(): Layout { return { size: 8, align: 8 }; } @@ -655,9 +644,6 @@ export class WithValueType extends Type { cppConvertInto(target: string, value: string, ctx: string, name: string): string { return this.inner.cppConvertInto(target, value, ctx, name); } - toCpp(): string { - throw new TypeError(".withValue cannot be combined with .default()"); - } cppConvert(value: string, ctx: string, name: string): string { return this.inner.cppConvert(value, ctx, name); } @@ -738,9 +724,6 @@ export class RefType extends Type { getHeaders(out: Set): void { if (this.header) out.add(this.header); } - toCpp(): string { - throw new TypeError(`${this.tsType} cannot have a default`); - } layout(): Layout { return { size: 8, align: 8 }; } @@ -871,7 +854,7 @@ export class DefaultType extends Type { this.onNull = false; } this.value = value; - this.payload.toCpp(value); + this.payload.cppAssign("", value); } get idlType(): string { return this.payload.idlType; @@ -1150,9 +1133,6 @@ export class DictionaryType extends NamedType { get names(): string[] { return this.members.flatMap(member => member.names); } - toCpp(value: unknown): string { - return `[] { ${this.cpp} value; ${this.cppAssign("value", value).replaceAll("\n", " ")} return value; }()`; - } cppAssign(target: string, value: unknown): string { assert(typeof value === "object" && value !== null); const given = value as Record; @@ -1243,9 +1223,6 @@ export class UnionType extends NamedType { get dependencies(): readonly Type[] { return this.alternatives.map(a => a.type); } - toCpp(value: unknown): string { - return `[] { ${this.cpp} value; ${this.cppAssign("value", value)} return value; }()`; - } cppAssign(target: string, value: unknown): string { const unit = value == null && this.alternatives.find(a => a.type instanceof UnitType && takes(a.type, `${value}`)); assert(unit, `${this.name}: only null or undefined can be a default, and only for an alternative that takes it`); @@ -1573,9 +1550,6 @@ export class ClassType extends NamedType { ...this.getters.map(getter => getter.type), ]; } - toCpp(): string { - throw new TypeError(`${this.name} cannot have a default`); - } layout(): Layout { return { size: 8, align: 8 }; } diff --git a/src/jsc/bindings/BindgenIdentifiers.cpp b/src/jsc/bindings/Bindgen.cpp similarity index 100% rename from src/jsc/bindings/BindgenIdentifiers.cpp rename to src/jsc/bindings/Bindgen.cpp index 35e18656ed20..cdde4c489ea8 100644 --- a/src/jsc/bindings/BindgenIdentifiers.cpp +++ b/src/jsc/bindings/Bindgen.cpp @@ -2,8 +2,8 @@ #include "Bindgen/DictionaryMemberReader.h" #include "Bindgen/Identifiers.h" #include "Bindgen/JSStringUTF8View.h" -#include "GeneratedBindings+Tables.h" #include "BunClientData.h" +#include "GeneratedBindings+Tables.h" #include "ObjectBindings.h" #include "ZigGlobalObject.h" #include diff --git a/src/jsc/bindings/Bindgen.h b/src/jsc/bindings/Bindgen.h deleted file mode 100644 index e1bcb166afd4..000000000000 --- a/src/jsc/bindings/Bindgen.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include "Bindgen/ExternTraits.h" -#include "Bindgen/IDLTypes.h" -#include "Bindgen/IDLConvertBase.h" diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 8eacdbacf3e3..0f54093de4a0 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -16,11 +16,11 @@ class DictionaryMemberReader { { } - // May throw. Out of line: inlined, it is most of the size of every generated convertDictionary. + // May throw. Out of line: inlined, it is most of the size of every generated dictionary converter. JSC::JSValue get(unsigned index); private: - // The lookup below stops before Object.prototype. Asked per member: converting one can run JavaScript. + // `get` stops before Object.prototype. Asked per member: converting one can run JavaScript. ALWAYS_INLINE bool ownPropertiesAreTheWholeAnswer() const { if (m_object->type() != JSC::FinalObjectType) diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 8922595e15db..5e89789066ff 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -5,10 +5,8 @@ #include #include #include -#include #include #include -#include #include #include "JSStringUTF8View.h" @@ -28,15 +26,11 @@ struct TrivialExtern { }; template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; enum ExternNullPtr : std::uint8_t {}; @@ -50,15 +44,6 @@ template<> struct ExternTraits { } }; -template<> struct ExternTraits { - using ExternType = ExternNullPtr; - - static ExternType convertToExtern(std::monostate cppValue) - { - return ExternType { 0 }; - } -}; - template struct ExternOptional { union { @@ -133,15 +118,6 @@ struct ExternWithValue { JSC::EncodedJSValue value; }; -template struct ExternTraits> { - using ExternType = T*; - - static ExternType convertToExtern(WTF::Ref&& cppValue) - { - return &cppValue.leakRef(); - } -}; - template struct ExternTraits> { using ExternType = T*; diff --git a/src/jsc/bindings/Bindgen/ExternVectorTraits.h b/src/jsc/bindings/Bindgen/ExternVectorTraits.h index ad609c7781d5..7bbe9f632365 100644 --- a/src/jsc/bindings/Bindgen/ExternVectorTraits.h +++ b/src/jsc/bindings/Bindgen/ExternVectorTraits.h @@ -68,8 +68,8 @@ struct ExternTraits(length), .capacity = static_cast(capacity), }; - } else if constexpr (sizeof(ExternElement) <= sizeof(T) - && alignof(ExternElement) <= MimallocMalloc::maxAlign) { + } else { + static_assert(sizeof(ExternElement) <= sizeof(T) && alignof(ExternElement) <= MimallocMalloc::maxAlign); // We can reuse the allocation, but we still need to convert the elements. alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; @@ -77,7 +77,6 @@ struct ExternTraitssize(); const std::size_t capacity = vec->capacity(); - const std::size_t allocSize = capacity * sizeof(T); T* const buffer = vec->mutableSpan().data(); Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); @@ -91,57 +90,24 @@ struct ExternTraits); - storage = static_cast( - MimallocMalloc::realloc(storage, newCapacity * sizeof(ExternElement))); - } - } - #if __cpp_lib_start_lifetime_as >= 202207L - ExternElement* data = std::start_lifetime_as_array(storage, newCapacity); + ExternElement* data = std::start_lifetime_as_array(storage, length); #else - // We need to start the lifetime of an object of type "array of `capacity` + // We need to start the lifetime of an object of type "array of `length` // `ExternElement`" without invalidating the object representation. Without // `std::start_lifetime_as_array`, one way to do this is to use a no-op `memmove`, // which implicitly creates objects, plus `std::launder` to obtain a pointer to // the created object. - std::memmove(storage, storage, newAllocSize); + std::memmove(storage, storage, length * sizeof(ExternElement)); ExternElement* data = std::launder(reinterpret_cast(storage)); #endif return ExternType { .data = data, .length = static_cast(length), - .capacity = static_cast(newCapacity), - }; - } - - const std::size_t length = cppValue.size(); - const std::size_t newAllocSize = sizeof(ExternElement) * length; - ExternElement* memory = reinterpret_cast( - alignof(ExternElement) > MimallocMalloc::maxAlign - ? MimallocMalloc::alignedMalloc(newAllocSize, alignof(ExternElement)) - : MimallocMalloc::malloc(newAllocSize)); - for (std::size_t i = 0; i < length; ++i) { - new (memory + i) ExternElement { - ExternTraits::convertToExtern(std::move(cppValue[i])), + // Rust converts these elements into a Vec of its own and frees this buffer without a size. + .capacity = static_cast(length), }; } - return ExternType { - .data = memory, - .length = static_cast(length), - .capacity = static_cast(length), - }; } }; diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index d3c679e6fa6f..d158c153d570 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "IDLTypes.h" #include "IDLConvertBase.h" @@ -18,20 +19,10 @@ template<> struct IDLHumanReadableName : IDLHumanRe namespace Bindgen { -inline bool isStringObject(JSC::JSValue value) +template +void throwNotUnsharedBufferSource(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { - return value.isCell() && (value.asCell()->type() == JSC::StringObjectType || value.asCell()->type() == JSC::DerivedStringObjectType); -} - -// A BigInt that is not a cell. -inline bool isBigInt32(JSC::JSValue value) -{ -#if USE(BIGINT32) - return value.isBigInt32(); -#else - UNUSED_PARAM(value); - return false; -#endif + ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView that is not shared"_s); } // std::nullopt for anything else, a shared buffer included. A detached or out-of-bounds view is empty. @@ -53,36 +44,44 @@ inline std::optional> unsharedBufferSourceSpan(JSC::JSV } } -template<> struct WebCore::Converter : Bun::DefaultContextConverter { - template - ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) +namespace Bun::Bindgen::Detail { +template +struct UTF8StringConverter : DefaultContextConverter { + template + ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, JSStringUTF8View& result) { if (value.isString()) [[likely]] return result.initialize(globalObject, JSC::asString(value)); - convertNonStringInto(globalObject, value, ctx, result); + convertIntoSlow(globalObject, value, ctx, result); } private: - template - NEVER_INLINE static void convertNonStringInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Bun::Bindgen::JSStringUTF8View& result) + template + NEVER_INLINE static void convertIntoSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, JSStringUTF8View& result) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if (!Bun::Bindgen::isStringObject(value)) { - ctx.throwNotString(globalObject, scope); - return; + if constexpr (std::same_as) { + if (!value.inherits()) { + ctx.throwNotString(globalObject, scope); + return; + } } JSC::JSString* string = value.toString(&globalObject); RETURN_IF_EXCEPTION(scope, void()); RELEASE_AND_RETURN(scope, result.initialize(globalObject, string)); } }; +} + +template<> struct WebCore::Converter : Bun::Bindgen::Detail::UTF8StringConverter {}; +template<> struct WebCore::Converter : Bun::Bindgen::Detail::UTF8StringConverter {}; template<> struct WebCore::Converter : Bun::DefaultTryConverter { template static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) { - if (value.isString() || Bun::Bindgen::isStringObject(value)) [[likely]] + if (value.isString() || value.inherits()) [[likely]] return value.toWTFString(&globalObject); return std::nullopt; } @@ -94,29 +93,7 @@ template<> struct WebCore::Converter struct WebCore::Converter : Bun::DefaultContextConverter { - template - ALWAYS_INLINE static void convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&, Bun::Bindgen::JSStringUTF8View& result) - { - if (value.isString()) [[likely]] - return result.initialize(globalObject, JSC::asString(value)); - convertNonStringInto(globalObject, value, result); - } - -private: - NEVER_INLINE static void convertNonStringInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Bun::Bindgen::JSStringUTF8View& result) - { - auto& vm = JSC::getVM(&globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - JSC::JSString* string = value.toString(&globalObject); - RETURN_IF_EXCEPTION(scope, void()); - RELEASE_AND_RETURN(scope, result.initialize(globalObject, string)); - } -}; - template<> struct WebCore::Converter : Bun::DefaultTryConverter { - static constexpr bool conversionHasSideEffects = false; - template static std::optional> tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) { @@ -130,13 +107,11 @@ template<> struct WebCore::Converter : Bun:: template static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { - ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView that is not shared"_s); + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); } }; template<> struct WebCore::Converter : Bun::DefaultTryConverter { - static constexpr bool conversionHasSideEffects = false; - template static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) { @@ -148,14 +123,12 @@ template<> struct WebCore::Converter : Bun::De template static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { - ctx.throwTypeMustBe(globalObject, scope, "an ArrayBuffer or ArrayBufferView that is not shared"_s); + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); } }; template struct WebCore::Converter> : Bun::DefaultTryConverter> { - static constexpr bool conversionHasSideEffects = false; - template static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) { diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 9319a5da0e12..91455230b36b 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -13,14 +13,12 @@ struct IDLStackOnlyDictionary : WebCore::IDLType { using NullableParameterType = const T&; }; -// A string or a `String` object, which is what `JSValue::is_string` in Rust has always meant. +// A string or a `String` object, which is what `JSValue::is_string` in Rust accepts. struct IDLStrictUTF8String : WebCore::IDLType { using SequenceStorageType = void; using NullableType = JSStringUTF8View; using NullableInnerParameterType = JSStringUTF8View; static JSStringUTF8View nullValue() { return {}; } - static bool isNullValue(const JSStringUTF8View& value) { return value.isNull(); } - template static U&& extractValueFromNullable(U&& value) { return std::forward(value); } }; // `ToString`. struct IDLLooseUTF8String : IDLStrictUTF8String {}; @@ -33,8 +31,6 @@ struct IDLBufferSourceBytes : WebCore::IDLType> { using NullableType = std::span; using NullableInnerParameterType = std::span; static NullableType nullValue() { return {}; } - static bool isNullValue(NullableType value) { return !value.data(); } - static ImplementationType extractValueFromNullable(NullableType value) { return value; } }; // The same values, as the `JSValue` itself. See Bun::IDLRawAny. @@ -49,8 +45,6 @@ struct IDLRawInterface : WebCore::IDLType { using NullableType = JSClass*; using NullableInnerParameterType = JSClass*; static JSClass* nullValue() { return nullptr; } - static bool isNullValue(JSClass* value) { return !value; } - static JSClass* extractValueFromNullable(JSClass* value) { return value; } }; } diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h index ff391fd1e977..2c194b04bd9e 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -14,7 +14,7 @@ namespace Bun::Bindgen { template class JSBindgenObject; } -// Reads the cell and nothing else: one of JSDOMWrapperType is a JSDestructibleObject, which holds its ClassInfo. +// A cell whose type is JSDOMWrapperType is a JSDestructibleObject, so its ClassInfo is in the cell: no Structure load. template struct JSC::JSCastingHelpers::InheritsTraits> { static constexpr std::optional typeRange { std::nullopt }; @@ -22,7 +22,7 @@ struct JSC::JSCastingHelpers::InheritsTraitstype() == static_cast(WebCore::JSDOMWrapperType) - && static_cast(static_cast(from))->classInfo() == Bun::Bindgen::JSBindgenObject::info(); + && uncheckedDowncast(from)->classInfo() == Bun::Bindgen::JSBindgenObject::info(); } }; @@ -30,7 +30,6 @@ namespace Bun::Bindgen { // One per class, defined by generated code. struct ClassDefinition { - WTF::ASCIILiteral className; const JSC::ClassInfo* info; JSC::Structure* (*createStructure)(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); std::span prototypeTableValues; diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 5da4a2acf588..7def3cf2e7a7 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -10,21 +10,16 @@ namespace Bun::Bindgen { // Bun::UTF8View for an argument of a host call. Lives in the host function's frame. class JSStringUTF8View { WTF_MAKE_NONCOPYABLE(JSStringUTF8View); + WTF_MAKE_NONMOVABLE(JSStringUTF8View); public: // Not `= default`: value-initialization would zero the buffer first. JSStringUTF8View() {} - JSStringUTF8View(JSStringUTF8View&&) = default; - JSStringUTF8View& operator=(JSStringUTF8View&&) = default; - explicit JSStringUTF8View(WTF::ASCIILiteral literal) - : m_view(nullptr, literal) - , m_isNull(false) - { - } - // Of a view that is null. It is large, so it is filled in where it lives. Resolving a rope and converting can throw. + // It is large, so it is filled in where it lives. Resolving a rope and converting can throw. void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { + ASSERT(m_isNull); auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); m_view = string->view(&globalObject); @@ -38,6 +33,7 @@ class JSStringUTF8View { void initialize(WTF::ASCIILiteral literal) { + ASSERT(m_isNull); m_view = { nullptr, literal }; m_isNull = false; } diff --git a/src/jsc/bindings/BindgenCustomEnforceRange.h b/src/jsc/bindings/BindgenCustomEnforceRange.h index a97f5aeda7e2..7d8dd48cff30 100644 --- a/src/jsc/bindings/BindgenCustomEnforceRange.h +++ b/src/jsc/bindings/BindgenCustomEnforceRange.h @@ -11,13 +11,9 @@ enum class BindgenCustomEnforceRangeKind { Web, }; -// This type implements conversion for: -// - t.*.validateInteger() -// - t.*.enforceRange(a, b) when A, B is not the integer's ABI size. -// - t.i32.validateInt32() -// - t.u32.validateUInt32() +// `.validateInteger(min, max)` and `.enforceRange(min, max)`. template< - typename NumericType, + std::integral NumericType, NumericType Min, NumericType Max, BindgenCustomEnforceRangeKind Kind> @@ -34,7 +30,7 @@ static String rangeErrorString(double value, double min, double max) namespace WebCore { template< - typename NumericType, + std::integral NumericType, NumericType Min, NumericType Max, Bun::BindgenCustomEnforceRangeKind Kind> @@ -58,38 +54,23 @@ struct Converter> } unrestricted = value.asNumber(); - // Node also validates that integer types are integers - if constexpr (std::is_integral_v) { - if (unrestricted != std::round(unrestricted)) { - // ERR_OUT_OF_RANGE "an integer" - Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), "an integer"_s, value); - return 0; - } - } else { - // When a range is specified (what this template is implementing), - // Node also throws on NaN being out of range - if (std::isnan(unrestricted)) { - // ERR_OUT_OF_RANGE `>= ${min} && <= ${max}` - Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), Min, Max, value); - return 0; - } + if (unrestricted != std::round(unrestricted)) { + // ERR_OUT_OF_RANGE "an integer" + Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), "an integer"_s, value); + return 0; } } else { - // WebIDL uses toNumber before applying range restrictions. This - // allows something like `true` to pass for `t.f64.enforceRange(-10, 10)`, - // but this behavior does not appear Node's validators. + // WebIDL uses toNumber before applying range restrictions, so `true` passes. Node's validators refuse it. unrestricted = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - if constexpr (std::is_integral_v) { - if (std::isnan(unrestricted) || std::isinf(unrestricted)) { - throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); - return 0; - } - - // IDL uses trunc to convert the double to an integer. - unrestricted = trunc(unrestricted); + if (std::isnan(unrestricted) || std::isinf(unrestricted)) { + throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); + return 0; } + + // IDL uses trunc to convert the double to an integer. + unrestricted = trunc(unrestricted); } bool inRange = unrestricted >= Min && unrestricted <= Max; diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 3469e14b6906..48f4ff181411 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -5,7 +5,6 @@ #include "BunIDLHumanReadable.h" #include "JSDOMConvert.h" #include -#include #include template<> struct WebCore::Converter : WebCore::DefaultConverter { @@ -15,77 +14,12 @@ template<> struct WebCore::Converter : WebCore::DefaultConverter } }; -template<> struct WebCore::Converter - : Bun::DefaultTryConverter { - - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isUndefinedOrNull()) { - return nullptr; - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotNull(globalObject, scope); - } -}; - -template<> struct WebCore::Converter - : Bun::DefaultTryConverter { - - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isUndefined()) { - return std::monostate {}; - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotUndefined(globalObject, scope); - } -}; - template struct WebCore::Converter> - : Bun::DefaultTryConverter> { + : Bun::DefaultContextConverter> { using ReturnType = WebCore::Converter>::ReturnType; - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (!value.toBoolean(&globalObject)) - return IDL::nullValue(); - return Bun::tryConvertIDL(globalObject, value, ctx); - } - template static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) { diff --git a/src/jsc/bindings/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index 5e3721263acf..8ccff9dc3078 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -3,7 +3,6 @@ #include "JSDOMConvertBase.h" #include #include -#include #include namespace Bun { @@ -27,11 +26,7 @@ ALWAYS_INLINE std::optional::ReturnType> tryConve JSC::JSValue value, Ctx& ctx) { - if constexpr (WebCore::Converter::takesContext) { - return WebCore::Converter::tryConvert(globalObject, value, ctx); - } else { - return WebCore::Converter::tryConvert(globalObject, value); - } + return WebCore::Converter::tryConvert(globalObject, value, ctx); } template @@ -39,12 +34,6 @@ struct DefaultContextConverter : WebCore::DefaultConverter { using typename WebCore::DefaultConverter::ReturnType; static constexpr bool takesContext = true; - - static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) - { - auto ctx = DefaultConversionContext {}; - return WebCore::Converter::convert(globalObject, value, ctx); - } }; template @@ -63,21 +52,13 @@ struct DefaultTryConverter : DefaultContextConverter { return std::move(*result); } RETURN_IF_EXCEPTION(scope, {}); - throwConversionFailed(globalObject, scope, ctx); + throwConversionFailedSlow(globalObject, scope, ctx); return ReturnType {}; } - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value) - { - auto ctx = DefaultConversionContext {}; - return WebCore::Converter::tryConvert(globalObject, value, ctx); - } - private: template - NEVER_INLINE static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + NEVER_INLINE static void throwConversionFailedSlow(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); } diff --git a/src/jsc/bindings/BunIDLConvertContext.h b/src/jsc/bindings/BunIDLConvertContext.h index 5dde10a73045..583f560f2fda 100644 --- a/src/jsc/bindings/BunIDLConvertContext.h +++ b/src/jsc/bindings/BunIDLConvertContext.h @@ -24,10 +24,6 @@ struct IDLUnionForDiagnostic { using Type = IDLOrderedUnion; }; -template -struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; -}; } template @@ -37,14 +33,6 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { derived().throwTypeErrorWithPredicate(global, scope, "is required"_s); } - void throwNumberNotFinite(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) - { - derived().throwRangeErrorWithPredicate( - global, - scope, - WTF::makeString("must be finite (received "_s, value, ')')); - } - void throwNumberNotInteger(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) { derived().throwRangeErrorWithPredicate( @@ -112,16 +100,6 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { derived().throwTypeMustBe(global, scope, "an object"_s); } - void throwNotNull(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) - { - derived().throwTypeMustBe(global, scope, "null"_s); - } - - void throwNotUndefined(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) - { - derived().throwTypeMustBe(global, scope, "undefined"_s); - } - void throwNotBufferSource(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { derived().throwTypeMustBe(global, scope, "an ArrayBuffer or TypedArray"_s); @@ -147,12 +125,6 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { WTF::makeString("an array of "_s, idlHumanReadableName())); } - template - void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) - { - derived().throwRangeErrorWithPredicate(global, scope, "is not a valid enumeration value"_s); - } - template void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { diff --git a/src/jsc/bindings/BunIDLConvertNumbers.h b/src/jsc/bindings/BunIDLConvertNumbers.h index 6748bb57e0db..2257ff1b1b45 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/BunIDLConvertNumbers.h @@ -2,7 +2,6 @@ #include "BunIDLTypes.h" #include "BunIDLConvertBase.h" #include -#include #include #include #include @@ -54,6 +53,15 @@ struct WebCore::Converter> return tryConvertSlow(globalObject, value, ctx); } + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } + private: template NEVER_INLINE static std::optional tryConvertSlow( @@ -69,11 +77,7 @@ struct WebCore::Converter> auto scope = DECLARE_THROW_SCOPE(vm); if (value.isInt32()) { - auto intValue = value.asInt32(); - if (std::cmp_greater_equal(intValue, minInt) && std::cmp_less_equal(intValue, maxInt)) { - return static_cast(intValue); - } - ctx.throwIntegerOutOfRange(globalObject, scope, intValue, minInt, maxInt); + ctx.throwIntegerOutOfRange(globalObject, scope, value.asInt32(), minInt, maxInt); return {}; } @@ -116,16 +120,6 @@ struct WebCore::Converter> ctx.throwIntegerOutOfRange(globalObject, scope, intVal, minInt, maxInt); return {}; } - -public: - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotNumber(globalObject, scope); - } }; template<> @@ -154,39 +148,6 @@ struct WebCore::Converter : Bun::DefaultTryConverter -struct WebCore::Converter : Bun::DefaultTryConverter { - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - auto& vm = JSC::getVM(&globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - if (!value.isNumber()) { - return std::nullopt; - } - double number = value.asNumber(); - if (std::isnan(number) || std::isinf(number)) { - ctx.throwNumberNotFinite(globalObject, scope, number); - return std::nullopt; - } - return number; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotNumber(globalObject, scope); - } -}; - template struct WebCore::Converter> : Bun::DefaultContextConverter> { diff --git a/src/jsc/bindings/BunIDLHumanReadable.h b/src/jsc/bindings/BunIDLHumanReadable.h index 239087715098..fa0c5b7a6e6d 100644 --- a/src/jsc/bindings/BunIDLHumanReadable.h +++ b/src/jsc/bindings/BunIDLHumanReadable.h @@ -3,7 +3,6 @@ #include "ConcatCStrings.h" #include #include -#include namespace Bun { @@ -53,10 +52,6 @@ template<> struct IDLHumanReadableName : BaseIDLHumanReadabl static constexpr auto humanReadableName = std::to_array("null"); }; -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("undefined"); -}; - template requires std::derived_from struct IDLHumanReadableName : BaseIDLHumanReadableName { diff --git a/src/jsc/bindings/BunIDLTypes.h b/src/jsc/bindings/BunIDLTypes.h index 17a2d9f83e0d..20526f28d1f4 100644 --- a/src/jsc/bindings/BunIDLTypes.h +++ b/src/jsc/bindings/BunIDLTypes.h @@ -3,7 +3,6 @@ #include #include #include -#include #include namespace WTF { @@ -20,20 +19,11 @@ struct IDLRawAny : WebCore::IDLType { // Storage in a sequence is explicitly unsupported, as this would create a // `Vector`, whose contents are invisible to the GC. using SequenceStorageType = void; - using NullableType = JSC::JSValue; - using NullableParameterType = JSC::JSValue; - using NullableInnerParameterType = JSC::JSValue; - static NullableType nullValue() { return JSC::jsUndefined(); } - static bool isNullValue(const NullableType& value) { return value.isUndefined(); } - static ImplementationType extractValueFromNullable(const NullableType& value) { return value; } }; // For use in unions, to represent a nullable union. struct IDLStrictNull : WebCore::IDLType {}; -// For use in unions, to represent an optional union. -struct IDLStrictUndefined : WebCore::IDLType {}; - // Treats all falsy values as null. template struct IDLLooseNullable : WebCore::IDLNullable {}; @@ -41,7 +31,6 @@ struct IDLLooseNullable : WebCore::IDLNullable {}; template struct IDLStrictInteger : WebCore::IDLInteger {}; struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; -struct IDLFiniteDouble : WebCore::IDLDouble {}; struct IDLStrictBoolean : WebCore::IDLBoolean {}; struct IDLStrictString : WebCore::IDLDOMString {}; @@ -49,8 +38,9 @@ struct IDLStrictString : WebCore::IDLDOMString {}; template struct IDLLooseInteger : IDLStrictInteger {}; +// The alternatives of a union, for IDLHumanReadableName. template -struct IDLOrderedUnion : WebCore::IDLType> {}; +struct IDLOrderedUnion; namespace Detail { template @@ -75,11 +65,6 @@ struct IDLBunInterface : WebCore::IDLType, R using NullableInnerParameterType = NullableType; static inline std::nullptr_t nullValue() { return nullptr; } - template static inline bool isNullValue(U&& value) { return !value; } - template static inline U&& extractValueFromNullable(U&& value) - { - return std::forward(value); - } }; struct IDLArrayBufferRef : IDLBunInterface {}; diff --git a/src/jsc/bindings/JSBindgenObject.cpp b/src/jsc/bindings/JSBindgenObject.cpp index 11201aa7deca..900ff8dba0c3 100644 --- a/src/jsc/bindings/JSBindgenObject.cpp +++ b/src/jsc/bindings/JSBindgenObject.cpp @@ -28,7 +28,7 @@ JSBindgenConstructor* JSBindgenConstructor::create(VM& vm, JSGlobalObject* globa { auto* structure = createStructure(vm, globalObject, globalObject->functionPrototype()); auto* constructor = new (NotNull, allocateCell(vm)) JSBindgenConstructor(vm, structure, definition.construct); - constructor->finishCreation(vm, definition.constructorLength, definition.className, PropertyAdditionMode::WithoutStructureTransition); + constructor->finishCreation(vm, definition.constructorLength, definition.info->className, PropertyAdditionMode::WithoutStructureTransition); Bun::reifyStaticPropertyTable(vm, info(), definition.constructorTableValues, *constructor); constructor->putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); return constructor; diff --git a/src/jsc/bindings/webcore/JSDOMConvertDictionary.h b/src/jsc/bindings/webcore/JSDOMConvertDictionary.h index 305063eb343f..e1e7cf91ac78 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertDictionary.h +++ b/src/jsc/bindings/webcore/JSDOMConvertDictionary.h @@ -36,16 +36,6 @@ template T convertDictionary(JSC::JSGlobalObject& lexicalGlobalObjec template struct Converter> : DefaultConverter> { using ReturnType = T; - static std::optional tryConvert( - JSC::JSGlobalObject& lexicalGlobalObject, - JSC::JSValue value) - { - if (value.isObject()) { - return convert(lexicalGlobalObject, value); - } - return std::nullopt; - } - static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { return convertDictionary(lexicalGlobalObject, value); diff --git a/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h b/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h index 52ef8d2bf9d0..2cd3d61dd236 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h @@ -44,19 +44,6 @@ template JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, template struct Converter> : DefaultConverter> { static constexpr bool takesContext = true; - // `tryConvert` for enumerations is strict: it returns null if the value is not a string. - template - static std::optional tryConvert( - JSC::JSGlobalObject& lexicalGlobalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isString()) { - return parseEnumeration(lexicalGlobalObject, value); - } - return std::nullopt; - } - // When converting with Context, the conversion is stricter: non-strings are disallowed. template static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) diff --git a/src/jsc/bindings/webcore/JSDOMConvertNullable.h b/src/jsc/bindings/webcore/JSDOMConvertNullable.h index 83b73e5a302f..bb0a9cb30d72 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertNullable.h +++ b/src/jsc/bindings/webcore/JSDOMConvertNullable.h @@ -73,17 +73,6 @@ template struct Converter> : DefaultConverter - static std::optional tryConvert( - JSC::JSGlobalObject& lexicalGlobalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isUndefinedOrNull()) - return T::nullValue(); - return Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); - } - template static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) { diff --git a/src/jsc/bindings/webcore/JSDOMConvertOptional.h b/src/jsc/bindings/webcore/JSDOMConvertOptional.h index 6052fd9ff638..86fc1bc94813 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertOptional.h +++ b/src/jsc/bindings/webcore/JSDOMConvertOptional.h @@ -37,21 +37,6 @@ template struct Converter> : DefaultConverter - static std::optional tryConvert( - JSC::JSGlobalObject& lexicalGlobalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isUndefined()) - return T::nullValue(); - auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); - if (result.has_value()) { - return std::move(*result); - } - return std::nullopt; - } - template static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) { From 88fa693f4781f16b044a6606b0c0e314ac2a0cbd Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 11:00:51 +0000 Subject: [PATCH 28/44] Apply the conventions and simplification reviews to the generator, its output, docs, errors and tests --- docs/project/bindgen.mdx | 34 +- scripts/build/CLAUDE.md | 2 +- scripts/build/codegen.ts | 15 +- src/codegen/bindgen-emit.ts | 317 ++++++++----------- src/codegen/bindgen-lib-internal.ts | 400 +++++++++++++----------- src/codegen/bindgen.ts | 4 +- src/codegen/replacements.ts | 2 +- src/js/internal-for-testing.ts | 2 +- src/js/private.d.ts | 1 + src/jsc/bindgen.rs | 3 +- src/jsc/bindings/Bindgen/ExternTraits.h | 10 + src/jsc/bindings/Bindgen/IDLConvert.h | 4 + src/jsc/bindings/Bindgen/IDLTypes.h | 4 + src/runtime/api/glob.rs | 2 +- src/runtime/node/node_os.bind.ts | 2 +- test/internal/bindgen.test.ts | 16 +- 16 files changed, 419 insertions(+), 399 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 798a7fb48e38..6f516c5954ac 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -110,7 +110,7 @@ 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 candidates are the signatures that do not require more arguments than were -passed. Then, argument by argument, those that take a value of its +passed, or, if every one requires more, those that require the fewest. Then, argument by argument, those that take a value of its [kind](#unions-and-kinds) stay; a signature that has no such argument ignores it. If several are left when the arguments run out, or none takes the kind, the call goes to the one that requires the most arguments, then to the one written first, and its @@ -120,22 +120,22 @@ 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, `.loose` or +or BigInt that is an integer in range, and nothing else. A failed strict, `.loose` or `.validateInteger` conversion throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary member; `.wrap` and `.enforceRange` throw what WebIDL does. Too few arguments throw `ERR_MISSING_ARGS`. -| Schema | Rust, as an argument | Accepts | +| 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 | an integer in range; see below | +| `b.u8` `b.u16` `b.u32` `b.u64` `b.usize` `b.i32` | the same | a number or BigInt that is an integer in range; see below | | `b.f64` | `f64` | a number | | `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` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | | `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) | -| `b.bufferSource.arrayBuffer` | `ExternalShared` | a typed array, `DataView` or `ArrayBuffer` | +| `b.bufferSource.arrayBuffer` | `ExternalShared` | the same | | `b.blob` | `ExternalShared` | a `Blob` | | `b.any` | `JsValue<'a>` | anything | | `b.null` | `()` | `null` or `undefined`; only in a union | @@ -200,8 +200,7 @@ A dictionary whose members can all be left out implements `Default`. 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 generated code reads the member straight out of the object's -property table. The two paths are indistinguishable to JavaScript; +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 a prebuilt `Structure`: one @@ -210,12 +209,12 @@ left out, and the object is then built one property at a time. The second argument of `b.dictionary` takes: -| Key | What it does | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `userFacingName` | what error messages call it, when that is not its name | -| `allowArray` | an array is read like any other object, not refused | -| `allowNonObject` | a value whose `typeof` is not `"object"` gives every member its default, 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, \|converted\| ..)`, for a `#[host_fn]` body | +| Key | What it does | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `userFacingName` | what error messages call it, when that is not its name | +| `allowArray` | an array is read like any other object, not refused | +| `nonObjectAsEmpty` | a value whose `typeof` is not `"object"` gives every member its default, 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, \|converted\| ..)`, for a `#[host_fn]` body | ### Enumerations @@ -239,7 +238,7 @@ in play apart: | 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 in its row of the table above. A type that coerces or +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`, an enumeration, a dictionary. @@ -252,8 +251,9 @@ 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. The generated C++ tests whether the value is a cell once, loads the type byte -once, makes no test twice, and converts into the place the result ends up in. +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 @@ -333,7 +333,7 @@ pub(crate) struct Glob { } impl GlobClass for Glob { - fn constructor<'a>(pattern: bun_core::Utf8Bytes<'static>) -> Self { + fn constructor(pattern: bun_core::Utf8Bytes<'static>) -> Self { Glob { pattern } } diff --git a/scripts/build/CLAUDE.md b/scripts/build/CLAUDE.md index fcfcbfaede6a..543d39793b69 100644 --- a/scripts/build/CLAUDE.md +++ b/scripts/build/CLAUDE.md @@ -185,7 +185,7 @@ CI's `build-bun` step builds the same graph (`ci-build` profile: release, prebui `--mode=codegen` configures a graph of the codegen steps only, default target `codegen`, for what needs their outputs and no binary. It has a build directory of its own (`build/debug-codegen`): its manifest and `compile_commands.json` have no native edges in them, and `build/debug` is where clangd reads `compile_commands.json`. `bun run build:types` (`--mode=codegen --target=generated-types`) is what `lint.yml` runs before typechecking `src/js`. -**`build/types/` (`cfg.typesDir`)** holds the type declarations `src/js/builtins.d.ts` references: `generated.d.ts`, `ErrorCode.d.ts`, `ZigGeneratedClasses.d.ts`, `WebCoreJSBuiltins.d.ts`. They are made from source alone (byte-identical between debug and release), so they do not live under a profile's build directory: every profile's graph declares the same files there, the way a dep's `vendor//.ref` is, and any build keeps them current for the editor. They are declared outputs of the edges that write them (`CodegenOutputs.generatedTypes`, target `generated-types`), so ninja regenerates one that is deleted or whose edge inputs changed. `src/runtime/bake/generated.ts` (the dev server's message enums, which the bake project imports) is in the same target, declared by the bake codegen edge that writes it. +**`build/types/` (`cfg.typesDir`)** holds the type declarations `src/js/builtins.d.ts` references: `generated.d.ts`, `ErrorCode.d.ts`, `ZigGeneratedClasses.d.ts`, `WebCoreJSBuiltins.d.ts`, `Bindgen.d.ts`. They are made from source alone (byte-identical between debug and release), so they do not live under a profile's build directory: every profile's graph declares the same files there, the way a dep's `vendor//.ref` is, and any build keeps them current for the editor. They are declared outputs of the edges that write them (`CodegenOutputs.generatedTypes`, target `generated-types`), so ninja regenerates one that is deleted or whose edge inputs changed. `src/runtime/bake/generated.ts` (the dev server's message enums, which the bake project imports) is in the same target, declared by the bake codegen edge that writes it. It is configured by `configureCodegen()`, not `configure()`, and resolves a `CodegenConfig`, not a `Config`: diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index 49ce41d5dd5a..c24c8bcdb413 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -870,7 +870,7 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void { desc: "JS modules (bundle-modules)", // Note: arg is BUILD_PATH (buildDir), not CODEGEN_PATH. The script // derives CODEGEN_DIR = join(BUILD_PATH, "codegen") internally. - args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, withoutBindings(cfg, sources)]), + args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, withoutBindingsFlag(cfg, sources)]), }, }); @@ -922,18 +922,21 @@ function emitBakeCodegen({ n, cfg, sources, o, dirStamp }: Ctx): void { } /** A `*_test.bind.ts` binds what exists for tests. Only a build where Rust has `debug_assertions` has it, and none ships. */ -function hasSchema(cfg: CodegenFields, src: string): boolean { +function hasBindings(cfg: CodegenFields, src: string): boolean { return cfg.debug || cfg.assertions || !src.endsWith("_test.bind.ts"); } -function withoutBindings(cfg: CodegenFields, sources: Sources): string { - return `--without-bindings=${sources.bindgen.filter(src => !hasSchema(cfg, src)).map(src => basename(src))}`; +function withoutBindingsFlag(cfg: CodegenFields, sources: Sources): string { + return `--without-bindings=${sources.bindgen + .filter(src => !hasBindings(cfg, src)) + .map(src => basename(src)) + .join(",")}`; } /** Exported for test/internal/build-codegen-declared-outputs.test.ts. */ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); - const sources = { ...allSources, bindgen: allSources.bindgen.filter(src => hasSchema(cfg, src)) }; + const sources = { ...allSources, bindgen: allSources.bindgen.filter(src => hasBindings(cfg, src)) }; // What bindgen.ts writes follows from the names and places of its inputs alone. const stems = sources.bindgen.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); @@ -964,7 +967,7 @@ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): script, // Every schema: build/types is shared by every profile, so Bindgen.d.ts describes them all. `--sources=${allSources.bindgen.map(src => `${crateOf(src)}=${src}`).join(",")}`, - withoutBindings(cfg, allSources), + withoutBindingsFlag(cfg, allSources), `--codegen-path=${cfg.codegenDir}`, `--types-path=${cfg.typesDir}`, ]), diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index c5c780402296..a50098ef932b 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -10,13 +10,14 @@ import { EnumType, type FunctionDefinition, type Getter, - type Kind, + indent, + type JSClass, type Method, type ModuleDefinition, NamedType, OptionalType, type Origin, - type OverloadChoice, + type OverloadDispatch, rustIdent, type RustScope, Signature, @@ -24,6 +25,7 @@ import { type Type, UnionType, UnitType, + type ValueKind, WithValueType, } from "./bindgen-lib-internal.ts"; import { cap, low } from "./helpers.ts"; @@ -49,14 +51,6 @@ export interface Tables { const banner = (origin: Origin | null) => `// Generated by src/codegen/bindgen.ts${origin ? ` from ${origin.stem}.bind.ts` : ""}. Do not edit.\n`; -function indent(text: string, by: number): string { - const pad = " ".repeat(by); - return text - .split("\n") - .map(line => (line.length > 0 ? pad + line : line)) - .join("\n"); -} - // ───────────────────────────── analysis ───────────────────────────── function walk(type: Type, visit: (type: Type) => void, seen = new Set()): void { @@ -142,73 +136,63 @@ const implementationType = (type: Type) => `${type.idlType}::ImplementationType` const externType = (type: Type): string => type instanceof WithValueType ? `::Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` - : `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::ExternType`; - -/** True of a value that is not a cell. */ -const valueTests: Partial string>> = { - undefined: value => `${value}.isUndefined()`, - null: value => `${value}.isNull()`, - boolean: value => `${value}.isBoolean()`, - number: value => `${value}.isNumber()`, - bigint: value => `${value}.isBigInt32()`, -}; -/** True of `cell`, whose type byte is `type`. */ -const cellTests: Partial> = { - bigint: "type == JSC::HeapBigIntType", - string: "type == JSC::StringType", - stringObject: "type == JSC::StringObjectType || type == JSC::DerivedStringObjectType", - array: "type == JSC::ArrayType || type == JSC::DerivedArrayType", - arrayBuffer: "type == JSC::ArrayBufferType", - arrayBufferView: "JSC::isTypedArrayTypeIncludingDataView(type)", + : `::Bun::Bindgen::ExternOf<${type.idlType}>`; +const toExtern = (variable: string) => `::Bun::Bindgen::toExtern(${variable})`; + +/** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` from `type`, the cell's type byte. */ +const kindTests: Record, { ofValue?: (value: string) => string; ofCell?: string }> = { + undefined: { ofValue: value => `${value}.isUndefined()` }, + null: { ofValue: value => `${value}.isNull()` }, + boolean: { ofValue: value => `${value}.isBoolean()` }, + number: { ofValue: value => `${value}.isNumber()` }, + bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "type == JSC::HeapBigIntType" }, + string: { ofCell: "type == JSC::StringType" }, + stringObject: { ofCell: "type == JSC::StringObjectType || type == JSC::DerivedStringObjectType" }, + array: { ofCell: "type == JSC::ArrayType || type == JSC::DerivedArrayType" }, + arrayBuffer: { ofCell: "type == JSC::ArrayBufferType" }, + arrayBufferView: { ofCell: "JSC::isTypedArrayTypeIncludingDataView(type)" }, }; -/** - * C++ that declares `choice`, an index into the `choices` it returns: the distinct values of `byKind`. One test of whether - * `value` is a cell, then the cheaper tests first, none twice, none that can run JavaScript, and none for a kind that is - * treated as "other" is. - */ -function cppChooseByKind(value: string, byKind: ReadonlyMap, key: (choice: T) => string) { - const choices: T[] = []; - const indexOf = (choice: T) => { - const index = choices.findIndex(other => key(other) === key(choice)); - return index >= 0 ? index : choices.push(choice) - 1; - }; - const other = indexOf(byKind.get("other")!); - const ofValue = new Map(); - const ofType = new Map(); - const ofClass = new Map(); - const add = (tests: Map, index: number, test: string | undefined) => { - if (test !== undefined && index !== other) tests.set(index, [...(tests.get(index) ?? []), test]); - }; - for (const [kind, choice] of byKind) { - if (kind === "other") continue; - const index = indexOf(choice); +/** C++ that declares `choice`, an index into the `outcomes` it returns: the distinct values of `byKind`, "other"'s first. */ +function cppChooseByKind(value: string, byKind: ReadonlyMap) { + const other = byKind.get("other")!; + const outcomes = [...new Set([other, ...byKind.values()])]; + const ofValue = new Map(); + const ofType = new Map(); + const ofClass = new Map(); + for (const [kind, outcome] of byKind) { + if (kind === "other" || outcome === other) continue; + const add = (tests: Map, test: string | undefined) => { + if (test !== undefined) tests.set(outcome, [...(tests.get(outcome) ?? []), test]); + }; if (typeof kind === "object") { - add(ofClass, index, `cell->inherits<${kind.cpp}>()`); + add(ofClass, `cell->inherits<${kind.cpp}>()`); } else { - add(ofValue, index, valueTests[kind]?.(value)); - add(ofType, index, cellTests[kind]); + add(ofValue, kindTests[kind].ofValue?.(value)); + add(ofType, kindTests[kind].ofCell); } } - const chain = (...tests: Map[]) => + const nullish = [`${value}.isUndefined()`, `${value}.isNull()`]; + const chain = (...tests: Map[]) => tests .flatMap(map => [...map]) - .map(([index, conditions]) => { - const nullish = [`${value}.isUndefined()`, `${value}.isNull()`]; + .map(([outcome, conditions]) => { const merged = nullish.every(test => conditions.includes(test)) ? [`${value}.isUndefinedOrNull()`, ...conditions.filter(test => !nullish.includes(test))] : conditions; - return `if (${merged.join(" || ")})\n choice = ${index};`; + return `if (${merged.join(" || ")})\n choice = ${outcomes.indexOf(outcome)};`; }) .join("\nelse "); - const cell = `${ofClass.size > 0 ? "auto* cell" : "JSC::JSType type"} = ${value}.asCell()${ofClass.size > 0 ? "" : "->type()"}; -${ofClass.size > 0 && ofType.size > 0 ? "JSC::JSType type = cell->type();\n" : ""}${chain(ofType, ofClass)}`; - const hasCellTests = ofType.size + ofClass.size > 0; - let tree = ""; - if (hasCellTests && ofValue.size > 0) tree = `if (${value}.isCell()) {\n${indent(cell, 4)}\n} else ${chain(ofValue)}`; - else if (hasCellTests) tree = `if (${value}.isCell()) {\n${indent(cell, 4)}\n}`; - else if (ofValue.size > 0) tree = chain(ofValue); - return { choices, code: `unsigned choice = ${other};${tree && "\n"}${tree}` }; + const lines = ["unsigned choice = 0;"]; + if (ofType.size + ofClass.size > 0) { + lines.push(`if (${value}.isCell()) {`); + if (ofClass.size > 0) lines.push(` auto* cell = ${value}.asCell();`); + if (ofType.size > 0) lines.push(` JSC::JSType type = ${value}.asCell()->type();`); + lines.push(indent(chain(ofType, ofClass), 4), ofValue.size > 0 ? `} else ${chain(ofValue)}` : "}"); + } else if (ofValue.size > 0) { + lines.push(chain(ofValue)); + } + return { outcomes, code: lines.join("\n") }; } function layoutAssert(cppType: string, type: Type): string { @@ -225,31 +209,39 @@ function humanReadableEnum(type: EnumType): string { /** Value-initialization zeroes a struct whose constructor is implicit, and these hold the buffers of string views. */ const cppDefaultConstructor = (name: string) => ` ${name}() {}`; -function cppHeaderForUnion(type: UnionType): string { - const { name, alternatives } = type; - const idlTypes = alternatives.map(a => a.type.idlType).join(", "); - const { choices, code } = cppChooseByKind("value", type.alternativesByKind, choice => choice.join()); - const cases = choices.map((choice, index) => { - if (choice.length === 0) return ""; - const attempts = choice.map(i => { - const alternative = alternatives[i]; - const member = `result.${alternative.name}`; - if (alternative.type.kinds !== "any" && alternative.type.refusesSomeOfItsKinds) - return `if (auto converted = ::Bun::tryConvertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx)) { +/** C++ that offers `value` to `candidates`, indices of alternatives, in order. All but the last may refuse it. */ +function cppConvertToAlternative(type: UnionType, candidates: readonly number[]): string { + return candidates + .map(i => { + const { name, type: alternative } = type.alternatives[i]; + const member = `result.${name}`; + if (!alternative.takesEveryValueOfItsKinds) + return `if (auto converted = ::Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { ${member} = ::std::move(*converted); result.tag = ${i}; return; }`; - if (alternative.type instanceof UnitType) return `result.tag = ${i};\nreturn;`; - const convert = alternative.type.convertsInPlace - ? cppEmplace(member, target => alternative.type.cppConvertInto(target, "value", "ctx", "")) - : `${member} = ::Bun::convertIDL<${alternative.type.idlType}>(lexicalGlobalObject, value, ctx);`; - return `result.tag = ${i};\nscope.release();\n${convert}\nreturn;`; - }); - const refused = attempts.at(-1)!.endsWith("return;") ? "" : "\n break;"; - return `case ${index}: {\n${indent(attempts.join("\n"), 4)}${refused}\n}`; - }); - const canMatchNothing = cases.some(text => text.length === 0 || text.endsWith("break;\n}")); + if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; + const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", ""); + return `result.tag = ${i}; +scope.release(); +${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} +return;`; + }) + .join("\n"); +} + +function cppHeaderForUnion(type: UnionType): string { + const { name, alternatives } = type; + const idlTypes = alternatives.map(a => a.type.idlType).join(", "); + const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); + const mayRefuse = (candidates: readonly number[]) => + candidates.length === 0 || !alternatives[candidates.at(-1)!].type.takesEveryValueOfItsKinds; + const cases = outcomes.flatMap((candidates, index) => + candidates.length === 0 + ? [] + : `case ${index}: {\n${indent(cppConvertToAlternative(type, candidates), 4)}${mayRefuse(candidates) ? "\n break;" : ""}\n}`, + ); const held = alternatives.filter(a => !(a.type instanceof UnitType)); return ` namespace Bun { @@ -268,7 +260,6 @@ ${alternatives.map(a => ` ${externType(a.type)} ${a.name};`).join("\n")} ::std::uint8_t tag; }; ${layoutAssert(`Extern${name}`, type)} -struct IDL${name} : ::WebCore::IDLType<${name}> {}; } template<> struct ExternTraits { using ExternType = Generated::Extern${name}; @@ -280,7 +271,7 @@ template<> struct ExternTraits { ${held .map( a => ` case ${alternatives.indexOf(a)}: - result.data.${a.name} = ExternTraits<${implementationType(a.type)}>::convertToExtern(::std::move(${a.type.convertsInPlace ? "*" : ""}value.${a.name})); + result.data.${a.name} = toExtern(${a.type.convertsInPlace ? "*" : ""}value.${a.name}); break;`, ) .join("\n")} @@ -289,7 +280,7 @@ ${held } }; } -template<> struct IDLHumanReadableName : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; +template<> struct IDLHumanReadableName<${type.idlType}> : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; } template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextConverter<${type.idlType}> {${ type.needsLifetime @@ -307,13 +298,13 @@ template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextCon template<::Bun::IDLConversionContext Ctx> static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) { - [[maybe_unused]] auto* globalObject = &lexicalGlobalObject; + auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); ${indent(code, 8)} switch (choice) { -${indent(cases.filter(text => text.length > 0).join("\n"), 8)} - }${canMatchNothing ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(lexicalGlobalObject, scope);` : ""} +${indent(cases.join("\n"), 8)} + }${outcomes.some(mayRefuse) ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);` : ""} } }; `; @@ -356,7 +347,6 @@ struct Extern${name} { ${type.members.map(member => ` ${externType(member.type)} ${member.internalName};`).join("\n")} }; ${layoutAssert(`Extern${name}`, type)} -void convert${name}(JSC::JSGlobalObject&, JSC::JSValue, ${name}& result); } template<> struct ExternTraits { using ExternType = Generated::Extern${name}; @@ -364,10 +354,7 @@ template<> struct ExternTraits { { return ExternType { ${type.members - .map( - member => - ` .${member.internalName} = ExternTraits<${implementationType(member.type)}>::convertToExtern(::std::move(value.${member.internalName})),`, - ) + .map(member => ` .${member.internalName} = toExtern(value.${member.internalName}),`) .join("\n")} }; } @@ -377,6 +364,7 @@ template<> struct IDLHumanReadableName<::WebCore::IDLDictionary void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue, ${type.cpp}&); ${ type.needsLifetime ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : ::Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` @@ -407,8 +395,7 @@ ${file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});` }`; } -const hostFunctionName = (fn: FunctionDefinition) => - `js${fn.name[0] === "_" ? fn.name : fn.name[0].toUpperCase() + fn.name.slice(1)}`; +const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name)}`; function cppSourceForEnum(type: EnumType): string { const pairs = type.spellings @@ -437,41 +424,29 @@ function cppSourceForDictionary(type: DictionaryType): string { if (!type.accepted) return ""; let nameIndex = 0; const members = type.members.map((member, i) => { - const reads = member.names.map((_, alt) => { - const read = `value${i} = members.get(${nameIndex++});\n RETURN_IF_EXCEPTION(scope, void());`; - return alt === 0 - ? read - : `if (value${i}.isUndefined()) {\n ${read.replace("\n", "\n ")}\n }`; - }); + const [first, ...others] = member.names.map( + () => `members.get(${nameIndex++});\nRETURN_IF_EXCEPTION(scope, void());`, + ); const qualified = `${type.userFacingName}.${member.name}`; - const convert = `${indent(member.type.cppConvertInto(`result.${member.internalName}`, `value${i}`, `ctx${i}`, qualified), 8).trimStart()}\n RETURN_IF_EXCEPTION(scope, void());`; - let body: string; - if (member.type instanceof DefaultType) { - const absent = member.type.onNull ? "isUndefinedOrNull" : "isUndefined"; - body = `if (value${i}.${absent}()) { - ${member.type.cppAssign(`result.${member.internalName}`, member.type.value).replaceAll("\n", "\n ")} - } else { - ${convert.replaceAll("\n", "\n ")} - }`; - } else if (member.type.permitsUndefined) { - body = convert; - } else { - body = `if (value${i}.isUndefined()) [[unlikely]] { - ctx${i}.throwRequired(lexicalGlobalObject, scope); - return; - } - ${convert}`; - } - return ` { - ::JSC::JSValue value${i}; - [[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} }; - ${reads.join("\n ")} - ${body} - }`; + const lines = [ + `[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} };`, + `JSC::JSValue value${i} = ${first}`, + ...others.map(read => `if (value${i}.isUndefined()) {\n${indent(`value${i} = ${read}`, 4)}\n}`), + ]; + if (!member.type.permitsUndefined) + lines.push(`if (value${i}.isUndefined()) [[unlikely]] { + ctx${i}.throwRequired(*globalObject, scope); + return; +}`); + lines.push( + member.type.cppConvertInto(`result.${member.internalName}`, `value${i}`, `ctx${i}`, qualified), + "RETURN_IF_EXCEPTION(scope, void());", + ); + return ` {\n${indent(lines.join("\n"), 8)}\n }`; }); const count = type.names.length; return ` -void Bun::Bindgen::Generated::convert${type.name}(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) +template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) { auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); @@ -479,13 +454,13 @@ void Bun::Bindgen::Generated::convert${type.name}(JSC::JSGlobalObject& lexicalGl [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; auto* object = value.getObject(); ${ - type.allowNonObject + type.nonObjectAsEmpty ? ` if (!object || object->isCallable()) { - ${type.cppAssign("result", {}).replaceAll("\n", "\n ")} +${indent(type.cppAssign("result", {}), 8)} return; }` : ` if (!object) [[unlikely]] { - ctx.throwNotObject(lexicalGlobalObject, scope); + ctx.throwNotObject(*globalObject, scope); return; }` } @@ -495,7 +470,7 @@ ${ : ` bool isArray = JSC::isArray(globalObject, value); RETURN_IF_EXCEPTION(scope, void()); if (isArray) [[unlikely]] { - ctx.throwNotObject(lexicalGlobalObject, scope); + ctx.throwNotObject(*globalObject, scope); return; } ` @@ -509,7 +484,7 @@ ${ template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { ${type.cpp} result; - ::Bun::Bindgen::Generated::convert${type.name}(globalObject, value, result); + ::Bun::Bindgen::convertDictionaryInto(globalObject, value, result); return result; } ` @@ -521,9 +496,9 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); ${type.cpp} converted; - ::Bun::Bindgen::Generated::convert${type.name}(*globalObject, JSC::JSValue::decode(value), converted); + ::Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted); RETURN_IF_EXCEPTION(scope, false); - *result = ::Bun::Bindgen::ExternTraits<${type.cpp}>::convertToExtern(::std::move(converted)); + *result = ${toExtern("converted")}; return true; } ` @@ -539,25 +514,8 @@ const thunkSymbol = (fn: FunctionDefinition, signature: Signature) => /** Lines that declare `variable` as what `value` converts to. */ function cppDeclareConverted(type: Type, variable: string, value: string, ctx: string, name: string): string[] { - const declaration = `${implementationType(type)} ${variable}`; - const absent = type instanceof DefaultType && `${value}.${type.onNull ? "isUndefinedOrNull" : "isUndefined"}()`; - if (!type.convertsInPlace) { - const convert = type.cppConvert(value, ctx, name); - return [`${declaration} = ${absent ? `${absent} ? ${type.toCpp(type.value)} : ${convert}` : convert};`]; - } - const into = type.cppConvertInto(variable, value, ctx, name).split("\n"); - if (!absent) return [`${declaration};`, ...into]; - return [ - `${declaration};`, - `if (${absent}) {`, - ...type - .cppAssign(variable, type.value) - .split("\n") - .map(line => ` ${line}`), - "} else {", - ...into.map(line => ` ${line}`), - "}", - ]; + if (!type.convertsInPlace) return [`auto ${variable} = ${type.cppConvert(value, ctx, name)};`]; + return [`${implementationType(type)} ${variable};`, ...type.cppConvertInto(variable, value, ctx, name).split("\n")]; } /** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */ @@ -570,21 +528,22 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str const rereads: string[] = []; signature.args.forEach(({ name, type }, i) => { const argument = i < signature.requiredCount ? "uncheckedArgument" : "argument"; - lines.push(`::JSC::EnsureStillAliveScope argument${i} = callFrame->${argument}(${i});`); + lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->${argument}(${i});`); lines.push(`[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); - const convert = type.cppConvert(`argument${i}.value()`, `ctx${i}`, name); // Converting a later argument can run JavaScript that detaches or resizes the buffer. if (type.borrowsHeap && i < signature.args.length - 1) - rereads.push(`value${i} = ${convert};`, `scope.assertNoException();`); + rereads.push( + `value${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`, + `scope.assertNoException();`, + ); lines.push(...cppDeclareConverted(type, `value${i}`, `argument${i}.value()`, `ctx${i}`, name)); lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); lines.push(...rereads); const args = signature.args.map(({ type }, i) => { - const converted = `::Bun::Bindgen::ExternTraits<${implementationType(type)}>::convertToExtern(::std::move(value${i}))`; return type instanceof WithValueType - ? `${externType(type)} { ${converted}, JSC::JSValue::encode(argument${i}.value()) }` - : converted; + ? `${externType(type)} { ${toExtern(`value${i}`)}, JSC::JSValue::encode(argument${i}.value()) }` + : toExtern(`value${i}`); }); lines.push(`RELEASE_AND_RETURN(scope, ${symbol}(${["globalObject", ...leading, ...args].join(", ")}));`); return lines.map(line => ` ${line}`).join("\n"); @@ -677,7 +636,7 @@ JSC_DEFINE_CUSTOM_GETTER(${get}, (JSC::JSGlobalObject * globalObject, JSC::Encod ${cppPrologue} // DOMAttribute: JSC has checked the receiver's ClassInfo. auto* thisObject = uncheckedDowncast<${type.cpp}>(JSC::JSValue::decode(thisValue)); - ::JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; + JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; ${ getter.internalFieldIndex < 0 ? ` RELEASE_AND_RETURN(scope, ${getSymbol}(globalObject, thisObject));` @@ -703,12 +662,12 @@ ${cppPrologue} JSC::throwDOMAttributeSetterTypeError(globalObject, scope, ${type.cpp}::info(), attributeName); return false; } - ::JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; - ::JSC::EnsureStillAliveScope ensureValueStillAlive { JSC::JSValue::decode(encodedValue) }; + JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; + JSC::EnsureStillAliveScope ensureValueStillAlive { JSC::JSValue::decode(encodedValue) }; [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }; ${cppDeclareConverted(getter.type, "value", "ensureValueStillAlive.value()", "ctx", getter.name).join("\n ")} RETURN_IF_EXCEPTION(scope, false); - RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, thisObject, ::Bun::Bindgen::ExternTraits<${implementationType(getter.type)}>::convertToExtern(::std::move(value)))); + RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, thisObject, ${toExtern("value")})); } `; } @@ -834,23 +793,23 @@ ${cppPrologue} ${cppCallBody(signature, thunkSymbol(fn, signature))} }`, ); - const ids = new Map(); - const choose = (choice: OverloadChoice): string => { - if (choice instanceof Signature) - return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, choice)}(globalObject, callFrame));`; - const { choices, code } = cppChooseByKind(`argument${choice.argument}`, choice.byKind, next => - String(ids.get(next) ?? ids.set(next, ids.size).size - 1), + const cppDispatch = (dispatch: OverloadDispatch): string => { + if (dispatch instanceof Signature) + return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, dispatch)}(globalObject, callFrame));`; + const { outcomes, code } = cppChooseByKind(`argument${dispatch.argument}`, dispatch.byKind); + const cases = outcomes.map( + (next, index) => `${index === 0 ? "default" : `case ${index}`}: {\n${indent(cppDispatch(next), 4)}\n}`, ); - return `JSC::JSValue argument${choice.argument} = callFrame->argument(${choice.argument}); + return `JSC::JSValue argument${dispatch.argument} = callFrame->argument(${dispatch.argument}); ${code} switch (choice) { -${choices.map((next, index) => `${index === choices.length - 1 ? "default" : `case ${index}`}: {\n${indent(choose(next), 4)}\n}`).join("\n")} +${[...cases.slice(1), cases[0]].join("\n")} }`; }; - const dispatch = fn.choices.map(({ atLeast, choice }, index) => - index === fn.choices.length - 1 - ? indent(choose(choice), 4) - : ` if (callFrame->argumentCount() >= ${atLeast}) {\n${indent(choose(choice), 8)}\n }`, + const dispatch = fn.dispatchByArgumentCount.map(({ atLeast, dispatch }, index) => + index === fn.dispatchByArgumentCount.length - 1 + ? indent(cppDispatch(dispatch), 4) + : ` if (callFrame->argumentCount() >= ${atLeast}) {\n${indent(cppDispatch(dispatch), 8)}\n }`, ); definition = `${overloads.join("\n")} JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -996,7 +955,7 @@ const infallible = (call: string, throws: boolean, returns: Type | null = null) const rustThunk = (symbol: string, parameters: string[], returns: string, body: string) => ` #[unsafe(no_mangle)] pub unsafe extern "C" fn ${symbol}(${["global: *mut JSGlobalObject", ...parameters].join(", ")}) -> ${returns} { - // SAFETY: called by generated C++, whose frame holds \`this\` and the arguments until this returns. + // SAFETY: called by generated C++, whose frame holds the arguments, \`this\` among them, until this returns. unsafe { bindgen::JsCall::with(global, |cx| { ${indent(body, 12)} @@ -1048,7 +1007,7 @@ impl${lifetime} ${type.name}${lifetime} { /// \`value\` was just filled in by generated C++ and is read once. #[doc(hidden)] pub unsafe fn from_extern(value: &Extern${type.name}) -> Self { - // SAFETY: C++ initialized the arm \`tag\` names. + // SAFETY: C++ initialized the alternative \`tag\` names. unsafe { match value.tag { ${type.alternatives diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 827fd90c4e1f..88e2c289dcb6 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -36,7 +36,7 @@ export function structLayout(fields: readonly Layout[]): Layout { } /** `{ union data; uint8_t tag; }`. */ -export function variantLayout(alternatives: readonly Layout[]): Layout { +export function taggedUnionLayout(alternatives: readonly Layout[]): Layout { const align = Math.max(...alternatives.map(a => a.align)); const union = { size: roundUp(Math.max(...alternatives.map(a => a.size)), align), align }; return structLayout([union, { size: 1, align: 1 }]); @@ -70,11 +70,8 @@ export function asciiLiteral(value: string): string { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"_s`; } -/** - * JavaScript values, divided by what tells them apart: the bits of the value, then the type byte in a cell's header, - * then, for the instances of a class, its ClassInfo. A value of none of these kinds is "other". - */ -export type Kind = +/** JavaScript values, divided by what tells them apart. One of none of these kinds is "other". See "Unions and kinds" in the docs. */ +export type ValueKind = | "undefined" | "null" | "boolean" @@ -92,30 +89,42 @@ export interface JSClass { readonly cpp: string; } -const takes = (type: Type, kind: Kind | "other") => type.kinds === "any" || type.kinds.includes(kind as Kind); +type ValueKinds = readonly ValueKind[] | "any"; -const kindsToTellApart = (types: readonly Type[]): (Kind | "other")[] => [ - ...new Set(types.flatMap(type => (type.kinds === "any" ? [] : type.kinds))), +const takes = (type: Type, kind: ValueKind | "other") => + type.valueKinds === "any" || (kind !== "other" && type.valueKinds.includes(kind)); + +const distinguishingKinds = (types: readonly Type[]): (ValueKind | "other")[] => [ + ...new Set(types.flatMap(type => (type.valueKinds === "any" ? [] : type.valueKinds))), "other", ]; -/** - * Which of `alternatives` get a value of each kind: the first, in written order, that takes the kind, preceded by those - * that take it but refuse some of its values. - */ -export function alternativesByKind(alternatives: readonly Type[]): Map { +/** Which of `alternatives` get a value of each kind, in order: all but the last refuse some. Equal lists are one array. */ +export function alternativesByKind(alternatives: readonly Type[]): Map { + const lists = new Map(); return new Map( - kindsToTellApart(alternatives).map(kind => { + distinguishingKinds(alternatives).map(kind => { const takers = alternatives.flatMap((type, i) => (takes(type, kind) ? [i] : [])); - const last = takers.findIndex(i => alternatives[i].kinds === "any" || !alternatives[i].refusesSomeOfItsKinds); - return [kind, last < 0 ? takers : takers.slice(0, last + 1)]; + const last = takers.findIndex(i => alternatives[i].takesEveryValueOfItsKinds); + const list = last < 0 ? takers : takers.slice(0, last + 1); + if (!lists.has(`${list}`)) lists.set(`${list}`, list); + return [kind, lists.get(`${list}`)!]; }), ); } +export function indent(text: string, by: number): string { + const pad = " ".repeat(by); + return text + .split("\n") + .map(line => (line.length > 0 ? pad + line : line)) + .join("\n"); +} + /** C++ statements that engage `optional`, a `std::optional`, and do to what it holds what `fill` says, given its name. */ export function cppEmplace(optional: string, fill: (target: string) => string): string { - const target = `${optional.replace(/\W+/g, "_")}_value`; + const [first, ...rest] = optional.split(/\W+/); + const target = `${first}${rest.map(part => part[0].toUpperCase() + part.slice(1)).join("")}Value`; return `auto& ${target} = ${optional}.emplace();\n${fill(target)}`; } @@ -155,12 +164,12 @@ export abstract class Type { return false; } /** The kinds of value it takes where there is a choice. "any": its own conversion decides, by coercing or by throwing. */ - get kinds(): readonly Kind[] | "any" { + get valueKinds(): ValueKinds { return "any"; } - /** Such as a shared buffer, or a Blob that was detached. In a union the next alternative gets them. */ - get refusesSomeOfItsKinds(): boolean { - return false; + /** False if it refuses a shared buffer, say, or a detached Blob. In a union the next alternative gets those. */ + get takesEveryValueOfItsKinds(): boolean { + return true; } /** Holds a JS value: the Rust type is branded with the call. */ get holdsJSValue(): boolean { @@ -176,7 +185,7 @@ export abstract class Type { } /** `rustType` is `Copy`. */ get rustIsCopy(): boolean { - return this.dependencies.length > 0 && this.dependencies.every(type => type.rustIsCopy); + return this.dependencies.every(type => type.rustIsCopy); } get needsLifetime(): boolean { return this.holdsJSValue || this.borrowsFrame || this.borrowsHeap; @@ -240,28 +249,33 @@ export abstract class Type { // ───────────────────────────── primitives ───────────────────────────── class PrimitiveType extends Type { - get rustIsCopy(): boolean { - return true; - } readonly idlType: string; readonly tsType: string; readonly rustName: string; readonly size: number; - constructor(idlType: string, tsType: string, rustName: string, size: number) { + readonly #valueKinds: ValueKinds; + constructor(idlType: string, tsType: string, rustName: string, size: number, valueKinds: ValueKinds) { super(); this.idlType = idlType; this.tsType = tsType; this.rustName = rustName; this.size = size; + this.#valueKinds = valueKinds; + } + get valueKinds(): ValueKinds { + return this.#valueKinds; + } + protected get cppName(): string { + return "double"; } toCpp(value: unknown): string { if (this.rustName === "bool") { - assert(typeof value === "boolean"); + assert(typeof value === "boolean", `${util.inspect(value)} is not a boolean`); return String(value); } - assert(typeof value === "number"); - if (this.rustName !== "f64") assert(Number.isSafeInteger(value)); - return `${this.idlType}::ImplementationType { ${value} }`; + assert(typeof value === "number", `${util.inspect(value)} is not a number`); + if (this.rustName !== "f64") assert(Number.isSafeInteger(value), `${value} is not an integer`); + return `${this.cppName} { ${value} }`; } toRust(value: unknown): string { return this.rustName === "f64" && Number.isInteger(value) ? `${value}.0` : String(value); @@ -281,15 +295,12 @@ class PrimitiveType extends Type { } export class BoolType extends PrimitiveType { - constructor(idlType = "::Bun::IDLStrictBoolean") { - super(idlType, "boolean", "bool", 1); + constructor(idlType = "::Bun::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { + super(idlType, "boolean", "bool", 1, valueKinds); } /** `ToBoolean`: any value is accepted. */ get loose(): BoolType { - return new BoolType("::WebCore::IDLBoolean"); - } - get kinds() { - return this.idlType === "::Bun::IDLStrictBoolean" ? (["boolean"] as const) : "any"; + return new BoolType("::WebCore::IDLBoolean", "any"); } } @@ -305,12 +316,19 @@ interface IntegerInfo { export class IntegerType extends PrimitiveType { readonly info: IntegerInfo; - constructor(info: IntegerInfo, idlType = `::Bun::IDLStrictInteger<${info.cpp}>`) { - super(idlType, "number", info.rust, info.size); + /** Only the strict conversion is given no `idlType`. */ + constructor(info: IntegerInfo, idlType?: string) { + super( + idlType ?? `::Bun::IDLStrictInteger<${info.cpp}>`, + "number", + info.rust, + info.size, + idlType ? "any" : ["number", "bigint"], + ); this.info = info; } - get kinds() { - return this.idlType.startsWith("::Bun::IDLStrictInteger<") ? (["number", "bigint"] as const) : "any"; + protected get cppName(): string { + return this.info.cpp; } /** `ToNumeric` first, then as strict as the default. */ get loose(): IntegerType { @@ -333,8 +351,11 @@ export class IntegerType extends PrimitiveType { export class RangeType extends IntegerType { readonly kind: "Web" | "Node"; constructor(info: IntegerInfo, min: number, max: number, kind: "Web" | "Node") { - assert(Number.isSafeInteger(min) && Number.isSafeInteger(max) && min <= max); - assert(min >= Number(info.min) && max <= Number(info.max), `range does not fit ${info.rust}`); + assert( + Number.isSafeInteger(min) && Number.isSafeInteger(max) && min <= max, + `[${min}, ${max}] is not a range of integers`, + ); + assert(min >= Number(info.min) && max <= Number(info.max), `[${min}, ${max}] does not fit ${info.rust}`); super( info, `::Bun::BindgenCustomEnforceRange<${info.cpp}, ${min}, ${max}, ::Bun::BindgenCustomEnforceRangeKind::${kind}>`, @@ -365,18 +386,12 @@ export const integers = { export class DoubleType extends PrimitiveType { constructor() { - super("::Bun::IDLStrictDouble", "number", "f64", 8); - } - get kinds() { - return ["number"] as const; + super("::Bun::IDLStrictDouble", "number", "f64", 8, ["number"]); } } /** Only as a union alternative or a return type. */ export class UnitType extends Type { - get rustIsCopy(): boolean { - return true; - } readonly idlType: string; readonly tsType: string; constructor(tsType: "null" | "undefined") { @@ -384,7 +399,7 @@ export class UnitType extends Type { this.tsType = tsType; this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : ""; } - get kinds(): readonly Kind[] { + get valueKinds(): ValueKinds { return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; } get permitsUndefined(): boolean { @@ -414,22 +429,25 @@ export class UnitType extends Type { export class StringType extends Type { readonly idlType: string; readonly tsType = "string"; - constructor(idlType = "::Bun::IDLStrictString") { + readonly #valueKinds: ValueKinds; + constructor(idlType = "::Bun::IDLStrictString", valueKinds: ValueKinds = ["string"]) { super(); this.idlType = idlType; + this.#valueKinds = valueKinds; + } + get valueKinds(): ValueKinds { + return this.#valueKinds; } - get kinds(): readonly Kind[] | "any" { - if (this.idlType === "::Bun::IDLStrictString") return ["string"]; - return this.idlType === "::Bun::Bindgen::IDLStrictStringOrStringObject" ? ["string", "stringObject"] : "any"; + get rustIsCopy(): boolean { + return false; } /** `ToString`: any value is accepted. */ get loose(): StringType { - return new StringType("::WebCore::IDLDOMString"); + return new StringType("::WebCore::IDLDOMString", "any"); } /** `&[u8]`, UTF-8, for the duration of the call, instead of an owned string. Also takes a `String` object. */ get utf8(): Utf8Type { - const isLoose = this.idlType === "::WebCore::IDLDOMString"; - return new Utf8Type(`::Bun::Bindgen::IDL${isLoose ? "Loose" : "Strict"}UTF8String`); + return new Utf8Type(this.valueKinds === "any"); } get nullRepresentation() { return "pointer" as const; @@ -460,6 +478,12 @@ export class StringType extends Type { /** UTF-8 to keep: the JS string's own buffer, shared, when it is 8-bit ASCII, otherwise a transcoded copy. */ export class OwnedUtf8Type extends StringType { + get loose(): never { + throw new TypeError("write b.string.loose.utf8.owned"); + } + get utf8(): never { + throw new TypeError("b.string.utf8.owned is already UTF-8"); + } toRust(): string { throw new TypeError("b.string.utf8.owned cannot have a default"); } @@ -476,22 +500,23 @@ export class OwnedUtf8Type extends StringType { /** UTF-8 bytes for the duration of the call: `Bun::Bindgen::JSStringUTF8View`. */ export class Utf8Type extends Type { - get rustIsCopy(): boolean { - return true; - } - readonly idlType: string; + readonly isLoose: boolean; readonly tsType = "string"; - constructor(idlType: string) { + constructor(isLoose: boolean) { super(); - this.idlType = idlType; + this.isLoose = isLoose; + } + get idlType(): string { + return `::Bun::Bindgen::IDL${this.isLoose ? "Loose" : "Strict"}UTF8String`; } /** `bun_core::Utf8Bytes<'static>`, for a body that stores the string. */ get owned(): OwnedUtf8Type { - const isLoose = this.idlType === "::Bun::Bindgen::IDLLooseUTF8String"; - return new OwnedUtf8Type(isLoose ? "::WebCore::IDLDOMString" : "::Bun::Bindgen::IDLStrictStringOrStringObject"); + return this.isLoose + ? new OwnedUtf8Type("::WebCore::IDLDOMString", "any") + : new OwnedUtf8Type("::Bun::Bindgen::IDLStrictStringOrStringObject", this.valueKinds); } - get kinds(): readonly Kind[] | "any" { - return this.idlType === "::Bun::Bindgen::IDLStrictUTF8String" ? ["string", "stringObject"] : "any"; + get valueKinds(): ValueKinds { + return this.isLoose ? "any" : ["string", "stringObject"]; } get borrowsFrame(): boolean { return true; @@ -535,21 +560,15 @@ export class Utf8Type extends Type { } } -const bufferSourceKinds = ["arrayBuffer", "arrayBufferView"] as const; +const bufferSourceKinds: ValueKinds = ["arrayBuffer", "arrayBufferView"]; /** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ export class BytesType extends Type { - get rustIsCopy(): boolean { - return true; - } readonly idlType = "::Bun::Bindgen::IDLBufferSourceBytes"; readonly tsType = "ArrayBuffer | ArrayBufferView"; - get kinds() { + get valueKinds(): ValueKinds { return bufferSourceKinds; } - get refusesSomeOfItsKinds(): boolean { - return true; - } get borrowsHeap(): boolean { return true; } @@ -572,16 +591,13 @@ export class BytesType extends Type { /** A typed array, `DataView` or `ArrayBuffer`, as a handle whose bytes are read through `with_bytes`. */ export class BufferSourceType extends Type { - get rustIsCopy(): boolean { - return true; - } readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; readonly tsType = "ArrayBuffer | ArrayBufferView"; - get kinds() { + get valueKinds(): ValueKinds { return bufferSourceKinds; } - get refusesSomeOfItsKinds(): boolean { - return true; + get takesEveryValueOfItsKinds(): boolean { + return false; } /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ get bytes(): BytesType { @@ -629,8 +645,8 @@ export class WithValueType extends Type { get permitsUndefined(): boolean { return this.inner.permitsUndefined; } - get kinds() { - return this.inner.kinds; + get valueKinds(): ValueKinds { + return this.inner.valueKinds; } get holdsJSValue(): boolean { return true; @@ -662,9 +678,6 @@ export class WithValueType extends Type { } export class AnyType extends Type { - get rustIsCopy(): boolean { - return true; - } readonly idlType = "::Bun::IDLRawAny"; readonly tsType = "any"; get holdsJSValue(): boolean { @@ -675,7 +688,7 @@ export class AnyType extends Type { } toCpp(value: unknown): string { assert(value === undefined, "b.any cannot have a default"); - return "::JSC::jsUndefined()"; + return "JSC::jsUndefined()"; } toRust(): string { return "bindgen::JsValue::UNDEFINED"; @@ -703,20 +716,23 @@ export class RefType extends Type { readonly tsType: string; readonly pointee: string; readonly header: string | null; - readonly wrapperKinds: readonly Kind[]; - constructor(idlType: string, tsType: string, pointee: string, header: string | null, wrapperKinds: readonly Kind[]) { + readonly #valueKinds: ValueKinds; + constructor(idlType: string, tsType: string, pointee: string, header: string | null, valueKinds: ValueKinds) { super(); this.idlType = idlType; this.tsType = tsType; this.pointee = pointee; this.header = header; - this.wrapperKinds = wrapperKinds; + this.#valueKinds = valueKinds; } - get kinds() { - return this.wrapperKinds; + get valueKinds(): ValueKinds { + return this.#valueKinds; } - get refusesSomeOfItsKinds(): boolean { - return true; + get takesEveryValueOfItsKinds(): boolean { + return false; + } + get rustIsCopy(): boolean { + return false; } get nullRepresentation() { return "pointer" as const; @@ -767,9 +783,9 @@ export class OptionalType extends Type { get permitsUndefined(): boolean { return true; } - get kinds(): readonly Kind[] | "any" { - if (this.payload.kinds === "any" || this.kind === "falsyAsNull") return "any"; - return [...this.payload.kinds, "undefined", ...(this.kind === "nullable" ? (["null"] as const) : [])]; + get valueKinds(): ValueKinds { + if (this.payload.valueKinds === "any" || this.kind === "falsyAsNull") return "any"; + return [...this.payload.valueKinds, "undefined", ...(this.kind === "nullable" ? (["null"] as const) : [])]; } get dependencies(): readonly Type[] { return [this.payload]; @@ -787,7 +803,7 @@ export class OptionalType extends Type { }[this.kind]; const convert = (slot: string) => this.payload.cppConvertInto(slot, value, ctx, name); const body = this.payload.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); - return `if (${provided}) {\n ${body.replaceAll("\n", "\n ")}\n}`; + return `if (${provided}) {\n${indent(body, 4)}\n}`; } toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); @@ -797,7 +813,7 @@ export class OptionalType extends Type { if (value == null) return ""; const assign = (slot: string) => this.payload.cppAssign(slot, value); return this.payload.nullRepresentation === "optional" - ? `{\n ${cppEmplace(target, assign).replaceAll("\n", "\n ")}\n}` + ? `{\n${indent(cppEmplace(target, assign), 4)}\n}` : assign(target); } toRust(value: unknown, scope: RustScope): string { @@ -805,7 +821,7 @@ export class OptionalType extends Type { } layout(): Layout { return this.payload.nullRepresentation === "optional" - ? variantLayout([{ size: 1, align: 1 }, this.payload.layout()]) + ? taggedUnionLayout([{ size: 1, align: 1 }, this.payload.layout()]) : this.payload.layout(); } rustExternType(scope: RustScope): string { @@ -844,7 +860,7 @@ export class DefaultType extends Type { readonly onNull: boolean; constructor(payload: Type, value: unknown) { super(); - assert(!(payload instanceof DefaultType)); + assert(!(payload instanceof DefaultType), "already has a default"); if (payload instanceof OptionalType) { assert(payload.kind === "nullable", ".default() follows a type or .nullable"); this.payload = payload.payload; @@ -874,19 +890,26 @@ export class DefaultType extends Type { toRust(value: unknown, scope: RustScope): string { return this.payload.toRust(value, scope); } - get kinds(): readonly Kind[] | "any" { - if (this.payload.kinds === "any") return "any"; - return [...this.payload.kinds, "undefined", ...(this.onNull ? (["null"] as const) : [])]; + get valueKinds(): ValueKinds { + if (this.payload.valueKinds === "any") return "any"; + return [...this.payload.valueKinds, "undefined", ...(this.onNull ? (["null"] as const) : [])]; + } + private cppIsAbsent(value: string): string { + return `${value}.${this.onNull ? "isUndefinedOrNull" : "isUndefined"}()`; } cppConvert(value: string, ctx: string, name: string): string { - return this.payload.cppConvert(value, ctx, name); + return `${this.cppIsAbsent(value)} ? ${this.toCpp(this.value)} : ${this.payload.cppConvert(value, ctx, name)}`; } get convertsInPlace(): boolean { return this.payload.convertsInPlace; } - /** Of a value that is provided. */ cppConvertInto(target: string, value: string, ctx: string, name: string): string { - return this.payload.cppConvertInto(target, value, ctx, name); + if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name); + return `if (${this.cppIsAbsent(value)}) { +${indent(this.cppAssign(target, this.value), 4)} +} else { +${indent(this.payload.cppConvertInto(target, value, ctx, name), 4)} +}`; } cppAssign(target: string, value: unknown): string { return this.payload.cppAssign(target, value); @@ -912,7 +935,7 @@ export class ArrayType extends Type { readonly element: Type; constructor(element: Type) { super(); - assert(!(element instanceof DefaultType)); + assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert( !element.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", @@ -921,14 +944,17 @@ export class ArrayType extends Type { } get idlType(): string { assert(!this.element.holdsJSValue, "an array argument cannot hold JS values: the GC does not scan it"); - assert(!this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string without .utf8"); + assert( + !this.element.borrowsFrame, + "an array argument cannot hold borrowed views; use b.string or b.string.utf8.owned", + ); return `::Bun::IDLArray<${this.element.idlType}>`; } get tsType(): string { return `(${this.element.tsType})[]`; } - get kinds() { - return ["array"] as const; + get valueKinds(): ValueKinds { + return ["array"]; } get dependencies(): readonly Type[] { return [this.element]; @@ -994,9 +1020,6 @@ export abstract class NamedType extends Type { } export class EnumType extends NamedType { - get rustIsCopy(): boolean { - return true; - } /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ @@ -1005,10 +1028,10 @@ export class EnumType extends NamedType { returned = false; constructor(values: readonly (string | readonly string[])[]) { super(); - assert(values.length > 0, "an enumeration has values"); + assert(values.length > 0, "an enumeration needs at least one value"); this.spellings = values.map(value => (typeof value === "string" ? [value] : value)); const all = this.spellings.flat(); - assert(new Set(all).size === all.length, "duplicate value"); + assert(new Set(all).size === all.length, `a value is listed twice: ${all.join(", ")}`); assert(new Set(this.variants).size === this.variants.length, "two values make the same identifier"); for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${variant} is not an identifier`); } @@ -1026,7 +1049,7 @@ export class EnumType extends NamedType { } toCpp(value: unknown): string { const index = this.spellings.findIndex(spellings => spellings.includes(value as string)); - assert(index >= 0, `${util.inspect(value)} is not a value of ${this.name}`); + assert(index >= 0, `${util.inspect(value)} is not one of ${this.tsType}`); return `${this.cpp}::${this.variants[index]}`; } toRust(value: unknown, scope: RustScope): string { @@ -1080,7 +1103,7 @@ export interface DictionaryOptions { /** An array is read like any other object, not refused. */ allowArray?: boolean; /** A value whose `typeof` is not "object" gives every member its default: Node's `if (typeof options !== "object") options = null`. */ - allowNonObject?: boolean; + nonObjectAsEmpty?: boolean; } export class DictionaryType extends NamedType { @@ -1097,13 +1120,19 @@ export class DictionaryType extends NamedType { super(); this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); - assert(this.members.length > 0, "a dictionary has members"); - assert(!this.allowNonObject || this.hasDefault, "allowNonObject: every member must be optional or have a default"); + assert(this.members.length > 0, "a dictionary needs at least one member"); + assert( + !this.nonObjectAsEmpty || this.hasDefault, + "nonObjectAsEmpty: every member must be optional or have a default", + ); assert( !this.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", ); - assert(!this.fromJS || !this.borrowsFrame, "from_js outlives the frame b.string.utf8 borrows from"); + assert( + !this.fromJS || !this.borrowsFrame, + "from_js outlives the frame b.string.utf8 borrows from; use b.string or b.string.utf8.owned", + ); } get userFacingName(): string { return this.options.userFacingName ?? this.name; @@ -1114,8 +1143,8 @@ export class DictionaryType extends NamedType { get allowArray(): boolean { return !!this.options.allowArray; } - get allowNonObject(): boolean { - return !!this.options.allowNonObject; + get nonObjectAsEmpty(): boolean { + return !!this.options.nonObjectAsEmpty; } get idlType(): string { return `::WebCore::IDLDictionary<${this.cpp}>`; @@ -1127,14 +1156,17 @@ export class DictionaryType extends NamedType { return true; } cppConvertInto(target: string, value: string): string { - return `::Bun::Bindgen::Generated::convert${this.name}(*globalObject, ${value}, ${target});`; + return `::Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target});`; } /** Every JS name read, in order. */ get names(): string[] { return this.members.flatMap(member => member.names); } cppAssign(target: string, value: unknown): string { - assert(typeof value === "object" && value !== null); + assert( + typeof value === "object" && value !== null, + `the default of a dictionary is an object, not ${util.inspect(value)}`, + ); const given = value as Record; for (const key of Object.keys(given)) assert( @@ -1180,11 +1212,8 @@ export class UnionType extends NamedType { readonly alternatives: readonly { name: string; variant: string; type: Type }[]; constructor(alternatives: Record) { super(); - this.alternatives = Object.entries(alternatives).map(([name, type]) => { - assert(!(type instanceof DefaultType)); - return { name, variant: pascalCase(name), type }; - }); - assert(this.alternatives.length > 0); + this.alternatives = Object.entries(alternatives).map(([name, type]) => ({ name, variant: pascalCase(name), type })); + assert(this.alternatives.length > 0, "a union needs at least one alternative"); assert( !this.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", @@ -1192,21 +1221,29 @@ export class UnionType extends NamedType { for (const { name, type } of this.alternatives) { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); + assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); + assert( + !(type instanceof RangeType), + `${name}: a range reports the name of its argument, which a union does not know`, + ); } const chosen = new Set([...this.alternativesByKind.values()].flat()); this.alternatives.forEach(({ name }, i) => - assert(chosen.has(i), `${name} is never chosen: the alternatives before it take every value it would`), + assert( + chosen.has(i), + `${name} is never chosen: the alternatives before it take every value it would. Move it earlier or remove it`, + ), ); } get alternativesByKind() { return alternativesByKind(this.alternatives.map(a => a.type)); } - get kinds(): readonly Kind[] | "any" { - const kinds = this.alternatives.map(a => a.type.kinds); - return kinds.includes("any") ? "any" : [...new Set(kinds.flat() as Kind[])]; + get valueKinds(): ValueKinds { + const kinds = distinguishingKinds(this.alternatives.map(a => a.type)).filter(kind => kind !== "other"); + return this.alternatives.some(a => a.type.valueKinds === "any") ? "any" : kinds; } get idlType(): string { - return `::Bun::Bindgen::Generated::IDL${this.name}`; + return `::Bun::Bindgen::IDLTaggedUnion<${this.cpp}>`; } get convertsInPlace(): boolean { return true; @@ -1223,20 +1260,19 @@ export class UnionType extends NamedType { get dependencies(): readonly Type[] { return this.alternatives.map(a => a.type); } - cppAssign(target: string, value: unknown): string { + private unitAlternative(value: unknown) { const unit = value == null && this.alternatives.find(a => a.type instanceof UnitType && takes(a.type, `${value}`)); - assert(unit, `${this.name}: only null or undefined can be a default, and only for an alternative that takes it`); - return `${target}.tag = ${this.alternatives.indexOf(unit)};`; + assert(unit, `${this.tsType}: the default of a union is null or undefined, and it needs b.null as an alternative`); + return unit; + } + cppAssign(target: string, value: unknown): string { + return `${target}.tag = ${this.alternatives.indexOf(this.unitAlternative(value))};`; } toRust(value: unknown, scope: RustScope): string { - const unit = this.alternatives.find( - a => a.type instanceof UnitType && (value === undefined || a.type.tsType === String(value)), - ); - assert(unit, `${this.name}: only null or undefined can be a default`); - return `${this.path(scope, this.name)}::${unit.variant}`; + return `${this.path(scope, this.name)}::${this.unitAlternative(value).variant}`; } layout(): Layout { - return variantLayout(this.alternatives.map(a => a.type.layout())); + return taggedUnionLayout(this.alternatives.map(a => a.type.layout())); } rustExternType(scope: RustScope): string { return this.path(scope, `Extern${this.name}`); @@ -1288,8 +1324,11 @@ export class Signature { `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`, ); } - assert(!(this.returns instanceof DefaultType)); - assert(!this.returns.holdsJSValue || this.capability !== "none", "returning a JS value takes a capability"); + assert(!(this.returns instanceof DefaultType), "a result has no default"); + assert( + !this.returns.holdsJSValue || this.capability !== "none", + 'returning a JS value takes capability: "heap" or "js"', + ); } get requiredCount(): number { return this.args.filter(arg => !arg.type.permitsUndefined).length; @@ -1297,63 +1336,59 @@ export class Signature { } /** Call a signature, or look at the kind of one more argument. */ -export type OverloadChoice = Signature | { argument: number; byKind: Map }; +export type OverloadDispatch = Signature | { argument: number; byKind: Map }; export class FunctionDefinition { readonly signatures: readonly Signature[]; - /** How the signature is chosen when at least this many arguments were passed. Most arguments first; the last is for any number. */ - readonly choices: readonly { atLeast: number; choice: OverloadChoice }[]; + /** Most arguments first. The last entry is for any number of them. */ + readonly dispatchByArgumentCount: readonly { atLeast: number; dispatch: OverloadDispatch }[]; name = ""; origin: Origin | null = null; constructor(signatures: readonly SignatureOptions[]) { - assert(signatures.length > 0); + assert(signatures.length > 0, "b.overloads needs at least one signature"); this.signatures = signatures.map(options => new Signature(options)); const counts = [...new Set(this.signatures.map(signature => signature.requiredCount))].sort((a, b) => b - a); - const made = new Map(); - this.choices = counts.map(atLeast => ({ + const nodes = new Map(); + this.dispatchByArgumentCount = counts.map(atLeast => ({ atLeast, - choice: this.choose( + dispatch: this.dispatchOn( this.signatures.filter(signature => signature.requiredCount <= atLeast), 0, - made, + nodes, ), })); - const chosen = new Set(made.values()); + const chosen = new Set(nodes.values()); this.signatures.forEach((signature, i) => - assert(chosen.has(signature), `overload ${i + 1} is never chosen: nothing tells it from the others`), + assert( + chosen.has(signature), + `overload ${i + 1} is never chosen: nothing tells it from the others. Give an argument a type of another kind`, + ), ); } - /** - * Argument by argument, keeps the candidates that take a value of its kind; one that has no such argument ignores it. - * What is left when the arguments run out, or when no candidate takes the kind, goes to the one that requires the most - * arguments, then to the one written first: its conversion reports what is wrong. - */ - private choose( + /** "Overloads" in the docs. The same candidates and argument give the same object of `nodes`: nodes are compared by identity. */ + private dispatchOn( candidates: readonly Signature[], argument: number, - made: Map, - ): OverloadChoice { + nodes: Map, + ): OverloadDispatch { const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; - const existing = made.get(key); + const existing = nodes.get(key); if (existing) return existing; const best = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); - let choice: OverloadChoice = best; + let dispatch: OverloadDispatch = best; if (candidates.length > 1 && candidates.some(signature => argument < signature.args.length)) { const typeOf = (signature: Signature) => signature.args[argument]?.type; - choice = { - argument, - byKind: new Map( - kindsToTellApart(candidates.flatMap(signature => typeOf(signature) ?? [])).map(kind => { - const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); - return [kind, this.choose(takers.length > 0 ? takers : [best], argument + 1, made)]; - }), - ), - }; - const outcomes = new Set(choice.byKind.values()); - if (outcomes.size === 1) [choice] = outcomes; + const byKind = new Map( + distinguishingKinds(candidates.flatMap(signature => typeOf(signature) ?? [])).map(kind => { + const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); + return [kind, this.dispatchOn(takers.length > 0 ? takers : [best], argument + 1, nodes)]; + }), + ); + const outcomes = new Set(byKind.values()); + dispatch = outcomes.size === 1 ? byKind.get("other")! : { argument, byKind }; } - made.set(key, choice); - return choice; + nodes.set(key, dispatch); + return dispatch; } get length(): number { const given = this.signatures.find(signature => signature.length !== undefined); @@ -1469,9 +1504,6 @@ export const selfType = new SelfType(); /** As an argument or a result: the wrapper, `JsWrapper<'a, T>`. */ export class ClassType extends NamedType { - get rustIsCopy(): boolean { - return true; - } readonly rustPath: string; readonly construct: Signature | null; readonly methods: readonly Method[]; @@ -1530,7 +1562,7 @@ export class ClassType extends NamedType { get idlType(): string { return `::Bun::Bindgen::IDLRawInterface<${this.cpp}>`; } - get kinds(): readonly Kind[] { + get valueKinds(): ValueKinds { return [this]; } get holdsJSValue(): boolean { diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 2302a5f721a8..c18ef3873d42 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -75,9 +75,9 @@ const sources = (args.sources as string) const allFiles: SchemaFile[] = []; for (const source of sources) allFiles.push(await load(source)); allFiles.forEach(sortTypes); +const withoutBindings = ((args["without-bindings"] as string | undefined) ?? "").split(","); /** What this build compiles. The types describe every schema. */ -const without = ((args["without-bindings"] as string | undefined) ?? "").split(","); -const files = allFiles.filter(file => !without.includes(`${file.origin.stem}.bind.ts`)); +const files = allFiles.filter(file => !withoutBindings.includes(`${file.origin.stem}.bind.ts`)); const owners = new Map(); for (const file of files) { for (const { name } of file.types) { diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index b5ce5e764aff..ab9d68430804 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -129,7 +129,7 @@ export const enums = { // These are passed to --define to the bundler const debug = process.argv[2] === "--debug=ON"; -/** Schemas this build leaves out (`hasSchema` in scripts/build/codegen.ts). `$bindgenFn` of one is `void 0`, which the minifier folds. */ +/** Schemas this build leaves out (`hasBindings` in scripts/build/codegen.ts). `$bindgenFn` of one is `void 0`, which the minifier folds. */ const withoutBindings = (process.argv.find(arg => arg.startsWith("--without-bindings=")) ?? "=") .split("=")[1] .split(","); diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index bb7570c981aa..bc0f76f981e4 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -242,7 +242,7 @@ export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFramework }; }; -// Only in a build with assertions: `hasSchema` in scripts/build/codegen.ts. +// Only in a build with assertions: `hasBindings` in scripts/build/codegen.ts. export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { add: $bindgenFn("bindgen_test.bind.ts", "add"), requiredAndOptionalArg: $bindgenFn("bindgen_test.bind.ts", "requiredAndOptionalArg"), diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 38d4c69e10c0..a021b4ce067f 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -276,6 +276,7 @@ declare function $newRustFunction any>( * Retrieves a handle to a function or class defined in native code, defined in a * `.bind.ts` file. For more information on how to define bindgen functions, see * [bindgen's documentation](https://bun.com/docs/project/bindgen). + * `undefined` for a `*_test.bind.ts` file in a build without assertions. * @param filename - The basename of the `.bind.ts` file. * @param symbol - The name of the export. */ diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 242cc35fea49..f088b155015c 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -521,7 +521,8 @@ pub mod to_js { mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsValue<'a>>, ) -> Result<'a, JsValue<'a>> { // As many as a `MarkedArgumentBuffer` holds inline, on this stack, where the GC sees them just the same. - let mut inline = [JSValue::ZERO; 8]; + const INLINE_CAPACITY: usize = 8; + let mut inline = [JSValue::ZERO; INLINE_CAPACITY]; let mut items = items.into_iter(); for count in 0..inline.len() { let Some(item) = items.next() else { diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 5e89789066ff..75b2bb972ccb 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -15,6 +15,16 @@ namespace Bun::Bindgen { template struct ExternTraits; +template +using ExternOf = typename ExternTraits::ExternType; + +// What Rust is passed. It owns what `value` owned. +template +ALWAYS_INLINE auto toExtern(T& value) +{ + return ExternTraits::convertToExtern(std::move(value)); +} + template struct TrivialExtern { using ExternType = T; diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index d158c153d570..caf764a07b62 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -19,6 +19,10 @@ template<> struct IDLHumanReadableName : IDLHumanRe namespace Bindgen { +// Specialized by generated code. `result` was just default-constructed. +template +void convertDictionaryInto(JSC::JSGlobalObject&, JSC::JSValue, T& result); + template void throwNotUnsharedBufferSource(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 91455230b36b..fafd357dff1e 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -13,6 +13,10 @@ struct IDLStackOnlyDictionary : WebCore::IDLType { using NullableParameterType = const T&; }; +// A generated union: a struct with a member for each alternative, and a tag. +template +struct IDLTaggedUnion : WebCore::IDLType {}; + // A string or a `String` object, which is what `JSValue::is_string` in Rust accepts. struct IDLStrictUTF8String : WebCore::IDLType { using SequenceStorageType = void; diff --git a/src/runtime/api/glob.rs b/src/runtime/api/glob.rs index de873eafad21..531ae0ea0165 100644 --- a/src/runtime/api/glob.rs +++ b/src/runtime/api/glob.rs @@ -409,7 +409,7 @@ impl Glob { impl GlobClass for Glob { type Error<'a> = ScanError<'a>; - fn constructor<'a>(pattern: bun_core::Utf8Bytes<'static>) -> Self { + fn constructor(pattern: bun_core::Utf8Bytes<'static>) -> Self { Glob { shape: bun_glob::matcher::Shape::of(pattern.slice()), pattern, diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 37fda4a2d070..92fdcc4e95fe 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -26,7 +26,7 @@ export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); export const UserInfoOptions = b.dictionary( { encoding: b.string.loose.utf8.default("") }, - { allowArray: true, allowNonObject: true }, + { allowArray: true, nonObjectAsEmpty: true }, ); export const userInfo = b.fn({ args: { diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index a5489c5c1e41..3996fddd4ec3 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -53,7 +53,6 @@ it("bindgen add example", () => { expect(bindgen.add(5555555555555, 0)).toBe(-2132125469); expect(bindgen.add(-5555555555555, 0)).toBe(2132125469); - // Test Zig error handling expect(() => bindgen.add(2147483647, 1)).toThrow("Integer overflow while adding"); }); @@ -490,7 +489,7 @@ describe("dictionaries", () => { }); describe("unions", () => { - it("picks the first alternative that fits", () => { + it("picks the alternative that takes the kind of the value", () => { expect(bindgen.describeValue(1.5)).toBe("number 1.5"); expect(bindgen.describeValue(true)).toBe("flag true"); expect(bindgen.describeValue("téxt")).toBe("text téxt"); @@ -559,7 +558,12 @@ describe("unions", () => { it("the alternative that was chosen reports what is wrong with the value", () => { expect(() => bindgen.describeAnything(1.5)).toThrow("value must be an integer (received 1.5)"); - expect(() => bindgen.describeAnything(2n ** 40n)).toThrow(RangeError); + expect(() => bindgen.describeAnything(2n ** 40n)).toThrow( + expect.objectContaining({ + code: "ERR_OUT_OF_RANGE", + message: "value must be in the range [-2147483648, 2147483647] (received 1099511627776)", + }), + ); expect(() => bindgen.describeAnything([1, "x"])).toThrow("element of value must be a number"); expect(() => bindgen.describeAnything({ count: "x" })).toThrow("TestOptions.count must be a number"); }); @@ -570,10 +574,12 @@ describe("unions", () => { (name: string) => (...args: unknown[]) => (calls.push(name), (Reflect as any)[name](...args)); const handler = Object.fromEntries(Reflect.ownKeys(Reflect).map(name => [name, trap(name as string)])); - expect(() => bindgen.describeValue(new Proxy({}, handler))).toThrow("value must be of type"); + expect(() => bindgen.describeValue(new Proxy({}, handler))).toThrow( + "value must be of type number, boolean, string, or array of integer", + ); expect(() => bindgen.describeValue({ toString: () => calls.push("toString"), valueOf: () => calls.push("valueOf") }), - ).toThrow("value must be of type"); + ).toThrow("value must be of type number, boolean, string, or array of integer"); expect(calls).toEqual([]); }); From 05362c90391afc32cbd29305c6a951136385e583 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 11:05:51 +0000 Subject: [PATCH 29/44] Make the list of a union's alternatives a complete type --- src/jsc/bindings/BunIDLTypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/bindings/BunIDLTypes.h b/src/jsc/bindings/BunIDLTypes.h index 20526f28d1f4..ea2395f92c6d 100644 --- a/src/jsc/bindings/BunIDLTypes.h +++ b/src/jsc/bindings/BunIDLTypes.h @@ -40,7 +40,7 @@ struct IDLLooseInteger : IDLStrictInteger {}; // The alternatives of a union, for IDLHumanReadableName. template -struct IDLOrderedUnion; +struct IDLOrderedUnion {}; namespace Detail { template From 98192319c513e8c7a8720aaebcbf471341bcbd72 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 11:27:28 +0000 Subject: [PATCH 30/44] Give a view's own bytes to b.bufferSource.arrayBuffer, make the array kind what b.array takes, make eight node:os functions plain functions as in Node, and reject more schemas when they load --- docs/project/bindgen.mdx | 2 +- src/codegen/bindgen-emit.ts | 7 ++- src/codegen/bindgen-lib-internal.ts | 56 +++++++++++++----- src/js/node/os.ts | 32 ++++++++--- src/jsc/bindings/BindgenCustomEnforceRange.h | 2 +- src/jsc/bindings/BunIDLConvert.h | 7 ++- src/jsc/bindings/BunIDLConvertNumbers.h | 3 +- test/internal/bindgen.test.ts | 18 ++++++ test/js/bun/net/socket.test.ts | 36 ++++++++++++ test/js/node/os/os.test.js | 60 +++++++++++++++----- 10 files changed, 178 insertions(+), 45 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 6f516c5954ac..cf0c42a84b14 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -135,7 +135,7 @@ what WebIDL does. Too few arguments throw `ERR_MISSING_ARGS`. | `b.string.utf8.owned` | `bun_core::Utf8Bytes<'static>` | as `b.string.utf8` | | `b.bufferSource` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | | `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) | -| `b.bufferSource.arrayBuffer` | `ExternalShared` | the same | +| `b.bufferSource.arrayBuffer` | `ExternalShared` | the same; the bytes of a view are copied | | `b.blob` | `ExternalShared` | a `Blob` | | `b.any` | `JsValue<'a>` | anything | | `b.null` | `()` | `null` or `undefined`; only in a union | diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index a50098ef932b..164b4808d64b 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -148,7 +148,7 @@ const kindTests: Record, { ofValue?: (value: string) bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "type == JSC::HeapBigIntType" }, string: { ofCell: "type == JSC::StringType" }, stringObject: { ofCell: "type == JSC::StringObjectType || type == JSC::DerivedStringObjectType" }, - array: { ofCell: "type == JSC::ArrayType || type == JSC::DerivedArrayType" }, + array: { ofCell: "type == JSC::ArrayType" }, arrayBuffer: { ofCell: "type == JSC::ArrayBufferType" }, arrayBufferView: { ofCell: "JSC::isTypedArrayTypeIncludingDataView(type)" }, }; @@ -220,7 +220,8 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) ${member} = ::std::move(*converted); result.tag = ${i}; return; -}`; +} +RETURN_IF_EXCEPTION(scope, void());`; if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", ""); return `result.tag = ${i}; @@ -250,7 +251,7 @@ namespace Generated { // A member for each alternative, so that the one \`tag\` names is converted where it stays. A large one is made when chosen. struct ${name} { ${cppDefaultConstructor(name)} -${held.map(a => ` ${a.type.convertsInPlace ? `::std::optional<${implementationType(a.type)}>` : implementationType(a.type)} ${a.name};`).join("\n")} +${held.map(a => ` ${a.type.convertsInPlace ? `::std::optional<${implementationType(a.type)}> ${a.name}` : `${implementationType(a.type)} ${a.name} {}`};`).join("\n")} ::std::uint8_t tag; }; struct Extern${name} { diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 88e2c289dcb6..c9d6da6ee2bb 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -274,6 +274,7 @@ class PrimitiveType extends Type { return String(value); } assert(typeof value === "number", `${util.inspect(value)} is not a number`); + assert(Number.isFinite(value), `${value} is not finite`); if (this.rustName !== "f64") assert(Number.isSafeInteger(value), `${value} is not an integer`); return `${this.cppName} { ${value} }`; } @@ -330,6 +331,15 @@ export class IntegerType extends PrimitiveType { protected get cppName(): string { return this.info.cpp; } + /** What a default has to be within. */ + protected get range(): readonly [min: number | bigint, max: number | bigint] { + return [this.info.min, this.info.max]; + } + toCpp(value: unknown): string { + const [min, max] = this.range; + assert(typeof value !== "number" || (value >= min && value <= max), `${value} is not in [${min}, ${max}]`); + return super.toCpp(value); + } /** `ToNumeric` first, then as strict as the default. */ get loose(): IntegerType { return new IntegerType(this.info, `::Bun::IDLLooseInteger<${this.info.cpp}>`); @@ -350,6 +360,7 @@ export class IntegerType extends PrimitiveType { export class RangeType extends IntegerType { readonly kind: "Web" | "Node"; + readonly #range: readonly [number, number]; constructor(info: IntegerInfo, min: number, max: number, kind: "Web" | "Node") { assert( Number.isSafeInteger(min) && Number.isSafeInteger(max) && min <= max, @@ -361,6 +372,10 @@ export class RangeType extends IntegerType { `::Bun::BindgenCustomEnforceRange<${info.cpp}, ${min}, ${max}, ::Bun::BindgenCustomEnforceRangeKind::${kind}>`, ); this.kind = kind; + this.#range = [min, max]; + } + protected get range() { + return this.#range; } getHeaders(out: Set): void { out.add("BindgenCustomEnforceRange.h"); @@ -405,6 +420,9 @@ export class UnitType extends Type { get permitsUndefined(): boolean { return true; } + cppConvert(): string { + throw new TypeError(`b.${this.tsType} is only an alternative of a union`); + } layout(): Layout { return { size: 1, align: 1 }; } @@ -770,6 +788,7 @@ export class OptionalType extends Type { this.kind = kind; } get idlType(): string { + assert(!(this.payload instanceof RangeType), "a range cannot be absent; give it a .default()"); const template = { optional: "::WebCore::IDLOptional", nullable: "::WebCore::IDLNullable", @@ -936,6 +955,8 @@ export class ArrayType extends Type { constructor(element: Type) { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); + assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); + assert(!(element instanceof WithValueType), ".withValue is only for an argument"); assert( !element.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", @@ -963,10 +984,6 @@ export class ArrayType extends Type { out.add("Bindgen/ExternVectorTraits.h"); super.getHeaders(out); } - toCpp(value: unknown): string { - assert(Array.isArray(value)); - return `${this.idlType}::ImplementationType { ${value.map(item => this.element.toCpp(item)).join(", ")} }`; - } layout(): Layout { return { size: 16, align: 8 }; } @@ -1121,6 +1138,8 @@ export class DictionaryType extends NamedType { this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); assert(this.members.length > 0, "a dictionary needs at least one member"); + for (const { name, type } of this.members) + assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); assert( !this.nonObjectAsEmpty || this.hasDefault, "nonObjectAsEmpty: every member must be optional or have a default", @@ -1222,6 +1241,8 @@ export class UnionType extends NamedType { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); + assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); + assert(name !== "tag", "tag: the generated struct has a member of that name"); assert( !(type instanceof RangeType), `${name}: a range reports the name of its argument, which a union does not know`, @@ -1349,11 +1370,12 @@ export class FunctionDefinition { this.signatures = signatures.map(options => new Signature(options)); const counts = [...new Set(this.signatures.map(signature => signature.requiredCount))].sort((a, b) => b - a); const nodes = new Map(); - this.dispatchByArgumentCount = counts.map(atLeast => ({ + this.dispatchByArgumentCount = counts.map((atLeast, i) => ({ atLeast, dispatch: this.dispatchOn( this.signatures.filter(signature => signature.requiredCount <= atLeast), 0, + i === 0 ? Infinity : counts[i - 1] - 1, nodes, ), })); @@ -1365,27 +1387,31 @@ export class FunctionDefinition { ), ); } - /** "Overloads" in the docs. The same candidates and argument give the same object of `nodes`: nodes are compared by identity. */ + /** "Overloads" in the docs, for a call with at most `passed` arguments. Equal inputs give the same object of `nodes`: nodes are compared by identity. */ private dispatchOn( candidates: readonly Signature[], argument: number, + passed: number, nodes: Map, ): OverloadDispatch { - const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; + const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument} ${passed}`; const existing = nodes.get(key); if (existing) return existing; const best = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); let dispatch: OverloadDispatch = best; if (candidates.length > 1 && candidates.some(signature => argument < signature.args.length)) { const typeOf = (signature: Signature) => signature.args[argument]?.type; - const byKind = new Map( - distinguishingKinds(candidates.flatMap(signature => typeOf(signature) ?? [])).map(kind => { - const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); - return [kind, this.dispatchOn(takers.length > 0 ? takers : [best], argument + 1, nodes)]; - }), - ); - const outcomes = new Set(byKind.values()); - dispatch = outcomes.size === 1 ? byKind.get("other")! : { argument, byKind }; + const next = (kind: ValueKind | "other") => { + const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); + return this.dispatchOn(takers.length > 0 ? takers : [best], argument + 1, passed, nodes); + }; + if (argument >= passed) { + dispatch = next("undefined"); + } else { + const kinds = distinguishingKinds(candidates.flatMap(signature => typeOf(signature) ?? [])); + const byKind = new Map(kinds.map(kind => [kind, next(kind)])); + dispatch = new Set(byKind.values()).size === 1 ? byKind.get("other")! : { argument, byKind }; + } } nodes.set(key, dispatch); return dispatch; diff --git a/src/js/node/os.ts b/src/js/node/os.ts index d2c90632d36c..bd468737bbf3 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -102,16 +102,28 @@ function bound(binding) { : $bundleError("TODO: endianness"); }, freemem: binding.freemem, - getPriority: binding.getPriority, - homedir: binding.homedir, - hostname: binding.hostname, - loadavg: binding.loadavg, - networkInterfaces: binding.networkInterfaces, + getPriority: function (pid) { + return binding.getPriority(pid); + }, + homedir: function () { + return binding.homedir(); + }, + hostname: function () { + return binding.hostname(); + }, + loadavg: function () { + return binding.loadavg(); + }, + networkInterfaces: function () { + return binding.networkInterfaces(); + }, platform: function () { return process.platform; }, release: binding.release, - setPriority: binding.setPriority, + setPriority: function (pid, priority) { + return priority === undefined ? binding.setPriority(pid) : binding.setPriority(pid, priority); + }, get tmpdir() { return tmpdir; }, @@ -127,8 +139,12 @@ function bound(binding) { ? "FreeBSD" : $bundleError("TODO: type"); }, - uptime: binding.uptime, - userInfo: binding.userInfo, + uptime: function () { + return binding.uptime(); + }, + userInfo: function (options) { + return binding.userInfo(options); + }, version: binding.version, machine: function () { // TODO: linux arm64 should also return "aarch64" (Node/uname compat) — diff --git a/src/jsc/bindings/BindgenCustomEnforceRange.h b/src/jsc/bindings/BindgenCustomEnforceRange.h index 7d8dd48cff30..ff8d94a40caa 100644 --- a/src/jsc/bindings/BindgenCustomEnforceRange.h +++ b/src/jsc/bindings/BindgenCustomEnforceRange.h @@ -54,7 +54,7 @@ struct Converter> } unrestricted = value.asNumber(); - if (unrestricted != std::round(unrestricted)) { + if (!std::isfinite(unrestricted) || unrestricted != std::round(unrestricted)) { // ERR_OUT_OF_RANGE "an integer" Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), "an integer"_s, value); return 0; diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 48f4ff181411..9318d10d1f20 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -126,7 +126,12 @@ template<> struct WebCore::Converter if (jsView->isShared()) { return std::nullopt; } - return jsView->unsharedBuffer(); + // A copy: the view's buffer can hold more than the view shows. + if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { + return bytes; + } + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::throwOutOfMemoryError(&globalObject, scope); } return std::nullopt; } diff --git a/src/jsc/bindings/BunIDLConvertNumbers.h b/src/jsc/bindings/BunIDLConvertNumbers.h index 2257ff1b1b45..2e7d8e10b86f 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/BunIDLConvertNumbers.h @@ -89,6 +89,7 @@ struct WebCore::Converter> if constexpr (maxInt < std::numeric_limits::max()) { if (auto result = Bun::Detail::tryBigIntToInt(value)) { ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); + return {}; } } ctx.throwBigIntOutOfRange(globalObject, scope, minInt, maxInt); @@ -100,7 +101,7 @@ struct WebCore::Converter> } double number = value.asNumber(); - if (number > maxSafeInteger || number < -maxSafeInteger) { + if (!(number <= maxSafeInteger && number >= -maxSafeInteger)) { ctx.throwNumberNotInteger(globalObject, scope, number); return {}; } diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 3996fddd4ec3..d87a5de3a267 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -521,6 +521,9 @@ describe("unions", () => { [new Uint8Array(4), "bytes 4"], [new Float64Array(2), "bytes 16"], [new DataView(new ArrayBuffer(5)), "bytes 5"], + [new DataView(new ArrayBuffer(5), 1, 2), "bytes 2"], + [new Uint8Array(7).subarray(2), "bytes 5"], + [new Uint16Array(new ArrayBuffer(8), 2, 1), "bytes 2"], [Buffer.alloc(6).buffer.slice(0, 6), "bytes 6"], [new Blob(["x"]), "blob"], [Bun.file("/nonexistent"), "blob"], @@ -583,6 +586,12 @@ describe("unions", () => { expect(calls).toEqual([]); }); + it("Array.prototype, which b.array refuses, is not of the array kind", () => { + expect(() => bindgen.describeValue(Array.prototype)).toThrow( + "value must be of type number, boolean, string, or array of integer", + ); + }); + itAlways("an alternative that cannot be chosen is an error in the schema", () => { expect(() => b.union({ anything: b.any, text: b.string })).toThrow("text is never chosen"); expect(() => b.union({ text: b.string.loose, flag: b.bool })).toThrow("flag is never chosen"); @@ -590,6 +599,7 @@ describe("unions", () => { expect(() => b.union({ integer: b.i32, number: b.f64 })).toThrow("number is never chosen"); expect(() => b.union({ none: b.null, inner: b.union({ text: b.string }) })).toThrow("list the alternatives"); expect(() => b.union({ text: b.string.nullable, flag: b.bool })).toThrow("add b.null as an alternative"); + expect(() => b.union({ tag: b.bool, text: b.string })).toThrow("has a member of that name"); // What one refuses, the next can take. b.union({ bytes: b.bufferSource, reference: b.bufferSource.arrayBuffer }); b.union({ text: b.string, boxed: b.string.utf8 }); @@ -638,6 +648,14 @@ describe("overloads", () => { expect(() => b.overloads({ args: { a: b.i32, b: b.bool } }, { args: { a: b.f64, b: b.bool } })).toThrow( "overload 2 is never chosen", ); + // With one argument the second is undefined, and with two the last signature takes everything. + expect(() => + b.overloads( + { args: { a: b.string, b: b.f64.optional } }, + { args: { a: b.string, b: b.string.optional } }, + { args: { a: b.string, b: b.string.loose } }, + ), + ).toThrow("overload 2 is never chosen"); b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } }); b.overloads({ args: { a: b.string, b: b.f64 } }, { args: { a: b.string, b: b.bool } }); b.overloads({ args: { a: b.bool } }, { args: { a: b.any } }); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index cfcedbaf61a4..1b97d2e9e5a0 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -2,6 +2,7 @@ import type { Socket } from "bun"; import { connect, fileURLToPath, SocketHandler, spawn } from "bun"; import { createSocketPair, setSocketOptions, socketFaultInjection } from "bun:internal-for-testing"; import { afterAll, afterEach, describe, expect, it, jest } from "bun:test"; +import { generateKeyPairSync } from "crypto"; import { closeSync, fstatSync, readFileSync } from "fs"; import { bunEnv, @@ -6566,6 +6567,41 @@ it("socket and tls options are not read from Object.prototype", async () => { } }); +it("a view of part of a buffer as a tls option is read as the view", () => { + const { privateKey: otherKey } = generateKeyPairSync("ec", { + namedCurve: "P-256", + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + publicKeyEncoding: { type: "spki", format: "pem" }, + }); + const listen = (key: unknown) => + Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} }, tls: { key, cert: tls.cert } as any }); + // OpenSSL reads the first key it finds, which does not go with the certificate. + const both = Buffer.concat([Buffer.from(otherKey), Buffer.from(tls.key)]); + expect(() => listen(both)).toThrow("KEY_TYPE_MISMATCH"); + const [offset, length] = [both.byteOffset + Buffer.byteLength(otherKey), Buffer.byteLength(tls.key)]; + for (const view of [ + both.subarray(Buffer.byteLength(otherKey)), + new Uint8Array(both.buffer, offset, length), + new DataView(both.buffer, offset, length), + ]) { + listen(view).stop(true); + listen([view]).stop(true); + } +}); + +it("Array.prototype as a tls option is none of the types one takes", () => { + for (const name of ["key", "cert", "ca", "crl"]) { + expect(() => + Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} }, tls: { [name]: Array.prototype } as any }), + ).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: `TLSOptions.${name} must be of type string, ArrayBuffer, Blob, array of , or null`, + }), + ); + } +}); + it("a view of shared memory as a tls option is refused", () => { const shared = new SharedArrayBuffer(8); const values = [shared, new Uint8Array(shared), new DataView(shared), Buffer.from(shared), [new Uint8Array(shared)]]; diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index f993347b7f4d..394ac2b119dc 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -353,24 +353,54 @@ it("userInfo ignores options that are not an object, like Node", () => { } }); -it("setPriority without arguments throws ERR_MISSING_ARGS", () => { +it("setPriority with an undefined priority takes the first argument as the priority", () => { expect(() => os.setPriority()).toThrow( - expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: 'The "priority" argument must be of type number. Received undefined', + }), ); + expect(() => os.setPriority(20, undefined)).toThrow( + expect.objectContaining({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: 'The value of "priority" is out of range. It must be >= -20 and <= 19. Received 20', + }), + ); + const priority = os.getPriority(); + os.setPriority(priority, undefined); + expect(os.getPriority()).toBe(priority); }); -it("native functions are not constructors", () => { - for (const name of [ - "freemem", - "getPriority", - "homedir", - "hostname", - "loadavg", - "release", - "setPriority", - "totalmem", - "userInfo", - ]) { - expect(() => new os[name]()).toThrow(TypeError); +it("an infinite pid or priority is not an integer", () => { + for (const value of [Infinity, -Infinity, NaN, 1.5]) { + expect(() => os.getPriority(value)).toThrow( + expect.objectContaining({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: `The value of "pid" is out of range. It must be an integer. Received ${value}`, + }), + ); + expect(() => os.setPriority(value)).toThrow( + expect.objectContaining({ + name: "RangeError", + code: "ERR_OUT_OF_RANGE", + message: `The value of "priority" is out of range. It must be an integer. Received ${value}`, + }), + ); + } +}); + +it("with new, each function does what it does in Node", () => { + expect(new os.loadavg()).toBeArrayOfSize(3); + expect(new os.networkInterfaces()).toEqual(os.networkInterfaces()); + expect(new os.userInfo()).toEqual(os.userInfo()); + for (const name of ["getPriority", "homedir", "hostname", "uptime"]) { + expect(new os[name]()).toBeObject(); + } + expect(() => new os.setPriority()).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); + for (const name of ["freemem", "totalmem", "release", "version"]) { + expect(() => new os[name]()).toThrow("is not a constructor"); } }); From 7affdf96a2eb3d37e7b14df686f735f99cb9be24 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 11:42:06 +0000 Subject: [PATCH 31/44] Stop looking at the number of arguments: one that was not passed is undefined, for conversions and for overloads --- docs/project/bindgen.mdx | 15 ++++++----- src/codegen/bindgen-emit.ts | 14 ++-------- src/codegen/bindgen-lib-internal.ts | 41 ++++++++++------------------- src/js/node/os.ts | 2 +- test/internal/bindgen.test.ts | 18 ++++++++----- test/js/bun/glob/proto.test.ts | 9 ++----- test/js/bun/shell/brace.test.ts | 7 +++-- 7 files changed, 41 insertions(+), 65 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index cf0c42a84b14..87238b14c8c8 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -109,12 +109,12 @@ export const read = b.overloads( 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 candidates are the signatures that do not require more arguments than were -passed, or, if every one requires more, those that require the fewest. Then, argument by argument, those that take a value of its -[kind](#unions-and-kinds) stay; a signature that has no such argument ignores it. -If several are left when the arguments run out, or none takes the kind, the call goes -to the one that requires the most arguments, then to the one written first, and its -conversion reports what is wrong. Choosing converts nothing and runs no JavaScript. A +Argument by argument, the signatures that take a value of its [kind](#unions-and-kinds) +stay. An argument that was not passed is `undefined`, which a required argument does not +take, so `f(x)` and `f(x, undefined)` are the same call. A signature that has no such +argument ignores it, and an argument that none of them takes rules nothing out. If +several are left when the arguments run out, the call goes to the one that requires the +most arguments, then to the one written first, and its conversion reports what is wrong. Choosing converts nothing and runs no JavaScript. A signature that no call can reach is an error when the schema is loaded. ## Types @@ -123,7 +123,8 @@ Conversions are strict unless the schema says otherwise: `b.i32` takes a number or BigInt that is an integer in range, and nothing else. A failed strict, `.loose` or `.validateInteger` conversion throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary member; `.wrap` and `.enforceRange` throw -what WebIDL does. Too few arguments throw `ERR_MISSING_ARGS`. +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 | | ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- | diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 164b4808d64b..326857397d56 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -522,14 +522,9 @@ function cppDeclareConverted(type: Type, variable: string, value: string, ctx: s /** Converts the arguments, then returns what `symbol(globalObject, ...leading, ...converted)` returns. */ function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string { const lines: string[] = []; - if (signature.requiredCount > 0) { - lines.push(`if (callFrame->argumentCount() < ${signature.requiredCount}) [[unlikely]] - return JSC::throwVMError(globalObject, scope, WebCore::createNotEnoughArgumentsErrorBun(globalObject));`); - } const rereads: string[] = []; signature.args.forEach(({ name, type }, i) => { - const argument = i < signature.requiredCount ? "uncheckedArgument" : "argument"; - lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->${argument}(${i});`); + lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->argument(${i});`); lines.push(`[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); // Converting a later argument can run JavaScript that detaches or resizes the buffer. if (type.borrowsHeap && i < signature.args.length - 1) @@ -807,16 +802,11 @@ switch (choice) { ${[...cases.slice(1), cases[0]].join("\n")} }`; }; - const dispatch = fn.dispatchByArgumentCount.map(({ atLeast, dispatch }, index) => - index === fn.dispatchByArgumentCount.length - 1 - ? indent(cppDispatch(dispatch), 4) - : ` if (callFrame->argumentCount() >= ${atLeast}) {\n${indent(cppDispatch(dispatch), 8)}\n }`, - ); definition = `${overloads.join("\n")} JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { ${cppPrologue} -${dispatch.join("\n")} +${indent(cppDispatch(fn.dispatch), 4)} }`; } return ` diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index c9d6da6ee2bb..b104b909781a 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1361,24 +1361,14 @@ export type OverloadDispatch = Signature | { argument: number; byKind: Map 0, "b.overloads needs at least one signature"); this.signatures = signatures.map(options => new Signature(options)); - const counts = [...new Set(this.signatures.map(signature => signature.requiredCount))].sort((a, b) => b - a); const nodes = new Map(); - this.dispatchByArgumentCount = counts.map((atLeast, i) => ({ - atLeast, - dispatch: this.dispatchOn( - this.signatures.filter(signature => signature.requiredCount <= atLeast), - 0, - i === 0 ? Infinity : counts[i - 1] - 1, - nodes, - ), - })); + this.dispatch = this.dispatchOn(this.signatures, 0, nodes); const chosen = new Set(nodes.values()); this.signatures.forEach((signature, i) => assert( @@ -1387,31 +1377,28 @@ export class FunctionDefinition { ), ); } - /** "Overloads" in the docs, for a call with at most `passed` arguments. Equal inputs give the same object of `nodes`: nodes are compared by identity. */ + /** "Overloads" in the docs. Equal inputs give the same object of `nodes`: nodes are compared by identity. */ private dispatchOn( candidates: readonly Signature[], argument: number, - passed: number, nodes: Map, ): OverloadDispatch { - const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument} ${passed}`; + const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; const existing = nodes.get(key); if (existing) return existing; - const best = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); - let dispatch: OverloadDispatch = best; + let dispatch: OverloadDispatch = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); if (candidates.length > 1 && candidates.some(signature => argument < signature.args.length)) { - const typeOf = (signature: Signature) => signature.args[argument]?.type; + const types = candidates.map(signature => signature.args[argument]?.type); const next = (kind: ValueKind | "other") => { - const takers = candidates.filter(signature => !typeOf(signature) || takes(typeOf(signature), kind)); - return this.dispatchOn(takers.length > 0 ? takers : [best], argument + 1, passed, nodes); + const takers = candidates.filter((signature, i) => { + if (!types[i]) return true; + return kind === "undefined" ? types[i].permitsUndefined : takes(types[i], kind); + }); + return this.dispatchOn(takers.length > 0 ? takers : candidates, argument + 1, nodes); }; - if (argument >= passed) { - dispatch = next("undefined"); - } else { - const kinds = distinguishingKinds(candidates.flatMap(signature => typeOf(signature) ?? [])); - const byKind = new Map(kinds.map(kind => [kind, next(kind)])); - dispatch = new Set(byKind.values()).size === 1 ? byKind.get("other")! : { argument, byKind }; - } + const kinds = new Set(["undefined" as const, ...distinguishingKinds(types.filter(type => type !== undefined))]); + const byKind = new Map([...kinds].map(kind => [kind, next(kind)])); + dispatch = new Set(byKind.values()).size === 1 ? byKind.get("other")! : { argument, byKind }; } nodes.set(key, dispatch); return dispatch; diff --git a/src/js/node/os.ts b/src/js/node/os.ts index bd468737bbf3..accf8b3932c6 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -122,7 +122,7 @@ function bound(binding) { }, release: binding.release, setPriority: function (pid, priority) { - return priority === undefined ? binding.setPriority(pid) : binding.setPriority(pid, priority); + return binding.setPriority(pid, priority); }, get tmpdir() { return tmpdir; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index d87a5de3a267..6eba85e2dd0b 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -157,10 +157,13 @@ describe("strings", () => { expect(bindgen.utf8Optional("")).toBe(""); }); - it("checks the argument count", () => { - expect(() => bindgen.utf8Echo()).toThrow( - expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), - ); + it("an argument that was not passed is undefined", () => { + for (const args of [[], [undefined]]) { + expect(() => bindgen.utf8Echo(...args)).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message: "text must be a string" }), + ); + expect(bindgen.utf8EchoLoose(...args)).toBe("undefined"); + } }); it("keeps the characters of a substring while its base is swapped for an atom", () => { @@ -617,6 +620,7 @@ describe("overloads", () => { [["ab", true], "text upper AB"], [[new String("ab"), false], "text upper ab"], [[1], "number 1"], + [[1, undefined], "number 1"], [[1, 2], "two numbers 1 2"], [[1, 2, 3], "two numbers 1 2"], [[[1]], "list [1.0]"], @@ -632,9 +636,9 @@ describe("overloads", () => { }); it("what fits none is reported by the one that requires the most arguments", () => { - expect(() => bindgen.overloaded()).toThrow(expect.objectContaining({ code: "ERR_MISSING_ARGS" })); + expect(() => bindgen.overloaded()).toThrow("text must be a string"); expect(() => bindgen.overloaded({})).toThrow("text must be a string"); - expect(() => bindgen.overloaded(null, 1)).toThrow("text must be a string"); + expect(() => bindgen.overloaded(null, 1)).toThrow("a must be a number"); expect(() => bindgen.overloaded("ab", "x")).toThrow("upper must be a boolean"); }); @@ -648,7 +652,7 @@ describe("overloads", () => { expect(() => b.overloads({ args: { a: b.i32, b: b.bool } }, { args: { a: b.f64, b: b.bool } })).toThrow( "overload 2 is never chosen", ); - // With one argument the second is undefined, and with two the last signature takes everything. + // The first takes an undefined second argument, and the last takes everything else. expect(() => b.overloads( { args: { a: b.string, b: b.f64.optional } }, diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index 8a6b2ad6f463..5dd0647e1605 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -48,15 +48,10 @@ test("Object prototype followSymlinks", async () => { test("arguments of the wrong type throw a TypeError", () => { const invalid = (message: string) => expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message }); - const missing = expect.objectContaining({ - name: "TypeError", - code: "ERR_MISSING_ARGS", - message: "Not enough arguments", - }); const glob = new Bun.Glob("*"); - expect(() => new (Bun.Glob as any)()).toThrow(missing); + expect(() => new (Bun.Glob as any)()).toThrow(invalid("pattern must be a string")); expect(() => new Bun.Glob(5 as any)).toThrow(invalid("pattern must be a string")); - expect(() => (glob.match as any)()).toThrow(missing); + expect(() => (glob.match as any)()).toThrow(invalid("str must be a string")); expect(() => glob.match(5 as any)).toThrow(invalid("str must be a string")); expect(() => glob.scanSync(5 as any)).toThrow(invalid("GlobScanOptions must be an object")); expect(() => glob.scan(5 as any)).toThrow(invalid("GlobScanOptions must be an object")); diff --git a/test/js/bun/shell/brace.test.ts b/test/js/bun/shell/brace.test.ts index b6b5e1339e80..eecddfced8a0 100644 --- a/test/js/bun/shell/brace.test.ts +++ b/test/js/bun/shell/brace.test.ts @@ -359,9 +359,8 @@ describe("$.braces arguments", () => { } }); - test("without arguments throws ERR_MISSING_ARGS", () => { - expect(() => ($.braces as any)()).toThrow( - expect.objectContaining({ name: "TypeError", code: "ERR_MISSING_ARGS", message: "Not enough arguments" }), - ); + test("an argument that was not passed is undefined", () => { + expect(($.braces as any)()).toEqual(["undefined"]); + expect($.braces(undefined as any)).toEqual(["undefined"]); }); }); From 8b7bab6861924ff3d3cbe53188e30509c565b84b Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 11:45:39 +0000 Subject: [PATCH 32/44] Give a dictionary that is only returned a lifetime only when it holds a JS value --- src/codegen/bindgen-emit.ts | 4 ++-- src/codegen/bindgen-lib-internal.ts | 4 ++++ src/js/internal-for-testing.ts | 1 + src/jsc/bindgen_test.bind.ts | 7 +++++++ src/jsc/bindgen_test.rs | 13 +++++++++++-- test/internal/bindgen.test.ts | 5 +++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 326857397d56..89f02c06641b 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -1085,8 +1085,8 @@ impl ${type.name}${lifetime ? "<'_>" : ""} { } return `${member.type.rustToJS(field, scope)}?.to_js()`; }; - text += `impl<'a> ${type.name}${lifetime} { - pub fn to_js(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + text += `impl${lifetime} ${type.name}${lifetime} { + pub fn to_js${lifetime ? "" : "<'a>"}(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { let values = [ ${type.members.map(member => ` ${value(member)},`).join("\n")} ]; diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index b104b909781a..922f9f5caaad 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1153,6 +1153,10 @@ export class DictionaryType extends NamedType { "from_js outlives the frame b.string.utf8 borrows from; use b.string or b.string.utf8.owned", ); } + /** One that is only returned owns its strings, so nothing but a JS value in it has a lifetime. */ + get needsLifetime(): boolean { + return this.accepted ? super.needsLifetime : this.holdsJSValue; + } get userFacingName(): string { return this.options.userFacingName ?? this.name; } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index bc0f76f981e4..b1ee597cb0e0 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -257,6 +257,7 @@ export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { describeAnything: $bindgenFn("bindgen_test.bind.ts", "describeAnything"), overloaded: $bindgenFn("bindgen_test.bind.ts", "overloaded"), describeDefaults: $bindgenFn("bindgen_test.bind.ts", "describeDefaults"), + names: $bindgenFn("bindgen_test.bind.ts", "names"), sumArray: $bindgenFn("bindgen_test.bind.ts", "sumArray"), range: $bindgenFn("bindgen_test.bind.ts", "range"), words: $bindgenFn("bindgen_test.bind.ts", "words"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index bdddf60670ed..2421cf27c7e8 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -52,6 +52,13 @@ export const TestOptionsEcho = b.dictionary({ }); export const optionsEcho = b.fn({ args: { options: TestOptions }, returns: TestOptionsEcho, capability: "heap" }); +export const TestNames = b.dictionary({ + first: b.string.utf8, + nickname: b.string.utf8.nullable, + all: b.array(b.string.utf8), +}); +export const names = b.fn({ args: { text: b.string.utf8 }, returns: TestNames }); + export const TestValue = b.union({ number: b.f64, flag: b.bool, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index f990dc3c2f68..86c9ca4d47a8 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -9,8 +9,8 @@ use crate::bindgen::{ }; use crate::generated_bindgen::bindgen_test::{ TestAnything, TestChoice, TestColor, TestCounterClass, TestCounterInternalFields as _, - TestFailure, TestHolderClass, TestOptions, TestOptionsEcho, TestOuter, TestTokenClass, - TestValue, + TestFailure, TestHolderClass, TestNames, TestOptions, TestOptionsEcho, TestOuter, + TestTokenClass, TestValue, }; // This example should be kept in sync with bindgen's documentation @@ -172,6 +172,15 @@ pub fn range(length: u32) -> core::ops::Range { 0..length } +pub fn names(text: &[u8]) -> TestNames { + let all: Vec> = words(text).map(<[u8]>::to_vec).collect(); + TestNames { + first: all.first().cloned().unwrap_or_default(), + nickname: all.get(1).cloned(), + all, + } +} + pub fn words(text: &[u8]) -> impl Iterator { bun_core::strings::tokenize(text, b" ") } diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 6eba85e2dd0b..c762ea2e2577 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -264,6 +264,11 @@ describe("dictionaries", () => { } }); + it("returns one that holds only strings", () => { + expect(bindgen.names("ana bé c")).toStrictEqual({ first: "ana", nickname: "bé", all: ["ana", "bé", "c"] }); + expect(bindgen.names("")).toStrictEqual({ first: "", nickname: null, all: [] }); + }); + it("returns ordinary objects", () => { const seen = bindgen.optionsEcho({ name: "a", renamed: 1 }); expect(Object.getPrototypeOf(seen)).toBe(Object.prototype); From 68559e51e85be55eab2083bd4d2c97c70a56c785 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 23:08:38 +0000 Subject: [PATCH 33/44] Rename what the naming review found false or ambiguous, apply the conventions review, and make the generated C++ plainer --- docs/project/bindgen.mdx | 48 +-- scripts/build/codegen.ts | 31 +- scripts/glob-sources.ts | 2 +- src/codegen/bindgen-emit.ts | 212 +++++++------ src/codegen/bindgen-lib-internal.ts | 297 +++++++++--------- src/codegen/bindgen-lib.ts | 10 +- src/js/node/os.ts | 1 + src/jsc/bindgen.rs | 23 +- src/jsc/bindgen/extern_types.rs | 28 +- src/jsc/bindgen_test.bind.ts | 4 +- src/jsc/bindgen_test.rs | 4 +- src/jsc/bindings/Bindgen.cpp | 22 +- .../bindings/Bindgen/DictionaryMemberReader.h | 6 +- src/jsc/bindings/Bindgen/ExternTraits.h | 1 - .../bindings/Bindgen/IDLConvertIntegerRange.h | 58 ++++ src/jsc/bindings/Bindgen/IDLTypes.h | 4 +- src/jsc/bindings/Bindgen/Identifiers.h | 4 +- src/jsc/bindings/Bindgen/JSBindgenObject.h | 2 +- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 4 +- .../bindings/BindgenAdditionsToGlobalObject.h | 2 +- src/jsc/bindings/BindgenCustomEnforceRange.h | 91 ------ src/jsc/bindings/BunIDLConvert.h | 6 +- src/jsc/bindings/BunIDLConvertContext.h | 7 +- src/jsc/bindings/BunIDLConvertNumbers.h | 1 + src/jsc/bindings/BunIDLHumanReadable.h | 6 +- src/jsc/bindings/BunIDLTypes.h | 4 +- src/jsc/bindings/StrongRef.h | 14 - .../webcore/JSDOMConvertEnumeration.h | 2 +- src/jsc/fmt_jsc.rs | 4 +- src/runtime/node/node_os.bind.ts | 3 - test/internal/bindgen.test.ts | 33 +- .../build-codegen-declared-outputs.test.ts | 4 +- test/js/bun/glob/proto.test.ts | 1 - test/js/bun/net/socket.test.ts | 6 +- 34 files changed, 474 insertions(+), 471 deletions(-) create mode 100644 src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h delete mode 100644 src/jsc/bindings/BindgenCustomEnforceRange.h diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 87238b14c8c8..dbf62f770f6d 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -60,8 +60,9 @@ pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { a function gives its full path instead. `length` sets the function's `.length`, which is otherwise the number of required arguments. -Nothing else is written by hand. If the Rust signature does not match the -schema, the generated code does not compile. +Nothing else is written by hand, except once per crate: `src//generated_bindgen.rs` +includes the generated file, and the crate root declares `mod generated_bindgen`. If the +Rust signature does not match the schema, the generated code does not compile. To reach the function: @@ -109,21 +110,28 @@ export const read = b.overloads( 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. -Argument by argument, the signatures that take a value of its [kind](#unions-and-kinds) -stay. An argument that was not passed is `undefined`, which a required argument does not -take, so `f(x)` and `f(x, undefined)` are the same call. A signature that has no such -argument ignores it, and an argument that none of them takes rules nothing out. If -several are left when the arguments run out, the call goes to the one that requires the -most arguments, then to the one written first, and its conversion reports what is wrong. Choosing converts nothing and runs no JavaScript. A -signature that no call can reach is an error when the schema is loaded. +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`, which a required argument does not + take. 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 that requires the most arguments wins, then + the one written first. Its conversion reports whatever is wrong. + +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 -or BigInt that is an integer in range, and nothing else. A failed strict, `.loose` or -`.validateInteger` conversion throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` -naming the argument or the dictionary member; `.wrap` and `.enforceRange` throw -what WebIDL does. An argument that was not passed is `undefined` and is converted like +or BigInt 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 | @@ -149,12 +157,12 @@ any other: nothing looks at how many there are. 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. -A type takes one of these, last (`b.any` already accepts `undefined` and `null`, and takes only `.withValue`): +These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.withValue` applies to it): -- `.optional`: `undefined` means "not given"; `Option`. -- `.nullable`: `null` or `undefined` means "not given"; `Option`. -- `.falsyAsNull`: every falsy value means "not given"; `Option`. -- `.default(value)`: what to use when not given; `T`. `.nullable.default(value)` also applies to `null`. +- `.optional`: `undefined` means absent; `Option`. +- `.nullable`: `null` or `undefined` means absent; `Option`. +- `.falsyAsNull`: every falsy value means absent; `Option`. +- `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. - `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything @@ -296,7 +304,7 @@ A JS value in Rust is a `JsValue<'a>`: a `JSValue` tied to the host call it belo 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. `JsValue<'a, T>` is one known to be a `JSString`, -`JSObject`, `JSFunction` or `ArrayBuffer`. `value.to_js()` gives the `JSValue`; +`JSObject`, `Callable` or `ArrayBuffer`. `value.to_js()` gives the `JSValue`; `JsValue::new(heap, value)` goes the other way. What outlives a call: @@ -387,7 +395,7 @@ long as nothing else can run: ```rust fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { - if let Some(listener) = this.listener_field(&cx.heap).as_function() { + 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)) diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index c24c8bcdb413..76ecbc5560c4 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -934,31 +934,32 @@ function withoutBindingsFlag(cfg: CodegenFields, sources: Sources): string { } /** Exported for test/internal/build-codegen-declared-outputs.test.ts. */ -export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): void { +export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); - const sources = { ...allSources, bindgen: allSources.bindgen.filter(src => hasBindings(cfg, src)) }; + const crates = new Map(sources.bindgen.map(src => [src, crateOf(src)])); + const compiled = sources.bindgen.filter(src => hasBindings(cfg, src)); // What bindgen.ts writes follows from the names and places of its inputs alone. - const stems = sources.bindgen.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); - stems.forEach((stem, i) => { - const first = stems.indexOf(stem); - if (first !== i) - throw new BuildError(`${sources.bindgen[first]} and ${sources.bindgen[i]} both generate ${stem}.h`); + const outputNames = compiled.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); + outputNames.forEach((name, i) => { + const first = outputNames.indexOf(name); + if (first !== i) throw new BuildError(`${compiled[first]} and ${compiled[i]} both generate ${name}.h`); }); - const cpp = stems.map(stem => resolve(cfg.codegenDir, `${stem}.cpp`)); + const cpp = outputNames.map(name => resolve(cfg.codegenDir, `${name}.cpp`)); const headers = [ - ...stems.map(stem => resolve(cfg.codegenDir, `${stem}.h`)), + ...outputNames.map(name => resolve(cfg.codegenDir, `${name}.h`)), resolve(cfg.codegenDir, "GeneratedBindings+Tables.h"), resolve(cfg.codegenDir, "GeneratedBindings+Counts.h"), ]; - const crates = sources.bindgen.map(crateOf); - const rust = [...new Set(crates)].map(crate => resolve(cfg.codegenDir, `generated_bindgen_${crate}.rs`)); + const rust = [...new Set(compiled.map(src => crates.get(src)))].map(crate => + resolve(cfg.codegenDir, `generated_bindgen_${crate}.rs`), + ); const types = resolve(cfg.typesDir, "Bindgen.d.ts"); n.build({ outputs: [...cpp, ...headers, ...rust, types], rule: "codegen", - inputs: [...allSources.bindgen, ...sources.bindgenInternal], + inputs: [...sources.bindgen, ...sources.bindgenCodegen], orderOnlyInputs: [dirStamp], vars: { cwd: cfg.cwd, @@ -966,8 +967,8 @@ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): args: shJoin(cfg, [ script, // Every schema: build/types is shared by every profile, so Bindgen.d.ts describes them all. - `--sources=${allSources.bindgen.map(src => `${crateOf(src)}=${src}`).join(",")}`, - withoutBindingsFlag(cfg, allSources), + `--sources=${sources.bindgen.map(src => `${crates.get(src)}=${src}`).join(",")}`, + withoutBindingsFlag(cfg, sources), `--codegen-path=${cfg.codegenDir}`, `--types-path=${cfg.typesDir}`, ]), @@ -981,7 +982,7 @@ export function emitBindgen({ n, cfg, sources: allSources, o, dirStamp }: Ctx): o.cppHeaders.push(...headers); } -/** Same transform as `pascalCase()` in src/codegen/bindgen-lib-internal.ts: `node_os` → `NodeOs`. */ +/** `node_os` → `NodeOs`. A copy of `pascalCase()` in src/codegen/bindgen-lib-internal.ts, which node warns about importing here (MODULE_TYPELESS_PACKAGE_JSON). build-codegen-declared-outputs.test.ts holds the two together. */ function pascalCase(s: string): string { return s .split(/[^A-Za-z0-9]+/) diff --git a/scripts/glob-sources.ts b/scripts/glob-sources.ts index 7ddb597dda7a..97a1848f84c3 100644 --- a/scripts/glob-sources.ts +++ b/scripts/glob-sources.ts @@ -72,7 +72,7 @@ const patterns = { paths: ["src/**/*.bind.ts"], }, /** bindgen generator code */ - bindgenInternal: { + bindgenCodegen: { paths: ["src/codegen/bindgen*.ts", "src/codegen/helpers.ts"], }, /** diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 89f02c06641b..575374162960 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -7,7 +7,7 @@ import { cppEmplace, DefaultType, DictionaryType, - EnumType, + EnumerationType, type FunctionDefinition, type Getter, indent, @@ -65,7 +65,7 @@ export function analyze(files: readonly SchemaFile[]): Tables { const tables: Tables = { memberNames: [], enumerationValues: [], objectStructures: [], classes: [] }; const markAccepted = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.accepted = true))); const markReturned = (type: Type) => - walk(type, t => void ((t instanceof DictionaryType || t instanceof EnumType) && (t.returned = true))); + walk(type, t => void ((t instanceof DictionaryType || t instanceof EnumerationType) && (t.returned = true))); for (const file of files) { for (const type of file.types) { if (type instanceof DictionaryType && type.fromJS) markAccepted(type); @@ -95,7 +95,7 @@ export function analyze(files: readonly SchemaFile[]): Tables { } for (const file of files) { for (const type of file.types) { - if (type instanceof EnumType) { + if (type instanceof EnumerationType) { if (!type.returned) continue; type.firstEnumerationValueIndex = tables.enumerationValues.length; tables.enumerationValues.push(...type.spellings.map(([canonical]) => canonical)); @@ -126,7 +126,8 @@ export function analyze(files: readonly SchemaFile[]): Tables { ...file.functions.flatMap(fn => fn.signatures.map(signature => thunkSymbol(fn, signature))), ...file.types.flatMap(type => (type instanceof ClassType ? classSymbols(type) : [])), ]); - assert(new Set(symbols).size === symbols.length, "two bindings make the same symbol"); + const repeated = symbols.filter((symbol, i) => symbols.indexOf(symbol) !== i); + assert(repeated.length === 0, `two bindings make the same symbol: ${repeated.join(", ")}`); return tables; } @@ -135,9 +136,9 @@ export function analyze(files: readonly SchemaFile[]): Tables { const implementationType = (type: Type) => `${type.idlType}::ImplementationType`; const externType = (type: Type): string => type instanceof WithValueType - ? `::Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` - : `::Bun::Bindgen::ExternOf<${type.idlType}>`; -const toExtern = (variable: string) => `::Bun::Bindgen::toExtern(${variable})`; + ? `Bun::Bindgen::ExternWithValue<${externType(type.inner)}>` + : `Bun::Bindgen::ExternOf<${type.idlType}>`; +const toExtern = (variable: string) => `Bun::Bindgen::toExtern(${variable})`; /** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` from `type`, the cell's type byte. */ const kindTests: Record, { ofValue?: (value: string) => string; ofCell?: string }> = { @@ -153,8 +154,11 @@ const kindTests: Record, { ofValue?: (value: string) arrayBufferView: { ofCell: "JSC::isTypedArrayTypeIncludingDataView(type)" }, }; -/** C++ that declares `choice`, an index into the `outcomes` it returns: the distinct values of `byKind`, "other"'s first. */ -function cppChooseByKind(value: string, byKind: ReadonlyMap) { +/** + * C++ that declares `choice`, an index into the `outcomes` it returns: the distinct values of `byKind`, "other"'s first. + * `condition`, if the choice is that simple, is true of `outcomes[1]` and false of `outcomes[0]`. + */ +function cppChooseByKind(value: string, byKind: ReadonlyMap, choice = "choice") { const other = byKind.get("other")!; const outcomes = [...new Set([other, ...byKind.values()])]; const ofValue = new Map(); @@ -180,19 +184,22 @@ function cppChooseByKind(value: string, byKind: ReadonlyMap conditions.includes(test)) ? [`${value}.isUndefinedOrNull()`, ...conditions.filter(test => !nullish.includes(test))] : conditions; - return `if (${merged.join(" || ")})\n choice = ${outcomes.indexOf(outcome)};`; - }) + return { condition: merged.join(" || "), index: outcomes.indexOf(outcome) }; + }); + const cppChain = (...tests: Map[]) => + chain(...tests) + .map(({ condition, index }) => `if (${condition})\n ${choice} = ${index};`) .join("\nelse "); - const lines = ["unsigned choice = 0;"]; + const lines = [`unsigned ${choice} = 0;`]; if (ofType.size + ofClass.size > 0) { - lines.push(`if (${value}.isCell()) {`); - if (ofClass.size > 0) lines.push(` auto* cell = ${value}.asCell();`); - if (ofType.size > 0) lines.push(` JSC::JSType type = ${value}.asCell()->type();`); - lines.push(indent(chain(ofType, ofClass), 4), ofValue.size > 0 ? `} else ${chain(ofValue)}` : "}"); + lines.push(`if (${value}.isCell()) {`, ` auto* cell = ${value}.asCell();`); + if (ofType.size > 0) lines.push(" JSC::JSType type = cell->type();"); + lines.push(indent(cppChain(ofType, ofClass), 4), ofValue.size > 0 ? `} else ${cppChain(ofValue)}` : "}"); } else if (ofValue.size > 0) { - lines.push(chain(ofValue)); + lines.push(cppChain(ofValue)); } - return { outcomes, code: lines.join("\n") }; + const isSimple = outcomes.length === 2 && ofType.size + ofClass.size === 0; + return { outcomes, code: lines.join("\n"), condition: isSimple ? chain(ofValue)[0].condition : undefined }; } function layoutAssert(cppType: string, type: Type): string { @@ -200,7 +207,7 @@ function layoutAssert(cppType: string, type: Type): string { return `static_assert(sizeof(${cppType}) == ${size} && alignof(${cppType}) == ${align});`; } -function humanReadableEnum(type: EnumType): string { +function humanReadableEnumeration(type: EnumerationType): string { const quoted = type.spellings.map(([canonical]) => `\\"${canonical}\\"`); if (quoted.length <= 2) return quoted.join(" or "); return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; @@ -215,9 +222,9 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) .map(i => { const { name, type: alternative } = type.alternatives[i]; const member = `result.${name}`; - if (!alternative.takesEveryValueOfItsKinds) - return `if (auto converted = ::Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { - ${member} = ::std::move(*converted); + if (alternative.mayRefuse) + return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { + ${member} = std::move(*converted); result.tag = ${i}; return; } @@ -236,35 +243,32 @@ function cppHeaderForUnion(type: UnionType): string { const { name, alternatives } = type; const idlTypes = alternatives.map(a => a.type.idlType).join(", "); const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); - const mayRefuse = (candidates: readonly number[]) => - candidates.length === 0 || !alternatives[candidates.at(-1)!].type.takesEveryValueOfItsKinds; + const mayMatchNone = (candidates: readonly number[]) => + candidates.length === 0 || alternatives[candidates.at(-1)!].type.mayRefuse; const cases = outcomes.flatMap((candidates, index) => candidates.length === 0 ? [] - : `case ${index}: {\n${indent(cppConvertToAlternative(type, candidates), 4)}${mayRefuse(candidates) ? "\n break;" : ""}\n}`, + : `case ${index}: {\n${indent(cppConvertToAlternative(type, candidates), 4)}${mayMatchNone(candidates) ? "\n break;" : ""}\n}`, ); const held = alternatives.filter(a => !(a.type instanceof UnitType)); return ` -namespace Bun { -namespace Bindgen { -namespace Generated { -// A member for each alternative, so that the one \`tag\` names is converted where it stays. A large one is made when chosen. +namespace Bun::Bindgen::Generated { struct ${name} { ${cppDefaultConstructor(name)} -${held.map(a => ` ${a.type.convertsInPlace ? `::std::optional<${implementationType(a.type)}> ${a.name}` : `${implementationType(a.type)} ${a.name} {}`};`).join("\n")} - ::std::uint8_t tag; +${held.map(a => ` ${a.type.convertsInPlace ? `std::optional<${implementationType(a.type)}> ${a.name}` : `${implementationType(a.type)} ${a.name} {}`};`).join("\n")} + std::uint8_t tag; }; struct Extern${name} { union { ${alternatives.map(a => ` ${externType(a.type)} ${a.name};`).join("\n")} } data; - ::std::uint8_t tag; + std::uint8_t tag; }; ${layoutAssert(`Extern${name}`, type)} } -template<> struct ExternTraits { - using ExternType = Generated::Extern${name}; - static ExternType convertToExtern(Generated::${name}&& value) +template<> struct Bun::Bindgen::ExternTraits<${type.cpp}> { + using ExternType = Bun::Bindgen::Generated::Extern${name}; + static ExternType convertToExtern(${type.cpp}&& value) { ExternType result; result.tag = value.tag; @@ -272,7 +276,7 @@ template<> struct ExternTraits { ${held .map( a => ` case ${alternatives.indexOf(a)}: - result.data.${a.name} = toExtern(${a.type.convertsInPlace ? "*" : ""}value.${a.name}); + result.data.${a.name} = ${toExtern(`${a.type.convertsInPlace ? "*" : ""}value.${a.name}`)}; break;`, ) .join("\n")} @@ -280,14 +284,12 @@ ${held return result; } }; -} -template<> struct IDLHumanReadableName<${type.idlType}> : IDLHumanReadableName<::Bun::IDLOrderedUnion<${idlTypes}>> {}; -} -template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextConverter<${type.idlType}> {${ +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::IDLHumanReadableName> {}; +template<> struct WebCore::Converter<${type.idlType}> : Bun::DefaultContextConverter<${type.idlType}> {${ type.needsLifetime ? "" : ` - template<::Bun::IDLConversionContext Ctx> + template static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) { ReturnType result; @@ -296,7 +298,7 @@ template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextCon } ` } - template<::Bun::IDLConversionContext Ctx> + template static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) { auto* globalObject = &lexicalGlobalObject; @@ -305,7 +307,7 @@ template<> struct WebCore::Converter<${type.idlType}> : ::Bun::DefaultContextCon ${indent(code, 8)} switch (choice) { ${indent(cases.join("\n"), 8)} - }${outcomes.some(mayRefuse) ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);` : ""} + }${outcomes.some(mayMatchNone) ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);` : ""} } }; `; @@ -314,21 +316,17 @@ ${indent(cases.join("\n"), 8)} function cppHeaderForType(type: NamedType): string { const { name } = type; if (type instanceof ClassType) return cppHeaderForClass(type); - if (type instanceof EnumType) { + if (type instanceof EnumerationType) { return ` -namespace Bun { -namespace Bindgen { -namespace Generated { -enum class ${name} : ::std::uint32_t { +namespace Bun::Bindgen::Generated { +enum class ${name} : std::uint32_t { ${type.variants.map(variant => ` ${variant},`).join("\n")} }; } -template<> struct ExternTraits : TrivialExtern {}; -} -template<> struct IDLHumanReadableName<${type.idlType}> : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = ::std::to_array("${humanReadableEnum(type)}"); +template<> struct Bun::Bindgen::ExternTraits<${type.cpp}> : Bun::Bindgen::TrivialExtern<${type.cpp}> {}; +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("${humanReadableEnumeration(type)}"); }; -} template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String&); template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue); `; @@ -337,9 +335,7 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC assert(type instanceof DictionaryType); if (!type.accepted) return ""; return ` -namespace Bun { -namespace Bindgen { -namespace Generated { +namespace Bun::Bindgen::Generated { struct ${name} { ${cppDefaultConstructor(name)} ${type.members.map(member => ` ${implementationType(member.type)} ${member.internalName};`).join("\n")} @@ -349,26 +345,24 @@ ${type.members.map(member => ` ${externType(member.type)} ${member.internalNa }; ${layoutAssert(`Extern${name}`, type)} } -template<> struct ExternTraits { - using ExternType = Generated::Extern${name}; - static ExternType convertToExtern(Generated::${name}&& value) +template<> struct Bun::Bindgen::ExternTraits<${type.cpp}> { + using ExternType = Bun::Bindgen::Generated::Extern${name}; + static ExternType convertToExtern(${type.cpp}&& value) { return ExternType { ${type.members - .map(member => ` .${member.internalName} = toExtern(value.${member.internalName}),`) + .map(member => ` .${member.internalName} = ${toExtern(`value.${member.internalName}`)},`) .join("\n")} }; } }; -} -template<> struct IDLHumanReadableName<::WebCore::IDLDictionary> : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = ::std::to_array("${type.userFacingName}"); +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("${type.userFacingName}"); }; -} template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue, ${type.cpp}&); ${ type.needsLifetime - ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : ::Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` + ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` : `template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue);` } `; @@ -398,15 +392,15 @@ ${file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});` const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name)}`; -function cppSourceForEnum(type: EnumType): string { +function cppSourceForEnum(type: EnumerationType): string { const pairs = type.spellings .flatMap((spellings, index) => spellings.map(spelling => [spelling, type.variants[index]] as const)) .sort(([a], [b]) => (a < b ? -1 : 1)); - const pair = `::std::pair<::WTF::ComparableASCIILiteral, ${type.cpp}>`; + const pair = `std::pair`; return ` template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) { - static constexpr ::WTF::SortedArrayMap mapping { ::std::to_array<${pair}>({ + static constexpr WTF::SortedArrayMap mapping { std::to_array<${pair}>({ ${pairs.map(([spelling, variant]) => ` ${pair} { ${asciiLiteral(spelling)}, ${type.cpp}::${variant} },`).join("\n")} }) }; if (auto* value = mapping.tryGet(string)) [[likely]] @@ -430,11 +424,11 @@ function cppSourceForDictionary(type: DictionaryType): string { ); const qualified = `${type.userFacingName}.${member.name}`; const lines = [ - `[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} };`, + `[[maybe_unused]] auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} };`, `JSC::JSValue value${i} = ${first}`, ...others.map(read => `if (value${i}.isUndefined()) {\n${indent(`value${i} = ${read}`, 4)}\n}`), ]; - if (!member.type.permitsUndefined) + if (!member.type.takesUndefined) lines.push(`if (value${i}.isUndefined()) [[unlikely]] { ctx${i}.throwRequired(*globalObject, scope); return; @@ -452,7 +446,7 @@ template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalOb auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; + [[maybe_unused]] auto ctx = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; auto* object = value.getObject(); ${ type.nonObjectAsEmpty @@ -475,7 +469,7 @@ ${ return; } ` -} ::Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); +} Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); ${members.join("\n")} } ${ @@ -485,19 +479,19 @@ ${ template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) { ${type.cpp} result; - ::Bun::Bindgen::convertDictionaryInto(globalObject, value, result); + Bun::Bindgen::convertDictionaryInto(globalObject, value, result); return result; } ` }${ type.fromJS ? ` -extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value, ::Bun::Bindgen::Generated::Extern${type.name}* result) +extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value, Bun::Bindgen::Generated::Extern${type.name}* result) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); ${type.cpp} converted; - ::Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted); + Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted); RETURN_IF_EXCEPTION(scope, false); *result = ${toExtern("converted")}; return true; @@ -525,21 +519,21 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str const rereads: string[] = []; signature.args.forEach(({ name, type }, i) => { lines.push(`JSC::EnsureStillAliveScope argument${i} = callFrame->argument(${i});`); - lines.push(`[[maybe_unused]] auto ctx${i} = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); + lines.push(`[[maybe_unused]] auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(name)} };`); // Converting a later argument can run JavaScript that detaches or resizes the buffer. if (type.borrowsHeap && i < signature.args.length - 1) rereads.push( - `value${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`, + `converted${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`, `scope.assertNoException();`, ); - lines.push(...cppDeclareConverted(type, `value${i}`, `argument${i}.value()`, `ctx${i}`, name)); + lines.push(...cppDeclareConverted(type, `converted${i}`, `argument${i}.value()`, `ctx${i}`, name)); lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); lines.push(...rereads); const args = signature.args.map(({ type }, i) => { return type instanceof WithValueType - ? `${externType(type)} { ${toExtern(`value${i}`)}, JSC::JSValue::encode(argument${i}.value()) }` - : toExtern(`value${i}`); + ? `${externType(type)} { ${toExtern(`converted${i}`)}, JSC::JSValue::encode(argument${i}.value()) }` + : toExtern(`converted${i}`); }); lines.push(`RELEASE_AND_RETURN(scope, ${symbol}(${["globalObject", ...leading, ...args].join(", ")}));`); return lines.map(line => ` ${line}`).join("\n"); @@ -592,7 +586,7 @@ using JS${name} = JSBindgenObject<${name}Traits>; extern const ClassDefinition JS${name}ClassDefinition; } template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { - static constexpr auto humanReadableName = ::std::to_array("${name}"); + static constexpr auto humanReadableName = std::to_array("${name}"); }; `; } @@ -612,7 +606,7 @@ ${ ? "" : ` auto* thisObject = dynamicDowncast<${type.cpp}>(callFrame->thisValue()); if (!thisObject) [[unlikely]] - RELEASE_AND_RETURN(scope, ::Bun::throwInvalidThisCallError(globalObject, callFrame, ${asciiLiteral(type.name)})); + RELEASE_AND_RETURN(scope, Bun::throwInvalidThisCallError(globalObject, callFrame, ${asciiLiteral(type.name)})); ` }${cppCallBody(method, symbol, isStatic ? [] : ["thisObject"])} } @@ -634,7 +628,7 @@ ${cppPrologue} auto* thisObject = uncheckedDowncast<${type.cpp}>(JSC::JSValue::decode(thisValue)); JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; ${ - getter.internalFieldIndex < 0 + !getter.cached ? ` RELEASE_AND_RETURN(scope, ${getSymbol}(globalObject, thisObject));` : ` auto& cachedValue = thisObject->internalField(${getter.internalFieldIndex}); if (JSC::JSValue cached = cachedValue.get()) @@ -660,7 +654,7 @@ ${cppPrologue} } JSC::EnsureStillAliveScope ensureThisStillAlive { thisObject }; JSC::EnsureStillAliveScope ensureValueStillAlive { JSC::JSValue::decode(encodedValue) }; - [[maybe_unused]] auto ctx = ::Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }; + [[maybe_unused]] auto ctx = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }; ${cppDeclareConverted(getter.type, "value", "ensureValueStillAlive.value()", "ctx", getter.name).join("\n ")} RETURN_IF_EXCEPTION(scope, false); RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, thisObject, ${toExtern("value")})); @@ -694,7 +688,7 @@ function cppSourceForClass(type: ClassType): string { row( builtin.name, "JSC::PropertyAttribute::Builtin", - `JSC::HashTableValue::BuiltinGeneratorType, ::WebCore::${low(name)}${cap(builtin.name)}CodeGenerator, ${builtin.length}`, + `JSC::HashTableValue::BuiltinGeneratorType, WebCore::${low(name)}${cap(builtin.name)}CodeGenerator, ${builtin.length}`, ), ), ]; @@ -713,7 +707,7 @@ static JSC_DECLARE_HOST_FUNCTION(construct${name}); JSC_DEFINE_HOST_FUNCTION(construct${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { ${cppPrologue} - JSC::Structure* structure = ::Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); + JSC::Structure* structure = Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); RETURN_IF_EXCEPTION(scope, {}); ${cppCallBody(construct, classSymbolName(type, "construct"), ["structure"])} } @@ -724,7 +718,7 @@ ${table(`JS${name}PrototypeTableValues`, prototypeRows)}${table(`JS${name}Constr privateMethods.length > 0 ? `static void finish${name}PrototypeCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* prototype) { - auto& names = ::WebCore::builtinNames(vm); + auto& names = WebCore::builtinNames(vm); ${privateMethods .map( method => @@ -735,7 +729,7 @@ ${privateMethods ` : "" } -const ::Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { +const Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { .info = ${type.cpp}::info(), .createStructure = ${type.cpp}::createStructure, .prototypeTableValues = ${span(`JS${name}PrototypeTableValues`, prototypeRows)}, @@ -748,7 +742,7 @@ const ::Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDef extern "C" JSC::JSCell* ${symbolName(type, "create")}(JSC::JSGlobalObject* globalObject, JSC::Structure* structure) { static_assert(${type.cpp}::offsetOfInternalFields() == ${offsetOfInternalFields} && ${type.cpp}::offsetOfPayload() == ${offsetOfPayload(type)}); - return ${type.cpp}::create(JSC::getVM(globalObject), structure ? structure : ::Bun::Bindgen::structureForClass(globalObject, ${type.classIndex})); + return ${type.cpp}::create(JSC::getVM(globalObject), structure ? structure : Bun::Bindgen::structureForClass(globalObject, ${type.classIndex})); } ${ @@ -756,11 +750,11 @@ ${ ? ` extern "C" JSC::EncodedJSValue ${symbolName(type, "getConstructor")}(JSC::JSGlobalObject* globalObject) { - return JSC::JSValue::encode(::Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); + return JSC::JSValue::encode(Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); } extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${type.origin!.stem}_${name}(Zig::GlobalObject* globalObject) { - return JSC::JSValue::encode(::Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); + return JSC::JSValue::encode(Bun::Bindgen::constructorForClass(globalObject, ${type.classIndex})); } ` : "" @@ -792,13 +786,17 @@ ${cppCallBody(signature, thunkSymbol(fn, signature))} const cppDispatch = (dispatch: OverloadDispatch): string => { if (dispatch instanceof Signature) return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, dispatch)}(globalObject, callFrame));`; - const { outcomes, code } = cppChooseByKind(`argument${dispatch.argument}`, dispatch.byKind); - const cases = outcomes.map( - (next, index) => `${index === 0 ? "default" : `case ${index}`}: {\n${indent(cppDispatch(next), 4)}\n}`, - ); - return `JSC::JSValue argument${dispatch.argument} = callFrame->argument(${dispatch.argument}); + const argument = `argument${dispatch.argument}`; + const choice = `choice${dispatch.argument}`; + const { outcomes, code, condition } = cppChooseByKind(argument, dispatch.byKind, choice); + const declaration = `JSC::JSValue ${argument} = callFrame->argument(${dispatch.argument});`; + const braced = (next: OverloadDispatch) => + next instanceof Signature ? `\n${indent(cppDispatch(next), 4)}` : ` {\n${indent(cppDispatch(next), 4)}\n}`; + if (condition) return `${declaration}\nif (${condition})${braced(outcomes[1])}\n${cppDispatch(outcomes[0])}`; + const cases = outcomes.map((next, index) => `${index === 0 ? "default" : `case ${index}`}:${braced(next)}`); + return `${declaration} ${code} -switch (choice) { +switch (${choice}) { ${[...cases.slice(1), cases[0]].join("\n")} }`; }; @@ -816,7 +814,7 @@ ${definition} } extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject) { - return JSC::JSValue::encode(JSC::JSFunction::create(globalObject->vm(), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}, JSC::ImplementationVisibility::Public)); + return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}, JSC::ImplementationVisibility::Public)); } `; } @@ -842,7 +840,7 @@ export function cppSource(file: SchemaFile): string { #include ${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types .map(type => - type instanceof EnumType + type instanceof EnumerationType ? cppSourceForEnum(type) : type instanceof DictionaryType ? cppSourceForDictionary(type) @@ -946,7 +944,7 @@ const infallible = (call: string, throws: boolean, returns: Type | null = null) const rustThunk = (symbol: string, parameters: string[], returns: string, body: string) => ` #[unsafe(no_mangle)] pub unsafe extern "C" fn ${symbol}(${["global: *mut JSGlobalObject", ...parameters].join(", ")}) -> ${returns} { - // SAFETY: called by generated C++, whose frame holds the arguments, \`this\` among them, until this returns. + // SAFETY: called by generated C++, whose frame holds what it passes until this returns. unsafe { bindgen::JsCall::with(global, |cx| { ${indent(body, 12)} @@ -955,7 +953,7 @@ ${indent(body, 12)} } `; -function rustForEnum(type: EnumType): string { +function rustForEnumeration(type: EnumerationType): string { return ` #[repr(u32)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -1080,8 +1078,8 @@ impl ${type.name}${lifetime ? "<'_>" : ""} { if (type.returned) { const value = (member: (typeof type.members)[number]) => { const field = `self.${member.rustName}`; - if (member.type instanceof OptionalType && member.type.kind === "optional") { - return `match ${field} { Some(value) => ${member.type.payload.rustToJS("value", scope)}?.to_js(), None => JSValue::ZERO }`; + if (member.type instanceof OptionalType && member.type.modifier === "optional") { + return `match ${field} { Some(value) => ${member.type.inner.rustToJS("value", scope)}?.to_js(), None => JSValue::ZERO }`; } return `${member.type.rustToJS(field, scope)}?.to_js()`; }; @@ -1337,8 +1335,8 @@ pub mod ${file.origin.rustModule} { ${indent( file.types .map(type => - type instanceof EnumType - ? rustForEnum(type) + type instanceof EnumerationType + ? rustForEnumeration(type) : type instanceof UnionType ? rustForUnion(type, file.origin) : type instanceof ClassType @@ -1358,14 +1356,14 @@ ${indent( export function dts(files: readonly SchemaFile[]): string { const parameters = (signature: Signature) => - signature.args.map(arg => `${arg.name}${arg.type.permitsUndefined ? "?" : ""}: ${arg.type.tsType}`).join(", "); + signature.args.map(arg => `${arg.name}${arg.type.takesUndefined ? "?" : ""}: ${arg.type.tsType}`).join(", "); const signatureType = (signature: Signature) => `(${parameters(signature)}) => ${signature.returns.tsType}`; const member = (name: string, optional: boolean, type: string) => ` ${JSON.stringify(name)}${optional ? "?" : ""}: ${type};`; const declarations = files.flatMap(file => file.types.map(type => { if (type instanceof DictionaryType) { - return ` interface ${type.name} {\n${type.members.map(m => member(m.name, m.type.permitsUndefined, m.type.tsType)).join("\n")}\n }`; + return ` interface ${type.name} {\n${type.members.map(m => member(m.name, m.type.takesUndefined, m.type.tsType)).join("\n")}\n }`; } if (!(type instanceof ClassType)) return ""; return ` interface ${type.name} { diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 922f9f5caaad..0fa093ba7f18 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -23,9 +23,9 @@ export interface Origin { /** Where Rust code is being emitted, so paths to named types can be spelled from there. */ export type RustScope = Pick; -export const roundUp = (n: number, align: number) => Math.ceil(n / align) * align; +const roundUp = (n: number, align: number) => Math.ceil(n / align) * align; -export function structLayout(fields: readonly Layout[]): Layout { +function structLayout(fields: readonly Layout[]): Layout { let size = 0; let align = 1; for (const field of fields) { @@ -36,7 +36,7 @@ export function structLayout(fields: readonly Layout[]): Layout { } /** `{ union data; uint8_t tag; }`. */ -export function taggedUnionLayout(alternatives: readonly Layout[]): Layout { +function taggedUnionLayout(alternatives: readonly Layout[]): Layout { const align = Math.max(...alternatives.map(a => a.align)); const union = { size: roundUp(Math.max(...alternatives.map(a => a.size)), align), align }; return structLayout([union, { size: 1, align: 1 }]); @@ -70,7 +70,7 @@ export function asciiLiteral(value: string): string { return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"_s`; } -/** JavaScript values, divided by what tells them apart. One of none of these kinds is "other". See "Unions and kinds" in the docs. */ +/** JavaScript values, divided by what tells them apart. A value of none of these kinds is "other". See "Unions and kinds" in the docs. */ export type ValueKind = | "undefined" | "null" @@ -100,12 +100,12 @@ const distinguishingKinds = (types: readonly Type[]): (ValueKind | "other")[] => ]; /** Which of `alternatives` get a value of each kind, in order: all but the last refuse some. Equal lists are one array. */ -export function alternativesByKind(alternatives: readonly Type[]): Map { +function alternativesByKind(alternatives: readonly Type[]): Map { const lists = new Map(); return new Map( distinguishingKinds(alternatives).map(kind => { const takers = alternatives.flatMap((type, i) => (takes(type, kind) ? [i] : [])); - const last = takers.findIndex(i => alternatives[i].takesEveryValueOfItsKinds); + const last = takers.findIndex(i => !alternatives[i].mayRefuse); const list = last < 0 ? takers : takers.slice(0, last + 1); if (!lists.has(`${list}`)) lists.set(`${list}`, list); return [kind, lists.get(`${list}`)!]; @@ -121,7 +121,7 @@ export function indent(text: string, by: number): string { .join("\n"); } -/** C++ statements that engage `optional`, a `std::optional`, and do to what it holds what `fill` says, given its name. */ +/** C++ statements that engage `optional`, a `std::optional`, then fill what it holds: `fill` is given a name for that. */ export function cppEmplace(optional: string, fill: (target: string) => string): string { const [first, ...rest] = optional.split(/\W+/); const target = `${first}${rest.map(part => part[0].toUpperCase() + part.slice(1)).join("")}Value`; @@ -160,16 +160,16 @@ export abstract class Type { return "optional"; } /** Converting `undefined` succeeds, so a dictionary member of this type is not required. */ - get permitsUndefined(): boolean { + get takesUndefined(): boolean { return false; } /** The kinds of value it takes where there is a choice. "any": its own conversion decides, by coercing or by throwing. */ get valueKinds(): ValueKinds { return "any"; } - /** False if it refuses a shared buffer, say, or a detached Blob. In a union the next alternative gets those. */ - get takesEveryValueOfItsKinds(): boolean { - return true; + /** It refuses some values of its kinds: a shared buffer, a detached Blob. In a union the next alternative gets those. */ + get mayRefuse(): boolean { + return false; } /** Holds a JS value: the Rust type is branded with the call. */ get holdsJSValue(): boolean { @@ -179,7 +179,7 @@ export abstract class Type { get borrowsHeap(): boolean { return this.dependencies.some(type => type.borrowsHeap); } - /** Points into the C++ thunk's frame: only usable for the duration of a host call. */ + /** Points into the C++ host function's frame: only usable for the duration of a host call. */ get borrowsFrame(): boolean { return this.dependencies.some(type => type.borrowsFrame); } @@ -211,7 +211,7 @@ export abstract class Type { } /** A C++ expression that converts `value`. `ctx` is a conversion context, `name` what to call the value. */ cppConvert(value: string, ctx: string, name: string): string { - return `::Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; + return `Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; } /** Large, or holds something large: built where it ends up, not returned and moved there. */ get convertsInPlace(): boolean { @@ -296,12 +296,12 @@ class PrimitiveType extends Type { } export class BoolType extends PrimitiveType { - constructor(idlType = "::Bun::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { + constructor(idlType = "Bun::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { super(idlType, "boolean", "bool", 1, valueKinds); } /** `ToBoolean`: any value is accepted. */ get loose(): BoolType { - return new BoolType("::WebCore::IDLBoolean", "any"); + return new BoolType("WebCore::IDLBoolean", "any"); } } @@ -320,7 +320,7 @@ export class IntegerType extends PrimitiveType { /** Only the strict conversion is given no `idlType`. */ constructor(info: IntegerInfo, idlType?: string) { super( - idlType ?? `::Bun::IDLStrictInteger<${info.cpp}>`, + idlType ?? `Bun::IDLStrictInteger<${info.cpp}>`, "number", info.rust, info.size, @@ -342,53 +342,50 @@ export class IntegerType extends PrimitiveType { } /** `ToNumeric` first, then as strict as the default. */ get loose(): IntegerType { - return new IntegerType(this.info, `::Bun::IDLLooseInteger<${this.info.cpp}>`); + return new IntegerType(this.info, `Bun::IDLLooseInteger<${this.info.cpp}>`); } /** WebIDL conversion: `ToNumber`, then wrap modulo the width. */ get wrap(): IntegerType { - return new IntegerType(this.info, `::WebCore::${this.info.webidl}`); + return new IntegerType(this.info, `WebCore::${this.info.webidl}`); } /** WebIDL `[EnforceRange]` over `[min, max]`. */ enforceRange(min = Number(this.info.min), max = Number(this.info.max)): RangeType { - return new RangeType(this.info, min, max, "Web"); + return new RangeType(this.info, min, max, "enforceRange"); } /** Node's `validateInteger` / `validateInt32` / `validateUint32`: `ERR_INVALID_ARG_TYPE`, `ERR_OUT_OF_RANGE`. */ validateInteger(min = Number(this.info.min), max = Number(this.info.max)): RangeType { - return new RangeType(this.info, min, max, "Node"); + return new RangeType(this.info, min, max, "validateInteger"); } } export class RangeType extends IntegerType { - readonly kind: "Web" | "Node"; + readonly modifier: "enforceRange" | "validateInteger"; readonly #range: readonly [number, number]; - constructor(info: IntegerInfo, min: number, max: number, kind: "Web" | "Node") { + constructor(info: IntegerInfo, min: number, max: number, modifier: "enforceRange" | "validateInteger") { assert( Number.isSafeInteger(min) && Number.isSafeInteger(max) && min <= max, `[${min}, ${max}] is not a range of integers`, ); assert(min >= Number(info.min) && max <= Number(info.max), `[${min}, ${max}] does not fit ${info.rust}`); - super( - info, - `::Bun::BindgenCustomEnforceRange<${info.cpp}, ${min}, ${max}, ::Bun::BindgenCustomEnforceRangeKind::${kind}>`, - ); - this.kind = kind; + const tag = modifier === "enforceRange" ? "IDLEnforceRange" : "IDLValidateInteger"; + super(info, `Bun::Bindgen::${tag}<${info.cpp}, ${min}, ${max}>`); + this.modifier = modifier; this.#range = [min, max]; } protected get range() { return this.#range; } getHeaders(out: Set): void { - out.add("BindgenCustomEnforceRange.h"); + out.add("Bindgen/IDLConvertIntegerRange.h"); } cppConvert(value: string, ctx: string, name: string): string { - return this.kind === "Node" - ? `::WebCore::convert<${this.idlType}>(*globalObject, ${value}, []() { return ${asciiLiteral(name)}; })` - : `::WebCore::convert<${this.idlType}>(*globalObject, ${value})`; + const reported = this.modifier === "validateInteger" ? `, ${asciiLiteral(name)}` : ""; + return `WebCore::Converter<${this.idlType}>::convert(*globalObject, ${value}${reported})`; } } const integer = (rust: string, cpp: string, size: number, webidl: string, min: number | bigint, max: number | bigint) => - new IntegerType({ rust, cpp: `::std::${cpp}`, size, webidl, min, max }); + new IntegerType({ rust, cpp: `std::${cpp}`, size, webidl, min, max }); export const integers = { u8: integer("u8", "uint8_t", 1, "IDLOctet", 0, 255), @@ -401,7 +398,7 @@ export const integers = { export class DoubleType extends PrimitiveType { constructor() { - super("::Bun::IDLStrictDouble", "number", "f64", 8, ["number"]); + super("Bun::IDLStrictDouble", "number", "f64", 8, ["number"]); } } @@ -412,12 +409,12 @@ export class UnitType extends Type { constructor(tsType: "null" | "undefined") { super(); this.tsType = tsType; - this.idlType = tsType === "null" ? "::Bun::IDLStrictNull" : ""; + this.idlType = tsType === "null" ? "Bun::IDLStrictNull" : ""; } get valueKinds(): ValueKinds { return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; } - get permitsUndefined(): boolean { + get takesUndefined(): boolean { return true; } cppConvert(): string { @@ -448,7 +445,7 @@ export class StringType extends Type { readonly idlType: string; readonly tsType = "string"; readonly #valueKinds: ValueKinds; - constructor(idlType = "::Bun::IDLStrictString", valueKinds: ValueKinds = ["string"]) { + constructor(idlType = "Bun::IDLStrictString", valueKinds: ValueKinds = ["string"]) { super(); this.idlType = idlType; this.#valueKinds = valueKinds; @@ -461,7 +458,7 @@ export class StringType extends Type { } /** `ToString`: any value is accepted. */ get loose(): StringType { - return new StringType("::WebCore::IDLDOMString", "any"); + return new StringType("WebCore::IDLDOMString", "any"); } /** `&[u8]`, UTF-8, for the duration of the call, instead of an owned string. Also takes a `String` object. */ get utf8(): Utf8Type { @@ -471,8 +468,8 @@ export class StringType extends Type { return "pointer" as const; } toCpp(value: unknown): string { - assert(typeof value === "string"); - return `::WTF::String { ${asciiLiteral(value)} }`; + assert(typeof value === "string", `${util.inspect(value)} is not a string`); + return `WTF::String { ${asciiLiteral(value)} }`; } toRust(value: unknown): string { return value === "" ? "bun_core::String::EMPTY" : `bun_core::String::static_(${JSON.stringify(value)})`; @@ -525,13 +522,13 @@ export class Utf8Type extends Type { this.isLoose = isLoose; } get idlType(): string { - return `::Bun::Bindgen::IDL${this.isLoose ? "Loose" : "Strict"}UTF8String`; + return `Bun::Bindgen::IDL${this.isLoose ? "Loose" : "Strict"}UTF8String`; } /** `bun_core::Utf8Bytes<'static>`, for a body that stores the string. */ get owned(): OwnedUtf8Type { return this.isLoose - ? new OwnedUtf8Type("::WebCore::IDLDOMString", "any") - : new OwnedUtf8Type("::Bun::Bindgen::IDLStrictStringOrStringObject", this.valueKinds); + ? new OwnedUtf8Type("WebCore::IDLDOMString", "any") + : new OwnedUtf8Type("Bun::Bindgen::IDLStrictStringOrStringObject", this.valueKinds); } get valueKinds(): ValueKinds { return this.isLoose ? "any" : ["string", "stringObject"]; @@ -546,10 +543,10 @@ export class Utf8Type extends Type { return true; } cppConvertInto(target: string, value: string, ctx: string): string { - return `::WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; + return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; } cppAssign(target: string, value: unknown): string { - assert(typeof value === "string"); + assert(typeof value === "string", `${util.inspect(value)} is not a string`); return `${target}.initialize(${asciiLiteral(value)});`; } toRust(value: unknown): string { @@ -582,7 +579,7 @@ const bufferSourceKinds: ValueKinds = ["arrayBuffer", "arrayBufferView"]; /** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ export class BytesType extends Type { - readonly idlType = "::Bun::Bindgen::IDLBufferSourceBytes"; + readonly idlType = "Bun::Bindgen::IDLBufferSourceBytes"; readonly tsType = "ArrayBuffer | ArrayBufferView"; get valueKinds(): ValueKinds { return bufferSourceKinds; @@ -609,21 +606,21 @@ export class BytesType extends Type { /** A typed array, `DataView` or `ArrayBuffer`, as a handle whose bytes are read through `with_bytes`. */ export class BufferSourceType extends Type { - readonly idlType = "::Bun::Bindgen::IDLRawBufferSource"; + readonly idlType = "Bun::Bindgen::IDLRawBufferSource"; readonly tsType = "ArrayBuffer | ArrayBufferView"; get valueKinds(): ValueKinds { return bufferSourceKinds; } - get takesEveryValueOfItsKinds(): boolean { - return false; + get mayRefuse(): boolean { + return true; } /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ get bytes(): BytesType { return new BytesType(); } - /** A reference on the `JSC::ArrayBuffer` behind it, which can be stored. */ + /** A reference on a `JSC::ArrayBuffer`, which can be stored: the buffer itself, or a copy of what a view shows. */ get arrayBuffer(): RefType { - return new RefType("::Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null, bufferSourceKinds); + return new RefType("Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null, bufferSourceKinds); } get holdsJSValue(): boolean { return true; @@ -645,7 +642,7 @@ export class BufferSourceType extends Type { } } -/** Converts as `inner` does; the thunk passes the argument itself along. */ +/** Converts as `inner` does; the host function passes the argument itself along. */ export class WithValueType extends Type { readonly inner: Type; constructor(inner: Type) { @@ -660,8 +657,8 @@ export class WithValueType extends Type { get tsType(): string { return this.inner.tsType; } - get permitsUndefined(): boolean { - return this.inner.permitsUndefined; + get takesUndefined(): boolean { + return this.inner.takesUndefined; } get valueKinds(): ValueKinds { return this.inner.valueKinds; @@ -696,12 +693,12 @@ export class WithValueType extends Type { } export class AnyType extends Type { - readonly idlType = "::Bun::IDLRawAny"; + readonly idlType = "Bun::IDLRawAny"; readonly tsType = "any"; get holdsJSValue(): boolean { return true; } - get permitsUndefined(): boolean { + get takesUndefined(): boolean { return true; } toCpp(value: unknown): string { @@ -746,8 +743,8 @@ export class RefType extends Type { get valueKinds(): ValueKinds { return this.#valueKinds; } - get takesEveryValueOfItsKinds(): boolean { - return false; + get mayRefuse(): boolean { + return true; } get rustIsCopy(): boolean { return false; @@ -776,174 +773,173 @@ export class RefType extends Type { // ───────────────────────────── combinators ───────────────────────────── export class OptionalType extends Type { - readonly payload: Type; - readonly kind: "optional" | "nullable" | "falsyAsNull"; - constructor(payload: Type, kind: "optional" | "nullable" | "falsyAsNull") { + readonly inner: Type; + readonly modifier: "optional" | "nullable" | "falsyAsNull"; + constructor(inner: Type, modifier: "optional" | "nullable" | "falsyAsNull") { super(); - assert(!(payload instanceof OptionalType), "already optional"); - assert(!(payload instanceof DefaultType), "put .default() last"); - assert(!(payload instanceof AnyType), "b.any already accepts undefined and null"); - assert(!(payload instanceof WithValueType), "put .withValue last"); - this.payload = payload; - this.kind = kind; + assert(!(inner instanceof OptionalType), "already optional"); + assert(!(inner instanceof DefaultType), "put .default() last"); + assert(!(inner instanceof AnyType), "b.any already accepts undefined and null"); + assert(!(inner instanceof WithValueType), "put .withValue last"); + this.inner = inner; + this.modifier = modifier; } get idlType(): string { - assert(!(this.payload instanceof RangeType), "a range cannot be absent; give it a .default()"); + assert(!(this.inner instanceof RangeType), "a range cannot be absent; give it a .default()"); const template = { - optional: "::WebCore::IDLOptional", - nullable: "::WebCore::IDLNullable", - falsyAsNull: "::Bun::IDLLooseNullable", - }[this.kind]; - return `${template}<${this.payload.idlType}>`; + optional: "WebCore::IDLOptional", + nullable: "WebCore::IDLNullable", + falsyAsNull: "Bun::IDLFalsyAsNull", + }[this.modifier]; + return `${template}<${this.inner.idlType}>`; } get tsType(): string { - return `${this.payload.tsType} | ${this.kind === "optional" ? "undefined" : "null | undefined"}`; + return `${this.inner.tsType} | ${this.modifier === "optional" ? "undefined" : "null | undefined"}`; } - get permitsUndefined(): boolean { + get takesUndefined(): boolean { return true; } get valueKinds(): ValueKinds { - if (this.payload.valueKinds === "any" || this.kind === "falsyAsNull") return "any"; - return [...this.payload.valueKinds, "undefined", ...(this.kind === "nullable" ? (["null"] as const) : [])]; + if (this.inner.valueKinds === "any" || this.modifier === "falsyAsNull") return "any"; + return [...this.inner.valueKinds, "undefined", ...(this.modifier === "nullable" ? (["null"] as const) : [])]; } get dependencies(): readonly Type[] { - return [this.payload]; + return [this.inner]; } get convertsInPlace(): boolean { - return this.payload.convertsInPlace; + return this.inner.convertsInPlace; } - /** What converts in place is null when default-constructed. Not everything else is: a pointer to a class instance. */ + /** What converts in place is null when default-constructed. A pointer to a class instance, which does not, is not. */ cppConvertInto(target: string, value: string, ctx: string, name: string): string { if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name); const provided = { optional: `!${value}.isUndefined()`, nullable: `!${value}.isUndefinedOrNull()`, falsyAsNull: `${value}.toBoolean(globalObject)`, - }[this.kind]; - const convert = (slot: string) => this.payload.cppConvertInto(slot, value, ctx, name); - const body = this.payload.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); + }[this.modifier]; + const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name); + const body = this.inner.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); return `if (${provided}) {\n${indent(body, 4)}\n}`; } toCpp(value: unknown): string { - return value == null ? `${this.idlType}::nullValue()` : this.payload.toCpp(value); + return value == null ? `${this.idlType}::nullValue()` : this.inner.toCpp(value); } cppAssign(target: string, value: unknown): string { if (!this.convertsInPlace) return super.cppAssign(target, value); if (value == null) return ""; - const assign = (slot: string) => this.payload.cppAssign(slot, value); - return this.payload.nullRepresentation === "optional" - ? `{\n${indent(cppEmplace(target, assign), 4)}\n}` - : assign(target); + const assign = (slot: string) => this.inner.cppAssign(slot, value); + return this.inner.nullRepresentation === "optional" ? cppEmplace(target, assign) : assign(target); } toRust(value: unknown, scope: RustScope): string { - return value == null ? "None" : `Some(${this.payload.toRust(value, scope)})`; + return value == null ? "None" : `Some(${this.inner.toRust(value, scope)})`; } layout(): Layout { - return this.payload.nullRepresentation === "optional" - ? taggedUnionLayout([{ size: 1, align: 1 }, this.payload.layout()]) - : this.payload.layout(); + return this.inner.nullRepresentation === "optional" + ? taggedUnionLayout([{ size: 1, align: 1 }, this.inner.layout()]) + : this.inner.layout(); } rustExternType(scope: RustScope): string { - return this.payload.nullRepresentation === "optional" - ? `bindgen::extern_types::ExternOptional<${this.payload.rustExternType(scope)}>` - : this.payload.rustExternType(scope); + return this.inner.nullRepresentation === "optional" + ? `bindgen::extern_types::ExternOptional<${this.inner.rustExternType(scope)}>` + : this.inner.rustExternType(scope); } rustType(scope: RustScope): string { - return `Option<${this.payload.rustType(scope)}>`; + return `Option<${this.inner.rustType(scope)}>`; } rustFromExtern(expr: string, scope: RustScope): string { - if (this.payload.nullRepresentation === "optional") { - const inner = this.payload.rustFromExtern("value", scope); + if (this.inner.nullRepresentation === "optional") { + const inner = this.inner.rustFromExtern("value", scope); return inner === "value" ? `${expr}.get()` : `${expr}.get().map(|value| ${inner})`; } - if (this.payload instanceof Utf8Type || this.payload instanceof BytesType) return `${expr}.get_optional()`; - return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.payload.rustFromExtern("value", scope)}) } }`; + if (this.inner instanceof Utf8Type || this.inner instanceof BytesType) return `${expr}.get_optional()`; + return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.inner.rustFromExtern("value", scope)}) } }`; } rustResultFieldType(scope: RustScope): string { - return `Option<${this.payload.rustResultFieldType(scope)}>`; + return `Option<${this.inner.rustResultFieldType(scope)}>`; } rustReturnType(scope: RustScope): string { - return `Option<${this.payload.rustReturnType(scope)}>`; + return `Option<${this.inner.rustReturnType(scope)}>`; } rustToJS(value: string, scope: RustScope): string { - const none = this.kind === "optional" ? "UNDEFINED" : "NULL"; - return `match ${value} { Some(value) => ${this.payload.rustToJS("value", scope)}, None => Ok(bindgen::JsValue::${none}) }`; + const none = this.modifier === "optional" ? "UNDEFINED" : "NULL"; + return `match ${value} { Some(value) => ${this.inner.rustToJS("value", scope)}, None => Ok(bindgen::JsValue::${none}) }`; } } /** Not a type of its own: marks an argument or member whose absence has a value. */ export class DefaultType extends Type { - readonly payload: Type; + readonly inner: Type; readonly value: unknown; /** `null` also takes the default. */ readonly onNull: boolean; - constructor(payload: Type, value: unknown) { + constructor(inner: Type, value: unknown) { super(); - assert(!(payload instanceof DefaultType), "already has a default"); - if (payload instanceof OptionalType) { - assert(payload.kind === "nullable", ".default() follows a type or .nullable"); - this.payload = payload.payload; + assert(!(inner instanceof DefaultType), "already has a default"); + if (inner instanceof OptionalType) { + assert(inner.modifier === "nullable", ".default() follows a type or .nullable"); + this.inner = inner.inner; this.onNull = true; } else { - this.payload = payload; + this.inner = inner; this.onNull = false; } this.value = value; - this.payload.cppAssign("", value); + // For what it asserts about `value`. + this.inner.cppAssign("", value); } get idlType(): string { - return this.payload.idlType; + return this.inner.idlType; } get tsType(): string { - return `${this.payload.tsType} | ${this.onNull ? "null | undefined" : "undefined"}`; + return `${this.inner.tsType} | ${this.onNull ? "null | undefined" : "undefined"}`; } - get permitsUndefined(): boolean { + get takesUndefined(): boolean { return true; } get dependencies(): readonly Type[] { - return [this.payload]; + return [this.inner]; } toCpp(value: unknown): string { - return this.payload.toCpp(value); + return this.inner.toCpp(value); } toRust(value: unknown, scope: RustScope): string { - return this.payload.toRust(value, scope); + return this.inner.toRust(value, scope); } get valueKinds(): ValueKinds { - if (this.payload.valueKinds === "any") return "any"; - return [...this.payload.valueKinds, "undefined", ...(this.onNull ? (["null"] as const) : [])]; + if (this.inner.valueKinds === "any") return "any"; + return [...this.inner.valueKinds, "undefined", ...(this.onNull ? (["null"] as const) : [])]; } private cppIsAbsent(value: string): string { return `${value}.${this.onNull ? "isUndefinedOrNull" : "isUndefined"}()`; } cppConvert(value: string, ctx: string, name: string): string { - return `${this.cppIsAbsent(value)} ? ${this.toCpp(this.value)} : ${this.payload.cppConvert(value, ctx, name)}`; + return `${this.cppIsAbsent(value)} ? ${this.toCpp(this.value)} : ${this.inner.cppConvert(value, ctx, name)}`; } get convertsInPlace(): boolean { - return this.payload.convertsInPlace; + return this.inner.convertsInPlace; } cppConvertInto(target: string, value: string, ctx: string, name: string): string { if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name); return `if (${this.cppIsAbsent(value)}) { ${indent(this.cppAssign(target, this.value), 4)} } else { -${indent(this.payload.cppConvertInto(target, value, ctx, name), 4)} +${indent(this.inner.cppConvertInto(target, value, ctx, name), 4)} }`; } cppAssign(target: string, value: unknown): string { - return this.payload.cppAssign(target, value); + return this.inner.cppAssign(target, value); } layout(): Layout { - return this.payload.layout(); + return this.inner.layout(); } rustExternType(scope: RustScope): string { - return this.payload.rustExternType(scope); + return this.inner.rustExternType(scope); } rustType(scope: RustScope): string { - return this.payload.rustType(scope); + return this.inner.rustType(scope); } rustFromExtern(expr: string, scope: RustScope): string { - return this.payload.rustFromExtern(expr, scope); + return this.inner.rustFromExtern(expr, scope); } } @@ -969,7 +965,7 @@ export class ArrayType extends Type { !this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string or b.string.utf8.owned", ); - return `::Bun::IDLArray<${this.element.idlType}>`; + return `Bun::IDLArray<${this.element.idlType}>`; } get tsType(): string { return `(${this.element.tsType})[]`; @@ -995,7 +991,7 @@ export class ArrayType extends Type { } rustFromExtern(expr: string, scope: RustScope): string { const element = this.element.rustFromExtern("value", scope); - return element === "value" ? `${expr}.into_vec_unconverted()` : `${expr}.into_vec(|value| ${element})`; + return element === "value" ? `${expr}.into_vec()` : `${expr}.into_vec_with(|value| ${element})`; } rustResultFieldType(scope: RustScope): string { return `Vec<${this.element.rustResultFieldType(scope)}>`; @@ -1015,7 +1011,7 @@ export abstract class NamedType extends Type { name = ""; origin: Origin | null = null; get cpp(): string { - return `::Bun::Bindgen::Generated::${this.name}`; + return `Bun::Bindgen::Generated::${this.name}`; } getHeaders(out: Set): void { assert(this.origin, `${this.constructor.name}: used but not exported from a .bind.ts file`); @@ -1036,7 +1032,7 @@ export abstract class NamedType extends Type { } } -export class EnumType extends NamedType { +export class EnumerationType extends NamedType { /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ @@ -1049,14 +1045,17 @@ export class EnumType extends NamedType { this.spellings = values.map(value => (typeof value === "string" ? [value] : value)); const all = this.spellings.flat(); assert(new Set(all).size === all.length, `a value is listed twice: ${all.join(", ")}`); - assert(new Set(this.variants).size === this.variants.length, "two values make the same identifier"); + assert( + new Set(this.variants).size === this.variants.length, + `two values make the same identifier: ${this.variants.join(", ")}`, + ); for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${variant} is not an identifier`); } get variants(): string[] { return this.spellings.map(([canonical]) => pascalCase(canonical)); } get idlType(): string { - return `::WebCore::IDLEnumeration<${this.cpp}>`; + return `WebCore::IDLEnumeration<${this.cpp}>`; } get tsType(): string { return this.spellings @@ -1170,7 +1169,7 @@ export class DictionaryType extends NamedType { return !!this.options.nonObjectAsEmpty; } get idlType(): string { - return `::WebCore::IDLDictionary<${this.cpp}>`; + return `WebCore::IDLDictionary<${this.cpp}>`; } get dependencies(): readonly Type[] { return this.members.map(member => member.type); @@ -1179,7 +1178,7 @@ export class DictionaryType extends NamedType { return true; } cppConvertInto(target: string, value: string): string { - return `::Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target});`; + return `Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target});`; } /** Every JS name read, in order. */ get names(): string[] { @@ -1200,14 +1199,14 @@ export class DictionaryType extends NamedType { let field: unknown; if (Object.hasOwn(given, member.name)) field = given[member.name]; else if (member.type instanceof DefaultType) field = member.type.value; - else assert(member.type.permitsUndefined, `missing key: ${member.name}`); + else assert(member.type.takesUndefined, `missing key: ${member.name}`); return member.type.cppAssign(`${target}.${member.internalName}`, field); }); return fields.filter(text => text.length > 0).join("\n"); } /** Every member can be left out. */ get hasDefault(): boolean { - return this.members.every(member => member.type.permitsUndefined); + return this.members.every(member => member.type.takesUndefined); } toRust(value: unknown, scope: RustScope): string { const given = value as Record; @@ -1268,19 +1267,19 @@ export class UnionType extends NamedType { return this.alternatives.some(a => a.type.valueKinds === "any") ? "any" : kinds; } get idlType(): string { - return `::Bun::Bindgen::IDLTaggedUnion<${this.cpp}>`; + return `Bun::Bindgen::IDLTaggedUnion<${this.cpp}>`; } get convertsInPlace(): boolean { return true; } cppConvertInto(target: string, value: string, ctx: string): string { - return `::WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; + return `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`; } get tsType(): string { return this.alternatives.map(a => a.type.tsType).join(" | "); } - get permitsUndefined(): boolean { - return this.alternatives.some(a => a.type.permitsUndefined); + get takesUndefined(): boolean { + return this.alternatives.some(a => a.type.takesUndefined); } get dependencies(): readonly Type[] { return this.alternatives.map(a => a.type); @@ -1340,7 +1339,7 @@ export class Signature { this.rustPath = options.rustPath; let optional = false; for (const { name, type } of this.args) { - if (type.permitsUndefined) optional = true; + if (type.takesUndefined) optional = true; else assert(!optional, `required argument ${name} follows an optional one`); } for (const { name, type } of this.args) { @@ -1356,7 +1355,7 @@ export class Signature { ); } get requiredCount(): number { - return this.args.filter(arg => !arg.type.permitsUndefined).length; + return this.args.filter(arg => !arg.type.takesUndefined).length; } } @@ -1396,7 +1395,7 @@ export class FunctionDefinition { const next = (kind: ValueKind | "other") => { const takers = candidates.filter((signature, i) => { if (!types[i]) return true; - return kind === "undefined" ? types[i].permitsUndefined : takes(types[i], kind); + return kind === "undefined" ? types[i].takesUndefined : takes(types[i], kind); }); return this.dispatchOn(takers.length > 0 ? takers : candidates, argument + 1, nodes); }; @@ -1485,6 +1484,8 @@ export class Getter { readonly throws: boolean; readonly hasSetter: boolean; /** Index of the internal field a cached value lives in, or -1. */ + readonly cached: boolean; + /** Of a cached one. Set by its class. */ internalFieldIndex = -1; constructor(name: string, options: GetterField) { this.name = name; @@ -1493,7 +1494,7 @@ export class Getter { this.throws = options.throws ?? false; this.hasSetter = options.setter ?? false; assert(!(options.cache && options.setter), `${name}: a cached getter has no setter`); - if (options.cache) this.internalFieldIndex = 0; + this.cached = !!options.cache; } get rustName(): string { return rustIdent(snakeCase(this.name)); @@ -1559,7 +1560,7 @@ export class ClassType extends NamedType { ); assert(this.construct || this.staticMethods.length === 0, "static members need a constructor object"); this.internalFields = options.internalFields ?? []; - this.cachedGetters = getters.filter(getter => getter.internalFieldIndex >= 0); + this.cachedGetters = getters.filter(getter => getter.cached); this.cachedGetters.forEach((getter, index) => (getter.internalFieldIndex = this.internalFields.length + index)); this.configurable = options.configurable ?? true; for (const method of this.methods) { @@ -1574,10 +1575,10 @@ export class ClassType extends NamedType { return this.internalFields.length + this.cachedGetters.length; } get cpp(): string { - return `::Bun::Bindgen::Generated::JS${this.name}`; + return `Bun::Bindgen::Generated::JS${this.name}`; } get idlType(): string { - return `::Bun::Bindgen::IDLRawInterface<${this.cpp}>`; + return `Bun::Bindgen::IDLRawInterface<${this.cpp}>`; } get valueKinds(): ValueKinds { return [this]; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index c4102bb23eab..612398cc2b32 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -10,7 +10,7 @@ import { type DictionaryOptions, DictionaryType, DoubleType, - EnumType, + EnumerationType, FunctionDefinition, integers, type MemberOptions, @@ -45,8 +45,8 @@ export const any = new AnyType(); export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ -export const blob = new RefType("::Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h", [ - { cpp: "::WebCore::JSBlob" }, +export const blob = new RefType("Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h", [ + { cpp: "WebCore::JSBlob" }, ]); const null_ = new UnitType("null"); @@ -56,7 +56,7 @@ export const array = (element: Type) => new ArrayType(element); // A named type is called what it is exported as: `export const Formatter = b.enumeration([..])`. /** An array in place of a value lists several spellings of it; the first is the one given back to JS. */ -export const enumeration = (values: readonly (string | readonly string[])[]) => new EnumType(values); +export const enumeration = (values: readonly (string | readonly string[])[]) => new EnumerationType(values); export const dictionary = (members: Record, options?: DictionaryOptions) => new DictionaryType(members, options); @@ -73,7 +73,7 @@ export { selfType as self } from "./bindgen-lib-internal.ts"; export const fn = (signature: SignatureOptions) => new FunctionDefinition([signature]); -/** One JS function, several signatures, told apart by how many arguments were passed and by their kinds. */ +/** One JS function, several signatures, told apart by the kinds of their arguments. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ diff --git a/src/js/node/os.ts b/src/js/node/os.ts index accf8b3932c6..348ef349bdec 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -102,6 +102,7 @@ function bound(binding) { : $bundleError("TODO: endianness"); }, freemem: binding.freemem, + // Plain functions where Node's are, so that `new os.homedir()` does what it does there. The other four are not constructors in Node either. getPriority: function (pid) { return binding.getPriority(pid); }, diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index f088b155015c..bdccb33e3396 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -6,7 +6,7 @@ use core::marker::{PhantomData, PhantomPinned}; use core::ptr::NonNull; use crate::{ - ArrayBuffer, JSFunction, JSGlobalObject, JSObject, JSString, JSValue, JsError, JsThread, + ArrayBuffer, JSGlobalObject, JSObject, JSString, JSValue, JsError, JsThread, MarkedArgumentBuffer, Strong, }; @@ -21,11 +21,11 @@ type OpaqueFfi = (UnsafeCell<[u8; 0]>, PhantomPinned); // ───────────────────────────── capabilities ───────────────────────────── -/// Allocate, read JS-owned memory, describe an error to throw. Cannot run JavaScript. +/// What allocates, reads JS-owned memory and describes an error to throw. It cannot run JavaScript. #[repr(C)] pub struct JsHeap<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); -/// Reach the Rust payloads of wrapper objects. +/// What reaches the Rust payloads of wrapper objects. #[repr(C)] pub struct JsPayloads<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); @@ -101,6 +101,7 @@ impl<'a> JsCall<'a> { self.heap.global() } + /// For the APIs that take the tree's own proof of being on the JS thread. #[inline] pub fn js_thread(&mut self) -> JsThread<'_> { self.heap.global().js_thread_of_caller_no_frame() @@ -147,6 +148,7 @@ impl<'a> Error<'a> { } } + /// Throws it now, for a caller that is not a generated thunk. #[cold] pub fn into_js_error(self, global: &JSGlobalObject) -> JsError { match self.0 { @@ -237,6 +239,7 @@ impl<'a> JsValue<'a> { pub const UNDEFINED: Self = JsValue(JSValue::UNDEFINED, PhantomData, PhantomData); pub const NULL: Self = JsValue(JSValue::NULL, PhantomData, PhantomData); + /// `value` is alive now, so it is for the rest of the call that `heap` belongs to. #[inline(always)] pub fn new(_: &JsHeap<'a>, value: JSValue) -> Self { debug_assert!(!value.is_empty()); @@ -247,13 +250,17 @@ impl<'a> JsValue<'a> { pub fn is_undefined(self) -> bool { self.0.is_undefined() } + #[inline] - pub fn as_function(self) -> Option> { + pub fn as_callable(self) -> Option> { self.0.is_callable().then(|| JsValue::wrap(self.0)) } } -impl<'a> JsValue<'a, JSFunction> { +/// Marks a [`JsValue`] that can be called: a function, or a proxy of one. +pub enum Callable {} + +impl<'a> JsValue<'a, Callable> { /// Runs JavaScript. pub fn call( self, @@ -379,6 +386,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { JsValue::wrap(JSValue::from_cell(self.0.as_ptr())) } + /// Lends the payload. `f` cannot run JavaScript: that takes the `&mut JsCall` the `JsPayloads` is borrowed from. #[inline(always)] pub fn with(self, _: &JsPayloads<'a>, f: impl FnOnce(&T) -> R) -> R { // SAFETY: every `&mut T` holds `JsPayloads` exclusively. @@ -387,6 +395,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { result } + /// As [`with`](Self::with), exclusively. #[inline(always)] pub fn with_mut(self, _: &mut JsPayloads<'a>, f: impl FnOnce(&mut T) -> R) -> R { // SAFETY: the exclusive borrow of `JsPayloads` excludes every other payload reference. @@ -453,7 +462,7 @@ impl JsWrapperStrong { // ───────────────────────────── results ───────────────────────────── -/// Rust to JS, one function per schema type: a `Vec` is a string or an array of numbers. +/// Rust to JS, one function per schema type, because the Rust type does not say which: a `Vec` is a string or an array of numbers. pub mod to_js { use super::*; @@ -520,7 +529,7 @@ pub mod to_js { items: I, mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsValue<'a>>, ) -> Result<'a, JsValue<'a>> { - // As many as a `MarkedArgumentBuffer` holds inline, on this stack, where the GC sees them just the same. + // The inline capacity of a `MarkedArgumentBuffer`. const INLINE_CAPACITY: usize = 8; let mut inline = [JSValue::ZERO; INLINE_CAPACITY]; let mut items = items.into_iter(); diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 7076f169b3cf..7750b0b0e997 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -36,7 +36,7 @@ impl ExternOptional { if self.tag == 0 { return None; } - // SAFETY: C++ initialized the arm `tag` names. + // SAFETY: C++ initialized the alternative `tag` names. Some(unsafe { self.data.some }) } } @@ -51,18 +51,25 @@ pub struct ExternVector { } impl ExternVector { /// The buffer becomes the `Vec`, where the global allocator is the one C++ used. + /// + /// # Safety + /// No copy of `self` is used again. #[inline] - pub fn into_vec_unconverted(self) -> Vec { + pub unsafe fn into_vec(self) -> Vec { if !bun_alloc::USE_MIMALLOC || self.data.is_null() { - return self.into_vec(|item| item); + // SAFETY: forwarded. + return unsafe { self.into_vec_with(|item| item) }; } // SAFETY: a mimalloc buffer of `capacity` elements, `length` of them initialized; ownership was transferred. unsafe { Vec::from_raw_parts(self.data, self.length as usize, self.capacity as usize) } } /// Converts every element and frees the buffer. + /// + /// # Safety + /// As [`into_vec`](Self::into_vec). #[inline] - pub fn into_vec(self, mut convert: impl FnMut(T) -> U) -> Vec { + pub unsafe fn into_vec_with(self, mut convert: impl FnMut(T) -> U) -> Vec { if self.data.is_null() { return Vec::new(); } @@ -110,23 +117,26 @@ pub struct ExternWithValue { } unsafe extern "C" { - pub fn Bun__Bindgen__constructArray( + pub(super) fn Bun__Bindgen__constructArray( global: *mut JSGlobalObject, values: *mut MarkedArgumentBuffer, ) -> JSValue; - pub fn Bun__Bindgen__constructArrayFromValues( + pub(super) fn Bun__Bindgen__constructArrayFromValues( global: *mut JSGlobalObject, values: *const JSValue, count: usize, ) -> JSValue; - pub fn Bun__Bindgen__createObject( + pub(super) fn Bun__Bindgen__createObject( global: *mut JSGlobalObject, structure_index: u32, values: *const JSValue, count: usize, ) -> JSValue; - pub fn Bun__Bindgen__enumerationValueToJS(global: *mut JSGlobalObject, index: u32) -> JSValue; - pub fn Bun__Bindgen__setInternalField( + pub(super) fn Bun__Bindgen__enumerationValueToJS( + global: *mut JSGlobalObject, + index: u32, + ) -> JSValue; + pub(super) fn Bun__Bindgen__setInternalField( global: *mut JSGlobalObject, cell: *mut c_void, offset: u32, diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 2421cf27c7e8..be1942c685bf 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -23,7 +23,7 @@ export const requiredAndOptionalArg = b.fn({ returns: b.i32, }); -export const utf8Echo = b.fn({ args: { text: b.string.utf8 }, returns: b.string.utf8 }); +export const utf8Echo = b.fn({ args: { text: b.string.utf8 }, returns: b.string.utf8, length: 3 }); export const utf8EchoLoose = b.fn({ args: { text: b.string.loose.utf8 }, returns: b.string.utf8 }); export const utf8Optional = b.fn({ args: { text: b.string.utf8.nullable }, returns: b.string.utf8.nullable }); export const utf8Owned = b.fn({ args: { text: b.string.utf8.owned }, returns: b.string.utf8 }); @@ -151,7 +151,7 @@ export const TestAnything = b.union({ }); export const describeAnything = b.fn({ args: { value: TestAnything }, returns: b.string.utf8, capability: "js" }); -/** Told apart by the number of arguments, by the first and by the second. */ +/** Told apart by the first argument, then by the second. */ export const overloaded = b.overloads( { args: { text: b.string.utf8, times: b.u32.default(1) }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 86c9ca4d47a8..3d2296e33f27 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -207,7 +207,7 @@ pub fn sum_before_and_after_callback<'a>( callback: JsValue<'a>, ) -> bindgen::Result<'a, [u32; 2]> { let before = buffer.with_bytes(&cx.heap, sum); - let Some(callback) = callback.as_function() else { + let Some(callback) = callback.as_callable() else { return Err(cx .heap .throw_type_error(format_args!("callback must be a function"))); @@ -298,7 +298,7 @@ impl TestCounterClass for TestCounter { } fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { - if let Some(listener) = this.listener_field(&cx.heap).as_function() { + 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)) diff --git a/src/jsc/bindings/Bindgen.cpp b/src/jsc/bindings/Bindgen.cpp index cdde4c489ea8..5f78e41b72c4 100644 --- a/src/jsc/bindings/Bindgen.cpp +++ b/src/jsc/bindings/Bindgen.cpp @@ -44,8 +44,8 @@ void Identifiers::visit(Visitor& visitor) visitor.appendUnbarriered(string); } } -template void Identifiers::visit(JSC::AbstractSlotVisitor&); -template void Identifiers::visit(JSC::SlotVisitor&); +template void Identifiers::visit(AbstractSlotVisitor&); +template void Identifiers::visit(SlotVisitor&); Identifiers& identifiers(VM& vm) { @@ -57,7 +57,7 @@ static_assert(std::ranges::all_of(Tables::objectStructures, [](const auto& struc Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) { auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; - const Identifier* memberNames = identifiers(vm).identifiersAt(firstMemberNameIndex, memberCount); + const Identifier* memberNames = identifiers(vm).memberNames(firstMemberNameIndex, memberCount); Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); for (unsigned i = 0; i < memberCount; ++i) { PropertyOffset offset; @@ -67,9 +67,9 @@ Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned return structure; } -NEVER_INLINE JSValue DictionaryMemberReader::get(unsigned index) +JSValue DictionaryMemberReader::get(unsigned index) { - if (ownPropertiesAreTheWholeAnswer()) [[likely]] { + if (canSkipPrototypeChain()) [[likely]] { unsigned attributes = 0; PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); if (offset == invalidOffset) @@ -105,15 +105,11 @@ bool JSStringUTF8View::convert() return m_converted.tryAppend(byteCast(lenient.span())); } -} - -using namespace JSC; - // An empty value is a member that is absent. extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) { VM& vm = getVM(globalObject); - ASSERT(count == Bun::Bindgen::Tables::objectStructures[structureIndex].memberCount); + ASSERT(count == Tables::objectStructures[structureIndex].memberCount); bool hasEveryMember = true; for (size_t i = 0; i < count; ++i) hasEveryMember &= !!values[i]; @@ -123,7 +119,7 @@ extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObjec object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); return JSValue::encode(object); } - const Identifier* memberNames = Bun::Bindgen::identifiers(vm).identifiersAt(Bun::Bindgen::Tables::objectStructures[structureIndex].firstMemberNameIndex, count); + const Identifier* memberNames = identifiers(vm).memberNames(Tables::objectStructures[structureIndex].firstMemberNameIndex, count); JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count); for (size_t i = 0; i < count; ++i) { if (values[i]) @@ -134,7 +130,7 @@ extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObjec extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* globalObject, unsigned index) { - return JSValue::encode(Bun::Bindgen::identifiers(getVM(globalObject)).stringAt(index)); + return JSValue::encode(identifiers(getVM(globalObject)).enumerationValue(index)); } extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) @@ -152,3 +148,5 @@ extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObj } RELEASE_AND_RETURN(scope, JSValue::encode(constructArray(globalObject, static_cast(nullptr), *values))); } + +} diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 0f54093de4a0..99628347bb85 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -12,16 +12,16 @@ class DictionaryMemberReader { : m_vm(vm) , m_globalObject(globalObject) , m_object(object) - , m_memberNames(identifiers(vm).identifiersAt(firstMemberNameIndex, memberNameCount)) + , m_memberNames(identifiers(vm).memberNames(firstMemberNameIndex, memberNameCount)) { } // May throw. Out of line: inlined, it is most of the size of every generated dictionary converter. - JSC::JSValue get(unsigned index); + NEVER_INLINE JSC::JSValue get(unsigned index); private: // `get` stops before Object.prototype. Asked per member: converting one can run JavaScript. - ALWAYS_INLINE bool ownPropertiesAreTheWholeAnswer() const + ALWAYS_INLINE bool canSkipPrototypeChain() const { if (m_object->type() != JSC::FinalObjectType) return false; diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 75b2bb972ccb..0b7a25951cbf 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h new file mode 100644 index 000000000000..d4bf7f84ba66 --- /dev/null +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -0,0 +1,58 @@ +#pragma once +#include "root.h" +#include "ErrorCode.h" +#include "IDLTypes.h" +#include "JSDOMConvertBase.h" + +namespace Bun::Bindgen { + +// `.enforceRange(min, max)`: WebIDL's [EnforceRange], with a range of its own. +template +struct IDLEnforceRange : WebCore::IDLType {}; + +// `.validateInteger(min, max)`: Node's validateInteger, with its errors. +template +struct IDLValidateInteger : WebCore::IDLType {}; + +} + +template +struct WebCore::Converter> : WebCore::DefaultConverter> { + static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + double number = value.toNumber(&globalObject); + RETURN_IF_EXCEPTION(scope, 0); + if (std::isfinite(number)) { + number = std::trunc(number); + if (number >= Min && number <= Max) + return static_cast(number); + } + throwTypeError(&globalObject, scope, makeString("Value "_s, number, " is outside the range ["_s, static_cast(Min), ", "_s, static_cast(Max), ']')); + return 0; + } +}; + +template +struct WebCore::Converter> : WebCore::DefaultConverter> { + static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!value.isNumber()) { + Bun::ERR::INVALID_ARG_TYPE(scope, &globalObject, name, "number"_s, value); + return 0; + } + double number = value.asNumber(); + if (!std::isfinite(number) || number != std::round(number)) { + Bun::ERR::OUT_OF_RANGE(scope, &globalObject, name, "an integer"_s, value); + return 0; + } + if (number < Min || number > Max) { + Bun::ERR::OUT_OF_RANGE(scope, &globalObject, name, Min, Max, value); + return 0; + } + return static_cast(number); + } +}; diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index fafd357dff1e..21217011e307 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -5,7 +5,7 @@ namespace Bun::Bindgen { -// Dictionaries that contain raw `JSValue`s must live on the stack. +// Dictionaries that hold a raw `JSValue` or a `JSStringUTF8View` must live on the stack. template struct IDLStackOnlyDictionary : WebCore::IDLType { using SequenceStorageType = void; @@ -13,7 +13,7 @@ struct IDLStackOnlyDictionary : WebCore::IDLType { using NullableParameterType = const T&; }; -// A generated union: a struct with a member for each alternative, and a tag. +// A generated union: a struct with a tag and a member for each alternative, so that the chosen one is converted where it stays. A large one is a std::optional, made when chosen. template struct IDLTaggedUnion : WebCore::IDLType {}; diff --git a/src/jsc/bindings/Bindgen/Identifiers.h b/src/jsc/bindings/Bindgen/Identifiers.h index 28a9699672ad..783b9d779598 100644 --- a/src/jsc/bindings/Bindgen/Identifiers.h +++ b/src/jsc/bindings/Bindgen/Identifiers.h @@ -14,7 +14,7 @@ class Identifiers { explicit Identifiers(JSC::VM&); // `count` identifiers starting at Tables::memberNames[first] (GeneratedBindings+Tables.h). - ALWAYS_INLINE const JSC::Identifier* identifiersAt(unsigned first, unsigned count) + ALWAYS_INLINE const JSC::Identifier* memberNames(unsigned first, unsigned count) { if (m_memberNames[first].isNull()) [[unlikely]] initializeMemberNames(first, count); @@ -22,7 +22,7 @@ class Identifiers { } // Tables::enumerationValues[index]. - ALWAYS_INLINE JSC::JSString* stringAt(unsigned index) + ALWAYS_INLINE JSC::JSString* enumerationValue(unsigned index) { if (!m_enumerationValues[index]) [[unlikely]] initializeEnumerationValue(index); diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSBindgenObject.h index 2c194b04bd9e..40566e328034 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSBindgenObject.h @@ -157,7 +157,7 @@ class JSBindgenConstructor final : public JSC::InternalFunction { static JSBindgenConstructor* create(JSC::VM&, JSC::JSGlobalObject*, const ClassDefinition&, JSC::JSObject* prototype); private: - JSBindgenConstructor(JSC::VM& vm, JSC::Structure* structure, JSC::NativeFunction construct); + JSBindgenConstructor(JSC::VM&, JSC::Structure*, JSC::NativeFunction construct); }; // By index into Tables::classes (GeneratedBindings+Tables.h). diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 7def3cf2e7a7..4548c32ece8a 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -38,9 +38,7 @@ class JSStringUTF8View { m_isNull = false; } - bool isNull() const { return m_isNull; } - - // Null data only when isNull(). + // Null data only for a view that was not initialized. std::span bytes() const { if (m_isNull) diff --git a/src/jsc/bindings/BindgenAdditionsToGlobalObject.h b/src/jsc/bindings/BindgenAdditionsToGlobalObject.h index 98b57211fa3d..b3f1b293078b 100644 --- a/src/jsc/bindings/BindgenAdditionsToGlobalObject.h +++ b/src/jsc/bindings/BindgenAdditionsToGlobalObject.h @@ -6,7 +6,7 @@ namespace Bun { -// What src/codegen/bindgen.ts generates has per global object, by index into Bindgen::Tables. +// The per-global-object state of generated bindings, by index into Bindgen::Tables. struct BindgenAdditionsToGlobalObject { BindgenAdditionsToGlobalObject(); void initialize(); diff --git a/src/jsc/bindings/BindgenCustomEnforceRange.h b/src/jsc/bindings/BindgenCustomEnforceRange.h deleted file mode 100644 index ff8d94a40caa..000000000000 --- a/src/jsc/bindings/BindgenCustomEnforceRange.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once -#include "root.h" -#include "IDLTypes.h" -#include "JSDOMConvertBase.h" -#include "ErrorCode.h" - -namespace Bun { - -enum class BindgenCustomEnforceRangeKind { - Node, - Web, -}; - -// `.validateInteger(min, max)` and `.enforceRange(min, max)`. -template< - std::integral NumericType, - NumericType Min, - NumericType Max, - BindgenCustomEnforceRangeKind Kind> -struct BindgenCustomEnforceRange : WebCore::IDLType { -}; - -} - -static String rangeErrorString(double value, double min, double max) -{ - return makeString("Value "_s, value, " is outside the range ["_s, min, ", "_s, max, ']'); -} - -namespace WebCore { - -template< - std::integral NumericType, - NumericType Min, - NumericType Max, - Bun::BindgenCustomEnforceRangeKind Kind> -struct Converter> - : DefaultConverter> { - template - static inline NumericType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower()) - { - auto scope = DECLARE_THROW_SCOPE(lexicalGlobalObject.vm()); - double unrestricted; - if constexpr (Kind == Bun::BindgenCustomEnforceRangeKind::Node) { - // In Node.js, `validateNumber`, `validateInt32`, `validateUint32`, - // and `validateInteger` all start with the following - // - // if (typeof value !== 'number') - // throw new ERR_INVALID_ARG_TYPE(name, 'number', value); - // - if (!value.isNumber()) { - Bun::ERR::INVALID_ARG_TYPE(scope, &lexicalGlobalObject, exceptionThrower(), "number"_s, value); - return 0; - } - unrestricted = value.asNumber(); - - if (!std::isfinite(unrestricted) || unrestricted != std::round(unrestricted)) { - // ERR_OUT_OF_RANGE "an integer" - Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), "an integer"_s, value); - return 0; - } - } else { - // WebIDL uses toNumber before applying range restrictions, so `true` passes. Node's validators refuse it. - unrestricted = value.toNumber(&lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, 0); - - if (std::isnan(unrestricted) || std::isinf(unrestricted)) { - throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); - return 0; - } - - // IDL uses trunc to convert the double to an integer. - unrestricted = trunc(unrestricted); - } - - bool inRange = unrestricted >= Min && unrestricted <= Max; - if (!inRange) { - if constexpr (Kind == Bun::BindgenCustomEnforceRangeKind::Node) { - Bun::ERR::OUT_OF_RANGE(scope, &lexicalGlobalObject, exceptionThrower(), Min, Max, value); - } else { - // WebKit range exception - throwTypeError(&lexicalGlobalObject, scope, rangeErrorString(unrestricted, Min, Max)); - } - return 0; - } - - return static_cast(unrestricted); - } -}; - -} diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 9318d10d1f20..2543947af402 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -15,8 +15,8 @@ template<> struct WebCore::Converter : WebCore::DefaultConverter }; template -struct WebCore::Converter> - : Bun::DefaultContextConverter> { +struct WebCore::Converter> + : Bun::DefaultContextConverter> { using ReturnType = WebCore::Converter>::ReturnType; @@ -119,6 +119,7 @@ template<> struct WebCore::Converter Ctx& ctx) { auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { return jsBuffer; } @@ -130,7 +131,6 @@ template<> struct WebCore::Converter if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { return bytes; } - auto scope = DECLARE_THROW_SCOPE(vm); JSC::throwOutOfMemoryError(&globalObject, scope); } return std::nullopt; diff --git a/src/jsc/bindings/BunIDLConvertContext.h b/src/jsc/bindings/BunIDLConvertContext.h index 583f560f2fda..b82a15ad6216 100644 --- a/src/jsc/bindings/BunIDLConvertContext.h +++ b/src/jsc/bindings/BunIDLConvertContext.h @@ -16,14 +16,13 @@ concept IDLConversionContext = std::derived_from struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; + using Type = IDLAlternatives; }; template struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; + using Type = IDLAlternatives; }; - } template @@ -126,7 +125,7 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { } template - void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + void throwBadEnumerationValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { derived().throwTypeMustBe(global, scope, idlHumanReadableName()); } diff --git a/src/jsc/bindings/BunIDLConvertNumbers.h b/src/jsc/bindings/BunIDLConvertNumbers.h index 2e7d8e10b86f..25f07a7e4322 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/BunIDLConvertNumbers.h @@ -101,6 +101,7 @@ struct WebCore::Converter> } double number = value.asNumber(); + // Written so that NaN is caught here: casting it to an integer is undefined. if (!(number <= maxSafeInteger && number >= -maxSafeInteger)) { ctx.throwNumberNotInteger(globalObject, scope, number); return {}; diff --git a/src/jsc/bindings/BunIDLHumanReadable.h b/src/jsc/bindings/BunIDLHumanReadable.h index fa0c5b7a6e6d..30e2b1188331 100644 --- a/src/jsc/bindings/BunIDLHumanReadable.h +++ b/src/jsc/bindings/BunIDLHumanReadable.h @@ -101,7 +101,7 @@ struct IDLHumanReadableName> : BaseIDLHumanReadableNam }; template -struct IDLHumanReadableName> +struct IDLHumanReadableName> : IDLHumanReadableName> {}; template @@ -118,10 +118,10 @@ struct IDLHumanReadableName> : BaseIDLHumanReadableNam }; template -struct IDLHumanReadableName> : IDLHumanReadableName {}; +struct IDLHumanReadableName> : IDLHumanReadableName {}; template -struct IDLHumanReadableName> : BaseIDLHumanReadableName { +struct IDLHumanReadableName> : BaseIDLHumanReadableName { static constexpr bool isDisjunction = sizeof...(IDL) > 1; static constexpr auto humanReadableName = Bun::joinCStringsAsList(Detail::nestedHumanReadableName()...); diff --git a/src/jsc/bindings/BunIDLTypes.h b/src/jsc/bindings/BunIDLTypes.h index ea2395f92c6d..91490c016065 100644 --- a/src/jsc/bindings/BunIDLTypes.h +++ b/src/jsc/bindings/BunIDLTypes.h @@ -26,7 +26,7 @@ struct IDLStrictNull : WebCore::IDLType {}; // Treats all falsy values as null. template -struct IDLLooseNullable : WebCore::IDLNullable {}; +struct IDLFalsyAsNull : WebCore::IDLNullable {}; template struct IDLStrictInteger : WebCore::IDLInteger {}; @@ -40,7 +40,7 @@ struct IDLLooseInteger : IDLStrictInteger {}; // The alternatives of a union, for IDLHumanReadableName. template -struct IDLOrderedUnion {}; +struct IDLAlternatives {}; namespace Detail { template diff --git a/src/jsc/bindings/StrongRef.h b/src/jsc/bindings/StrongRef.h index 26143c1a5bad..a7bff3d33419 100644 --- a/src/jsc/bindings/StrongRef.h +++ b/src/jsc/bindings/StrongRef.h @@ -1,6 +1,5 @@ #pragma once #include -#include namespace Bun { // One occupied slot in a StrongRootBlock; see StrongRef.cpp. @@ -10,16 +9,3 @@ struct StrongRefImpl; extern "C" void Bun__StrongRef__delete(Bun::StrongRefImpl* _Nonnull ref); extern "C" Bun::StrongRefImpl* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); extern "C" void Bun__StrongRef__set(Bun::StrongRefImpl* _Nonnull ref, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); - -namespace Bun { - -struct StrongRefDeleter { - void operator()(StrongRefImpl* _Nonnull ref) - { - Bun__StrongRef__delete(ref); - } -}; - -using StrongRef = std::unique_ptr; - -} diff --git a/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h b/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h index 2cd3d61dd236..2d149d0aa54b 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/jsc/bindings/webcore/JSDOMConvertEnumeration.h @@ -59,7 +59,7 @@ template struct Converter> : DefaultConverter>(lexicalGlobalObject, throwScope); + ctx.template throwBadEnumerationValue>(lexicalGlobalObject, throwScope); return {}; } diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index 5285f16bc18f..925864278536 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -1,6 +1,4 @@ -//! Bindgen target for `fmt_jsc.bind.ts`. The actual formatters live in -//! `src/bun_core/fmt.rs`; only the JS-facing wrapper -//! lives here so `bun_core/` stays JSC-free. +//! The bodies of `fmt_jsc.bind.ts`. The formatters are in `src/bun_core/fmt.rs`, which stays JSC-free. use std::io::Write as _; diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 92fdcc4e95fe..49e300ec54c4 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -14,7 +14,6 @@ export const getPriority = b.fn({ returns: b.i32, capability: "js", throws: true, - length: 1, }); export const homedir = b.fn({ returns: b.string, capability: "js", throws: true }); export const hostname = b.fn({ returns: b.any, capability: "js", throws: true }); @@ -35,7 +34,6 @@ export const userInfo = b.fn({ returns: b.any, capability: "js", throws: true, - length: 1, }); export const version = b.fn({ returns: b.string, throws: true }); @@ -50,7 +48,6 @@ export const setPriority = b.overloads( capability: "js", throws: true, rustPath: "crate::node::os::set_priority", - length: 2, }, { args: { diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index c762ea2e2577..bf3312a24df1 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -94,6 +94,31 @@ itAlways("unsigned integer dictionary fields reject negative values", () => { } }); +describe("integers", () => { + it("strict takes an integer in range, as a number or a BigInt", () => { + for (const value of [5, 5n, 5.0]) expect(bindgen.makeToken(value).id).toBe(5); + expect(bindgen.makeToken(-0).id).toBe(0); + const outOfRange = (message: string) => + expect.objectContaining({ name: "RangeError", code: "ERR_OUT_OF_RANGE", message }); + for (const value of [NaN, Infinity, -Infinity, 1.5, 2 ** 53]) { + expect(() => bindgen.makeToken(value)).toThrow(outOfRange(`id must be an integer (received ${value})`)); + } + for (const value of [-1, 2 ** 32, 2n ** 40n]) { + expect(() => bindgen.makeToken(value)).toThrow( + outOfRange(`id must be in the range [0, 4294967295] (received ${value})`), + ); + } + for (const value of [-1n, 2n ** 70n]) { + expect(() => bindgen.makeToken(value)).toThrow(outOfRange("id must be in the range [0, 4294967295]")); + } + for (const value of ["5", true, null, undefined, {}]) { + expect(() => bindgen.makeToken(value)).toThrow( + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message: "id must be a number" }), + ); + } + }); +}); + describe("strings", () => { const rope = (a: string, b: string) => a + b + a; // long enough that JSC keeps it a rope const texts = [ @@ -157,6 +182,12 @@ describe("strings", () => { expect(bindgen.utf8Optional("")).toBe(""); }); + it("length is the number of required arguments, or what the schema says", () => { + expect(bindgen.utf8EchoLoose.length).toBe(1); + expect(bindgen.utf8Optional.length).toBe(0); + expect(bindgen.utf8Echo.length).toBe(3); + }); + it("an argument that was not passed is undefined", () => { for (const args of [[], [undefined]]) { expect(() => bindgen.utf8Echo(...args)).toThrow( @@ -615,7 +646,7 @@ describe("unions", () => { }); describe("overloads", () => { - it("are told apart by the number of arguments and by their kinds", () => { + it("are told apart by the kinds of their arguments", () => { const { TestCounter } = bindgen as any; const cases: [unknown[], string][] = [ [["ab"], "text times ab"], diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index f33822ece9cc..5a6c77ea13dc 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -166,7 +166,7 @@ describe("emitBindgen", () => { resolve(cfg.cwd, "src", "jsc", "fmt_jsc.bind.ts"), ]; - emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenInternal: [] }), o, dirStamp }); + emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenCodegen: [] }), o, dirStamp }); const inDir = (name: string) => resolve(cfg.codegenDir, name); const cpp = ["GeneratedFmtJsc.cpp", "GeneratedNodeOs.cpp"].map(inDir); @@ -195,7 +195,7 @@ describe("emitBindgen", () => { const bindgen = repoBindFiles(cfg); expect(bindgen).not.toBeEmpty(); - emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenInternal: [] }), o, dirStamp }); + emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenCodegen: [] }), o, dirStamp }); const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); const typesDir = resolve(String(dir), "types"); diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index 5dd0647e1605..3d2c6de3fcc1 100644 --- a/test/js/bun/glob/proto.test.ts +++ b/test/js/bun/glob/proto.test.ts @@ -76,7 +76,6 @@ test("scan options", () => { expect(scan("*", { cwd: new String(path.join(cwd, "sub")) })).toEqual(["b.txt"]); // An array is read like any other object. - expect(scan("package.json", [])).toEqual(scan("package.json", {})); expect(scan("*", Object.assign([], { cwd, dot: true }))).toEqual([".dot", "a.txt"]); }); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 1b97d2e9e5a0..8862b84fb875 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6584,8 +6584,10 @@ it("a view of part of a buffer as a tls option is read as the view", () => { new Uint8Array(both.buffer, offset, length), new DataView(both.buffer, offset, length), ]) { - listen(view).stop(true); - listen([view]).stop(true); + for (const key of [view, [view]]) { + using listener = listen(key); + expect(listener.port).toBeGreaterThan(0); + } } }); From 1129bcf865cf5b8ead926c222c7572dca02e7276 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 23:10:25 +0000 Subject: [PATCH 34/44] Declare the buffer converter's throw scope where it throws --- docs/project/bindgen.mdx | 2 +- src/codegen/bindgen-lib-internal.ts | 14 +++++++------- src/jsc/bindings/BunIDLConvert.h | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index dbf62f770f6d..6bae2ce55586 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -204,7 +204,7 @@ export const BracesOptions = b.dictionary({ 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, internalName, altNames }` to rename its field or to accept other spellings. -A dictionary whose members can all be left out implements `Default`. +A dictionary whose members can all be absent implements `Default`. 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 diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 0fa093ba7f18..828802c44653 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -129,15 +129,15 @@ export function cppEmplace(optional: string, fill: (target: string) => string): } export abstract class Type { - /** `undefined` means "not provided". */ + /** `undefined` means absent. */ get optional(): OptionalType { return new OptionalType(this, "optional"); } - /** `null` or `undefined` means "not provided". */ + /** `null` or `undefined` means absent. */ get nullable(): OptionalType { return new OptionalType(this, "nullable"); } - /** Every falsy value means "not provided". */ + /** Every falsy value means absent. */ get falsyAsNull(): OptionalType { return new OptionalType(this, "falsyAsNull"); } @@ -145,7 +145,7 @@ export abstract class Type { get withValue(): WithValueType { return new WithValueType(this); } - /** Value to use when not provided. Only for arguments and dictionary members. */ + /** Value to use when absent. Only for arguments and dictionary members. */ default(value: unknown): DefaultType { return new DefaultType(this, value); } @@ -812,14 +812,14 @@ export class OptionalType extends Type { /** What converts in place is null when default-constructed. A pointer to a class instance, which does not, is not. */ cppConvertInto(target: string, value: string, ctx: string, name: string): string { if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name); - const provided = { + const present = { optional: `!${value}.isUndefined()`, nullable: `!${value}.isUndefinedOrNull()`, falsyAsNull: `${value}.toBoolean(globalObject)`, }[this.modifier]; const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name); const body = this.inner.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); - return `if (${provided}) {\n${indent(body, 4)}\n}`; + return `if (${present}) {\n${indent(body, 4)}\n}`; } toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.inner.toCpp(value); @@ -1204,7 +1204,7 @@ export class DictionaryType extends NamedType { }); return fields.filter(text => text.length > 0).join("\n"); } - /** Every member can be left out. */ + /** Every member can be absent. */ get hasDefault(): boolean { return this.members.every(member => member.type.takesUndefined); } diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h index 2543947af402..124bb3147986 100644 --- a/src/jsc/bindings/BunIDLConvert.h +++ b/src/jsc/bindings/BunIDLConvert.h @@ -119,7 +119,6 @@ template<> struct WebCore::Converter Ctx& ctx) { auto& vm = JSC::getVM(&globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { return jsBuffer; } @@ -131,6 +130,8 @@ template<> struct WebCore::Converter if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { return bytes; } + // Declared where it throws: at the top, every caller would owe a check for the paths that cannot. + auto scope = DECLARE_THROW_SCOPE(vm); JSC::throwOutOfMemoryError(&globalObject, scope); } return std::nullopt; From b81d83a1dfae9d25b5c5d3fbd33e77fffb63dc6e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 23:37:05 +0000 Subject: [PATCH 35/44] Rename JsValue, JsCall, the error makers, $bindgenFn, internalName and the socket option types, drop userFacingName, and break overload ties by written order --- docs/project/bindgen.mdx | 45 +++-- scripts/build/codegen.ts | 14 +- src/codegen/bindgen-emit.ts | 36 ++-- src/codegen/bindgen-lib-internal.ts | 41 ++--- src/codegen/bindgen-lib.ts | 4 +- src/codegen/bindgen.ts | 8 +- src/codegen/bundle-functions.ts | 2 +- src/codegen/generate-js2native.ts | 2 +- src/codegen/replacements.ts | 12 +- src/js/internal-for-testing.ts | 58 +++--- src/js/node/os.ts | 28 +-- src/js/private.d.ts | 6 +- src/jsc/bindgen.rs | 172 +++++++++--------- src/jsc/bindgen_test.rs | 56 +++--- src/jsc/fmt_jsc.bind.ts | 1 - src/jsc/fmt_jsc.rs | 16 +- src/runtime/api/BunObject.rs | 10 +- src/runtime/api/glob.rs | 22 +-- src/runtime/node/node_os.rs | 22 +-- src/runtime/socket/Handlers.rs | 29 ++- src/runtime/socket/SSLConfig.bind.ts | 27 ++- src/runtime/socket/SSLConfig.rs | 28 +-- src/runtime/socket/SocketConfig.bind.ts | 56 +++--- test/internal-for-testing.d.ts | 2 +- test/internal/bindgen.test.ts | 63 +++---- .../build-codegen-declared-outputs.test.ts | 2 +- 26 files changed, 372 insertions(+), 390 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 6bae2ce55586..71e6bc0a8316 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -51,7 +51,7 @@ 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.throw(format_args!("Integer overflow while adding"))) + .ok_or_else(|| heap.error(format_args!("Integer overflow while adding"))) } ``` @@ -66,7 +66,7 @@ Rust signature does not match the schema, the generated code does not compile. To reach the function: -- from `src/js`: `$bindgenFn("bindgen_test.bind.ts", "add")`, which is typed from the schema; +- 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`. ### What a function may do: `capability` @@ -74,11 +74,11 @@ To reach the function: 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 JsCall<'a>` | also run JavaScript, or anything that can call back into native code | +| `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 @@ -86,8 +86,8 @@ and `&mut self` for its object, because nothing can invalidate either while it runs. `throws: true` makes the return type a `Result`. The error is a -`bindgen::Error<'a>`, made by `heap.throw(..)`, `heap.throw_type_error(..)`, -`heap.throw_range_error(..)` or `heap.throw_value(value)`, or any type that +`bindgen::Error<'a>`, made by `heap.error(..)`, `heap.type_error(..)`, +`heap.range_error(..)` or `bindgen::Error::from_value(value)`, or any type that implements `bindgen::IntoError` (`bun_sys::Error` and `JsError` do), which lets a `capability: "none"` function fail too. @@ -119,8 +119,8 @@ arguments from left to right: take. 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 that requires the most arguments wins, then - the one written first. Its conversion reports whatever is wrong. +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. @@ -142,11 +142,11 @@ any other: nothing looks at how many there are. | `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` | `JsValue<'a, ArrayBuffer>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | +| `b.bufferSource` | `JsLocal<'a, BufferSource>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | | `b.bufferSource.bytes` | `&'a [u8]` | the same; see [Buffers](#buffers) | | `b.bufferSource.arrayBuffer` | `ExternalShared` | the same; the bytes of a view are copied | | `b.blob` | `ExternalShared` | a `Blob` | -| `b.any` | `JsValue<'a>` | anything | +| `b.any` | `JsLocal<'a>` | anything | | `b.null` | `()` | `null` or `undefined`; only in a union | | `b.array(T)` | `Vec` | an array | | `b.enumeration(values)` | a generated `enum` | one of the strings | @@ -203,7 +203,7 @@ export const BracesOptions = b.dictionary({ 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, internalName, altNames }` to rename its field or to accept other spellings. +`{ type, rustName, altNames }` to rename its field or to accept other spellings. A dictionary whose members can all be absent implements `Default`. A member is looked up on the object and its prototype chain, **stopping before @@ -220,7 +220,6 @@ The second argument of `b.dictionary` takes: | Key | What it does | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `userFacingName` | what error messages call it, when that is not its name | | `allowArray` | an array is read like any other object, not refused | | `nonObjectAsEmpty` | a value whose `typeof` is not `"object"` gives every member its default, 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, \|converted\| ..)`, for a `#[host_fn]` body | @@ -274,7 +273,7 @@ JavaScript takes `b.bufferSource` itself and looks the bytes up when it needs th ```rust let before = buffer.with_bytes(&cx.heap, sum); -callback.call(cx, JsValue::UNDEFINED, &[])?; +callback.call(cx, JsLocal::UNDEFINED, &[])?; let after = buffer.with_bytes(&cx.heap, sum); ``` @@ -300,17 +299,17 @@ fn set_name<'a>(&mut self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, name: W ## Values and lifetimes -A JS value in Rust is a `JsValue<'a>`: a `JSValue` tied to the host call it belongs +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. `JsValue<'a, T>` is one known to be a `JSString`, -`JSObject`, `Callable` or `ArrayBuffer`. `value.to_js()` gives the `JSValue`; -`JsValue::new(heap, value)` goes the other way. +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 `JsValue::new(heap, strong.get())`. It is made, read and dropped on the JS thread. +- 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`: the GC does not look inside heap allocations. @@ -394,7 +393,7 @@ undefined behaviour, so it gets the wrapper instead and borrows the payload for long as nothing else can run: ```rust -fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { +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(), &[])?; } @@ -421,4 +420,4 @@ 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; `$bindgenFn` 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. +- `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. diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index 76ecbc5560c4..6c4a646223ce 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -870,7 +870,7 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void { desc: "JS modules (bundle-modules)", // Note: arg is BUILD_PATH (buildDir), not CODEGEN_PATH. The script // derives CODEGEN_DIR = join(BUILD_PATH, "codegen") internally. - args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, withoutBindingsFlag(cfg, sources)]), + args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, typesOnlyFlag(cfg, sources)]), }, }); @@ -922,13 +922,13 @@ function emitBakeCodegen({ n, cfg, sources, o, dirStamp }: Ctx): void { } /** A `*_test.bind.ts` binds what exists for tests. Only a build where Rust has `debug_assertions` has it, and none ships. */ -function hasBindings(cfg: CodegenFields, src: string): boolean { +function isCompiled(cfg: CodegenFields, src: string): boolean { return cfg.debug || cfg.assertions || !src.endsWith("_test.bind.ts"); } -function withoutBindingsFlag(cfg: CodegenFields, sources: Sources): string { - return `--without-bindings=${sources.bindgen - .filter(src => !hasBindings(cfg, src)) +function typesOnlyFlag(cfg: CodegenFields, sources: Sources): string { + return `--types-only=${sources.bindgen + .filter(src => !isCompiled(cfg, src)) .map(src => basename(src)) .join(",")}`; } @@ -937,7 +937,7 @@ function withoutBindingsFlag(cfg: CodegenFields, sources: Sources): string { export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { const script = resolve(cfg.cwd, "src", "codegen", "bindgen.ts"); const crates = new Map(sources.bindgen.map(src => [src, crateOf(src)])); - const compiled = sources.bindgen.filter(src => hasBindings(cfg, src)); + const compiled = sources.bindgen.filter(src => isCompiled(cfg, src)); // What bindgen.ts writes follows from the names and places of its inputs alone. const outputNames = compiled.map(src => `Generated${pascalCase(basename(src, ".bind.ts"))}`); @@ -968,7 +968,7 @@ export function emitBindgen({ n, cfg, sources, o, dirStamp }: Ctx): void { script, // Every schema: build/types is shared by every profile, so Bindgen.d.ts describes them all. `--sources=${sources.bindgen.map(src => `${crates.get(src)}=${src}`).join(",")}`, - withoutBindingsFlag(cfg, sources), + typesOnlyFlag(cfg, sources), `--codegen-path=${cfg.codegenDir}`, `--types-path=${cfg.typesDir}`, ]), diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 575374162960..010dae02609f 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -338,10 +338,10 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC namespace Bun::Bindgen::Generated { struct ${name} { ${cppDefaultConstructor(name)} -${type.members.map(member => ` ${implementationType(member.type)} ${member.internalName};`).join("\n")} +${type.members.map(member => ` ${implementationType(member.type)} ${member.fieldName};`).join("\n")} }; struct Extern${name} { -${type.members.map(member => ` ${externType(member.type)} ${member.internalName};`).join("\n")} +${type.members.map(member => ` ${externType(member.type)} ${member.fieldName};`).join("\n")} }; ${layoutAssert(`Extern${name}`, type)} } @@ -350,14 +350,12 @@ template<> struct Bun::Bindgen::ExternTraits<${type.cpp}> { static ExternType convertToExtern(${type.cpp}&& value) { return ExternType { -${type.members - .map(member => ` .${member.internalName} = ${toExtern(`value.${member.internalName}`)},`) - .join("\n")} +${type.members.map(member => ` .${member.fieldName} = ${toExtern(`value.${member.fieldName}`)},`).join("\n")} }; } }; template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("${type.userFacingName}"); + static constexpr auto humanReadableName = std::to_array("${type.name}"); }; template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue, ${type.cpp}&); ${ @@ -422,7 +420,7 @@ function cppSourceForDictionary(type: DictionaryType): string { const [first, ...others] = member.names.map( () => `members.get(${nameIndex++});\nRETURN_IF_EXCEPTION(scope, void());`, ); - const qualified = `${type.userFacingName}.${member.name}`; + const qualified = `${type.name}.${member.name}`; const lines = [ `[[maybe_unused]] auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(qualified)} };`, `JSC::JSValue value${i} = ${first}`, @@ -434,7 +432,7 @@ function cppSourceForDictionary(type: DictionaryType): string { return; }`); lines.push( - member.type.cppConvertInto(`result.${member.internalName}`, `value${i}`, `ctx${i}`, qualified), + member.type.cppConvertInto(`result.${member.fieldName}`, `value${i}`, `ctx${i}`, qualified), "RETURN_IF_EXCEPTION(scope, void());", ); return ` {\n${indent(lines.join("\n"), 8)}\n }`; @@ -446,7 +444,7 @@ template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalOb auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - [[maybe_unused]] auto ctx = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.userFacingName)} }; + [[maybe_unused]] auto ctx = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.name)} }; auto* object = value.getObject(); ${ type.nonObjectAsEmpty @@ -903,7 +901,7 @@ ${array( // ───────────────────────────── Rust ───────────────────────────── -const jsValueResult = "bindgen::Result<'a, bindgen::JsValue<'a>>"; +const jsValueResult = "bindgen::Result<'a, bindgen::JsLocal<'a>>"; function rustLayoutAssert(rustType: string, type: Type): string { const { size, align } = type.layout(); @@ -929,7 +927,7 @@ const rustArguments = (signature: Signature, scope: RustScope) => const capabilityParameter = { none: [], heap: ["heap: &bindgen::JsHeap<'a>"], - js: ["cx: &mut bindgen::JsCall<'a>"], + js: ["cx: &mut bindgen::JsScope<'a>"], }; const capabilityArgument = { none: [], heap: ["&cx.heap"], js: ["cx"] }; @@ -946,7 +944,7 @@ const rustThunk = (symbol: string, parameters: string[], returns: string, body: pub unsafe extern "C" fn ${symbol}(${["global: *mut JSGlobalObject", ...parameters].join(", ")}) -> ${returns} { // SAFETY: called by generated C++, whose frame holds what it passes until this returns. unsafe { - bindgen::JsCall::with(global, |cx| { + bindgen::JsScope::with(global, |cx| { ${indent(body, 12)} }) } @@ -1140,9 +1138,9 @@ function rustForClass(type: ClassType, scope: RustScope): string { bindgen::host_fn_return(cx, result, |heap, value| bindgen::to_js::any(heap, bindgen::JsWrapper::create_with_structure(heap, structure, value).as_value()))`, ); declarations.push(`/// The constructor function, \`${name}\` in JavaScript. - fn get_constructor<'a>(heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { + fn get_constructor<'a>(heap: &bindgen::JsHeap<'a>) -> bindgen::JsLocal<'a> { // SAFETY: cannot throw; makes the class's objects on first use. - unsafe { bindgen::JsValue::from_extern(${symbolName(type, "getConstructor")}(heap.global_ptr())) } + unsafe { bindgen::JsLocal::from_extern(${symbolName(type, "getConstructor")}(heap.global_ptr())) } }`); } @@ -1219,20 +1217,20 @@ let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiv const fieldDeclarations = type.internalFields .map(field => { const id = snakeCase(field); - return ` fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a>; - fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>);`; + return ` fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsLocal<'a>; + fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsLocal<'a, T>);`; }) .join("\n"); const fieldDefinitions = type.internalFields .map((field, index) => { const id = snakeCase(field); return ` #[inline(always)] - fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsValue<'a> { + fn ${id}_field(self, heap: &bindgen::JsHeap<'a>) -> bindgen::JsLocal<'a> { // SAFETY: internal field ${index} of ${type.numberOfInternalFields}. unsafe { self.internal_field(heap, ${index}) } } #[inline(always)] - fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsValue<'a, T>) { + fn set_${id}_field(self, heap: &bindgen::JsHeap<'a>, value: bindgen::JsLocal<'a, T>) { // SAFETY: internal field ${index} of ${type.numberOfInternalFields}. unsafe { self.set_internal_field(heap, ${index}, value) } }`; @@ -1399,7 +1397,7 @@ ${[ ]; return `${banner(null)}declare namespace $Bindgen { ${declarations.filter(text => text.length > 0).join("\n")} - /** What \`$bindgenFn(file, name)\` returns. */ + /** What \`$bindgen(file, name)\` returns. */ interface Exports { ${files .filter(file => exported(file).length > 0) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 828802c44653..4367d8d6eef2 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -240,7 +240,7 @@ export abstract class Type { rustReturnType(scope: RustScope): string { return this.rustResultFieldType(scope); } - /** A Rust expression of type `bindgen::Result<'a, bindgen::JsValue<'a>>` that consumes `value`; `heap` is in scope. */ + /** A Rust expression of type `bindgen::Result<'a, bindgen::JsLocal<'a>>` that consumes `value`; `heap` is in scope. */ rustToJS(value: string, scope: RustScope): string { throw new TypeError(`${this.tsType} cannot be returned to JavaScript`); } @@ -632,10 +632,10 @@ export class BufferSourceType extends Type { return "JSValue"; } rustType(): string { - return "bindgen::JsValue<'a, bun_jsc::ArrayBuffer>"; + return "bindgen::JsLocal<'a, bindgen::BufferSource>"; } rustFromExtern(expr: string): string { - return `bindgen::JsValue::from_extern(${expr})`; + return `bindgen::JsLocal::from_extern(${expr})`; } rustToJS(value: string): string { return `bindgen::to_js::any(heap, ${value})`; @@ -688,7 +688,7 @@ export class WithValueType extends Type { return `bindgen::WithValue<'a, ${this.inner.rustType(scope)}>`; } rustFromExtern(expr: string, scope: RustScope): string { - return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsValue::from_extern(with_value.value) } }`; + return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsLocal::from_extern(with_value.value) } }`; } } @@ -706,7 +706,7 @@ export class AnyType extends Type { return "JSC::jsUndefined()"; } toRust(): string { - return "bindgen::JsValue::UNDEFINED"; + return "bindgen::JsLocal::UNDEFINED"; } layout(): Layout { return { size: 8, align: 8 }; @@ -715,10 +715,10 @@ export class AnyType extends Type { return "JSValue"; } rustType(): string { - return "bindgen::JsValue<'a>"; + return "bindgen::JsLocal<'a>"; } rustFromExtern(expr: string): string { - return `bindgen::JsValue::from_extern(${expr})`; + return `bindgen::JsLocal::from_extern(${expr})`; } rustToJS(value: string): string { return `bindgen::to_js::any(heap, ${value})`; @@ -862,7 +862,7 @@ export class OptionalType extends Type { } rustToJS(value: string, scope: RustScope): string { const none = this.modifier === "optional" ? "UNDEFINED" : "NULL"; - return `match ${value} { Some(value) => ${this.inner.rustToJS("value", scope)}, None => Ok(bindgen::JsValue::${none}) }`; + return `match ${value} { Some(value) => ${this.inner.rustToJS("value", scope)}, None => Ok(bindgen::JsLocal::${none}) }`; } } @@ -1085,35 +1085,35 @@ export class EnumerationType extends NamedType { export interface MemberOptions { type: Type; - /** Name of the C++ field and, in snake_case, of the Rust one. Defaults to the JS name. */ - internalName?: string; + /** Name of the field in the Rust struct, and in the C++ one that mirrors it. Defaults to the JS name in snake_case. */ + rustName?: string; /** Other JS names, tried in order while the value is `undefined`. */ altNames?: readonly string[]; } export class Member { readonly names: readonly string[]; - readonly internalName: string; + /** Of the field in the generated C++ struct and, keywords aside, in the Rust one. */ + readonly fieldName: string; readonly type: Type; constructor(name: string, spec: Type | MemberOptions) { const options: MemberOptions = spec instanceof Type ? { type: spec } : spec; this.names = [name, ...(options.altNames ?? [])]; for (const name of this.names) assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a member is named by an identifier`); - this.internalName = options.internalName ?? name; + this.fieldName = options.rustName ?? snakeCase(name); + assert(/^[a-z_][a-z0-9_]*$/.test(this.fieldName), `${this.fieldName}: rustName is written in snake_case`); this.type = options.type; } get name(): string { return this.names[0]; } get rustName(): string { - return rustIdent(snakeCase(this.internalName)); + return rustIdent(this.fieldName); } } export interface DictionaryOptions { - /** What error messages call it. Defaults to its name. */ - userFacingName?: string; /** Also generate `Name::from_js`, for Rust that holds a JS value and no typed signature. */ fromJS?: boolean; /** An array is read like any other object, not refused. */ @@ -1156,9 +1156,6 @@ export class DictionaryType extends NamedType { get needsLifetime(): boolean { return this.accepted ? super.needsLifetime : this.holdsJSValue; } - get userFacingName(): string { - return this.options.userFacingName ?? this.name; - } get fromJS(): boolean { return !!this.options.fromJS; } @@ -1200,7 +1197,7 @@ export class DictionaryType extends NamedType { if (Object.hasOwn(given, member.name)) field = given[member.name]; else if (member.type instanceof DefaultType) field = member.type.value; else assert(member.type.takesUndefined, `missing key: ${member.name}`); - return member.type.cppAssign(`${target}.${member.internalName}`, field); + return member.type.cppAssign(`${target}.${member.fieldName}`, field); }); return fields.filter(text => text.length > 0).join("\n"); } @@ -1308,7 +1305,7 @@ export class UnionType extends NamedType { // ───────────────────────────── functions ───────────────────────────── -/** What the Rust body is handed: nothing, `&JsHeap` (allocates JS values) or `&mut JsCall` (runs JavaScript). */ +/** What the Rust body is handed: nothing, `&JsHeap` (allocates JS values) or `&mut JsScope` (runs JavaScript). */ export type Capability = "none" | "heap" | "js"; export interface SignatureOptions { @@ -1376,7 +1373,7 @@ export class FunctionDefinition { this.signatures.forEach((signature, i) => assert( chosen.has(signature), - `overload ${i + 1} is never chosen: nothing tells it from the others. Give an argument a type of another kind`, + `overload ${i + 1} is never chosen: the ones before it take every call it would. Move it earlier or remove it`, ), ); } @@ -1389,7 +1386,7 @@ export class FunctionDefinition { const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; const existing = nodes.get(key); if (existing) return existing; - let dispatch: OverloadDispatch = candidates.reduce((a, b) => (b.requiredCount > a.requiredCount ? b : a)); + let dispatch: OverloadDispatch = candidates[0]; if (candidates.length > 1 && candidates.some(signature => argument < signature.args.length)) { const types = candidates.map(signature => signature.args[argument]?.type); const next = (kind: ValueKind | "other") => { diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 612398cc2b32..7148eede7779 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -38,10 +38,10 @@ export const f64 = new DoubleType(); /** An owned `bun_core::String`. Strict: only a string. `.loose` for `ToString`, then `.utf8` for `&[u8]`. */ export const string = new StringType(); -/** The JS value itself, as a `JsValue<'a>`. */ +/** The JS value itself, as a `JsLocal<'a>`. */ export const any = new AnyType(); -/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsValue<'a, ArrayBuffer>`. `.bytes`, `.arrayBuffer`. */ +/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, ArrayBuffer>`. `.bytes`, `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index c18ef3873d42..5e49697e8602 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1,5 +1,5 @@ #!/usr/bin/env bun -// Usage: bindgen.ts --sources==,.. --without-bindings=,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) +// Usage: bindgen.ts --sources==,.. --types-only=,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) import assert from "node:assert"; import path from "node:path"; @@ -65,7 +65,7 @@ function sortTypes(file: SchemaFile): void { file.types = sorted; } -const args = argParse(["sources", "without-bindings", "codegen-path", "types-path"]); +const args = argParse(["sources", "types-only", "codegen-path", "types-path"]); const codegenPath: string = args["codegen-path"]; const sources = (args.sources as string) .split(",") @@ -75,9 +75,9 @@ const sources = (args.sources as string) const allFiles: SchemaFile[] = []; for (const source of sources) allFiles.push(await load(source)); allFiles.forEach(sortTypes); -const withoutBindings = ((args["without-bindings"] as string | undefined) ?? "").split(","); +const typesOnly = ((args["types-only"] as string | undefined) ?? "").split(","); /** What this build compiles. The types describe every schema. */ -const files = allFiles.filter(file => !withoutBindings.includes(`${file.origin.stem}.bind.ts`)); +const files = allFiles.filter(file => !typesOnly.includes(`${file.origin.stem}.bind.ts`)); const owners = new Map(); for (const file of files) { for (const { name } of file.types) { diff --git a/src/codegen/bundle-functions.ts b/src/codegen/bundle-functions.ts index 13c3fe971bd3..2bda8d231a18 100644 --- a/src/codegen/bundle-functions.ts +++ b/src/codegen/bundle-functions.ts @@ -17,7 +17,7 @@ // to refer to the private slot. // - Some magic macros (see replacements.ts) such as `$assert()` (which forcibly // deletes the argument in a release build, unlike a define macro), and the -// magical `$cpp` and `$bindgenFn` bindgen stuff -- a story for another day. +// magical `$cpp` and `$bindgen` bindgen stuff -- a story for another day. // // Part of the transpilation process involves removing type annotations. And // that is done with Bun's bundler. But we need to construct an IIFE, and the diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index 1622de50bf5e..ab54aac0fa0e 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -126,7 +126,7 @@ function resolveNativeFileId(call_type: NativeCallType, filename: string) { const resolved = sourceFiles.find(file => file.endsWith(sep + filename)); if (!resolved) { - const fnName = call_type === "bind" ? "bindgenFn" : call_type; + const fnName = call_type === "bind" ? "bindgen" : call_type; throw new Error(`Could not find file ${filename} in $${fnName} call`); } diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index ab9d68430804..23a73fccbb88 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -129,10 +129,8 @@ export const enums = { // These are passed to --define to the bundler const debug = process.argv[2] === "--debug=ON"; -/** Schemas this build leaves out (`hasBindings` in scripts/build/codegen.ts). `$bindgenFn` of one is `void 0`, which the minifier folds. */ -const withoutBindings = (process.argv.find(arg => arg.startsWith("--without-bindings=")) ?? "=") - .split("=")[1] - .split(","); +/** Schemas this build only takes types from (`isCompiled` in scripts/build/codegen.ts). `$bindgen` of one is `void 0`, which the minifier folds. */ +const typesOnly = (process.argv.find(arg => arg.startsWith("--types-only=")) ?? "=").split("=")[1].split(","); export const define: Record = { "process.env.NODE_ENV": JSON.stringify(debug ? "development" : "production"), "IS_BUN_DEVELOPMENT": String(debug), @@ -175,7 +173,7 @@ export const function_replacements = [ "$isPromiseFulfilled", "$isPromiseRejected", "$isPromisePending", - "$bindgenFn", + "$bindgen", ]; const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll("$", "")})`); @@ -271,7 +269,7 @@ export function applyReplacements(src: string, length: number) { args = `(__intrinsic__peekPromiseStatus${inner.result} === ${status})`; } return [slice.slice(0, match.index) + args, inner.rest, true]; - } else if (name === "bindgenFn") { + } else if (name === "bindgen") { const inner = sliceSourceCode(rest, true); let args; try { @@ -290,7 +288,7 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } - if (withoutBindings.includes(args[0])) return [slice.slice(0, match.index) + "void 0", inner.rest, true]; + if (typesOnly.includes(args[0])) return [slice.slice(0, match.index) + "void 0", inner.rest, true]; const id = registerNativeCall("bind", args[0], args[1], null); diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index b1ee597cb0e0..6206d2f6a4bd 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -7,7 +7,7 @@ // In a debug build, the import is always allowed. // It is disallowed in release builds unless run in Bun's CI. -const fmtBinding = $bindgenFn("fmt_jsc.bind.ts", "fmtString"); +const fmtBinding = $bindgen("fmt_jsc.bind.ts", "fmtString"); export const highlightJavaScript = (code: string) => fmtBinding(code, "highlight-javascript"); export const highlightJavaScriptRedacted = (code: string) => fmtBinding(code, "highlight-javascript-redacted"); @@ -243,33 +243,33 @@ export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFramework }; // Only in a build with assertions: `hasBindings` in scripts/build/codegen.ts. -export const bindgen = $bindgenFn("bindgen_test.bind.ts", "add") && { - add: $bindgenFn("bindgen_test.bind.ts", "add"), - requiredAndOptionalArg: $bindgenFn("bindgen_test.bind.ts", "requiredAndOptionalArg"), - utf8Echo: $bindgenFn("bindgen_test.bind.ts", "utf8Echo"), - utf8EchoLoose: $bindgenFn("bindgen_test.bind.ts", "utf8EchoLoose"), - utf8Optional: $bindgenFn("bindgen_test.bind.ts", "utf8Optional"), - utf8Owned: $bindgenFn("bindgen_test.bind.ts", "utf8Owned"), - stringEcho: $bindgenFn("bindgen_test.bind.ts", "stringEcho"), - nextColor: $bindgenFn("bindgen_test.bind.ts", "nextColor"), - optionsEcho: $bindgenFn("bindgen_test.bind.ts", "optionsEcho"), - describeValue: $bindgenFn("bindgen_test.bind.ts", "describeValue"), - describeAnything: $bindgenFn("bindgen_test.bind.ts", "describeAnything"), - overloaded: $bindgenFn("bindgen_test.bind.ts", "overloaded"), - describeDefaults: $bindgenFn("bindgen_test.bind.ts", "describeDefaults"), - names: $bindgenFn("bindgen_test.bind.ts", "names"), - sumArray: $bindgenFn("bindgen_test.bind.ts", "sumArray"), - range: $bindgenFn("bindgen_test.bind.ts", "range"), - words: $bindgenFn("bindgen_test.bind.ts", "words"), - sumBytes: $bindgenFn("bindgen_test.bind.ts", "sumBytes"), - sumBytesOptional: $bindgenFn("bindgen_test.bind.ts", "sumBytesOptional"), - sumBytesThenText: $bindgenFn("bindgen_test.bind.ts", "sumBytesThenText"), - sumBeforeAndAfterCallback: $bindgenFn("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), - withValueEcho: $bindgenFn("bindgen_test.bind.ts", "withValueEcho"), - fail: $bindgenFn("bindgen_test.bind.ts", "fail"), - TestCounter: $bindgenFn("bindgen_test.bind.ts", "TestCounter"), - TestHolder: $bindgenFn("bindgen_test.bind.ts", "TestHolder"), - makeToken: $bindgenFn("bindgen_test.bind.ts", "makeToken"), +export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { + add: $bindgen("bindgen_test.bind.ts", "add"), + requiredAndOptionalArg: $bindgen("bindgen_test.bind.ts", "requiredAndOptionalArg"), + utf8Echo: $bindgen("bindgen_test.bind.ts", "utf8Echo"), + utf8EchoLoose: $bindgen("bindgen_test.bind.ts", "utf8EchoLoose"), + utf8Optional: $bindgen("bindgen_test.bind.ts", "utf8Optional"), + utf8Owned: $bindgen("bindgen_test.bind.ts", "utf8Owned"), + stringEcho: $bindgen("bindgen_test.bind.ts", "stringEcho"), + nextColor: $bindgen("bindgen_test.bind.ts", "nextColor"), + optionsEcho: $bindgen("bindgen_test.bind.ts", "optionsEcho"), + describeValue: $bindgen("bindgen_test.bind.ts", "describeValue"), + describeAnything: $bindgen("bindgen_test.bind.ts", "describeAnything"), + overloaded: $bindgen("bindgen_test.bind.ts", "overloaded"), + describeDefaults: $bindgen("bindgen_test.bind.ts", "describeDefaults"), + names: $bindgen("bindgen_test.bind.ts", "names"), + sumArray: $bindgen("bindgen_test.bind.ts", "sumArray"), + range: $bindgen("bindgen_test.bind.ts", "range"), + words: $bindgen("bindgen_test.bind.ts", "words"), + sumBytes: $bindgen("bindgen_test.bind.ts", "sumBytes"), + sumBytesOptional: $bindgen("bindgen_test.bind.ts", "sumBytesOptional"), + sumBytesThenText: $bindgen("bindgen_test.bind.ts", "sumBytesThenText"), + sumBeforeAndAfterCallback: $bindgen("bindgen_test.bind.ts", "sumBeforeAndAfterCallback"), + withValueEcho: $bindgen("bindgen_test.bind.ts", "withValueEcho"), + fail: $bindgen("bindgen_test.bind.ts", "fail"), + TestCounter: $bindgen("bindgen_test.bind.ts", "TestCounter"), + TestHolder: $bindgen("bindgen_test.bind.ts", "TestHolder"), + makeToken: $bindgen("bindgen_test.bind.ts", "makeToken"), }; export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting"); @@ -605,7 +605,7 @@ export const decodeURIComponentSIMD = $newCppFunction( 1, ); -export const getDevServerDeinitCount = $bindgenFn("DevServer.bind.ts", "getDeinitCountForTesting"); +export const getDevServerDeinitCount = $bindgen("DevServer.bind.ts", "getDeinitCountForTesting"); export const getCounters = $newRustFunction("Counters.rs", "createCountersObject", 0); export const linearFifoOrderedRemoveProbe = $newRustFunction( "collections/linear_fifo.rs", diff --git a/src/js/node/os.ts b/src/js/node/os.ts index 348ef349bdec..39ca250ef850 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -170,20 +170,20 @@ function bound(binding) { } const out = bound({ - hostCpuCount: $bindgenFn("node_os.bind.ts", "hostCpuCount")(), - cpus: $bindgenFn("node_os.bind.ts", "cpus"), - freemem: $bindgenFn("node_os.bind.ts", "freemem"), - getPriority: $bindgenFn("node_os.bind.ts", "getPriority"), - homedir: $bindgenFn("node_os.bind.ts", "homedir"), - hostname: $bindgenFn("node_os.bind.ts", "hostname"), - loadavg: $bindgenFn("node_os.bind.ts", "loadavg"), - networkInterfaces: $bindgenFn("node_os.bind.ts", "networkInterfaces"), - release: $bindgenFn("node_os.bind.ts", "release"), - totalmem: $bindgenFn("node_os.bind.ts", "totalmem"), - uptime: $bindgenFn("node_os.bind.ts", "uptime"), - userInfo: $bindgenFn("node_os.bind.ts", "userInfo"), - version: $bindgenFn("node_os.bind.ts", "version"), - setPriority: $bindgenFn("node_os.bind.ts", "setPriority"), + hostCpuCount: $bindgen("node_os.bind.ts", "hostCpuCount")(), + cpus: $bindgen("node_os.bind.ts", "cpus"), + freemem: $bindgen("node_os.bind.ts", "freemem"), + getPriority: $bindgen("node_os.bind.ts", "getPriority"), + homedir: $bindgen("node_os.bind.ts", "homedir"), + hostname: $bindgen("node_os.bind.ts", "hostname"), + loadavg: $bindgen("node_os.bind.ts", "loadavg"), + networkInterfaces: $bindgen("node_os.bind.ts", "networkInterfaces"), + release: $bindgen("node_os.bind.ts", "release"), + totalmem: $bindgen("node_os.bind.ts", "totalmem"), + uptime: $bindgen("node_os.bind.ts", "uptime"), + userInfo: $bindgen("node_os.bind.ts", "userInfo"), + version: $bindgen("node_os.bind.ts", "version"), + setPriority: $bindgen("node_os.bind.ts", "setPriority"), }); symbolToStringify(out, "arch"); diff --git a/src/js/private.d.ts b/src/js/private.d.ts index a021b4ce067f..53ffd831248e 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -278,11 +278,11 @@ declare function $newRustFunction any>( * [bindgen's documentation](https://bun.com/docs/project/bindgen). * `undefined` for a `*_test.bind.ts` file in a build without assertions. * @param filename - The basename of the `.bind.ts` file. - * @param symbol - The name of the export. + * @param name - The name of the export. */ -declare function $bindgenFn( +declare function $bindgen( filename: File, - symbol: Name, + name: Name, ): $Bindgen.Exports[File][Name]; // NOTE: $debug, $assert, and $isPromiseFulfilled omitted diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index bdccb33e3396..535d800b2a82 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -29,9 +29,9 @@ pub struct JsHeap<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); #[repr(C)] pub struct JsPayloads<'a>(OpaqueFfi, InvariantLifetime<'a>, NotSend); -/// `&mut JsCall` is what runs JavaScript. It is the `JSGlobalObject*`: both fields are zero-sized at offset 0. +/// `&mut JsScope` is what runs JavaScript. It is the `JSGlobalObject*`: both fields are zero-sized at offset 0. #[repr(C)] -pub struct JsCall<'a> { +pub struct JsScope<'a> { pub heap: JsHeap<'a>, pub payloads: JsPayloads<'a>, } @@ -39,7 +39,7 @@ pub struct JsCall<'a> { impl<'a> JsHeap<'a> { #[inline(always)] fn global(&self) -> &JSGlobalObject { - // SAFETY: a `JsHeap` only exists at the address of a live global (see `JsCall::with`). + // SAFETY: a `JsHeap` only exists at the address of a live global (see `JsScope::with`). unsafe { JSGlobalObject::opaque_ref_nn(core::ptr::from_ref(self).cast()) } } @@ -50,52 +50,45 @@ impl<'a> JsHeap<'a> { } #[inline] - pub fn create_utf8_string(&self, utf8: &[u8]) -> Result<'a, JsValue<'a, JSString>> { - JsValue::from_js_result(crate::bun_string_jsc::create_utf8_for_js( + pub fn create_utf8_string(&self, utf8: &[u8]) -> Result<'a, JsLocal<'a, JSString>> { + JsLocal::from_js_result(crate::bun_string_jsc::create_utf8_for_js( self.global(), utf8, )) } + /// An `Error` for the body to return. It is thrown after that. #[cold] - pub fn throw(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::to_throw(self.global().create_error_instance(args)) + pub fn error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::unthrown(self.global().create_error_instance(args)) } + /// As [`error`](Self::error), a `TypeError`. #[cold] - pub fn throw_type_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::to_throw(self.global().create_type_error_instance(args)) + pub fn type_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::unthrown(self.global().create_type_error_instance(args)) } + /// As [`error`](Self::error), a `RangeError`. #[cold] - pub fn throw_range_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { - Error::to_throw(self.global().create_range_error_instance(args)) - } - - #[cold] - pub fn throw_out_of_memory(&self) -> Error<'a> { - Error::pending(JsError::OutOfMemory) - } - - #[cold] - pub fn throw_value(&self, value: JsValue<'a, T>) -> Error<'a> { - Error::to_throw(value.0) + pub fn range_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::unthrown(self.global().create_range_error_instance(args)) } } -impl<'a> JsCall<'a> { +impl<'a> JsScope<'a> { /// # Safety /// `global` is this thread's live global object and the caller was entered by the engine or the event loop. #[inline(always)] pub unsafe fn with( global: *mut JSGlobalObject, - f: impl for<'call> FnOnce(&mut JsCall<'call>) -> R, + f: impl for<'call> FnOnce(&mut JsScope<'call>) -> R, ) -> R { - // SAFETY: `JsCall` is zero-sized and `global` is non-null. - f(unsafe { &mut *global.cast::>() }) + // SAFETY: `JsScope` is zero-sized and `global` is non-null. + f(unsafe { &mut *global.cast::>() }) } - /// Anything can be done with a `&JSGlobalObject`, so it borrows the whole `JsCall`. + /// Anything can be done with a `&JSGlobalObject`, so it borrows the whole `JsScope`. #[inline(always)] pub fn global(&mut self) -> &JSGlobalObject { self.heap.global() @@ -125,10 +118,16 @@ pub type Result<'a, T> = core::result::Result>; impl<'a> Error<'a> { #[inline(always)] - fn to_throw(value: JSValue) -> Self { + fn unthrown(value: JSValue) -> Self { Error(ErrorState::ToThrow(value), PhantomData, PhantomData) } + /// `throw value`, once the body has returned. + #[cold] + pub fn from_value(value: JsLocal<'a, T>) -> Self { + Self::unthrown(value.0) + } + #[inline(always)] fn pending(error: JsError) -> Self { Error(ErrorState::Pending(error), PhantomData, PhantomData) @@ -137,7 +136,7 @@ impl<'a> Error<'a> { #[doc(hidden)] #[cold] #[inline(never)] - pub fn into_host_fn_return(self, cx: &mut JsCall<'a>) -> JSValue { + pub fn into_host_fn_return(self, cx: &mut JsScope<'a>) -> JSValue { let global = cx.global(); match self.0 { ErrorState::Pending(error) => crate::host_fn::host_call_error_value(global, error), @@ -177,7 +176,7 @@ impl<'a> IntoError<'a> for JsError { impl<'a> IntoError<'a> for bun_sys::Error { #[cold] fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { - Error::to_throw(crate::SysErrorJsc::to_js(&self, heap.global())) + Error::unthrown(crate::SysErrorJsc::to_js(&self, heap.global())) } } impl<'a> IntoError<'a> for core::convert::Infallible { @@ -190,24 +189,24 @@ impl<'a> IntoError<'a> for core::convert::Infallible { /// A [`JSValue`] held by a host call, known to be a `T`. Lists of them go in a [`MarkedArgumentBuffer`]. #[repr(transparent)] -pub struct JsValue<'a, T = JSValue>(JSValue, InvariantLifetime<'a>, PhantomData<*const T>); -impl Clone for JsValue<'_, T> { +pub struct JsLocal<'a, T = JSValue>(JSValue, InvariantLifetime<'a>, PhantomData<*const T>); +impl Clone for JsLocal<'_, T> { #[inline(always)] fn clone(&self) -> Self { *self } } -impl Copy for JsValue<'_, T> {} +impl Copy for JsLocal<'_, T> {} -impl<'a, T> JsValue<'a, T> { +impl<'a, T> JsLocal<'a, T> { #[inline(always)] - fn wrap(value: JSValue) -> Self { - JsValue(value, PhantomData, PhantomData) + fn from_raw(value: JSValue) -> Self { + JsLocal(value, PhantomData, PhantomData) } #[inline(always)] fn from_js_result(result: crate::JsResult) -> Result<'a, Self> { - result.map(Self::wrap).map_err(Error::pending) + result.map(Self::from_raw).map_err(Error::pending) } /// # Safety @@ -215,7 +214,7 @@ impl<'a, T> JsValue<'a, T> { #[doc(hidden)] #[inline(always)] pub unsafe fn from_extern(value: JSValue) -> Self { - Self::wrap(value) + Self::from_raw(value) } #[inline(always)] @@ -224,8 +223,8 @@ impl<'a, T> JsValue<'a, T> { } #[inline(always)] - pub fn as_value(self) -> JsValue<'a> { - JsValue::wrap(self.0) + pub fn as_value(self) -> JsLocal<'a> { + JsLocal::from_raw(self.0) } /// Keeps the value past the call. @@ -235,15 +234,15 @@ impl<'a, T> JsValue<'a, T> { } } -impl<'a> JsValue<'a> { - pub const UNDEFINED: Self = JsValue(JSValue::UNDEFINED, PhantomData, PhantomData); - pub const NULL: Self = JsValue(JSValue::NULL, PhantomData, PhantomData); +impl<'a> JsLocal<'a> { + pub const UNDEFINED: Self = JsLocal(JSValue::UNDEFINED, PhantomData, PhantomData); + pub const NULL: Self = JsLocal(JSValue::NULL, PhantomData, PhantomData); /// `value` is alive now, so it is for the rest of the call that `heap` belongs to. #[inline(always)] pub fn new(_: &JsHeap<'a>, value: JSValue) -> Self { debug_assert!(!value.is_empty()); - Self::wrap(value) + Self::from_raw(value) } #[inline(always)] @@ -252,29 +251,32 @@ impl<'a> JsValue<'a> { } #[inline] - pub fn as_callable(self) -> Option> { - self.0.is_callable().then(|| JsValue::wrap(self.0)) + pub fn as_callable(self) -> Option> { + self.0.is_callable().then(|| JsLocal::from_raw(self.0)) } } -/// Marks a [`JsValue`] that can be called: a function, or a proxy of one. +/// Marks a [`JsLocal`] that can be called: a function, or a proxy of one. pub enum Callable {} -impl<'a> JsValue<'a, Callable> { +impl<'a> JsLocal<'a, Callable> { /// Runs JavaScript. pub fn call( self, - cx: &mut JsCall<'a>, - this: JsValue<'a, T>, - args: &[JsValue<'a>], - ) -> Result<'a, JsValue<'a>> { - // SAFETY: `JsValue` is `repr(transparent)` over `JSValue`. + cx: &mut JsScope<'a>, + this: JsLocal<'a, T>, + args: &[JsLocal<'a>], + ) -> Result<'a, JsLocal<'a>> { + // SAFETY: `JsLocal` is `repr(transparent)` over `JSValue`. let args = unsafe { &*(core::ptr::from_ref(args) as *const [JSValue]) }; - JsValue::from_js_result(self.0.call(cx.heap.global(), this.0, args)) + JsLocal::from_js_result(self.0.call(cx.heap.global(), this.0, args)) } } -impl<'a> JsValue<'a, ArrayBuffer> { +/// Marks a [`JsLocal`] that is a typed array, a `DataView` or an `ArrayBuffer`. +pub enum BufferSource {} + +impl<'a> JsLocal<'a, BufferSource> { /// The bytes as they are now. A detached or out-of-bounds view is empty. #[inline] pub fn with_bytes(self, heap: &JsHeap<'a>, f: impl FnOnce(&[u8]) -> R) -> R { @@ -289,7 +291,7 @@ impl<'a> JsValue<'a, ArrayBuffer> { #[derive(Clone, Copy)] pub struct WithValue<'a, T> { pub converted: T, - pub value: JsValue<'a>, + pub value: JsLocal<'a>, } // ───────────────────────────── classes ───────────────────────────── @@ -382,11 +384,11 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { } #[inline(always)] - pub fn as_value(self) -> JsValue<'a, JSObject> { - JsValue::wrap(JSValue::from_cell(self.0.as_ptr())) + pub fn as_value(self) -> JsLocal<'a, JSObject> { + JsLocal::from_raw(JSValue::from_cell(self.0.as_ptr())) } - /// Lends the payload. `f` cannot run JavaScript: that takes the `&mut JsCall` the `JsPayloads` is borrowed from. + /// Lends the payload. `f` cannot run JavaScript: that takes the `&mut JsScope` the `JsPayloads` is borrowed from. #[inline(always)] pub fn with(self, _: &JsPayloads<'a>, f: impl FnOnce(&T) -> R) -> R { // SAFETY: every `&mut T` holds `JsPayloads` exclusively. @@ -414,9 +416,9 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { /// `index` is one of the class's `internalFields`. #[doc(hidden)] #[inline(always)] - pub unsafe fn internal_field(self, _: &JsHeap<'a>, index: usize) -> JsValue<'a> { + pub unsafe fn internal_field(self, _: &JsHeap<'a>, index: usize) -> JsLocal<'a> { // SAFETY: generated offset inside the cell; they are initialized to `undefined`. - JsValue::wrap(unsafe { + JsLocal::from_raw(unsafe { self.0 .as_ptr() .byte_add(T::INTERNAL_FIELDS_OFFSET) @@ -434,7 +436,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { self, heap: &JsHeap<'a>, index: usize, - value: JsValue<'a, V>, + value: JsLocal<'a, V>, ) { // SAFETY: as above; C++ stores through the `WriteBarrier`. unsafe { @@ -469,8 +471,8 @@ pub mod to_js { macro_rules! number { ($($name:ident: $t:ty => $f:expr;)*) => {$( #[inline(always)] - pub fn $name<'a>(_: &JsHeap<'a>, value: $t) -> Result<'a, JsValue<'a>> { - Ok(JsValue::wrap($f(value))) + pub fn $name<'a>(_: &JsHeap<'a>, value: $t) -> Result<'a, JsLocal<'a>> { + Ok(JsLocal::from_raw($f(value))) } )*}; } @@ -486,49 +488,49 @@ pub mod to_js { } #[inline(always)] - pub fn undefined<'a>(_: &JsHeap<'a>, (): ()) -> Result<'a, JsValue<'a>> { - Ok(JsValue::UNDEFINED) + pub fn undefined<'a>(_: &JsHeap<'a>, (): ()) -> Result<'a, JsLocal<'a>> { + Ok(JsLocal::UNDEFINED) } /// A body may also return a plain [`JSValue`]: it goes straight back to JSC. pub trait Any<'a> { - fn into_value(self, heap: &JsHeap<'a>) -> JsValue<'a>; + fn into_value(self, heap: &JsHeap<'a>) -> JsLocal<'a>; } - impl<'a, T> Any<'a> for JsValue<'a, T> { + impl<'a, T> Any<'a> for JsLocal<'a, T> { #[inline(always)] - fn into_value(self, _: &JsHeap<'a>) -> JsValue<'a> { + fn into_value(self, _: &JsHeap<'a>) -> JsLocal<'a> { self.as_value() } } impl<'a> Any<'a> for JSValue { #[inline(always)] - fn into_value(self, heap: &JsHeap<'a>) -> JsValue<'a> { - JsValue::new(heap, self) + fn into_value(self, heap: &JsHeap<'a>) -> JsLocal<'a> { + JsLocal::new(heap, self) } } #[inline(always)] - pub fn any<'a>(heap: &JsHeap<'a>, value: impl Any<'a>) -> Result<'a, JsValue<'a>> { + pub fn any<'a>(heap: &JsHeap<'a>, value: impl Any<'a>) -> Result<'a, JsLocal<'a>> { Ok(value.into_value(heap)) } #[inline] - pub fn utf8<'a>(heap: &JsHeap<'a>, value: impl AsRef<[u8]>) -> Result<'a, JsValue<'a>> { + pub fn utf8<'a>(heap: &JsHeap<'a>, value: impl AsRef<[u8]>) -> Result<'a, JsLocal<'a>> { heap.create_utf8_string(value.as_ref()) - .map(JsValue::as_value) + .map(JsLocal::as_value) } #[inline] - pub fn string<'a>(heap: &JsHeap<'a>, value: bun_core::String) -> Result<'a, JsValue<'a>> { - JsValue::from_js_result(crate::StringJsc::into_js(value, heap.global())) + pub fn string<'a>(heap: &JsHeap<'a>, value: bun_core::String) -> Result<'a, JsLocal<'a>> { + JsLocal::from_js_result(crate::StringJsc::into_js(value, heap.global())) } #[inline] pub fn array<'a, I: IntoIterator>( heap: &JsHeap<'a>, items: I, - mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsValue<'a>>, - ) -> Result<'a, JsValue<'a>> { + mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, + ) -> Result<'a, JsLocal<'a>> { // The inline capacity of a `MarkedArgumentBuffer`. const INLINE_CAPACITY: usize = 8; let mut inline = [JSValue::ZERO; INLINE_CAPACITY]; @@ -536,7 +538,7 @@ pub mod to_js { for count in 0..inline.len() { let Some(item) = items.next() else { // SAFETY: `count` initialized values. - return JsValue::from_js_result(crate::call_zero_is_throw( + return JsLocal::from_js_result(crate::call_zero_is_throw( heap.global(), || unsafe { extern_types::Bun__Bindgen__constructArrayFromValues( @@ -557,7 +559,7 @@ pub mod to_js { values.append(convert(heap, item)?.0); } // SAFETY: `values` is live for the call. - JsValue::from_js_result(crate::call_zero_is_throw(heap.global(), || unsafe { + JsLocal::from_js_result(crate::call_zero_is_throw(heap.global(), || unsafe { extern_types::Bun__Bindgen__constructArray(heap.global_ptr(), values) })) }) @@ -570,9 +572,9 @@ pub mod to_js { heap: &JsHeap<'a>, structure_index: u32, values: &[JSValue], - ) -> Result<'a, JsValue<'a>> { + ) -> Result<'a, JsLocal<'a>> { // SAFETY: `values` is on the caller's stack, where the GC sees it; this cannot throw. - Ok(JsValue::wrap(unsafe { + Ok(JsLocal::from_raw(unsafe { extern_types::Bun__Bindgen__createObject( heap.global_ptr(), structure_index, @@ -584,9 +586,9 @@ pub mod to_js { #[doc(hidden)] #[inline] - pub fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsValue<'a>> { + pub fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsLocal<'a>> { // SAFETY: a generated index; this cannot throw. - Ok(JsValue::wrap(unsafe { + Ok(JsLocal::from_raw(unsafe { extern_types::Bun__Bindgen__enumerationValueToJS(heap.global_ptr(), index) })) } @@ -595,9 +597,9 @@ pub mod to_js { #[doc(hidden)] #[inline(always)] pub fn host_fn_return<'a, T, E: IntoError<'a>>( - cx: &mut JsCall<'a>, + cx: &mut JsScope<'a>, result: core::result::Result, - convert: impl FnOnce(&JsHeap<'a>, T) -> Result<'a, JsValue<'a>>, + convert: impl FnOnce(&JsHeap<'a>, T) -> Result<'a, JsLocal<'a>>, ) -> JSValue { let error = match result { Ok(value) => match convert(&cx.heap, value) { diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 3d2296e33f27..e1c3de004e1e 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -2,10 +2,9 @@ use core::sync::atomic::{AtomicUsize, Ordering}; -use crate::ArrayBuffer; use crate::bindgen::{ - self, IntoError as _, JsCall, JsHeap, JsPayload as _, JsValue, JsWrapper, JsWrapperStrong, - WithValue, + self, BufferSource, IntoError as _, JsHeap, JsLocal, JsPayload as _, JsScope, JsWrapper, + JsWrapperStrong, WithValue, }; use crate::generated_bindgen::bindgen_test::{ TestAnything, TestChoice, TestColor, TestCounterClass, TestCounterInternalFields as _, @@ -16,7 +15,7 @@ use crate::generated_bindgen::bindgen_test::{ // This example should be kept in sync with bindgen's documentation pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { a.checked_add(b) - .ok_or_else(|| heap.throw(format_args!("Integer overflow while adding"))) + .ok_or_else(|| heap.error(format_args!("Integer overflow while adding"))) } pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { @@ -86,7 +85,7 @@ pub fn describe_value(value: TestValue<'_>) -> Vec { } } -pub fn describe_anything<'a>(cx: &mut JsCall<'a>, value: TestAnything<'a>) -> Vec { +pub fn describe_anything<'a>(cx: &mut JsScope<'a>, value: TestAnything<'a>) -> Vec { match value { TestAnything::Nothing => b"nothing".to_vec(), TestAnything::Flag(flag) => format!("flag {flag}").into_bytes(), @@ -94,7 +93,7 @@ pub fn describe_anything<'a>(cx: &mut JsCall<'a>, value: TestAnything<'a>) -> Ve TestAnything::Text(text) => format!("text {text}").into_bytes(), TestAnything::Bytes(buffer) => { // SAFETY: `buffer` holds a ref on it, and nothing else borrows it here. - let buffer: ArrayBuffer = unsafe { (*buffer.get()).as_array_buffer() }; + let buffer: crate::ArrayBuffer = unsafe { (*buffer.get()).as_array_buffer() }; format!("bytes {}", buffer.byte_slice().len()).into_bytes() } TestAnything::Blob(_) => b"blob".to_vec(), @@ -157,7 +156,10 @@ pub fn overloaded_list(list: Vec) -> Vec { format!("list {:?}", list.into_boxed_slice()).into_bytes() } -pub fn overloaded_counter<'a>(cx: &mut JsCall<'a>, counter: JsWrapper<'a, TestCounter>) -> Vec { +pub fn overloaded_counter<'a>( + cx: &mut JsScope<'a>, + counter: JsWrapper<'a, TestCounter>, +) -> Vec { let count = counter.with(&cx.payloads, |counter| counter.count); format!("counter {count}").into_bytes() } @@ -202,23 +204,23 @@ pub fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { } pub fn sum_before_and_after_callback<'a>( - cx: &mut JsCall<'a>, - buffer: JsValue<'a, ArrayBuffer>, - callback: JsValue<'a>, + cx: &mut JsScope<'a>, + buffer: JsLocal<'a, BufferSource>, + callback: JsLocal<'a>, ) -> bindgen::Result<'a, [u32; 2]> { let before = buffer.with_bytes(&cx.heap, sum); let Some(callback) = callback.as_callable() else { return Err(cx .heap - .throw_type_error(format_args!("callback must be a function"))); + .type_error(format_args!("callback must be a function"))); }; - callback.call(cx, JsValue::UNDEFINED, &[])?; + callback.call(cx, JsLocal::UNDEFINED, &[])?; Ok([before, buffer.with_bytes(&cx.heap, sum)]) } -pub fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsValue<'a> { +pub fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsLocal<'a> { if text.converted.is_empty() { - return JsValue::UNDEFINED; + return JsLocal::UNDEFINED; } text.value } @@ -226,16 +228,16 @@ pub fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsV pub fn fail<'a>( heap: &JsHeap<'a>, how: TestFailure, - value: JsValue<'a>, + value: JsLocal<'a>, ) -> bindgen::Result<'a, ()> { Err(match how { - TestFailure::Type => heap.throw_type_error(format_args!("a type error")), - TestFailure::Range => heap.throw_range_error(format_args!("a range error")), - TestFailure::Plain => heap.throw(format_args!("an error")), + TestFailure::Type => heap.type_error(format_args!("a type error")), + TestFailure::Range => heap.range_error(format_args!("a range error")), + TestFailure::Plain => heap.error(format_args!("an error")), TestFailure::Syscall => { bun_sys::Error::from_code(bun_sys::E::ENOENT, bun_sys::Tag::open).into_error(heap) } - TestFailure::Value => heap.throw_value(value), + TestFailure::Value => bindgen::Error::from_value(value), }) } @@ -267,7 +269,7 @@ impl TestCounterClass for TestCounter { fn constructor<'a>(heap: &JsHeap<'a>, start: i32) -> bindgen::Result<'a, Self> { if start < 0 { - return Err(heap.throw_range_error(format_args!("start must not be negative"))); + return Err(heap.range_error(format_args!("start must not be negative"))); } Ok(Self::new(start)) } @@ -284,7 +286,7 @@ impl TestCounterClass for TestCounter { self.label = value.to_vec(); } - fn tag<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a>> { + fn tag<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { bindgen::to_js::array(heap, [self.count], bindgen::to_js::i32) } @@ -293,18 +295,22 @@ impl TestCounterClass for TestCounter { self.count } - fn listen<'a>(&self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, listener: JsValue<'a>) { + fn listen<'a>(&self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, listener: JsLocal<'a>) { this.set_listener_field(heap, listener); } - fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> bindgen::Result<'a, i32> { + 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)) } - fn plus<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>, other: JsWrapper<'a, Self>) -> i32 { + fn plus<'a>( + this: JsWrapper<'a, Self>, + cx: &mut JsScope<'a>, + other: JsWrapper<'a, Self>, + ) -> i32 { let other = other.with(&cx.payloads, |counter| counter.count); this.with(&cx.payloads, |counter| counter.count.wrapping_add(other)) } @@ -333,7 +339,7 @@ impl TestHolderClass for TestHolder { self.counter.get(heap) } - fn increment_counter<'a>(this: JsWrapper<'a, Self>, cx: &mut JsCall<'a>) -> i32 { + fn increment_counter<'a>(this: JsWrapper<'a, Self>, cx: &mut JsScope<'a>) -> i32 { let counter = this.with(&cx.payloads, |holder| holder.counter.get(&cx.heap)); counter.with_mut(&mut cx.payloads, |counter| counter.increment(1)) } diff --git a/src/jsc/fmt_jsc.bind.ts b/src/jsc/fmt_jsc.bind.ts index d0a7bcb9f9b3..39a766e9eaf4 100644 --- a/src/jsc/fmt_jsc.bind.ts +++ b/src/jsc/fmt_jsc.bind.ts @@ -10,6 +10,5 @@ export const fmtString = b.fn({ formatter: Formatter, }, returns: b.string, - capability: "heap", throws: true, }); diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index 925864278536..388f53f3e2f6 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -2,21 +2,17 @@ use std::io::Write as _; -use crate::bindgen::{self, JsHeap}; +use crate::JsError; use bun_core::fmt; use bun_core::{MutableString, String}; pub mod js_bindings { use super::*; - pub use crate::generated_bindgen::fmt_jsc::Formatter; + use crate::generated_bindgen::fmt_jsc::Formatter; /// Internal function for testing in highlighter.test.ts - pub fn fmt_string<'a>( - heap: &JsHeap<'a>, - code: &[u8], - formatter_id: Formatter, - ) -> bindgen::Result<'a, String> { + pub fn fmt_string(code: &[u8], formatter_id: Formatter) -> Result { let mut buffer = MutableString::init_empty(); let writer = buffer.writer(); @@ -30,7 +26,7 @@ pub mod js_bindings { ..Default::default() }, ); - write!(writer, "{}", formatter).map_err(|_| heap.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| JsError::OutOfMemory)?; } Formatter::HighlightJavascriptRedacted => { let formatter = fmt::fmt_javascript( @@ -41,11 +37,11 @@ pub mod js_bindings { redact_sensitive_information: true, }, ); - write!(writer, "{}", formatter).map_err(|_| heap.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| JsError::OutOfMemory)?; } Formatter::EscapePowershell => { write!(writer, "{}", fmt::escape_powershell(code)) - .map_err(|_| heap.throw_out_of_memory())?; + .map_err(|_| JsError::OutOfMemory)?; } } diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 00e7a23ab17c..c9ae697f5a35 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -94,7 +94,7 @@ use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::BracesOptions; use crate::generated_bindgen::glob::GlobClass as _; -use bun_jsc::bindgen::JsCall; +use bun_jsc::bindgen::JsScope; // ─── wrap_static_method adapters ─────────────────────────────────────────── // `#[bun_jsc::host_fn(static)]` is not yet emitted, @@ -387,13 +387,13 @@ fn shell_escape(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult } /// `Bun.gc(force)`: heap size after the collection. -pub(crate) fn gc_from_js(cx: &mut JsCall<'_>, force: bool) -> usize { +pub(crate) fn gc_from_js(cx: &mut JsScope<'_>, force: bool) -> usize { cx.global().bun_vm().as_mut().garbage_collect_from_js(force) } #[allow(clippy::needless_pass_by_value)] // the generated thunk hands over the string it owns pub(crate) fn braces( - cx: &mut JsCall<'_>, + cx: &mut JsScope<'_>, brace_str: BunString, opts: BracesOptions, ) -> JsResult { @@ -1745,9 +1745,9 @@ fn get_archive_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValu } fn get_glob_constructor(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - // SAFETY: a lazy property callback, entered from JSC with no `JsCall` borrowed. + // SAFETY: a lazy property callback, entered from JSC with no `JsScope` borrowed. unsafe { - JsCall::with(global_this.as_mut_ptr(), |cx| { + JsScope::with(global_this.as_mut_ptr(), |cx| { crate::api::glob::Glob::get_constructor(&cx.heap).to_js() }) } diff --git a/src/runtime/api/glob.rs b/src/runtime/api/glob.rs index 531ae0ea0165..325fe832984a 100644 --- a/src/runtime/api/glob.rs +++ b/src/runtime/api/glob.rs @@ -1,6 +1,6 @@ use bun_glob::BunGlobWalker as GlobWalker; use bun_glob::walk; -use bun_jsc::bindgen::{self, IntoError, JsCall, JsHeap, JsValue, JsWrapper}; +use bun_jsc::bindgen::{self, IntoError, JsHeap, JsLocal, JsScope, JsWrapper}; use bun_jsc::bun_string_jsc; use bun_jsc::{ JSGlobalObject, JSPromiseStrong, JSValue, Job, JobContext, JsResult, JsThread, SysErrorJsc as _, @@ -30,7 +30,7 @@ impl<'a> IntoError<'a> for ScanError<'a> { match self { ScanError::Sys(err) => err.into_error(heap), ScanError::Runtime(err) => bun_jsc::JsError::from(err).into_error(heap), - ScanError::CwdTooLong(fn_name) => heap.throw(format_args!( + ScanError::CwdTooLong(fn_name) => heap.error(format_args!( "{fn_name}: invalid `cwd`, longer than {MAX_PATH_BYTES} bytes" )), ScanError::Js(err) => err, @@ -262,11 +262,11 @@ impl AnyGlobWalker { } } - fn matched_paths_to_js<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsValue<'a>> { + fn matched_paths_to_js<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { fn to_js<'a, A: walk::Accessor>( walker: &walk::GlobWalker, heap: &JsHeap<'a>, - ) -> bindgen::Result<'a, JsValue<'a>> { + ) -> bindgen::Result<'a, JsLocal<'a>> { bindgen::to_js::array(heap, walker.matched_paths.keys(), bindgen::to_js::utf8) } match self { @@ -331,12 +331,12 @@ impl JobContext for WalkTask { promise.reject_with_async_stack(global, err.to_js(global))?; return Ok(()); } - let settle = |cx: &mut JsCall<'_>| match this.walker.matched_paths_to_js(&cx.heap) { + let settle = |cx: &mut JsScope<'_>| match this.walker.matched_paths_to_js(&cx.heap) { Ok(paths) => promise.resolve(global, paths.to_js()), Err(err) => promise.reject(global, Err(err.into_js_error(global))), }; - // SAFETY: a job's completion, entered from the event loop with no `JsCall` borrowed. - unsafe { JsCall::with(global.as_mut_ptr(), settle) } + // SAFETY: a job's completion, entered from the event loop with no `JsScope` borrowed. + unsafe { JsScope::with(global.as_mut_ptr(), settle) } } } @@ -428,9 +428,9 @@ impl GlobClass for Glob { fn scan<'a>( this: JsWrapper<'a, Self>, - cx: &mut JsCall<'a>, + cx: &mut JsScope<'a>, options: Option>, - ) -> Result, ScanError<'a>> { + ) -> Result, ScanError<'a>> { let walker = this.with(&cx.payloads, |glob| glob.make_glob_walker(options, "scan"))?; let promise = JSPromiseStrong::init(cx.global()); let value = promise.value(); @@ -439,14 +439,14 @@ impl GlobClass for Glob { WalkTask { walker, err: None }, WalkJs { promise }, ); - Ok(JsValue::new(&cx.heap, value)) + Ok(JsLocal::new(&cx.heap, value)) } fn scan_sync<'a>( &self, heap: &JsHeap<'a>, options: Option>, - ) -> Result, ScanError<'a>> { + ) -> Result, ScanError<'a>> { let mut walker = self.make_glob_walker(options, "scanSync")?; walker .walk() diff --git a/src/runtime/node/node_os.rs b/src/runtime/node/node_os.rs index ef6ea8cbd342..5770b3bd7a66 100644 --- a/src/runtime/node/node_os.rs +++ b/src/runtime/node/node_os.rs @@ -3,7 +3,7 @@ use core::ffi::c_int; use core::ffi::{c_char, c_uint, c_void}; use bun_core::String as BunString; -use bun_jsc::bindgen::JsCall; +use bun_jsc::bindgen::JsScope; use bun_jsc::bun_string_jsc; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; @@ -110,7 +110,7 @@ mod _impl { } } - pub(crate) fn cpus(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn cpus(cx: &mut JsScope<'_>) -> JsResult { let global = cx.global(); #[cfg(any(target_os = "linux", target_os = "android"))] let result = cpus_impl_linux(global); @@ -530,7 +530,7 @@ mod _impl { safe fn get_process_priority(pid: i32) -> i32; } - pub(crate) fn get_priority(cx: &mut JsCall<'_>, pid: i32) -> JsResult { + pub(crate) fn get_priority(cx: &mut JsScope<'_>, pid: i32) -> JsResult { let global = cx.global(); let result = get_process_priority(pid); if result == i32::MAX { @@ -549,7 +549,7 @@ mod _impl { Ok(result) } - pub(crate) fn homedir(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn homedir(cx: &mut JsScope<'_>) -> JsResult { let global = cx.global(); // In Node.js, this is a wrapper around uv_os_homedir. #[cfg(windows)] @@ -653,7 +653,7 @@ mod _impl { } } - pub(crate) fn hostname(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn hostname(cx: &mut JsScope<'_>) -> JsResult { let global = cx.global(); #[cfg(windows)] { @@ -738,7 +738,7 @@ mod _impl { pub(crate) use network_interfaces_windows as network_interfaces; #[cfg(unix)] - pub(crate) fn network_interfaces_posix(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn network_interfaces_posix(cx: &mut JsScope<'_>) -> JsResult { let global_this = cx.global(); // getifaddrs sets a pointer to a linked list let mut interface_start: *mut libc::ifaddrs = core::ptr::null_mut(); @@ -1032,7 +1032,7 @@ mod _impl { } #[cfg(windows)] - pub(crate) fn network_interfaces_windows(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn network_interfaces_windows(cx: &mut JsScope<'_>) -> JsResult { let global_this = cx.global(); let mut ifaces: *mut libuv::uv_interface_address_t = core::ptr::null_mut(); let mut count: c_int = 0; @@ -1266,13 +1266,13 @@ mod _impl { } pub(crate) fn set_priority_of_current_process( - cx: &mut JsCall<'_>, + cx: &mut JsScope<'_>, priority: i32, ) -> JsResult<()> { set_priority(cx, 0, priority) } - pub(crate) fn set_priority(cx: &mut JsCall<'_>, pid: i32, priority: i32) -> JsResult<()> { + pub(crate) fn set_priority(cx: &mut JsScope<'_>, pid: i32, priority: i32) -> JsResult<()> { let global = cx.global(); let errno = set_process_priority_impl(pid, priority); if errno == bun_sys::E::SUCCESS { @@ -1318,7 +1318,7 @@ mod _impl { } } - pub(crate) fn uptime(cx: &mut JsCall<'_>) -> JsResult { + pub(crate) fn uptime(cx: &mut JsScope<'_>) -> JsResult { let global = cx.global(); #[cfg(windows)] { @@ -1357,7 +1357,7 @@ mod _impl { } pub(crate) fn user_info( - cx: &mut JsCall<'_>, + cx: &mut JsScope<'_>, options: crate::generated_bindgen::node_os::UserInfoOptions<'_>, ) -> JsResult { let _ = options; // TODO: diff --git a/src/runtime/socket/Handlers.rs b/src/runtime/socket/Handlers.rs index 46b24ac68a93..74802e4be6d7 100644 --- a/src/runtime/socket/Handlers.rs +++ b/src/runtime/socket/Handlers.rs @@ -3,8 +3,7 @@ use core::ptr::NonNull; use std::rc::Rc; use crate::generated_bindgen::socket_config::{ - SocketConfig as GeneratedSocketConfig, SocketConfigBinaryType as GeneratedBinaryType, - SocketConfigHandlers as GeneratedSocketConfigHandlers, SocketConfigTLS as GeneratedTls, + SocketHandler, SocketHandlerBinaryType, SocketOptions, SocketOptionsTLS, }; use bun_core::Utf8Bytes; use bun_jsc::array_buffer::BinaryType; @@ -76,11 +75,11 @@ pub(crate) struct ReloadedHandlers { pub(crate) binary_type: BinaryType, } -fn binary_type_from_generated(binary_type: GeneratedBinaryType) -> BinaryType { +fn binary_type_from_generated(binary_type: SocketHandlerBinaryType) -> BinaryType { match binary_type { - GeneratedBinaryType::Arraybuffer => BinaryType::ArrayBuffer, - GeneratedBinaryType::Buffer => BinaryType::Buffer, - GeneratedBinaryType::Uint8array => BinaryType::Uint8Array, + SocketHandlerBinaryType::Arraybuffer => BinaryType::ArrayBuffer, + SocketHandlerBinaryType::Buffer => BinaryType::Buffer, + SocketHandlerBinaryType::Uint8array => BinaryType::Uint8Array, } } @@ -311,14 +310,14 @@ impl Handlers { opts: JSValue, mode: SocketMode, ) -> JsResult> { - GeneratedSocketConfigHandlers::from_js(global_object, opts, |generated| { + SocketHandler::from_js(global_object, opts, |generated| { Self::from_generated(global_object, &generated, mode) }) } pub(crate) fn from_generated( global_object: &JSGlobalObject, - generated: &GeneratedSocketConfigHandlers<'_>, + generated: &SocketHandler<'_>, mode: SocketMode, ) -> JsResult> { let callbacks = Self::validate_callbacks(global_object, generated)?; @@ -345,7 +344,7 @@ impl Handlers { /// `JSValue::ZERO`. Array order matches `Bun::JSSocketHandlers::Field`. fn validate_callbacks( global_object: &JSGlobalObject, - generated: &GeneratedSocketConfigHandlers<'_>, + generated: &SocketHandler<'_>, ) -> JsResult<[JSValue; CALLBACK_COUNT]> { macro_rules! validated_callback { ($field:ident, $name:literal) => {{ @@ -408,7 +407,7 @@ impl Handlers { global_object: &JSGlobalObject, opts: JSValue, ) -> JsResult { - GeneratedSocketConfigHandlers::from_js(global_object, opts, |generated| { + SocketHandler::from_js(global_object, opts, |generated| { let callbacks = Self::validate_callbacks(global_object, &generated)?; Ok(ReloadedHandlers { callbacks, @@ -496,20 +495,20 @@ impl SocketConfig { pub(crate) fn from_generated( vm: &'static VirtualMachine, global: &JSGlobalObject, - generated: GeneratedSocketConfig<'_>, + generated: SocketOptions<'_>, mode: SocketMode, ) -> JsResult { let mut result: SocketConfig = 'blk: { let ssl: Option = match &generated.tls { - GeneratedTls::None => None, - GeneratedTls::Boolean(b) => { + SocketOptionsTLS::None => None, + SocketOptionsTLS::Boolean(b) => { if *b { Some(super::tls_true_defaults(vm, mode.is_server())) } else { None } } - GeneratedTls::Object(ssl) => { + SocketOptionsTLS::Object(ssl) => { SSLConfig::from_generated(vm, global, ssl, mode.is_server())? } }; @@ -602,7 +601,7 @@ impl SocketConfig { global_object: &JSGlobalObject, mode: SocketMode, ) -> JsResult { - GeneratedSocketConfig::from_js(global_object, opts, |generated| { + SocketOptions::from_js(global_object, opts, |generated| { Self::from_generated(vm, global_object, generated, mode) }) } diff --git a/src/runtime/socket/SSLConfig.bind.ts b/src/runtime/socket/SSLConfig.bind.ts index f08e95d1f059..ee2439f7df31 100644 --- a/src/runtime/socket/SSLConfig.bind.ts +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -1,17 +1,17 @@ import * as b from "../../codegen/bindgen-lib.ts"; -export const SSLConfigSingleFile = b.union({ +export const TLSOptionsSingleFile = b.union({ string: b.string, buffer: b.bufferSource.arrayBuffer, file: b.blob, }); -export const SSLConfigFile = b.union({ +export const TLSOptionsFile = b.union({ none: b.null, string: b.string, buffer: b.bufferSource.arrayBuffer, file: b.blob, - array: b.array(SSLConfigSingleFile), + array: b.array(TLSOptionsSingleFile), }); export const ALPNProtocols = b.union({ @@ -20,7 +20,7 @@ export const ALPNProtocols = b.union({ buffer: b.bufferSource.arrayBuffer, }); -export const SSLConfig = b.dictionary( +export const TLSOptions = b.dictionary( { passphrase: b.string.nullable, dhParamsFile: b.string.nullable, @@ -28,27 +28,24 @@ export const SSLConfig = b.dictionary( lowMemoryMode: b.bool.default(false), rejectUnauthorized: b.bool.nullable, requestCert: b.bool.default(false), - ca: SSLConfigFile, - cert: SSLConfigFile, - key: SSLConfigFile, + ca: TLSOptionsFile, + cert: TLSOptionsFile, + key: TLSOptionsFile, secureOptions: b.u32.default(0), - minVersion: { type: b.i32.default(0), internalName: "ssl_min_version" }, - maxVersion: { type: b.i32.default(0), internalName: "ssl_max_version" }, + minVersion: { type: b.i32.default(0), rustName: "ssl_min_version" }, + maxVersion: { type: b.i32.default(0), rustName: "ssl_max_version" }, keyFile: b.string.nullable, certFile: b.string.nullable, caFile: b.string.nullable, - ALPNProtocols: { type: ALPNProtocols, internalName: "alpn_protocols" }, + ALPNProtocols: { type: ALPNProtocols, rustName: "alpn_protocols" }, ciphers: b.string.nullable, clientRenegotiationLimit: b.u32.default(0), clientRenegotiationWindow: b.u32.default(0), - crl: SSLConfigFile, + crl: TLSOptionsFile, allowPartialTrustChain: b.bool.default(false), sessionTimeout: b.i32.default(0), sigalgs: b.string.nullable, ecdhCurve: b.string.nullable, }, - { - userFacingName: "TLSOptions", - fromJS: true, - }, + { fromJS: true }, ); diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index 30bdb5a47c9e..63e38560e90b 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -133,7 +133,7 @@ pub(crate) trait SSLConfigFromJs: Sized { fn from_generated( vm: &VirtualMachine, global: &JSGlobalObject, - generated: &generated::SSLConfig, + generated: &generated::TLSOptions, is_server: bool, ) -> JsResult>; } @@ -145,7 +145,7 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { - generated::SSLConfig::from_js(global, value, |generated| { + generated::TLSOptions::from_js(global, value, |generated| { Self::from_generated(vm, global, &generated, is_server) }) } @@ -153,7 +153,7 @@ impl SSLConfigFromJs for SSLConfig { fn from_generated( vm: &VirtualMachine, global: &JSGlobalObject, - generated: &generated::SSLConfig, + generated: &generated::TLSOptions, is_server: bool, ) -> JsResult> { let mut result = SSLConfig::zero(); @@ -368,7 +368,7 @@ fn handle_path( fn handle_file_for_field( global: &JSGlobalObject, field: &'static str, - file: &generated::SSLConfigFile, + file: &generated::TLSOptionsFile, ) -> JsResult { match handle_file(global, file) { Ok(v) => Ok(v), @@ -387,22 +387,22 @@ fn handle_file_for_field( fn handle_file( global: &JSGlobalObject, - file: &generated::SSLConfigFile, + file: &generated::TLSOptionsFile, ) -> Result { let single = handle_single_file( global, match file { - generated::SSLConfigFile::None => return Ok(None), - generated::SSLConfigFile::String(val) => SingleFile::String(val), - generated::SSLConfigFile::Buffer(val) => { + generated::TLSOptionsFile::None => return Ok(None), + generated::TLSOptionsFile::String(val) => SingleFile::String(val), + generated::TLSOptionsFile::Buffer(val) => { // SAFETY: `file` holds a ref on the buffer, and nothing else borrows it here. SingleFile::Buffer(unsafe { &mut *val.get() }) } - generated::SSLConfigFile::File(val) => { + generated::TLSOptionsFile::File(val) => { // SAFETY: as above, for the `Blob`. SingleFile::File(unsafe { &mut *val.get() }) } - generated::SSLConfigFile::Array(list) => { + generated::TLSOptionsFile::Array(list) => { return handle_file_array(global, list); } }, @@ -414,7 +414,7 @@ fn handle_file( fn handle_file_array( global: &JSGlobalObject, - elements: &[generated::SSLConfigSingleFile], + elements: &[generated::TLSOptionsSingleFile], ) -> Result { if elements.is_empty() { return Ok(None); @@ -432,12 +432,12 @@ fn handle_file_array( guard.push(handle_single_file( global, match elem { - generated::SSLConfigSingleFile::String(val) => SingleFile::String(val), - generated::SSLConfigSingleFile::Buffer(val) => { + generated::TLSOptionsSingleFile::String(val) => SingleFile::String(val), + generated::TLSOptionsSingleFile::Buffer(val) => { // SAFETY: see `handle_file`. SingleFile::Buffer(unsafe { &mut *val.get() }) } - generated::SSLConfigSingleFile::File(val) => { + generated::TLSOptionsSingleFile::File(val) => { // SAFETY: see `handle_file`. SingleFile::File(unsafe { &mut *val.get() }) } diff --git a/src/runtime/socket/SocketConfig.bind.ts b/src/runtime/socket/SocketConfig.bind.ts index 9e6020667d5c..e1ccfcda5bc4 100644 --- a/src/runtime/socket/SocketConfig.bind.ts +++ b/src/runtime/socket/SocketConfig.bind.ts @@ -1,61 +1,55 @@ import * as b from "../../codegen/bindgen-lib.ts"; -import { SSLConfig } from "./SSLConfig.bind.ts"; +import { TLSOptions } from "./SSLConfig.bind.ts"; -export const SocketConfigBinaryType = b.enumeration([ +export const SocketHandlerBinaryType = b.enumeration([ ["arraybuffer", "ArrayBuffer"], ["buffer", "Buffer"], ["uint8array", "Uint8Array"], ]); -export const SocketConfigHandlers = b.dictionary( +export const SocketHandler = b.dictionary( { - open: { type: b.any, internalName: "onOpen" }, - close: { type: b.any, internalName: "onClose" }, - error: { type: b.any, internalName: "onError" }, - data: { type: b.any, internalName: "onData" }, - drain: { type: b.any, internalName: "onWritable" }, - handshake: { type: b.any, internalName: "onHandshake" }, - end: { type: b.any, internalName: "onEnd" }, - connectError: { type: b.any, internalName: "onConnectError" }, - timeout: { type: b.any, internalName: "onTimeout" }, - session: { type: b.any, internalName: "onSession" }, - keylog: { type: b.any, internalName: "onKeylog" }, - serverName: { type: b.any, internalName: "onServerName" }, - alpnCallback: { type: b.any, internalName: "onALPNCallback" }, - binaryType: SocketConfigBinaryType.default("buffer"), - }, - { - userFacingName: "SocketHandler", - fromJS: true, + open: { type: b.any, rustName: "on_open" }, + close: { type: b.any, rustName: "on_close" }, + error: { type: b.any, rustName: "on_error" }, + data: { type: b.any, rustName: "on_data" }, + drain: { type: b.any, rustName: "on_writable" }, + handshake: { type: b.any, rustName: "on_handshake" }, + end: { type: b.any, rustName: "on_end" }, + connectError: { type: b.any, rustName: "on_connect_error" }, + timeout: { type: b.any, rustName: "on_timeout" }, + session: { type: b.any, rustName: "on_session" }, + keylog: { type: b.any, rustName: "on_keylog" }, + serverName: { type: b.any, rustName: "on_server_name" }, + alpnCallback: { type: b.any, rustName: "on_alpn_callback" }, + binaryType: SocketHandlerBinaryType.default("buffer"), }, + { fromJS: true }, ); -export const SocketConfigTLS = b.union({ +export const SocketOptionsTLS = b.union({ none: b.null, boolean: b.bool, - object: SSLConfig, + object: TLSOptions, }); -export const SocketConfig = b.dictionary( +export const SocketOptions = b.dictionary( { - socket: { type: SocketConfigHandlers, internalName: "handlers" }, + socket: { type: SocketHandler, rustName: "handlers" }, data: b.any, allowHalfOpen: b.bool.default(false), hostname: { type: b.string.loose.falsyAsNull, altNames: ["host"] }, port: b.u16.loose.nullable, - tls: SocketConfigTLS, + tls: SocketOptionsTLS, exclusive: b.bool.default(false), reusePort: b.bool.default(false), ipv6Only: b.bool.default(false), pauseOnConnect: b.bool.loose.default(false), unix: { type: b.string.falsyAsNull, - internalName: "unix_", // `unix` is a predefined C macro... + rustName: "unix_", // The C++ struct has the same field, and `unix` is a predefined C macro. }, fd: b.i32.optional, }, - { - userFacingName: "SocketOptions", - fromJS: true, - }, + { fromJS: true }, ); diff --git a/test/internal-for-testing.d.ts b/test/internal-for-testing.d.ts index 59a091ae287e..49905583bb42 100644 --- a/test/internal-for-testing.d.ts +++ b/test/internal-for-testing.d.ts @@ -5,7 +5,7 @@ declare function $cpp(filename: string, symbol: string): T; declare function $rust(filename: string, symbol: string): T; declare function $newCppFunction any>(filename: string, symbol: string, argCount: number): T; declare function $newRustFunction any>(filename: string, symbol: string, argCount: number): T; -declare function $bindgenFn any>(filename: string, symbol: string): T; +declare function $bindgen any>(filename: string, name: string): T; declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; declare function $ERR_IPC_ONE_PIPE(): Error; declare function $webStreamClosedPromise(stream: ReadableStream | WritableStream): Promise; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index bf3312a24df1..82aebbf4d8aa 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -198,28 +198,29 @@ describe("strings", () => { }); it("keeps the characters of a substring while its base is swapped for an atom", () => { - const fill = (length: number, char: string) => Buffer.alloc(length, char).toString("latin1"); - const overwriteStack = (depth: number): number => (depth > 0 ? overwriteStack(depth - 1) + depth : 0); + const flat = (text: string) => Buffer.from(text).toString("latin1"); const keys: Record = {}; - const strings = { base: fill(300, "q"), equal: fill(300, "q") }; - keys[strings.equal] = 1; - const options = { - name: strings.base.substring(10, 290), - // Read after `name` is converted. - get renamed() { - // The base takes the atom's characters; two more lookups push its own out of the VM's last-atomized slot. - keys[strings.base]; - keys[fill(33, "q")]; - keys[fill(34, "q")]; - overwriteStack(200); - Bun.gc(true); - const reuse = Array.from({ length: 5000 }, () => fill(300, "Z")); - Bun.gc(true); - return reuse.length ? 1 : 0; - }, - }; - overwriteStack(300); - expect(bindgen.optionsEcho(options).name).toBe(fill(280, "q")); + // Whether a collection frees the base's old characters depends on more than this test controls, so it tries many. + for (let round = 0; round < 20; round++) { + const text = `${round}`.padStart(4, "0") + Buffer.alloc(296, "q").toString(); + const strings = { base: flat(text), equal: flat(text) }; + keys[strings.equal] = 1; + const options = { + name: strings.base.substring(10, 290), + // Read after `name` is converted. + get renamed() { + // The base takes the atom's characters; two more lookups push its own out of the VM's last-atomized slot. + keys[strings.base]; + keys[flat(`${round} a`)]; + keys[flat(`${round} b`)]; + Bun.gc(true); + const reuse = Array.from({ length: 2000 }, () => Buffer.alloc(300, "Z").toString("latin1")); + Bun.gc(true); + return reuse.length ? 1 : 0; + }, + }; + expect(bindgen.optionsEcho(options).name).toBe(text.substring(10, 290)); + } }); }); @@ -671,11 +672,11 @@ describe("overloads", () => { expect(bindgen.overloaded([1], 5)).toBe("list [1.0]"); }); - it("what fits none is reported by the one that requires the most arguments", () => { + it("what fits none is reported by the first signature still in the running", () => { expect(() => bindgen.overloaded()).toThrow("text must be a string"); expect(() => bindgen.overloaded({})).toThrow("text must be a string"); - expect(() => bindgen.overloaded(null, 1)).toThrow("a must be a number"); - expect(() => bindgen.overloaded("ab", "x")).toThrow("upper must be a boolean"); + expect(() => bindgen.overloaded(null, 1)).toThrow("text must be a string"); + expect(() => bindgen.overloaded("ab", "x")).toThrow("times must be a number"); }); itAlways("a signature that cannot be chosen is an error in the schema", () => { @@ -688,15 +689,11 @@ describe("overloads", () => { expect(() => b.overloads({ args: { a: b.i32, b: b.bool } }, { args: { a: b.f64, b: b.bool } })).toThrow( "overload 2 is never chosen", ); - // The first takes an undefined second argument, and the last takes everything else. - expect(() => - b.overloads( - { args: { a: b.string, b: b.f64.optional } }, - { args: { a: b.string, b: b.string.optional } }, - { args: { a: b.string, b: b.string.loose } }, - ), - ).toThrow("overload 2 is never chosen"); - b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } }); + // The shorter one ignores a second argument, so written first it takes every call. + expect(() => b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } })).toThrow( + "overload 2 is never chosen", + ); + b.overloads({ args: { a: b.string, b: b.bool } }, { args: { a: b.string } }); b.overloads({ args: { a: b.string, b: b.f64 } }, { args: { a: b.string, b: b.bool } }); b.overloads({ args: { a: b.bool } }, { args: { a: b.any } }); }); diff --git a/test/internal/build-codegen-declared-outputs.test.ts b/test/internal/build-codegen-declared-outputs.test.ts index 5a6c77ea13dc..29c3469a9a13 100644 --- a/test/internal/build-codegen-declared-outputs.test.ts +++ b/test/internal/build-codegen-declared-outputs.test.ts @@ -202,7 +202,7 @@ describe("emitBindgen", () => { const schemas = n .toString() .replaceAll("$:", ":") - .match(/--(sources|without-bindings)=[^\s'"]*/g)!; + .match(/--(sources|types-only)=[^\s'"]*/g)!; expect( await bunRun([ "run", From 994b62d4e38df01e1ed1e70a2a72dd4986d59f50 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 15 Oct 2026 23:43:33 +0000 Subject: [PATCH 36/44] Move everything only generated bindings use into Bindgen/ and Bun::Bindgen, name the wrapper cell JSWrapper, and drop a test that passes without its fix --- scripts/glob-sources.ts | 1 + src/codegen/bindgen-emit.ts | 12 +- src/codegen/bindgen-lib-internal.ts | 26 ++- src/codegen/bindgen-lib.ts | 10 +- src/jsc/bindings/Bindgen.cpp | 152 ---------------- .../Bindgen/AdditionsToGlobalObject.cpp | 32 ++++ .../AdditionsToGlobalObject.h} | 8 +- .../Bindgen/DictionaryMemberReader.cpp | 23 +++ .../bindings/Bindgen/DictionaryMemberReader.h | 4 +- src/jsc/bindings/Bindgen/IDLConvert.h | 172 +++++++++++++++++- src/jsc/bindings/Bindgen/IDLConvertBase.h | 4 +- .../IDLConvertBlob.h} | 10 +- .../bindings/Bindgen/IDLConvertIntegerRange.h | 2 +- .../IDLConvertNumbers.h} | 20 +- src/jsc/bindings/Bindgen/IDLTypes.h | 73 +++++++- src/jsc/bindings/Bindgen/JSStringUTF8View.cpp | 33 ++++ .../JSWrapper.cpp} | 48 ++--- .../{JSBindgenObject.h => JSWrapper.h} | 35 ++-- src/jsc/bindings/Bindgen/SchemaStrings.cpp | 46 +++++ .../{Identifiers.h => SchemaStrings.h} | 17 +- src/jsc/bindings/Bindgen/ToJS.cpp | 74 ++++++++ src/jsc/bindings/Bindgen/ToJS.h | 9 + src/jsc/bindings/BunClientData.cpp | 4 +- src/jsc/bindings/BunClientData.h | 4 +- src/jsc/bindings/BunIDLConvert.h | 148 --------------- src/jsc/bindings/BunIDLConvertContext.h | 15 +- src/jsc/bindings/BunIDLHumanReadable.h | 30 +-- src/jsc/bindings/BunIDLTypes.h | 75 -------- src/jsc/bindings/JSDOMWrapper.h | 2 +- src/jsc/bindings/ZigGlobalObject.cpp | 1 - src/jsc/bindings/ZigGlobalObject.h | 4 +- test/internal/bindgen.test.ts | 26 --- 32 files changed, 562 insertions(+), 558 deletions(-) delete mode 100644 src/jsc/bindings/Bindgen.cpp create mode 100644 src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp rename src/jsc/bindings/{BindgenAdditionsToGlobalObject.h => Bindgen/AdditionsToGlobalObject.h} (86%) create mode 100644 src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp rename src/jsc/bindings/{BunIDLConvertBlob.h => Bindgen/IDLConvertBlob.h} (69%) rename src/jsc/bindings/{BunIDLConvertNumbers.h => Bindgen/IDLConvertNumbers.h} (88%) create mode 100644 src/jsc/bindings/Bindgen/JSStringUTF8View.cpp rename src/jsc/bindings/{JSBindgenObject.cpp => Bindgen/JSWrapper.cpp} (61%) rename src/jsc/bindings/Bindgen/{JSBindgenObject.h => JSWrapper.h} (82%) create mode 100644 src/jsc/bindings/Bindgen/SchemaStrings.cpp rename src/jsc/bindings/Bindgen/{Identifiers.h => SchemaStrings.h} (67%) create mode 100644 src/jsc/bindings/Bindgen/ToJS.cpp create mode 100644 src/jsc/bindings/Bindgen/ToJS.h delete mode 100644 src/jsc/bindings/BunIDLConvert.h delete mode 100644 src/jsc/bindings/BunIDLTypes.h diff --git a/scripts/glob-sources.ts b/scripts/glob-sources.ts index 97a1848f84c3..1a96331213cc 100644 --- a/scripts/glob-sources.ts +++ b/scripts/glob-sources.ts @@ -97,6 +97,7 @@ const patterns = { "src/io/*.cpp", "src/jsc/modules/*.cpp", "src/jsc/bindings/*.cpp", + "src/jsc/bindings/Bindgen/*.cpp", "src/jsc/bindings/webcore/*.cpp", "src/jsc/bindings/webcore/streams/*.cpp", "src/jsc/bindings/sqlite/*.cpp", diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 010dae02609f..3f1fce095398 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -241,7 +241,11 @@ return;`; function cppHeaderForUnion(type: UnionType): string { const { name, alternatives } = type; - const idlTypes = alternatives.map(a => a.type.idlType).join(", "); + /** As error messages list them: null last. */ + const idlTypes = alternatives + .toSorted((a, b) => Number(a.type instanceof UnitType) - Number(b.type instanceof UnitType)) + .map(a => a.type.idlType) + .join(", "); const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); const mayMatchNone = (candidates: readonly number[]) => candidates.length === 0 || alternatives[candidates.at(-1)!].type.mayRefuse; @@ -369,7 +373,7 @@ ${ export function cppHeader(file: SchemaFile): string { const headers = new Set(); for (const type of file.types) for (const dependency of type.dependencies) dependency.getHeaders(headers); - if (file.types.some(type => type instanceof ClassType)) headers.add("Bindgen/JSBindgenObject.h"); + if (file.types.some(type => type instanceof ClassType)) headers.add("Bindgen/JSWrapper.h"); headers.delete(`Generated${file.origin.namespace}.h`); return `${banner(file.origin)}#pragma once #include "root.h" @@ -580,7 +584,7 @@ struct ${name}Traits { static size_t payloadSize() { return ${symbolName(type, "payloadSize")}; } static void finalize(void* payload) { ${classSymbolName(type, "finalize")}(payload); } }; -using JS${name} = JSBindgenObject<${name}Traits>; +using JS${name} = JSWrapper<${name}Traits>; extern const ClassDefinition JS${name}ClassDefinition; } template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { @@ -865,7 +869,7 @@ export function cppTables(tables: Tables): string { ${items.join(",\n ")} } };`; return `${banner(null)}#pragma once -#include "Bindgen/JSBindgenObject.h" +#include "Bindgen/JSWrapper.h" #include #include diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 4367d8d6eef2..a5b2948af9dd 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -296,7 +296,7 @@ class PrimitiveType extends Type { } export class BoolType extends PrimitiveType { - constructor(idlType = "Bun::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { + constructor(idlType = "Bun::Bindgen::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { super(idlType, "boolean", "bool", 1, valueKinds); } /** `ToBoolean`: any value is accepted. */ @@ -320,7 +320,7 @@ export class IntegerType extends PrimitiveType { /** Only the strict conversion is given no `idlType`. */ constructor(info: IntegerInfo, idlType?: string) { super( - idlType ?? `Bun::IDLStrictInteger<${info.cpp}>`, + idlType ?? `Bun::Bindgen::IDLStrictInteger<${info.cpp}>`, "number", info.rust, info.size, @@ -342,7 +342,7 @@ export class IntegerType extends PrimitiveType { } /** `ToNumeric` first, then as strict as the default. */ get loose(): IntegerType { - return new IntegerType(this.info, `Bun::IDLLooseInteger<${this.info.cpp}>`); + return new IntegerType(this.info, `Bun::Bindgen::IDLLooseInteger<${this.info.cpp}>`); } /** WebIDL conversion: `ToNumber`, then wrap modulo the width. */ get wrap(): IntegerType { @@ -398,7 +398,7 @@ export const integers = { export class DoubleType extends PrimitiveType { constructor() { - super("Bun::IDLStrictDouble", "number", "f64", 8, ["number"]); + super("Bun::Bindgen::IDLStrictDouble", "number", "f64", 8, ["number"]); } } @@ -409,7 +409,7 @@ export class UnitType extends Type { constructor(tsType: "null" | "undefined") { super(); this.tsType = tsType; - this.idlType = tsType === "null" ? "Bun::IDLStrictNull" : ""; + this.idlType = tsType === "null" ? "Bun::Bindgen::IDLStrictNull" : ""; } get valueKinds(): ValueKinds { return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; @@ -445,7 +445,7 @@ export class StringType extends Type { readonly idlType: string; readonly tsType = "string"; readonly #valueKinds: ValueKinds; - constructor(idlType = "Bun::IDLStrictString", valueKinds: ValueKinds = ["string"]) { + constructor(idlType = "Bun::Bindgen::IDLStrictString", valueKinds: ValueKinds = ["string"]) { super(); this.idlType = idlType; this.#valueKinds = valueKinds; @@ -620,7 +620,13 @@ export class BufferSourceType extends Type { } /** A reference on a `JSC::ArrayBuffer`, which can be stored: the buffer itself, or a copy of what a view shows. */ get arrayBuffer(): RefType { - return new RefType("Bun::IDLArrayBufferRef", this.tsType, "bun_jsc::JSCArrayBuffer", null, bufferSourceKinds); + return new RefType( + "Bun::Bindgen::IDLArrayBufferRef", + this.tsType, + "bun_jsc::JSCArrayBuffer", + null, + bufferSourceKinds, + ); } get holdsJSValue(): boolean { return true; @@ -693,7 +699,7 @@ export class WithValueType extends Type { } export class AnyType extends Type { - readonly idlType = "Bun::IDLRawAny"; + readonly idlType = "Bun::Bindgen::IDLRawAny"; readonly tsType = "any"; get holdsJSValue(): boolean { return true; @@ -789,7 +795,7 @@ export class OptionalType extends Type { const template = { optional: "WebCore::IDLOptional", nullable: "WebCore::IDLNullable", - falsyAsNull: "Bun::IDLFalsyAsNull", + falsyAsNull: "Bun::Bindgen::IDLFalsyAsNull", }[this.modifier]; return `${template}<${this.inner.idlType}>`; } @@ -965,7 +971,7 @@ export class ArrayType extends Type { !this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string or b.string.utf8.owned", ); - return `Bun::IDLArray<${this.element.idlType}>`; + return `Bun::Bindgen::IDLArray<${this.element.idlType}>`; } get tsType(): string { return `(${this.element.tsType})[]`; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 7148eede7779..b936a05f2d63 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -45,9 +45,13 @@ export const any = new AnyType(); export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ -export const blob = new RefType("Bun::IDLBlobRef", "Blob", "bun_jsc::webcore_types::Blob", "BunIDLConvertBlob.h", [ - { cpp: "WebCore::JSBlob" }, -]); +export const blob = new RefType( + "Bun::Bindgen::IDLBlobRef", + "Blob", + "bun_jsc::webcore_types::Blob", + "Bindgen/IDLConvertBlob.h", + [{ cpp: "WebCore::JSBlob" }], +); const null_ = new UnitType("null"); diff --git a/src/jsc/bindings/Bindgen.cpp b/src/jsc/bindings/Bindgen.cpp deleted file mode 100644 index 5f78e41b72c4..000000000000 --- a/src/jsc/bindings/Bindgen.cpp +++ /dev/null @@ -1,152 +0,0 @@ -#include "root.h" -#include "Bindgen/DictionaryMemberReader.h" -#include "Bindgen/Identifiers.h" -#include "Bindgen/JSStringUTF8View.h" -#include "BunClientData.h" -#include "GeneratedBindings+Tables.h" -#include "ObjectBindings.h" -#include "ZigGlobalObject.h" -#include -#include -#include -#include -#include -#include - -namespace Bun::Bindgen { -using namespace JSC; - -Identifiers::Identifiers(VM& vm) - : m_vm(vm) - , m_memberNames(std::make_unique(Tables::memberNames.size())) - , m_enumerationValues(std::make_unique(Tables::enumerationValues.size())) -{ -} - -void Identifiers::initializeMemberNames(unsigned first, unsigned count) -{ - ASSERT(m_vm.currentThreadIsHoldingAPILock()); - for (unsigned i = first; i < first + count; ++i) - m_memberNames[i] = Identifier::fromString(m_vm, Tables::memberNames[i]); -} - -void Identifiers::initializeEnumerationValue(unsigned index) -{ - ASSERT(m_vm.currentThreadIsHoldingAPILock()); - m_enumerationValues[index] = jsString(m_vm, AtomString(Tables::enumerationValues[index])); -} - -template -void Identifiers::visit(Visitor& visitor) -{ - for (unsigned i = 0; i < Tables::enumerationValues.size(); ++i) { - if (auto* string = m_enumerationValues[i]) - visitor.appendUnbarriered(string); - } -} -template void Identifiers::visit(AbstractSlotVisitor&); -template void Identifiers::visit(SlotVisitor&); - -Identifiers& identifiers(VM& vm) -{ - return static_cast(vm.clientData)->bindgenIdentifiers; -} - -static_assert(std::ranges::all_of(Tables::objectStructures, [](const auto& structure) { return structure.memberCount <= JSFinalObject::maxInlineCapacity; }), "a returned dictionary has more members than an object holds inline"); - -Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) -{ - auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; - const Identifier* memberNames = identifiers(vm).memberNames(firstMemberNameIndex, memberCount); - Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); - for (unsigned i = 0; i < memberCount; ++i) { - PropertyOffset offset; - structure = Structure::addPropertyTransition(vm, structure, memberNames[i], 0, offset); - RELEASE_ASSERT(offset == static_cast(i)); - } - return structure; -} - -JSValue DictionaryMemberReader::get(unsigned index) -{ - if (canSkipPrototypeChain()) [[likely]] { - unsigned attributes = 0; - PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); - if (offset == invalidOffset) - return jsUndefined(); - if (!(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) [[likely]] - return m_object->getDirect(offset); - } - return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); -} - -bool JSStringUTF8View::convert() -{ - StringView view = m_view; - m_isConverted = true; - if (view.is8Bit()) { - auto span = view.span8(); - if (!m_converted.tryGrow(span.size() * 2)) - return false; - m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span.data()), span.size(), byteCast(m_converted.mutableSpan().data()))); - return true; - } - auto span = view.span16(); - if (!m_converted.tryGrow(span.size() * 3)) - return false; - size_t length = simdutf::convert_utf16le_to_utf8(span.data(), span.size(), byteCast(m_converted.mutableSpan().data())); - if (length) [[likely]] { - m_converted.shrink(length); - return true; - } - // simdutf refuses unpaired surrogates; each becomes U+FFFD. - auto lenient = view.utf8(WTF::ConversionMode::StrictConversionReplacingUnpairedSurrogatesWithFFFD); - m_converted.shrink(0); - return m_converted.tryAppend(byteCast(lenient.span())); -} - -// An empty value is a member that is absent. -extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) -{ - VM& vm = getVM(globalObject); - ASSERT(count == Tables::objectStructures[structureIndex].memberCount); - bool hasEveryMember = true; - for (size_t i = 0; i < count; ++i) - hasEveryMember &= !!values[i]; - if (hasEveryMember) [[likely]] { - JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->m_bindgenAdditions.m_objectStructures[structureIndex].getInitializedOnMainThread(globalObject)); - for (size_t i = 0; i < count; ++i) - object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); - return JSValue::encode(object); - } - const Identifier* memberNames = identifiers(vm).memberNames(Tables::objectStructures[structureIndex].firstMemberNameIndex, count); - JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count); - for (size_t i = 0; i < count; ++i) { - if (values[i]) - object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); - } - return JSValue::encode(object); -} - -extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* globalObject, unsigned index) -{ - return JSValue::encode(identifiers(getVM(globalObject)).enumerationValue(index)); -} - -extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) -{ - return JSValue::encode(constructArray(globalObject, static_cast(nullptr), values, count)); -} - -extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) -{ - VM& vm = getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - if (values->hasOverflowed()) [[unlikely]] { - throwOutOfMemoryError(globalObject, scope); - return {}; - } - RELEASE_AND_RETURN(scope, JSValue::encode(constructArray(globalObject, static_cast(nullptr), *values))); -} - -} diff --git a/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp new file mode 100644 index 000000000000..4d21145cad1f --- /dev/null +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp @@ -0,0 +1,32 @@ +#include "root.h" +#include "Bindgen/AdditionsToGlobalObject.h" +#include "Bindgen/JSWrapper.h" +#include "Bindgen/ToJS.h" +#include "GeneratedBindings+Tables.h" +#include "ZigGlobalObject.h" + +namespace Bun::Bindgen { + +AdditionsToGlobalObject::AdditionsToGlobalObject() + : m_objectStructures(Tables::objectStructures.size()) + , m_classes(Tables::classes.size()) +{ +} + +void AdditionsToGlobalObject::initialize() +{ + for (auto& structure : m_objectStructures) { + structure.initLater([](const JSC::LazyProperty::Initializer& init) { + auto& additions = defaultGlobalObject(init.owner)->m_bindgenAdditions; + init.set(createObjectStructure(init.vm, init.owner, &init.property - additions.m_objectStructures.begin())); + }); + } + for (auto& lazyClass : m_classes) { + lazyClass.initLater([](JSC::LazyClassStructure::Initializer& init) { + auto& additions = defaultGlobalObject(init.global)->m_bindgenAdditions; + setupClassStructure(init, &init.classStructure - additions.m_classes.begin()); + }); + } +} + +} diff --git a/src/jsc/bindings/BindgenAdditionsToGlobalObject.h b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h similarity index 86% rename from src/jsc/bindings/BindgenAdditionsToGlobalObject.h rename to src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h index b3f1b293078b..ec44591965cf 100644 --- a/src/jsc/bindings/BindgenAdditionsToGlobalObject.h +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h @@ -4,11 +4,11 @@ #include #include -namespace Bun { +namespace Bun::Bindgen { -// The per-global-object state of generated bindings, by index into Bindgen::Tables. -struct BindgenAdditionsToGlobalObject { - BindgenAdditionsToGlobalObject(); +// The per-global-object state of generated bindings, by index into Tables. +struct AdditionsToGlobalObject { + AdditionsToGlobalObject(); void initialize(); template diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp new file mode 100644 index 000000000000..cd55f05f0195 --- /dev/null +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp @@ -0,0 +1,23 @@ +#include "root.h" +#include "Bindgen/DictionaryMemberReader.h" +#include "ObjectBindings.h" +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +JSValue DictionaryMemberReader::get(unsigned index) +{ + if (canSkipPrototypeChain()) [[likely]] { + unsigned attributes = 0; + PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); + if (offset == invalidOffset) + return jsUndefined(); + if (!(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) [[likely]] + return m_object->getDirect(offset); + } + return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); +} + +} diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 99628347bb85..ecea97954cb2 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -1,6 +1,6 @@ #pragma once #include "root.h" -#include "Bindgen/Identifiers.h" +#include "Bindgen/SchemaStrings.h" #include namespace Bun::Bindgen { @@ -12,7 +12,7 @@ class DictionaryMemberReader { : m_vm(vm) , m_globalObject(globalObject) , m_object(object) - , m_memberNames(identifiers(vm).memberNames(firstMemberNameIndex, memberNameCount)) + , m_memberNames(schemaStrings(vm).memberNames(firstMemberNameIndex, memberNameCount)) { } diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index caf764a07b62..87884bc252ee 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -1,12 +1,38 @@ #pragma once -#include +#include "Bindgen/IDLConvertBase.h" +#include "Bindgen/IDLConvertNumbers.h" +#include "Bindgen/IDLTypes.h" +#include "JSDOMConvert.h" +#include #include #include #include -#include "IDLTypes.h" -#include "IDLConvertBase.h" +#include namespace Bun { +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("null"); +}; + +template +struct IDLHumanReadableName> + : IDLHumanReadableName> {}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool hasPreposition = true; + static constexpr auto humanReadableName + = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("Blob"); +}; + template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { static constexpr auto humanReadableName = std::to_array("string"); }; @@ -147,3 +173,143 @@ struct WebCore::Converter> : Bun::Default ctx.throwTypeMustBe(globalObject, scope, WTF::makeString("an instance of "_s, JSClass::info()->className)); } }; + +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static JSC::JSValue convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return value; + } +}; + +template +struct WebCore::Converter> + : Bun::DefaultContextConverter> { + + using ReturnType = WebCore::Converter>::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + if (!value.toBoolean(&globalObject)) + return IDL::nullValue(); + return Bun::convertIDL(globalObject, value, ctx); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isBoolean()) { + return value.asBoolean(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBoolean(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return value.toWTFString(&globalObject); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotString(globalObject, scope); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + template + static std::optional::ImplementationType> tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (JSC::isJSArray(value)) { + return Bun::convertIDL::Base>(globalObject, value, ctx); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNotArray(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { + return jsBuffer; + } + if (auto* jsView = dynamicDowncast(value)) { + if (jsView->isShared()) { + return std::nullopt; + } + // A copy: the view's buffer can hold more than the view shows. + if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { + return bytes; + } + // Declared where it throws: at the top, every caller would owe a check for the paths that cannot. + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::throwOutOfMemoryError(&globalObject, scope); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBufferSource(globalObject, scope); + } +}; diff --git a/src/jsc/bindings/Bindgen/IDLConvertBase.h b/src/jsc/bindings/Bindgen/IDLConvertBase.h index e73a664c1fb6..7e7a86284d2d 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertBase.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBase.h @@ -1,6 +1,6 @@ #pragma once -#include -#include +#include "BunIDLConvertBase.h" +#include "ErrorCode.h" #include namespace Bun::Bindgen { diff --git a/src/jsc/bindings/BunIDLConvertBlob.h b/src/jsc/bindings/Bindgen/IDLConvertBlob.h similarity index 69% rename from src/jsc/bindings/BunIDLConvertBlob.h rename to src/jsc/bindings/Bindgen/IDLConvertBlob.h index 2cb5a229756d..ed2132208e41 100644 --- a/src/jsc/bindings/BunIDLConvertBlob.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBlob.h @@ -1,18 +1,18 @@ #pragma once -#include "BunIDLTypes.h" +#include "Bindgen/IDLTypes.h" #include "BunIDLConvertBase.h" #include "blob.h" #include "ZigGeneratedClasses.h" -namespace Bun { -struct IDLBlobRef : IDLBunInterface {}; +namespace Bun::Bindgen { +struct IDLBlobRef : IDLRef {}; } -template<> struct WebCore::Converter : Bun::DefaultTryConverter { +template<> struct WebCore::Converter : Bun::DefaultTryConverter { static constexpr bool conversionHasSideEffects = false; template - static std::optional tryConvert( + static std::optional tryConvert( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h index d4bf7f84ba66..f593631a2c6a 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -1,7 +1,7 @@ #pragma once #include "root.h" +#include "Bindgen/IDLTypes.h" #include "ErrorCode.h" -#include "IDLTypes.h" #include "JSDOMConvertBase.h" namespace Bun::Bindgen { diff --git a/src/jsc/bindings/BunIDLConvertNumbers.h b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h similarity index 88% rename from src/jsc/bindings/BunIDLConvertNumbers.h rename to src/jsc/bindings/Bindgen/IDLConvertNumbers.h index 25f07a7e4322..53f309394745 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h @@ -1,12 +1,12 @@ #pragma once -#include "BunIDLTypes.h" +#include "Bindgen/IDLTypes.h" #include "BunIDLConvertBase.h" #include #include #include #include -namespace Bun::Detail { +namespace Bun::Bindgen::Detail { template std::optional tryBigIntToInt(JSC::JSValue value) { @@ -36,8 +36,8 @@ std::optional tryBigIntToInt(JSC::JSValue value) template requires(sizeof(T) <= sizeof(std::uint64_t)) -struct WebCore::Converter> - : Bun::DefaultTryConverter> { +struct WebCore::Converter> + : Bun::DefaultTryConverter> { static constexpr bool conversionHasSideEffects = false; @@ -83,11 +83,11 @@ struct WebCore::Converter> using Largest = std::conditional_t, std::int64_t, std::uint64_t>; if (value.isBigInt()) { - if (auto result = Bun::Detail::tryBigIntToInt(value)) { + if (auto result = Bun::Bindgen::Detail::tryBigIntToInt(value)) { return *result; } if constexpr (maxInt < std::numeric_limits::max()) { - if (auto result = Bun::Detail::tryBigIntToInt(value)) { + if (auto result = Bun::Bindgen::Detail::tryBigIntToInt(value)) { ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); return {}; } @@ -125,7 +125,7 @@ struct WebCore::Converter> }; template<> -struct WebCore::Converter : Bun::DefaultTryConverter { +struct WebCore::Converter : Bun::DefaultTryConverter { static constexpr bool conversionHasSideEffects = false; template @@ -151,8 +151,8 @@ struct WebCore::Converter : Bun::DefaultTryConverter -struct WebCore::Converter> - : Bun::DefaultContextConverter> { +struct WebCore::Converter> + : Bun::DefaultContextConverter> { template static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -163,6 +163,6 @@ struct WebCore::Converter> RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN( scope, - Bun::convertIDL>(globalObject, numeric, ctx)); + Bun::convertIDL>(globalObject, numeric, ctx)); } }; diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 21217011e307..36bfd8130d82 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -1,10 +1,77 @@ #pragma once -#include +#include "Bindgen/JSStringUTF8View.h" +// WebCore's. In quotes it would be this file. +#include +#include +#include #include -#include "JSStringUTF8View.h" +#include + +namespace WTF { +struct CrashOnOverflow; +} + +namespace Bun { +struct MimallocMalloc; +} namespace Bun::Bindgen { +// Like `IDLAny`, but always stored as a raw `JSValue`. This should only be +// used in contexts where the `JSValue` will be stored on the stack. +struct IDLRawAny : WebCore::IDLType { + // Storage in a sequence is explicitly unsupported, as this would create a + // `Vector`, whose contents are invisible to the GC. + using SequenceStorageType = void; +}; + +// For use in unions, to represent a nullable union. +struct IDLStrictNull : WebCore::IDLType {}; + +// Treats all falsy values as null. +template +struct IDLFalsyAsNull : WebCore::IDLNullable {}; + +template +struct IDLStrictInteger : WebCore::IDLInteger {}; +struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; +struct IDLStrictBoolean : WebCore::IDLBoolean {}; +struct IDLStrictString : WebCore::IDLDOMString {}; + +// Converts to a number first. +template +struct IDLLooseInteger : IDLStrictInteger {}; + +namespace Detail { +template +using IDLMimallocSequence = WebCore::IDLSequence< + IDL, + WTF::Vector< + typename IDL::SequenceStorageType, + 0, + WTF::CrashOnOverflow, + 16, + Bun::MimallocMalloc>>; +} + +template +struct IDLArray : Detail::IDLMimallocSequence { + using Base = Detail::IDLMimallocSequence; +}; + +template> +struct IDLRef : WebCore::IDLType, RefDerefTraits>> { + using NullableType = WTF::RefPtr, RefDerefTraits>; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } +}; + +struct IDLArrayBufferRef : IDLRef {}; + +// Defined in IDLConvertBlob.h +struct IDLBlobRef; + // Dictionaries that hold a raw `JSValue` or a `JSStringUTF8View` must live on the stack. template struct IDLStackOnlyDictionary : WebCore::IDLType { @@ -37,7 +104,7 @@ struct IDLBufferSourceBytes : WebCore::IDLType> { static NullableType nullValue() { return {}; } }; -// The same values, as the `JSValue` itself. See Bun::IDLRawAny. +// The same values, as the `JSValue` itself. See IDLRawAny. struct IDLRawBufferSource : WebCore::IDLType { using SequenceStorageType = void; }; diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp new file mode 100644 index 000000000000..013ed367844e --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp @@ -0,0 +1,33 @@ +#include "root.h" +#include "Bindgen/JSStringUTF8View.h" +#include + +namespace Bun::Bindgen { +using namespace JSC; + +bool JSStringUTF8View::convert() +{ + StringView view = m_view; + m_isConverted = true; + if (view.is8Bit()) { + auto span = view.span8(); + if (!m_converted.tryGrow(span.size() * 2)) + return false; + m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span.data()), span.size(), byteCast(m_converted.mutableSpan().data()))); + return true; + } + auto span = view.span16(); + if (!m_converted.tryGrow(span.size() * 3)) + return false; + size_t length = simdutf::convert_utf16le_to_utf8(span.data(), span.size(), byteCast(m_converted.mutableSpan().data())); + if (length) [[likely]] { + m_converted.shrink(length); + return true; + } + // simdutf refuses unpaired surrogates; each becomes U+FFFD. + auto lenient = view.utf8(WTF::ConversionMode::StrictConversionReplacingUnpairedSurrogatesWithFFFD); + m_converted.shrink(0); + return m_converted.tryAppend(byteCast(lenient.span())); +} + +} diff --git a/src/jsc/bindings/JSBindgenObject.cpp b/src/jsc/bindings/Bindgen/JSWrapper.cpp similarity index 61% rename from src/jsc/bindings/JSBindgenObject.cpp rename to src/jsc/bindings/Bindgen/JSWrapper.cpp index 900ff8dba0c3..67a6d5a4826e 100644 --- a/src/jsc/bindings/JSBindgenObject.cpp +++ b/src/jsc/bindings/Bindgen/JSWrapper.cpp @@ -1,5 +1,5 @@ #include "root.h" -#include "Bindgen/JSBindgenObject.h" +#include "Bindgen/JSWrapper.h" #include "GeneratedBindings+Tables.h" #include #include @@ -7,34 +7,34 @@ namespace Bun::Bindgen { using namespace JSC; -const ClassInfo JSBindgenConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBindgenConstructor) }; +const ClassInfo JSClassConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSClassConstructor) }; -static JSC_DECLARE_HOST_FUNCTION(callJSBindgenConstructor); -JSC_DEFINE_HOST_FUNCTION(callJSBindgenConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) +static JSC_DECLARE_HOST_FUNCTION(callJSClassConstructor); +JSC_DEFINE_HOST_FUNCTION(callJSClassConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, makeString(constructor->name(), " constructor cannot be invoked without 'new'"_s)); return {}; } -JSBindgenConstructor::JSBindgenConstructor(VM& vm, Structure* structure, NativeFunction construct) - : Base(vm, structure, callJSBindgenConstructor, construct) +JSClassConstructor::JSClassConstructor(VM& vm, Structure* structure, NativeFunction construct) + : Base(vm, structure, callJSClassConstructor, construct) { } -JSBindgenConstructor* JSBindgenConstructor::create(VM& vm, JSGlobalObject* globalObject, const ClassDefinition& definition, JSObject* prototype) +JSClassConstructor* JSClassConstructor::create(VM& vm, JSGlobalObject* globalObject, const ClassDefinition& definition, JSObject* prototype) { auto* structure = createStructure(vm, globalObject, globalObject->functionPrototype()); - auto* constructor = new (NotNull, allocateCell(vm)) JSBindgenConstructor(vm, structure, definition.construct); + auto* constructor = new (NotNull, allocateCell(vm)) JSClassConstructor(vm, structure, definition.construct); constructor->finishCreation(vm, definition.constructorLength, definition.info->className, PropertyAdditionMode::WithoutStructureTransition); Bun::reifyStaticPropertyTable(vm, info(), definition.constructorTableValues, *constructor); constructor->putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); return constructor; } -static void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIndex) +void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIndex) { const ClassDefinition& definition = *Tables::classes[classIndex]; VM& vm = init.vm; @@ -48,7 +48,7 @@ static void setupClassStructure(LazyClassStructure::Initializer& init, unsigned init.setPrototype(prototype); init.setStructure(definition.createStructure(vm, init.global, prototype)); if (definition.construct) - init.setConstructor(JSBindgenConstructor::create(vm, init.global, definition, prototype)); + init.setConstructor(JSClassConstructor::create(vm, init.global, definition, prototype)); } Structure* structureForClass(JSGlobalObject* globalObject, unsigned classIndex) @@ -78,32 +78,6 @@ Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFr } -namespace Bun { - -BindgenAdditionsToGlobalObject::BindgenAdditionsToGlobalObject() - : m_objectStructures(Bindgen::Tables::objectStructures.size()) - , m_classes(Bindgen::Tables::classes.size()) -{ -} - -void BindgenAdditionsToGlobalObject::initialize() -{ - for (auto& structure : m_objectStructures) { - structure.initLater([](const JSC::LazyProperty::Initializer& init) { - auto& additions = defaultGlobalObject(init.owner)->m_bindgenAdditions; - init.set(Bindgen::createObjectStructure(init.vm, init.owner, &init.property - additions.m_objectStructures.begin())); - }); - } - for (auto& lazyClass : m_classes) { - lazyClass.initLater([](JSC::LazyClassStructure::Initializer& init) { - auto& additions = defaultGlobalObject(init.global)->m_bindgenAdditions; - Bindgen::setupClassStructure(init, &init.classStructure - additions.m_classes.begin()); - }); - } -} - -} - extern "C" void Bun__Bindgen__setInternalField(JSC::JSGlobalObject* globalObject, JSC::JSCell* cell, uint32_t offset, JSC::EncodedJSValue value) { auto* field = reinterpret_cast*>(reinterpret_cast(cell) + offset); diff --git a/src/jsc/bindings/Bindgen/JSBindgenObject.h b/src/jsc/bindings/Bindgen/JSWrapper.h similarity index 82% rename from src/jsc/bindings/Bindgen/JSBindgenObject.h rename to src/jsc/bindings/Bindgen/JSWrapper.h index 40566e328034..266815595846 100644 --- a/src/jsc/bindings/Bindgen/JSBindgenObject.h +++ b/src/jsc/bindings/Bindgen/JSWrapper.h @@ -11,18 +11,18 @@ #include namespace Bun::Bindgen { -template class JSBindgenObject; +template class JSWrapper; } // A cell whose type is JSDOMWrapperType is a JSDestructibleObject, so its ClassInfo is in the cell: no Structure load. template -struct JSC::JSCastingHelpers::InheritsTraits> { +struct JSC::JSCastingHelpers::InheritsTraits> { static constexpr std::optional typeRange { std::nullopt }; template static inline bool inherits(From* from) { return from->type() == static_cast(WebCore::JSDOMWrapperType) - && uncheckedDowncast(from)->classInfo() == Bun::Bindgen::JSBindgenObject::info(); + && uncheckedDowncast(from)->classInfo() == Bun::Bindgen::JSWrapper::info(); } }; @@ -43,7 +43,7 @@ struct ClassDefinition { // JSDestructibleObject | WriteBarrier internalFields[numberOfInternalFields] | the Rust payload template -class JSBindgenObject final : public JSC::JSDestructibleObject { +class JSWrapper final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; static constexpr unsigned StructureFlags = Base::StructureFlags; @@ -77,16 +77,16 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { } // The payload is uninitialized: the caller writes it before anything else can run. - static JSBindgenObject* create(JSC::VM& vm, JSC::Structure* structure) + static JSWrapper* create(JSC::VM& vm, JSC::Structure* structure) { - auto* object = new (NotNull, JSC::allocateCell(vm, allocationSize())) JSBindgenObject(vm, structure); + auto* object = new (NotNull, JSC::allocateCell(vm, allocationSize())) JSWrapper(vm, structure); object->finishCreation(vm); return object; } static void destroy(JSC::JSCell* cell) { - Traits::finalize(static_cast(cell)->payload()); + Traits::finalize(static_cast(cell)->payload()); } static constexpr size_t offsetOfInternalFields() { return sizeof(Base); } @@ -101,7 +101,7 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { } private: - JSBindgenObject(JSC::VM& vm, JSC::Structure* structure) + JSWrapper(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) { // Rust reads the schema's `internalFields`, which start as `undefined`. The rest hold cached getters: empty until computed. @@ -116,24 +116,24 @@ class JSBindgenObject final : public JSC::JSDestructibleObject { template template -void JSBindgenObject::visitChildrenImpl(JSC::JSCell* cell, Visitor& visitor) +void JSWrapper::visitChildrenImpl(JSC::JSCell* cell, Visitor& visitor) { - auto* thisObject = uncheckedDowncast(cell); + auto* thisObject = uncheckedDowncast(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); for (auto& field : thisObject->internalFields()) visitor.append(field); } -DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template, JSBindgenObject); +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template, JSWrapper); template -const JSC::ClassInfo JSBindgenObject::s_info = { Traits::className, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBindgenObject) }; +const JSC::ClassInfo JSWrapper::s_info = { Traits::className, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWrapper) }; -template struct ExternTraits*> : TrivialExtern*> {}; +template struct ExternTraits*> : TrivialExtern*> {}; // The constructor object of every generated class that has one. -class JSBindgenConstructor final : public JSC::InternalFunction { +class JSClassConstructor final : public JSC::InternalFunction { public: using Base = JSC::InternalFunction; static constexpr unsigned StructureFlags = Base::StructureFlags; @@ -145,7 +145,7 @@ class JSBindgenConstructor final : public JSC::InternalFunction { { if constexpr (mode == JSC::SubspaceAccess::Concurrently) return nullptr; - return WebCore::subspaceForImpl( + return WebCore::subspaceForImpl( vm, BUN_SUBSPACE_SLOTS(m_clientSubspaceForBunClassConstructor, m_subspaceForBunClassConstructor)); } @@ -154,14 +154,15 @@ class JSBindgenConstructor final : public JSC::InternalFunction { return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); } - static JSBindgenConstructor* create(JSC::VM&, JSC::JSGlobalObject*, const ClassDefinition&, JSC::JSObject* prototype); + static JSClassConstructor* create(JSC::VM&, JSC::JSGlobalObject*, const ClassDefinition&, JSC::JSObject* prototype); private: - JSBindgenConstructor(JSC::VM&, JSC::Structure*, JSC::NativeFunction construct); + JSClassConstructor(JSC::VM&, JSC::Structure*, JSC::NativeFunction construct); }; // By index into Tables::classes (GeneratedBindings+Tables.h). JSC::Structure* structureForClass(JSC::JSGlobalObject*, unsigned classIndex); +void setupClassStructure(JSC::LazyClassStructure::Initializer&, unsigned classIndex); JSC::JSObject* constructorForClass(JSC::JSGlobalObject*, unsigned classIndex); // The class's own, or one for `new.target` (a subclass, Reflect.construct). May throw. JSC::Structure* structureForNewTarget(JSC::JSGlobalObject*, JSC::CallFrame*, unsigned classIndex); diff --git a/src/jsc/bindings/Bindgen/SchemaStrings.cpp b/src/jsc/bindings/Bindgen/SchemaStrings.cpp new file mode 100644 index 000000000000..38bc67803647 --- /dev/null +++ b/src/jsc/bindings/Bindgen/SchemaStrings.cpp @@ -0,0 +1,46 @@ +#include "root.h" +#include "Bindgen/SchemaStrings.h" +#include "BunClientData.h" +#include "GeneratedBindings+Tables.h" +#include + +namespace Bun::Bindgen { +using namespace JSC; + +SchemaStrings::SchemaStrings(VM& vm) + : m_vm(vm) + , m_memberNames(std::make_unique(Tables::memberNames.size())) + , m_enumerationValues(std::make_unique(Tables::enumerationValues.size())) +{ +} + +void SchemaStrings::initializeMemberNames(unsigned first, unsigned count) +{ + ASSERT(m_vm.currentThreadIsHoldingAPILock()); + for (unsigned i = first; i < first + count; ++i) + m_memberNames[i] = Identifier::fromString(m_vm, Tables::memberNames[i]); +} + +void SchemaStrings::initializeEnumerationValue(unsigned index) +{ + ASSERT(m_vm.currentThreadIsHoldingAPILock()); + m_enumerationValues[index] = jsString(m_vm, AtomString(Tables::enumerationValues[index])); +} + +template +void SchemaStrings::visit(Visitor& visitor) +{ + for (unsigned i = 0; i < Tables::enumerationValues.size(); ++i) { + if (auto* string = m_enumerationValues[i]) + visitor.appendUnbarriered(string); + } +} +template void SchemaStrings::visit(AbstractSlotVisitor&); +template void SchemaStrings::visit(SlotVisitor&); + +SchemaStrings& schemaStrings(VM& vm) +{ + return static_cast(vm.clientData)->bindgenStrings; +} + +} diff --git a/src/jsc/bindings/Bindgen/Identifiers.h b/src/jsc/bindings/Bindgen/SchemaStrings.h similarity index 67% rename from src/jsc/bindings/Bindgen/Identifiers.h rename to src/jsc/bindings/Bindgen/SchemaStrings.h index 783b9d779598..8f48ee5475f0 100644 --- a/src/jsc/bindings/Bindgen/Identifiers.h +++ b/src/jsc/bindings/Bindgen/SchemaStrings.h @@ -5,15 +5,15 @@ namespace Bun::Bindgen { -// Identifiers for dictionary member names and JSStrings for enumeration values, each made on first use. -class Identifiers { - WTF_MAKE_NONCOPYABLE(Identifiers); - WTF_MAKE_NONMOVABLE(Identifiers); +// The strings schemas name, per VM: Identifiers for dictionary members and JSStrings for enumeration values, each made on first use. +class SchemaStrings { + WTF_MAKE_NONCOPYABLE(SchemaStrings); + WTF_MAKE_NONMOVABLE(SchemaStrings); public: - explicit Identifiers(JSC::VM&); + explicit SchemaStrings(JSC::VM&); - // `count` identifiers starting at Tables::memberNames[first] (GeneratedBindings+Tables.h). + // `count` of them, starting at Tables::memberNames[first] (GeneratedBindings+Tables.h). ALWAYS_INLINE const JSC::Identifier* memberNames(unsigned first, unsigned count) { if (m_memberNames[first].isNull()) [[unlikely]] @@ -42,9 +42,6 @@ class Identifiers { const std::unique_ptr m_enumerationValues; }; -Identifiers& identifiers(JSC::VM&); - -// The Structure of a returned dictionary, by index into Tables::objectStructures. -JSC::Structure* createObjectStructure(JSC::VM&, JSC::JSGlobalObject*, unsigned index); +SchemaStrings& schemaStrings(JSC::VM&); } diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp new file mode 100644 index 000000000000..846da14c8b2f --- /dev/null +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -0,0 +1,74 @@ +#include "root.h" +#include "Bindgen/ToJS.h" +#include "Bindgen/SchemaStrings.h" +#include "GeneratedBindings+Tables.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +static_assert(std::ranges::all_of(Tables::objectStructures, [](const auto& structure) { return structure.memberCount <= JSFinalObject::maxInlineCapacity; }), "a returned dictionary has more members than an object holds inline"); + +Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) +{ + auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; + const Identifier* memberNames = schemaStrings(vm).memberNames(firstMemberNameIndex, memberCount); + Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); + for (unsigned i = 0; i < memberCount; ++i) { + PropertyOffset offset; + structure = Structure::addPropertyTransition(vm, structure, memberNames[i], 0, offset); + RELEASE_ASSERT(offset == static_cast(i)); + } + return structure; +} + +// An empty value is a member that is absent. +extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) +{ + VM& vm = getVM(globalObject); + ASSERT(count == Tables::objectStructures[structureIndex].memberCount); + bool hasEveryMember = true; + for (size_t i = 0; i < count; ++i) + hasEveryMember &= !!values[i]; + if (hasEveryMember) [[likely]] { + JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->m_bindgenAdditions.m_objectStructures[structureIndex].getInitializedOnMainThread(globalObject)); + for (size_t i = 0; i < count; ++i) + object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); + return JSValue::encode(object); + } + const Identifier* memberNames = schemaStrings(vm).memberNames(Tables::objectStructures[structureIndex].firstMemberNameIndex, count); + JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count); + for (size_t i = 0; i < count; ++i) { + if (values[i]) + object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); + } + return JSValue::encode(object); +} + +extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* globalObject, unsigned index) +{ + return JSValue::encode(schemaStrings(getVM(globalObject)).enumerationValue(index)); +} + +extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) +{ + return JSValue::encode(constructArray(globalObject, static_cast(nullptr), values, count)); +} + +extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (values->hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + RELEASE_AND_RETURN(scope, JSValue::encode(constructArray(globalObject, static_cast(nullptr), *values))); +} + +} diff --git a/src/jsc/bindings/Bindgen/ToJS.h b/src/jsc/bindings/Bindgen/ToJS.h new file mode 100644 index 000000000000..2e40facb936a --- /dev/null +++ b/src/jsc/bindings/Bindgen/ToJS.h @@ -0,0 +1,9 @@ +#pragma once +#include "root.h" + +namespace Bun::Bindgen { + +// The Structure of a returned dictionary, by index into Tables::objectStructures. +JSC::Structure* createObjectStructure(JSC::VM&, JSC::JSGlobalObject*, unsigned index); + +} diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index 22f1ea9de5b3..cb90640a0ea5 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -57,7 +57,7 @@ JSHeapData::~JSHeapData() = default; JSVMClientData::JSVMClientData(VM& vm, RefPtr sourceProvider) : commonStrings(vm) - , bindgenIdentifiers(vm) + , bindgenStrings(vm) , m_builtinNames(vm) , m_builtinFunctions(makeUnique(vm, sourceProvider, m_builtinNames)) , m_heapData(JSHeapData::ensureHeapData(vm.heap)) @@ -184,7 +184,7 @@ void JSVMClientData::create(VM* vm, void* bunVM, WorkerMessagingProxy* worker) MAKE_MARKING_CONSTRAINT_EXECUTOR_PAIR(([clientData](auto& visitor) { JSC::SetRootMarkReasonScope rootScope(visitor, JSC::RootMarkReason::StrongHandles); clientData->commonStrings.visit(visitor); - clientData->bindgenIdentifiers.visit(visitor); + clientData->bindgenStrings.visit(visitor); })), JSC::ConstraintVolatility::GreyedByExecution)); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 55b545c95fa9..7cb7b4efbb63 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -72,7 +72,7 @@ class DOMWrapperWorld; #include "JSCTaskScheduler.h" #include "HTTPHeaderIdentifiers.h" #include "BunCommonStrings.h" -#include "Bindgen/Identifiers.h" +#include "Bindgen/SchemaStrings.h" #include "DOMURLBaseCache.h" #include "NodeHTTPRequestHeaderValues.h" #include "NodeVMOptionNames.h" @@ -246,7 +246,7 @@ class JSVMClientData : public JSC::VM::ClientData { // Public so Bun::commonStrings(vm) below is a static_cast and a member load. Bun::CommonStrings commonStrings; - Bun::Bindgen::Identifiers bindgenIdentifiers; + Bun::Bindgen::SchemaStrings bindgenStrings; WebCore::DOMURLBaseCache& urlBaseCache() { return m_urlBaseCache; } Bun::NodeHTTPRequestHeaderValues& nodeHTTPRequestHeaderValues() { return m_nodeHTTPRequestHeaderValues; } diff --git a/src/jsc/bindings/BunIDLConvert.h b/src/jsc/bindings/BunIDLConvert.h deleted file mode 100644 index 124bb3147986..000000000000 --- a/src/jsc/bindings/BunIDLConvert.h +++ /dev/null @@ -1,148 +0,0 @@ -#pragma once -#include "BunIDLTypes.h" -#include "BunIDLConvertBase.h" -#include "BunIDLConvertNumbers.h" -#include "BunIDLHumanReadable.h" -#include "JSDOMConvert.h" -#include -#include - -template<> struct WebCore::Converter : WebCore::DefaultConverter { - static JSC::JSValue convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) - { - return value; - } -}; - -template -struct WebCore::Converter> - : Bun::DefaultContextConverter> { - - using ReturnType = WebCore::Converter>::ReturnType; - - template - static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) - { - if (!value.toBoolean(&globalObject)) - return IDL::nullValue(); - return Bun::convertIDL(globalObject, value, ctx); - } -}; - -template<> struct WebCore::Converter - : Bun::DefaultTryConverter { - - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isBoolean()) { - return value.asBoolean(); - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotBoolean(globalObject, scope); - } -}; - -template<> struct WebCore::Converter - : Bun::DefaultTryConverter { - - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (value.isString()) { - return value.toWTFString(&globalObject); - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotString(globalObject, scope); - } -}; - -template -struct WebCore::Converter> : Bun::DefaultTryConverter> { - template - static std::optional::ImplementationType> tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - if (JSC::isJSArray(value)) { - return Bun::convert::Base>(globalObject, value, ctx); - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.template throwNotArray(globalObject, scope); - } -}; - -template<> struct WebCore::Converter - : Bun::DefaultTryConverter { - - static constexpr bool conversionHasSideEffects = false; - - template - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value, - Ctx& ctx) - { - auto& vm = JSC::getVM(&globalObject); - if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { - return jsBuffer; - } - if (auto* jsView = dynamicDowncast(value)) { - if (jsView->isShared()) { - return std::nullopt; - } - // A copy: the view's buffer can hold more than the view shows. - if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { - return bytes; - } - // Declared where it throws: at the top, every caller would owe a check for the paths that cannot. - auto scope = DECLARE_THROW_SCOPE(vm); - JSC::throwOutOfMemoryError(&globalObject, scope); - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotBufferSource(globalObject, scope); - } -}; diff --git a/src/jsc/bindings/BunIDLConvertContext.h b/src/jsc/bindings/BunIDLConvertContext.h index b82a15ad6216..b4f5af7d367d 100644 --- a/src/jsc/bindings/BunIDLConvertContext.h +++ b/src/jsc/bindings/BunIDLConvertContext.h @@ -13,18 +13,6 @@ struct IDLConversionContextMarker {}; template concept IDLConversionContext = std::derived_from; -namespace Detail { -template -struct IDLUnionForDiagnostic { - using Type = IDLAlternatives; -}; - -template -struct IDLUnionForDiagnostic { - using Type = IDLAlternatives; -}; -} - template struct IDLConversionContextBase : Detail::IDLConversionContextMarker { void throwRequired(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) @@ -134,11 +122,10 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { requires(sizeof...(Alternatives) > 0) void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { - using Union = Detail::IDLUnionForDiagnostic::Type; derived().throwTypeErrorWithPredicate( global, scope, - WTF::makeString("must be of type "_s, idlHumanReadableName())); + WTF::makeString("must be of type "_s, idlHumanReadableName>())); } template diff --git a/src/jsc/bindings/BunIDLHumanReadable.h b/src/jsc/bindings/BunIDLHumanReadable.h index 30e2b1188331..a2e2198b532b 100644 --- a/src/jsc/bindings/BunIDLHumanReadable.h +++ b/src/jsc/bindings/BunIDLHumanReadable.h @@ -1,5 +1,6 @@ #pragma once -#include "BunIDLTypes.h" +#include "IDLTypes.h" +#include #include "ConcatCStrings.h" #include #include @@ -9,6 +10,10 @@ namespace Bun { template struct IDLHumanReadableName; +// A list of types, named "a, b, or c". +template +struct IDLAlternatives {}; + template concept HasIDLHumanReadableName = requires { IDLHumanReadableName::humanReadableName; }; @@ -48,10 +53,6 @@ static constexpr auto separatorForHumanReadableBinaryDisjunction() } } -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("null"); -}; - template requires std::derived_from struct IDLHumanReadableName : BaseIDLHumanReadableName { @@ -100,17 +101,6 @@ struct IDLHumanReadableName> : BaseIDLHumanReadableNam "undefined"); }; -template -struct IDLHumanReadableName> - : IDLHumanReadableName> {}; - -template -struct IDLHumanReadableName> : BaseIDLHumanReadableName { - static constexpr bool hasPreposition = true; - static constexpr auto humanReadableName - = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); -}; - // Will generally be overridden by each specific dictionary type. template struct IDLHumanReadableName> : BaseIDLHumanReadableName { @@ -127,12 +117,4 @@ struct IDLHumanReadableName> : BaseIDLHumanReadable = Bun::joinCStringsAsList(Detail::nestedHumanReadableName()...); }; -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); -}; - -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - static constexpr auto humanReadableName = std::to_array("Blob"); -}; - } diff --git a/src/jsc/bindings/BunIDLTypes.h b/src/jsc/bindings/BunIDLTypes.h deleted file mode 100644 index 91490c016065..000000000000 --- a/src/jsc/bindings/BunIDLTypes.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once -#include "IDLTypes.h" -#include -#include -#include -#include - -namespace WTF { -struct CrashOnOverflow; -} - -namespace Bun { - -struct MimallocMalloc; - -// Like `IDLAny`, but always stored as a raw `JSValue`. This should only be -// used in contexts where the `JSValue` will be stored on the stack. -struct IDLRawAny : WebCore::IDLType { - // Storage in a sequence is explicitly unsupported, as this would create a - // `Vector`, whose contents are invisible to the GC. - using SequenceStorageType = void; -}; - -// For use in unions, to represent a nullable union. -struct IDLStrictNull : WebCore::IDLType {}; - -// Treats all falsy values as null. -template -struct IDLFalsyAsNull : WebCore::IDLNullable {}; - -template -struct IDLStrictInteger : WebCore::IDLInteger {}; -struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; -struct IDLStrictBoolean : WebCore::IDLBoolean {}; -struct IDLStrictString : WebCore::IDLDOMString {}; - -// Converts to a number first. -template -struct IDLLooseInteger : IDLStrictInteger {}; - -// The alternatives of a union, for IDLHumanReadableName. -template -struct IDLAlternatives {}; - -namespace Detail { -template -using IDLMimallocSequence = WebCore::IDLSequence< - IDL, - WTF::Vector< - typename IDL::SequenceStorageType, - 0, - WTF::CrashOnOverflow, - 16, - MimallocMalloc>>; -} - -template -struct IDLArray : Detail::IDLMimallocSequence { - using Base = Detail::IDLMimallocSequence; -}; - -template> -struct IDLBunInterface : WebCore::IDLType, RefDerefTraits>> { - using NullableType = WTF::RefPtr, RefDerefTraits>; - using NullableInnerParameterType = NullableType; - - static inline std::nullptr_t nullValue() { return nullptr; } -}; - -struct IDLArrayBufferRef : IDLBunInterface {}; - -// Defined in BunIDLConvertBlob.h -struct IDLBlobRef; - -} diff --git a/src/jsc/bindings/JSDOMWrapper.h b/src/jsc/bindings/JSDOMWrapper.h index c5a30afa7a49..eb56032214ab 100644 --- a/src/jsc/bindings/JSDOMWrapper.h +++ b/src/jsc/bindings/JSDOMWrapper.h @@ -45,7 +45,7 @@ class ScriptExecutionContext; // offset | 7 | 6 | 5 | 4 | 3 2 1 0 | // value | 1 | 1 | 1 | 1 | NodeType | -// Only for a JSDestructibleObject: Bun::Bindgen::JSBindgenObject's cast reads classInfo() from any cell of this type. +// Only for a JSDestructibleObject: Bun::Bindgen::JSWrapper's cast reads classInfo() from any cell of this type. inline constexpr uint8_t JSDOMWrapperType = 0b11101110; inline constexpr uint8_t JSEventType = 0b11101111; inline constexpr uint8_t JSNodeType = 0b11110000; diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 16c41f733001..bfe0c0f85bbe 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -67,7 +67,6 @@ #include "AddEventListenerOptions.h" #include "AsyncContextFrame.h" #include "BunClientData.h" -#include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" #include "BunPlugin.h" diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 08f06173190d..8839072297d9 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -71,7 +71,7 @@ struct node_module; #include #include #include "BakeAdditionsToGlobalObject.h" -#include "BindgenAdditionsToGlobalObject.h" +#include "Bindgen/AdditionsToGlobalObject.h" #include "NativeModuleList.h" #include "streams/JSStreamsRuntime.h" @@ -500,7 +500,7 @@ class GlobalObject : public Bun::GlobalScope { #define FOR_EACH_GLOBALOBJECT_GC_MEMBER(V) \ V(public, Bun::BakeAdditionsToGlobalObject, m_bakeAdditions) \ - V(public, Bun::BindgenAdditionsToGlobalObject, m_bindgenAdditions) \ + V(public, Bun::Bindgen::AdditionsToGlobalObject, m_bindgenAdditions) \ \ /* TODO: these should use LazyProperty */ \ \ diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 82aebbf4d8aa..fdafa969ba0d 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -196,32 +196,6 @@ describe("strings", () => { expect(bindgen.utf8EchoLoose(...args)).toBe("undefined"); } }); - - it("keeps the characters of a substring while its base is swapped for an atom", () => { - const flat = (text: string) => Buffer.from(text).toString("latin1"); - const keys: Record = {}; - // Whether a collection frees the base's old characters depends on more than this test controls, so it tries many. - for (let round = 0; round < 20; round++) { - const text = `${round}`.padStart(4, "0") + Buffer.alloc(296, "q").toString(); - const strings = { base: flat(text), equal: flat(text) }; - keys[strings.equal] = 1; - const options = { - name: strings.base.substring(10, 290), - // Read after `name` is converted. - get renamed() { - // The base takes the atom's characters; two more lookups push its own out of the VM's last-atomized slot. - keys[strings.base]; - keys[flat(`${round} a`)]; - keys[flat(`${round} b`)]; - Bun.gc(true); - const reuse = Array.from({ length: 2000 }, () => Buffer.alloc(300, "Z").toString("latin1")); - Bun.gc(true); - return reuse.length ? 1 : 0; - }, - }; - expect(bindgen.optionsEcho(options).name).toBe(text.substring(10, 290)); - } - }); }); describe("enumerations", () => { From ae57651f37e9b2efe4bcc3e316c243b81245e396 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:44:43 +0000 Subject: [PATCH 37/44] [autofix.ci] apply automated fixes --- src/jsc/bindings/ZigGlobalObject.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 8839072297d9..0c563bf2e734 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -500,7 +500,7 @@ class GlobalObject : public Bun::GlobalScope { #define FOR_EACH_GLOBALOBJECT_GC_MEMBER(V) \ V(public, Bun::BakeAdditionsToGlobalObject, m_bakeAdditions) \ - V(public, Bun::Bindgen::AdditionsToGlobalObject, m_bindgenAdditions) \ + V(public, Bun::Bindgen::AdditionsToGlobalObject, m_bindgenAdditions) \ \ /* TODO: these should use LazyProperty */ \ \ From 638d42ffe4cfecc829aba54ccf8c6bfb524c4a9a Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 01:35:09 +0000 Subject: [PATCH 38/44] Tie every borrowed argument to its call, purify NaN results, use JSC's own accessors and allocation tier, and tighten what the generator accepts --- CLAUDE.md | 1 + docs/project/bindgen.mdx | 58 ++--- src/codegen/bindgen-emit.ts | 130 +++++++---- src/codegen/bindgen-lib-internal.ts | 210 ++++++++++++++---- src/codegen/bindgen-lib.ts | 3 +- src/codegen/bindgen.ts | 17 +- src/js/internal-for-testing.ts | 2 +- src/jsc/NodeModuleModule.rs | 2 +- src/jsc/bindgen.rs | 77 +++++-- src/jsc/bindgen/extern_types.rs | 9 +- src/jsc/bindgen_test.bind.ts | 4 + src/jsc/bindgen_test.rs | 83 +++---- .../Bindgen/DictionaryMemberReader.cpp | 19 +- .../bindings/Bindgen/DictionaryMemberReader.h | 12 - src/jsc/bindings/Bindgen/ExternTraits.h | 2 +- src/jsc/bindings/Bindgen/IDLConvert.h | 8 +- .../bindings/Bindgen/IDLConvertIntegerRange.h | 5 +- src/jsc/bindings/Bindgen/IDLConvertNumbers.h | 8 +- src/jsc/bindings/Bindgen/JSStringUTF8View.cpp | 6 +- src/jsc/bindings/Bindgen/JSWrapper.cpp | 14 +- src/jsc/bindings/Bindgen/JSWrapper.h | 38 ++-- src/jsc/bindings/Bindgen/SchemaStrings.cpp | 2 +- src/jsc/bindings/Bindgen/ToJS.cpp | 5 +- src/jsc/bindings/BunClientData.cpp | 4 +- src/jsc/bindings/BunClientData.h | 2 +- src/jsc/bindings/BunIDLConvertContext.h | 5 - src/jsc/bindings/IDLTypes.h | 2 +- src/jsc/fmt_jsc.rs | 4 +- src/jsc/lib.rs | 6 +- src/jsc/modules/NodeModuleModule.cpp | 2 +- src/runtime/node/node_os.bind.ts | 2 +- src/runtime/node/node_os.rs | 10 +- src/runtime/socket/SSLConfig.bind.ts | 2 +- src/runtime/socket/SSLConfig.rs | 5 +- test/internal/bindgen.test.ts | 113 +++++++--- test/js/bun/net/socket.test.ts | 4 + .../js/node/module/node-module-module.test.js | 9 + test/js/node/os/os.test.js | 9 +- 38 files changed, 598 insertions(+), 296 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2ff91567b42..d06270cfd9d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -165,6 +165,7 @@ When implementing JavaScript classes in C++: Code generation happens automatically as part of the build process. The main scripts are: +- `src/codegen/bindgen.ts` - Generates Rust, C++ & TypeScript bindings from `*.bind.ts` files (`docs/project/bindgen.mdx`) - `src/codegen/generate-classes.ts` - Generates Rust & C++ bindings from `*.classes.ts` files - `src/codegen/generate-jssink.ts` - Generates stream-related classes - `src/codegen/bundle-modules.ts` - Bundles built-in modules like `node:fs` diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 71e6bc0a8316..33af3cb1c085 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -58,7 +58,9 @@ pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { `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 required arguments. +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//generated_bindgen.rs` includes the generated file, and the crate root declares `mod generated_bindgen`. If the @@ -85,11 +87,15 @@ 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. + `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(..)` or `bindgen::Error::from_value(value)`, or any type that -implements `bindgen::IntoError` (`bun_sys::Error` and `JsError` do), which lets a -`capability: "none"` function fail too. +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`, and a plain `JSValue` under @@ -103,7 +109,7 @@ 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.bufferSource, offset: b.u32.default(0) }, rustPath: "crate::read_fd" }, + { args: { fd: b.i32, buffer: b.bufferSource.bytes, offset: b.u32.default(0) }, rustPath: "crate::read_fd" }, ); ``` @@ -115,8 +121,8 @@ 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`, which a required argument does not - take. So `f(x)` and `f(x, undefined)` are the same call. +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 @@ -163,7 +169,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. -- `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). +- `.withValue`, for arguments: see [Keeping the original value](#keeping-the-original-value). Not with `.default()`. As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that is `IntoIterator`. @@ -182,7 +188,8 @@ 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`. +`.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 @@ -222,7 +229,7 @@ The second argument of `b.dictionary` takes: | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowArray` | an array is read like any other object, not refused | | `nonObjectAsEmpty` | a value whose `typeof` is not `"object"` gives every member its default, 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, \|converted\| ..)`, for a `#[host_fn]` body | +| `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 @@ -248,14 +255,14 @@ in play apart: 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`, an enumeration, a dictionary. +`.validateInteger`, `b.any`, 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. A value no -alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. +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 @@ -311,8 +318,8 @@ 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`: the GC does not -look inside heap allocations. +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 @@ -354,16 +361,17 @@ impl GlobClass for Glob { 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. - -| Key | What it declares | -| ---------------- | -------------------------------------------------------------------------------------------- | -| `rustPath` | the struct | -| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.to_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 | +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: @@ -376,7 +384,7 @@ A property of `proto` is one of: 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 in its own signatures. `Glob::get_constructor(heap)` 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 diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 3f1fce095398..f6d6eb840211 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -56,6 +56,10 @@ const banner = (origin: Origin | null) => function walk(type: Type, visit: (type: Type) => void, seen = new Set()): void { if (seen.has(type)) return; seen.add(type); + assert( + !(type instanceof NamedType) || type.origin, + `${type.constructor.name}: used but not exported from a .bind.ts file`, + ); visit(type); for (const dependency of type.dependencies) walk(dependency, visit, seen); } @@ -208,7 +212,7 @@ function layoutAssert(cppType: string, type: Type): string { } function humanReadableEnumeration(type: EnumerationType): string { - const quoted = type.spellings.map(([canonical]) => `\\"${canonical}\\"`); + const quoted = type.spellings.map(([canonical]) => asciiLiteral(`"${canonical}"`).slice(1, -3)); if (quoted.length <= 2) return quoted.join(" or "); return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; } @@ -222,7 +226,7 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) .map(i => { const { name, type: alternative } = type.alternatives[i]; const member = `result.${name}`; - if (alternative.mayRefuse) + if (alternative.refuses) return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { ${member} = std::move(*converted); result.tag = ${i}; @@ -248,7 +252,7 @@ function cppHeaderForUnion(type: UnionType): string { .join(", "); const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); const mayMatchNone = (candidates: readonly number[]) => - candidates.length === 0 || alternatives[candidates.at(-1)!].type.mayRefuse; + candidates.length === 0 || !!alternatives[candidates.at(-1)!].type.refuses; const cases = outcomes.flatMap((candidates, index) => candidates.length === 0 ? [] @@ -365,7 +369,9 @@ template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalOb ${ type.needsLifetime ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` - : `template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue);` + : type.convertedByValue + ? `template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue);` + : "" } `; } @@ -392,7 +398,7 @@ ${file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});` }`; } -const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name)}`; +const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name.replace(/^_+/, ""))}`; function cppSourceForEnum(type: EnumerationType): string { const pairs = type.spellings @@ -435,10 +441,8 @@ function cppSourceForDictionary(type: DictionaryType): string { ctx${i}.throwRequired(*globalObject, scope); return; }`); - lines.push( - member.type.cppConvertInto(`result.${member.fieldName}`, `value${i}`, `ctx${i}`, qualified), - "RETURN_IF_EXCEPTION(scope, void());", - ); + lines.push(member.type.cppConvertInto(`result.${member.fieldName}`, `value${i}`, `ctx${i}`, qualified)); + if (member.type.conversionThrows) lines.push("RETURN_IF_EXCEPTION(scope, void());"); return ` {\n${indent(lines.join("\n"), 8)}\n }`; }); const count = type.names.length; @@ -475,7 +479,7 @@ ${ ${members.join("\n")} } ${ - type.needsLifetime + type.needsLifetime || !type.convertedByValue ? "" : ` template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) @@ -529,7 +533,7 @@ function cppCallBody(signature: Signature, symbol: string, leading: readonly str `scope.assertNoException();`, ); lines.push(...cppDeclareConverted(type, `converted${i}`, `argument${i}.value()`, `ctx${i}`, name)); - lines.push(`RETURN_IF_EXCEPTION(scope, {});`); + if (type.conversionThrows) lines.push(`RETURN_IF_EXCEPTION(scope, {});`); }); lines.push(...rereads); const args = signature.args.map(({ type }, i) => { @@ -555,8 +559,10 @@ const classSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${ty const protoSymbolName = (type: ClassType, member: string) => `Bun__Bindgen__${type.name}Prototype__${member}`; const methodSymbolName = (type: ClassType, method: Method) => (method.receiver === "static" ? classSymbolName : protoSymbolName)(type, method.name.replace(/^_+/, "")); -const getterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `get${cap(getter.name)}`); -const setterSymbolName = (type: ClassType, getter: Getter) => protoSymbolName(type, `set${cap(getter.name)}`); +const getterSymbolName = (type: ClassType, getter: Getter) => + `Bun__Bindgen__${type.name}PrototypeGetter__${getter.name}`; +const setterSymbolName = (type: ClassType, getter: Getter) => + `Bun__Bindgen__${type.name}PrototypeSetter__${getter.name}`; const classSymbols = (type: ClassType) => [ ...[...type.methods, ...type.staticMethods].map(method => methodSymbolName(type, method)), @@ -724,7 +730,7 @@ ${table(`JS${name}PrototypeTableValues`, prototypeRows)}${table(`JS${name}Constr ${privateMethods .map( method => - ` prototype->putDirect(vm, names.${method.privateSymbol}PrivateName(), JSC::JSFunction::create(vm, globalObject, ${method.length ?? method.requiredCount}, ${asciiLiteral(method.name)}, ${methodHostFunctionName(type, method)}, JSC::ImplementationVisibility::Private), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | 0);`, + ` prototype->putDirect(vm, names.${method.privateSymbol}PrivateName(), JSC::JSFunction::create(vm, globalObject, ${method.length ?? method.requiredCount}, ${asciiLiteral(method.name)}, ${methodHostFunctionName(type, method)}, JSC::ImplementationVisibility::Private), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum);`, ) .join("\n")} } @@ -733,7 +739,6 @@ ${privateMethods } const Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { .info = ${type.cpp}::info(), - .createStructure = ${type.cpp}::createStructure, .prototypeTableValues = ${span(`JS${name}PrototypeTableValues`, prototypeRows)}, .constructorTableValues = ${span(`JS${name}ConstructorTableValues`, constructorRows)}, .finishPrototypeCreation = ${privateMethods.length > 0 ? `finish${name}PrototypeCreation` : "nullptr"}, @@ -832,15 +837,15 @@ export function cppSource(file: SchemaFile): string { if (type.builtins.length > 0) headers.add("WebCoreJSBuiltins.h"); } headers.delete(`Generated${file.origin.namespace}.h`); + const accepted = file.types.filter(type => type instanceof DictionaryType && type.accepted) as DictionaryType[]; + if (accepted.length > 0) headers.add("Bindgen/DictionaryMemberReader.h"); + const systemHeaders = ["JavaScriptCore/JSFunction.h"]; + if (accepted.some(type => !type.allowArray)) systemHeaders.unshift("JavaScriptCore/ArrayConstructor.h"); + if (file.types.some(type => type instanceof EnumerationType)) systemHeaders.push("wtf/SortedArrayMap.h"); return `${banner(file.origin)}#include "root.h" #include "Generated${file.origin.namespace}.h" -#include "Bindgen/DictionaryMemberReader.h" -#include "JSDOMOperation.h" #include "ZigGlobalObject.h" -#include -#include -#include -${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types +${[...headers].map(header => `#include "${header}"\n`).join("")}${systemHeaders.map(header => `#include <${header}>\n`).join("")}${file.types .map(type => type instanceof EnumerationType ? cppSourceForEnum(type) @@ -869,10 +874,13 @@ export function cppTables(tables: Tables): string { ${items.join(",\n ")} } };`; return `${banner(null)}#pragma once -#include "Bindgen/JSWrapper.h" #include #include +namespace Bun::Bindgen { +struct ClassDefinition; +} + namespace Bun::Bindgen::Generated { ${tables.classes.map(type => `extern const ClassDefinition JS${type.name}ClassDefinition;`).join("\n")} } @@ -925,8 +933,17 @@ const rustParameters = (signature: Signature, scope: RustScope) => signature.args.map(arg => `${rustArgumentName(arg)}: ${arg.type.rustType(scope)}`); const rustExternParameters = (signature: Signature, scope: RustScope) => signature.args.map(arg => `${rustArgumentName(arg)}: ${arg.type.rustExternType(scope)}`); -const rustArguments = (signature: Signature, scope: RustScope) => - signature.args.map(arg => arg.type.rustFromExtern(rustArgumentName(arg), scope)); +const rustArguments = (signature: Signature) => signature.args.map(rustArgumentName); +/** Each argument gets the call's lifetime here: left to inference in the call, a body could ask for `'static`. */ +function rustTakeArguments(args: readonly { name: string; type: Type }[], scope: RustScope): string { + const taken = args.flatMap(arg => { + const name = rustArgumentName(arg); + const converted = arg.type.rustFromExtern(name, scope); + return converted === name ? [] : [`let ${name} = ${converted};\n`]; + }); + if (taken.some(line => /\bheap\b/.test(line))) taken.unshift("let heap = &cx.heap;\n"); + return taken.join(""); +} const capabilityParameter = { none: [], @@ -967,7 +984,8 @@ ${ ? `impl ${type.name} { #[inline] pub fn to_js<'a>(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { - bindgen::to_js::enumeration_value(heap, ${type.firstEnumerationValueIndex} + self as u32) + // SAFETY: the values of this enumeration start there in the table. + unsafe { bindgen::to_js::enumeration_value(heap, ${type.firstEnumerationValueIndex} + self as u32) } } } ` @@ -995,9 +1013,9 @@ ${type.alternatives.map(a => (a.type instanceof UnitType ? ` ${a.variant},` : } impl${lifetime} ${type.name}${lifetime} { /// # Safety - /// \`value\` was just filled in by generated C++ and is read once. + /// \`value\` was just filled in by generated C++${lifetime && " for the call `heap` belongs to"} and is read once. #[doc(hidden)] - pub unsafe fn from_extern(value: &Extern${type.name}) -> Self { + pub unsafe fn from_extern(value: &Extern${type.name}${lifetime && ", heap: &bindgen::JsHeap<'a>"}) -> Self { // SAFETY: C++ initialized the alternative \`tag\` names. unsafe { match value.tag { @@ -1044,9 +1062,9 @@ ${type.members.map(member => ` ${member.rustName}: ${member.type.rustExternTy ${rustLayoutAssert(`Extern${type.name}`, type)} impl${lifetime} ${type.name}${lifetime} { /// # Safety - /// \`value\` was just filled in by generated C++ and is read once. + /// \`value\` was just filled in by generated C++${lifetime && " for the call `heap` belongs to"} and is read once. #[doc(hidden)] - pub unsafe fn from_extern(value: &Extern${type.name}) -> Self { + pub unsafe fn from_extern(value: &Extern${type.name}${lifetime && ", heap: &bindgen::JsHeap<'a>"}) -> Self { // SAFETY: as above, member by member. unsafe { Self { @@ -1062,17 +1080,25 @@ ${type.members.map(member => ` ${member.rustName}: ${member.type. fn Bun__Bindgen__${type.name}__fromJS(global: *mut JSGlobalObject, value: JSValue, result: *mut Extern${type.name}) -> bool; } impl ${type.name}${lifetime ? "<'_>" : ""} { - /// Converts \`value\` and hands it to \`f\`. + ${ + lifetime + ? `/// Converts \`value\` and lends it to \`f\`. pub fn from_js( global: &JSGlobalObject, value: JSValue, - f: impl ${lifetime ? `for<'call> FnOnce(${type.name}<'call>)` : `FnOnce(${type.name})`} -> bun_jsc::JsResult, - ) -> bun_jsc::JsResult { + f: impl for<'call> FnOnce(${type.name}<'call>) -> bun_jsc::JsResult, + ) -> bun_jsc::JsResult {` + : `pub fn from_js(global: &JSGlobalObject, value: JSValue) -> bun_jsc::JsResult {` + } let mut result = core::mem::MaybeUninit::::uninit(); // SAFETY: C++ fills \`result\` exactly when it returns true. bun_jsc::call_false_is_throw(global, || unsafe { Bun__Bindgen__${type.name}__fromJS(global.as_mut_ptr(), value, result.as_mut_ptr()) })?; - // SAFETY: as above; \`value\` is on the caller's stack for all of \`'call\`. - f(unsafe { ${type.name}::from_extern(result.assume_init_ref()) }) + // SAFETY: as above${lifetime && "; the scope only names the time until `f` returns"}. + ${ + lifetime + ? `unsafe { bindgen::JsScope::with(global.as_mut_ptr(), |cx| f(${type.name}::from_extern(result.assume_init_ref(), &cx.heap))) }` + : `Ok(unsafe { ${type.name}::from_extern(result.assume_init_ref()) })` + } } } `; @@ -1086,11 +1112,12 @@ impl ${type.name}${lifetime ? "<'_>" : ""} { return `${member.type.rustToJS(field, scope)}?.to_js()`; }; text += `impl${lifetime} ${type.name}${lifetime} { - pub fn to_js${lifetime ? "" : "<'a>"}(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + pub fn into_js${lifetime ? "" : "<'a>"}(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { let values = [ ${type.members.map(member => ` ${value(member)},`).join("\n")} ]; - bindgen::to_js::object_with_structure(heap, ${type.structureIndex}, &values) + // SAFETY: structure ${type.structureIndex} has these members, in this order. + unsafe { bindgen::to_js::object_with_structure(heap, ${type.structureIndex}, &values) } } } `; @@ -1113,7 +1140,7 @@ function rustForClass(type: ClassType, scope: RustScope): string { static: [], }; const receiverArgument = { ref: ["this.payload_ref()"], mut: ["this.payload_mut()"], wrapper: ["this"], static: [] }; - const takeThis = `let this = bindgen::JsWrapper::<${rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(this));`; + const takeThis = `let this = bindgen::JsWrapper::<${rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(this), &cx.heap);`; const declare = (fn: string, parameters: string[], returns: string) => { const signature = `(${parameters.join(", ")}) -> ${returns};`; return `fn ${fn}${signature.includes("'a") ? "<'a>" : ""}${signature}`; @@ -1133,18 +1160,18 @@ function rustForClass(type: ClassType, scope: RustScope): string { result("Self", signature.throws), ), ); - const call = `<${rustPath} as ${trait}>::constructor(${[...capabilityArgument[signature.capability], ...rustArguments(signature, scope)].join(", ")})`; + const call = `<${rustPath} as ${trait}>::constructor(${[...capabilityArgument[signature.capability], ...rustArguments(signature)].join(", ")})`; thunks += rustThunk( classSymbolName(type, "construct"), ["structure: *mut core::ffi::c_void", ...rustExternParameters(signature, scope)], "JSValue", - `let result = ${infallible(call, signature.throws)}; + `${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws)}; bindgen::host_fn_return(cx, result, |heap, value| bindgen::to_js::any(heap, bindgen::JsWrapper::create_with_structure(heap, structure, value).as_value()))`, ); declarations.push(`/// The constructor function, \`${name}\` in JavaScript. fn get_constructor<'a>(heap: &bindgen::JsHeap<'a>) -> bindgen::JsLocal<'a> { // SAFETY: cannot throw; makes the class's objects on first use. - unsafe { bindgen::JsLocal::from_extern(${symbolName(type, "getConstructor")}(heap.global_ptr())) } + unsafe { bindgen::JsLocal::from_extern(${symbolName(type, "getConstructor")}(heap.global_ptr()), heap) } }`); } @@ -1166,13 +1193,13 @@ bindgen::host_fn_return(cx, result, |heap, value| bindgen::to_js::any(heap, bind ...receiverArgument[method.receiver], ...(method.passThis ? ["this"] : []), ...capabilityArgument[method.capability], - ...rustArguments(method, scope), + ...rustArguments(method), ].join(", ")})`; thunks += rustThunk( methodSymbolName(type, method), [...(isStatic ? [] : ["this: *mut core::ffi::c_void"]), ...rustExternParameters(method, scope)], "JSValue", - `${isStatic ? "" : `${takeThis}\n`}let result = ${infallible(call, method.throws, method.returns)}; + `${isStatic ? "" : `${takeThis}\n`}${rustTakeArguments(method.args, scope)}let result = ${infallible(call, method.throws, method.returns)}; bindgen::host_fn_return(cx, result, |heap, value| ${method.returns.rustToJS("value", scope)})`, ); } @@ -1213,7 +1240,7 @@ bindgen::host_fn_return(cx, result, |heap, value| ${getter.type.rustToJS("value" ["this: *mut core::ffi::c_void", `value: ${getter.type.rustExternType(scope)}`], "bool", `${takeThis} -let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiverArgument[setterReceiver], ...capabilityArgument[getter.capability], getter.type.rustFromExtern("value", scope)].join(", ")})`, getter.throws, unit)}; +${rustTakeArguments([{ name: "value", type: getter.type }], scope)}let result = ${infallible(`<${rustPath} as ${trait}>::${setterName}(${[...receiverArgument[setterReceiver], ...capabilityArgument[getter.capability], "value"].join(", ")})`, getter.throws, unit)}; !bindgen::host_fn_return(cx, result, bindgen::to_js::undefined).is_empty()`, ); } @@ -1265,7 +1292,7 @@ unsafe impl bindgen::JsPayload for ${rustPath} { unsafe { ${symbolName(type, "create")}(global, structure) } } } -// Cells are 16-byte aligned and the payload starts at a multiple of 8. +// A cell is at a multiple of 8, and so is the payload in it. const _: () = assert!(align_of::<${rustPath}>() <= 8); #[unsafe(no_mangle)] pub static ${symbolName(type, "payloadSize")}: usize = size_of::<${rustPath}>(); @@ -1294,18 +1321,19 @@ function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string { .map(signature => { let path = signature.rustPath; if (!path) { + assert(fn.signatures.length === 1, `${fn.name}: each overload has a rustPath of its own`); assert( file.moduleDefinition, `${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`, ); path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`; } - const call = `${path}(${[...capabilityArgument[signature.capability], ...rustArguments(signature, scope)].join(", ")})`; + const call = `${path}(${[...capabilityArgument[signature.capability], ...rustArguments(signature)].join(", ")})`; return rustThunk( thunkSymbol(fn, signature), rustExternParameters(signature, scope), "JSValue", - `let result = ${infallible(call, signature.throws, signature.returns)}; + `${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws, signature.returns)}; bindgen::host_fn_return(cx, result, |heap, value| ${signature.returns.rustToJS("value", scope)})`, ); }) @@ -1358,8 +1386,13 @@ ${indent( export function dts(files: readonly SchemaFile[]): string { const parameters = (signature: Signature) => - signature.args.map(arg => `${arg.name}${arg.type.takesUndefined ? "?" : ""}: ${arg.type.tsType}`).join(", "); - const signatureType = (signature: Signature) => `(${parameters(signature)}) => ${signature.returns.tsType}`; + signature.args + .map((arg, i) => `${arg.name}${i < signature.requiredCount ? "" : "?"}: ${arg.type.tsType}`) + .join(", "); + /** A result that is absent is `null`, never `undefined` too. */ + const resultType = (type: Type) => + type instanceof OptionalType && type.modifier === "nullable" ? `${type.inner.tsType} | null` : type.tsType; + const signatureType = (signature: Signature) => `(${parameters(signature)}) => ${resultType(signature.returns)}`; const member = (name: string, optional: boolean, type: string) => ` ${JSON.stringify(name)}${optional ? "?" : ""}: ${type};`; const declarations = files.flatMap(file => @@ -1376,6 +1409,7 @@ ${[ ...type.getters.map( getter => ` ${getter.hasSetter ? "" : "readonly "}${JSON.stringify(getter.name)}: ${getter.type.tsType};`, ), + ...type.builtins.map(builtin => member(builtin.name, false, "(...args: any[]) => any")), ].join("\n")} }${ type.construct diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index a5b2948af9dd..795705b59bb9 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -63,7 +63,15 @@ const rustKeywords = new Set( " ", ), ); -export const rustIdent = (name: string) => (rustKeywords.has(name) ? `r#${name}` : name); +export function rustIdent(name: string): string { + assert(!/^(self|Self|super|crate)$/.test(name), `${name} cannot name anything in Rust, even as r#${name}`); + return rustKeywords.has(name) ? `r#${name}` : name; +} + +function assertDistinct(names: readonly string[], what: string): void { + const repeated = names.find((name, i) => names.indexOf(name) !== i); + assert(repeated === undefined, `two ${what} are both ${repeated}`); +} export function asciiLiteral(value: string): string { assert(!/[^\x20-\x7e]/.test(value), `must be printable ASCII: ${util.inspect(value)}`); @@ -104,9 +112,13 @@ function alternativesByKind(alternatives: readonly Type[]): Map(); return new Map( distinguishingKinds(alternatives).map(kind => { - const takers = alternatives.flatMap((type, i) => (takes(type, kind) ? [i] : [])); - const last = takers.findIndex(i => !alternatives[i].mayRefuse); - const list = last < 0 ? takers : takers.slice(0, last + 1); + const list: number[] = []; + for (const [i, type] of alternatives.entries()) { + // What one refused, another that refuses the same would too. + if (!takes(type, kind) || list.some(earlier => alternatives[earlier].refuses === type.refuses)) continue; + list.push(i); + if (!type.refuses) break; + } if (!lists.has(`${list}`)) lists.set(`${list}`, list); return [kind, lists.get(`${list}`)!]; }), @@ -167,10 +179,18 @@ export abstract class Type { get valueKinds(): ValueKinds { return "any"; } - /** It refuses some values of its kinds: a shared buffer, a detached Blob. In a union the next alternative gets those. */ - get mayRefuse(): boolean { + /** The values of its kinds that it does not take, if any. In a union the next alternative gets those. */ + get refuses(): string | null { + return null; + } + /** Written `.optional`, `.nullable`, `.falsyAsNull` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ + get isOptional(): boolean { return false; } + /** Converting can throw, so an exception check follows. */ + get conversionThrows(): boolean { + return true; + } /** Holds a JS value: the Rust type is branded with the call. */ get holdsJSValue(): boolean { return this.dependencies.some(type => type.holdsJSValue); @@ -227,7 +247,7 @@ export abstract class Type { abstract rustExternType(scope: RustScope): string; /** What a Rust body sees. */ abstract rustType(scope: RustScope): string; - /** A Rust expression (inside `unsafe`) that consumes `expr`, a `rustExternType`. */ + /** A Rust expression (inside `unsafe`) that consumes `expr`, a `rustExternType`. What has a lifetime takes it from `heap`, in scope there. */ rustFromExtern(expr: string, scope: RustScope): string { return expr; } @@ -279,7 +299,7 @@ class PrimitiveType extends Type { return `${this.cppName} { ${value} }`; } toRust(value: unknown): string { - return this.rustName === "f64" && Number.isInteger(value) ? `${value}.0` : String(value); + return this.rustName === "f64" ? `${value}_f64` : String(value); } layout(): Layout { return { size: this.size, align: this.size }; @@ -303,6 +323,9 @@ export class BoolType extends PrimitiveType { get loose(): BoolType { return new BoolType("WebCore::IDLBoolean", "any"); } + get conversionThrows(): boolean { + return this.valueKinds !== "any"; + } } interface IntegerInfo { @@ -375,6 +398,12 @@ export class RangeType extends IntegerType { protected get range() { return this.#range; } + get loose(): never { + throw new TypeError(`.${this.modifier}() says how the value converts; .loose is another way`); + } + get wrap(): never { + throw new TypeError(`.${this.modifier}() says how the value converts; .wrap is another way`); + } getHeaders(out: Set): void { out.add("Bindgen/IDLConvertIntegerRange.h"); } @@ -499,7 +528,7 @@ export class OwnedUtf8Type extends StringType { get utf8(): never { throw new TypeError("b.string.utf8.owned is already UTF-8"); } - toRust(): string { + toCpp(): never { throw new TypeError("b.string.utf8.owned cannot have a default"); } rustType(): string { @@ -562,7 +591,7 @@ export class Utf8Type extends Type { return "&'a [u8]"; } rustFromExtern(expr: string): string { - return `${expr}.get()`; + return `${expr}.get(heap)`; } rustResultFieldType(): string { return "Vec"; @@ -576,6 +605,7 @@ export class Utf8Type extends Type { } const bufferSourceKinds: ValueKinds = ["arrayBuffer", "arrayBufferView"]; +const sharedBuffers = "shared buffers"; /** The bytes of a typed array, `DataView` or `ArrayBuffer`, as `&[u8]`. */ export class BytesType extends Type { @@ -600,7 +630,7 @@ export class BytesType extends Type { return "&'a [u8]"; } rustFromExtern(expr: string): string { - return `${expr}.get()`; + return `${expr}.get(heap)`; } } @@ -611,8 +641,8 @@ export class BufferSourceType extends Type { get valueKinds(): ValueKinds { return bufferSourceKinds; } - get mayRefuse(): boolean { - return true; + get refuses() { + return sharedBuffers; } /** `&[u8]` for the duration of the call. Not for `capability: "js"`, where JavaScript could detach it. */ get bytes(): BytesType { @@ -626,6 +656,7 @@ export class BufferSourceType extends Type { "bun_jsc::JSCArrayBuffer", null, bufferSourceKinds, + sharedBuffers, ); } get holdsJSValue(): boolean { @@ -641,7 +672,7 @@ export class BufferSourceType extends Type { return "bindgen::JsLocal<'a, bindgen::BufferSource>"; } rustFromExtern(expr: string): string { - return `bindgen::JsLocal::from_extern(${expr})`; + return `bindgen::JsLocal::from_extern(${expr}, heap)`; } rustToJS(value: string): string { return `bindgen::to_js::any(heap, ${value})`; @@ -666,6 +697,12 @@ export class WithValueType extends Type { get takesUndefined(): boolean { return this.inner.takesUndefined; } + get isOptional(): boolean { + return this.inner.isOptional; + } + get conversionThrows(): boolean { + return this.inner.conversionThrows; + } get valueKinds(): ValueKinds { return this.inner.valueKinds; } @@ -694,7 +731,7 @@ export class WithValueType extends Type { return `bindgen::WithValue<'a, ${this.inner.rustType(scope)}>`; } rustFromExtern(expr: string, scope: RustScope): string { - return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsLocal::from_extern(with_value.value) } }`; + return `{ let with_value = ${expr}; bindgen::WithValue { converted: ${this.inner.rustFromExtern("with_value.converted", scope)}, value: bindgen::JsLocal::from_extern(with_value.value, heap) } }`; } } @@ -707,6 +744,9 @@ export class AnyType extends Type { get takesUndefined(): boolean { return true; } + get conversionThrows(): boolean { + return false; + } toCpp(value: unknown): string { assert(value === undefined, "b.any cannot have a default"); return "JSC::jsUndefined()"; @@ -724,7 +764,7 @@ export class AnyType extends Type { return "bindgen::JsLocal<'a>"; } rustFromExtern(expr: string): string { - return `bindgen::JsLocal::from_extern(${expr})`; + return `bindgen::JsLocal::from_extern(${expr}, heap)`; } rustToJS(value: string): string { return `bindgen::to_js::any(heap, ${value})`; @@ -738,19 +778,28 @@ export class RefType extends Type { readonly pointee: string; readonly header: string | null; readonly #valueKinds: ValueKinds; - constructor(idlType: string, tsType: string, pointee: string, header: string | null, valueKinds: ValueKinds) { + readonly #refuses: string; + constructor( + idlType: string, + tsType: string, + pointee: string, + header: string | null, + valueKinds: ValueKinds, + refuses: string, + ) { super(); this.idlType = idlType; this.tsType = tsType; this.pointee = pointee; this.header = header; this.#valueKinds = valueKinds; + this.#refuses = refuses; } get valueKinds(): ValueKinds { return this.#valueKinds; } - get mayRefuse(): boolean { - return true; + get refuses() { + return this.#refuses; } get rustIsCopy(): boolean { return false; @@ -787,6 +836,7 @@ export class OptionalType extends Type { assert(!(inner instanceof DefaultType), "put .default() last"); assert(!(inner instanceof AnyType), "b.any already accepts undefined and null"); assert(!(inner instanceof WithValueType), "put .withValue last"); + assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); this.inner = inner; this.modifier = modifier; } @@ -805,6 +855,9 @@ export class OptionalType extends Type { get takesUndefined(): boolean { return true; } + get isOptional(): boolean { + return true; + } get valueKinds(): ValueKinds { if (this.inner.valueKinds === "any" || this.modifier === "falsyAsNull") return "any"; return [...this.inner.valueKinds, "undefined", ...(this.modifier === "nullable" ? (["null"] as const) : [])]; @@ -857,7 +910,7 @@ export class OptionalType extends Type { const inner = this.inner.rustFromExtern("value", scope); return inner === "value" ? `${expr}.get()` : `${expr}.get().map(|value| ${inner})`; } - if (this.inner instanceof Utf8Type || this.inner instanceof BytesType) return `${expr}.get_optional()`; + if (this.inner instanceof Utf8Type || this.inner instanceof BytesType) return `${expr}.get_optional(heap)`; return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.inner.rustFromExtern("value", scope)}) } }`; } rustResultFieldType(scope: RustScope): string { @@ -902,6 +955,12 @@ export class DefaultType extends Type { get takesUndefined(): boolean { return true; } + get isOptional(): boolean { + return true; + } + get conversionThrows(): boolean { + return this.inner.conversionThrows; + } get dependencies(): readonly Type[] { return [this.inner]; } @@ -963,7 +1022,13 @@ export class ArrayType extends Type { !element.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", ); + assert(!(element instanceof UnitType), "an array of nothing but null"); this.element = element; + const markConvertedByValue = (type: Type) => { + if (type instanceof DictionaryType) type.convertedByValue = true; + else if (!(type instanceof NamedType)) type.dependencies.forEach(markConvertedByValue); + }; + markConvertedByValue(element); } get idlType(): string { assert(!this.element.holdsJSValue, "an array argument cannot hold JS values: the GC does not scan it"); @@ -1055,11 +1120,14 @@ export class EnumerationType extends NamedType { new Set(this.variants).size === this.variants.length, `two values make the same identifier: ${this.variants.join(", ")}`, ); - for (const variant of this.variants) assert(/^[A-Z]/.test(variant), `${variant} is not an identifier`); + for (const variant of this.variants) assert(/^[A-Z]/.test(rustIdent(variant)), `${variant} is not an identifier`); } get variants(): string[] { return this.spellings.map(([canonical]) => pascalCase(canonical)); } + get valueKinds(): ValueKinds { + return ["string"]; + } get idlType(): string { return `WebCore::IDLEnumeration<${this.cpp}>`; } @@ -1138,11 +1206,17 @@ export class DictionaryType extends NamedType { /** Reachable from an argument or `fromJS` / from a return type. Set by the generator. */ accepted = false; returned = false; + /** An element of an array: everywhere else it is converted in place. */ + convertedByValue = false; constructor(members: Record, options: DictionaryOptions = {}) { super(); this.options = options; this.members = Object.entries(members).map(([name, spec]) => new Member(name, spec)); assert(this.members.length > 0, "a dictionary needs at least one member"); + assertDistinct( + this.members.map(member => member.fieldName), + "fields", + ); for (const { name, type } of this.members) assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); assert( @@ -1213,10 +1287,11 @@ export class DictionaryType extends NamedType { } toRust(value: unknown, scope: RustScope): string { const given = value as Record; - const fields = this.members - .filter(member => Object.hasOwn(given, member.name)) - .map(member => `${member.rustName}: ${member.type.toRust(given[member.name], scope)}, `); - return `${this.path(scope, this.name)} { ${fields.join("")}..Default::default() }`; + const fields = this.members.map(member => { + const field = member.type instanceof DefaultType ? member.type.value : undefined; + return `${member.rustName}: ${member.type.toRust(Object.hasOwn(given, member.name) ? given[member.name] : field, scope)}`; + }); + return `${this.path(scope, this.name)} { ${fields.join(", ")} }`; } layout(): Layout { return structLayout(this.members.map(member => member.type.layout())); @@ -1225,10 +1300,10 @@ export class DictionaryType extends NamedType { return this.path(scope, `Extern${this.name}`); } rustFromExtern(expr: string, scope: RustScope): string { - return `${this.path(scope, this.name)}::from_extern(&${expr})`; + return `${this.path(scope, this.name)}::from_extern(&${expr}${this.needsLifetime ? ", heap" : ""})`; } rustToJS(value: string, scope: RustScope): string { - return `${value}.to_js(heap)`; + return `${value}.into_js(heap)`; } } @@ -1239,6 +1314,10 @@ export class UnionType extends NamedType { super(); this.alternatives = Object.entries(alternatives).map(([name, type]) => ({ name, variant: pascalCase(name), type })); assert(this.alternatives.length > 0, "a union needs at least one alternative"); + assertDistinct( + this.alternatives.map(alternative => alternative.variant), + "variants", + ); assert( !this.borrowsHeap, "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", @@ -1248,6 +1327,7 @@ export class UnionType extends NamedType { assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); + assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); assert( !(type instanceof RangeType), @@ -1287,16 +1367,25 @@ export class UnionType extends NamedType { get dependencies(): readonly Type[] { return this.alternatives.map(a => a.type); } - private unitAlternative(value: unknown) { - const unit = value == null && this.alternatives.find(a => a.type instanceof UnitType && takes(a.type, `${value}`)); - assert(unit, `${this.tsType}: the default of a union is null or undefined, and it needs b.null as an alternative`); - return unit; + /** The alternative `null` or `undefined` goes to: `b.null` or `b.any`. */ + private alternativeOfAbsent(value: unknown) { + const alternative = value == null && this.alternatives.find(a => takes(a.type, `${value}`)); + assert( + alternative && + (alternative.type instanceof UnitType || (alternative.type instanceof AnyType && value === undefined)), + `${this.tsType}: the default of a union is null or undefined, and it needs b.null as an alternative`, + ); + return alternative; } cppAssign(target: string, value: unknown): string { - return `${target}.tag = ${this.alternatives.indexOf(this.unitAlternative(value))};`; + const alternative = this.alternativeOfAbsent(value); + const tag = `${target}.tag = ${this.alternatives.indexOf(alternative)};`; + if (alternative.type instanceof UnitType) return tag; + return `${alternative.type.cppAssign(`${target}.${alternative.name}`, value)}\n${tag}`; } toRust(value: unknown, scope: RustScope): string { - return `${this.path(scope, this.name)}::${this.unitAlternative(value).variant}`; + const { type, variant } = this.alternativeOfAbsent(value); + return `${this.path(scope, this.name)}::${variant}${type instanceof UnitType ? "" : `(${type.toRust(value, scope)})`}`; } layout(): Layout { return taggedUnionLayout(this.alternatives.map(a => a.type.layout())); @@ -1305,7 +1394,7 @@ export class UnionType extends NamedType { return this.path(scope, `Extern${this.name}`); } rustFromExtern(expr: string, scope: RustScope): string { - return `${this.path(scope, this.name)}::from_extern(&${expr})`; + return `${this.path(scope, this.name)}::from_extern(&${expr}${this.needsLifetime ? ", heap" : ""})`; } } @@ -1340,11 +1429,14 @@ export class Signature { this.capability = options.capability ?? "none"; this.throws = options.throws ?? false; this.rustPath = options.rustPath; - let optional = false; - for (const { name, type } of this.args) { - if (type.takesUndefined) optional = true; - else assert(!optional, `required argument ${name} follows an optional one`); - } + assertDistinct( + this.args.map(arg => snakeCase(arg.name)), + "arguments", + ); + assert( + this.length === undefined || (Number.isInteger(this.length) && this.length >= 0), + `length: ${this.length} is not a count`, + ); for (const { name, type } of this.args) { assert( !type.borrowsHeap || this.capability !== "js", @@ -1357,8 +1449,9 @@ export class Signature { 'returning a JS value takes capability: "heap" or "js"', ); } + /** The arguments up to the last that is not optional: what a caller writes at least. */ get requiredCount(): number { - return this.args.filter(arg => !arg.type.takesUndefined).length; + return this.args.findLastIndex(arg => !arg.type.isOptional) + 1; } } @@ -1373,6 +1466,7 @@ export class FunctionDefinition { constructor(signatures: readonly SignatureOptions[]) { assert(signatures.length > 0, "b.overloads needs at least one signature"); this.signatures = signatures.map(options => new Signature(options)); + assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once"); const nodes = new Map(); this.dispatch = this.dispatchOn(this.signatures, 0, nodes); const chosen = new Set(nodes.values()); @@ -1453,7 +1547,7 @@ export interface ClassOptions { /** Properties of the prototype. */ proto?: Record; /** Properties of the constructor. */ - static?: Record; + static?: Record>; /** JS values the object holds. The GC visits them; the wrapper gets `x_field` / `set_x_field` for each. */ internalFields?: readonly string[]; /** `false`: prototype properties are non-configurable. */ @@ -1486,9 +1580,8 @@ export class Getter { readonly capability: Capability; readonly throws: boolean; readonly hasSetter: boolean; - /** Index of the internal field a cached value lives in, or -1. */ readonly cached: boolean; - /** Of a cached one. Set by its class. */ + /** Where a cached one keeps the value. Set by its class. */ internalFieldIndex = -1; constructor(name: string, options: GetterField) { this.name = name; @@ -1497,6 +1590,10 @@ export class Getter { this.throws = options.throws ?? false; this.hasSetter = options.setter ?? false; assert(!(options.cache && options.setter), `${name}: a cached getter has no setter`); + assert( + !this.type.holdsJSValue || this.capability !== "none", + `${name}: returning a JS value takes capability: "heap" or "js"`, + ); this.cached = !!options.cache; } get rustName(): string { @@ -1509,7 +1606,9 @@ class SelfType extends Type { readonly idlType = ""; readonly tsType = "this"; toCpp(): string { - throw new TypeError("b.self is only for the arguments and results of a class's own members"); + throw new TypeError( + "b.self is an argument, a result or a getter of the class's own members, as it is: not .nullable, not in an array", + ); } layout(): Layout { return this.toCpp() as never; @@ -1552,8 +1651,10 @@ export class ClassType extends NamedType { const builtins: { name: string; length: number }[] = []; for (const [name, field] of Object.entries(options.proto ?? {})) { if ("builtin" in field) builtins.push({ name, length: field.length }); - else if ("getter" in field) getters.push(new Getter(name, field)); - else methods.push(new Method(name, resolve(field))); + else if ("getter" in field) { + assert(!("args" in field || "returns" in field), `${name}: a getter has no args or returns`); + getters.push(new Getter(name, { ...field, getter: own(field.getter) })); + } else methods.push(new Method(name, resolve(field))); } this.methods = methods; this.getters = getters; @@ -1563,13 +1664,24 @@ export class ClassType extends NamedType { ); assert(this.construct || this.staticMethods.length === 0, "static members need a constructor object"); this.internalFields = options.internalFields ?? []; + for (const field of this.internalFields) + assert(/^[a-z][A-Za-z0-9]*$/.test(field), `${field}: an internal field is named in camelCase`); + assertDistinct( + [ + "constructor", + "get_constructor", + ...[...this.methods, ...this.staticMethods].map(method => method.rustName), + ...getters.flatMap(getter => [getter.rustName, ...(getter.hasSetter ? [`set_${snakeCase(getter.name)}`] : [])]), + ], + "members' Rust functions", + ); + assertDistinct(this.internalFields.map(snakeCase), "internal fields"); this.cachedGetters = getters.filter(getter => getter.cached); this.cachedGetters.forEach((getter, index) => (getter.internalFieldIndex = this.internalFields.length + index)); this.configurable = options.configurable ?? true; for (const method of this.methods) { assert( - method.receiver !== "mut" || - !method.args.some(arg => arg.type.dependencies.includes(this) || arg.type === this), + method.receiver !== "mut" || !method.args.some(arg => arg.type === this), `${method.name}: &mut self and an argument of the same class may be the same object`, ); } @@ -1613,7 +1725,7 @@ export class ClassType extends NamedType { return `bindgen::JsWrapper<'a, ${this.rustPath}>`; } rustFromExtern(expr: string): string { - return `bindgen::JsWrapper::<${this.rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(${expr}))`; + return `bindgen::JsWrapper::<${this.rustPath}>::from_extern(core::ptr::NonNull::new_unchecked(${expr}), heap)`; } rustToJS(value: string): string { return `bindgen::to_js::any(heap, ${value}.as_value())`; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index b936a05f2d63..1ae732912544 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -41,7 +41,7 @@ export const string = new StringType(); /** The JS value itself, as a `JsLocal<'a>`. */ export const any = new AnyType(); -/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, ArrayBuffer>`. `.bytes`, `.arrayBuffer`. */ +/** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, BufferSource>`. `.bytes`, `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); /** The native `Blob` behind a JS `Blob`. */ @@ -51,6 +51,7 @@ export const blob = new RefType( "bun_jsc::webcore_types::Blob", "Bindgen/IDLConvertBlob.h", [{ cpp: "WebCore::JSBlob" }], + "detached Blobs", ); const null_ = new UnitType("null"); diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index 5e49697e8602..80bc8cf72b35 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -31,7 +31,10 @@ async function load(source: string): Promise { const stem = path.basename(file, ".bind.ts"); const origin: Origin = { stem, namespace: pascalCase(stem), rustModule: snakeCase(stem), crate }; const result: SchemaFile = { origin, moduleDefinition: null, types: [], functions: [] }; - for (const [name, value] of Object.entries((await import(pathToFileURL(file).href)) as Record)) { + const exports: Record = await import(pathToFileURL(file).href).catch(error => { + throw new Error(`${file}: ${error.message}`, { cause: error }); + }); + for (const [name, value] of Object.entries(exports)) { if (value instanceof ModuleDefinition) { assert(name === "default", `${stem}.bind.ts: b.module() is the default export`); result.moduleDefinition = value; @@ -87,9 +90,17 @@ for (const file of files) { } const tables = analyze(files); +/** What a schema is found to get wrong only as text is made of it. */ +function emit(file: SchemaFile, text: (file: SchemaFile) => T): T { + try { + return text(file); + } catch (error) { + throw new Error(`${file.origin.stem}.bind.ts: ${(error as Error).message}`, { cause: error }); + } +} for (const file of files) { - writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.h`), cppHeader(file)); - writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.cpp`), cppSource(file)); + writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.h`), emit(file, cppHeader)); + writeIfNotChanged(path.join(codegenPath, `Generated${file.origin.namespace}.cpp`), emit(file, cppSource)); } writeIfNotChanged(path.join(codegenPath, "GeneratedBindings+Tables.h"), cppTables(tables)); writeIfNotChanged(path.join(codegenPath, "GeneratedBindings+Counts.h"), cppCounts(tables)); diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 6206d2f6a4bd..fa6de0bb5c6a 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -242,7 +242,7 @@ export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFramework }; }; -// Only in a build with assertions: `hasBindings` in scripts/build/codegen.ts. +// Only in a build with assertions: `isCompiled` in scripts/build/codegen.ts. export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { add: $bindgen("bindgen_test.bind.ts", "add"), requiredAndOptionalArg: $bindgen("bindgen_test.bind.ts", "requiredAndOptionalArg"), diff --git a/src/jsc/NodeModuleModule.rs b/src/jsc/NodeModuleModule.rs index c6873982fda1..e172f76a747d 100644 --- a/src/jsc/NodeModuleModule.rs +++ b/src/jsc/NodeModuleModule.rs @@ -100,7 +100,7 @@ fn find_path_inner( .ok()) } -pub fn stat(path: &[u8]) -> i32 { +pub(crate) fn stat(path: &[u8]) -> i32 { // PERF: `exists_at_type` // takes a `&ZStr`, so we copy into a NUL-terminated heap buffer here. let zpath = bun_core::ZBox::from_bytes(path); diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 535d800b2a82..4361b15d8b1f 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -78,7 +78,8 @@ impl<'a> JsHeap<'a> { impl<'a> JsScope<'a> { /// # Safety - /// `global` is this thread's live global object and the caller was entered by the engine or the event loop. + /// `global` is this thread's live global object, JavaScript may run here (the engine or the event loop entered the + /// caller, and not to finalize or visit a cell), and nothing borrowed through another `JsScope` is still in use. #[inline(always)] pub unsafe fn with( global: *mut JSGlobalObject, @@ -179,6 +180,18 @@ impl<'a> IntoError<'a> for bun_sys::Error { Error::unthrown(crate::SysErrorJsc::to_js(&self, heap.global())) } } +impl<'a> IntoError<'a> for crate::SystemError { + #[cold] + fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { + Error::unthrown(self.to_error_instance(heap.global())) + } +} +impl<'a, E: IntoError<'a>> IntoError<'a> for Box { + #[cold] + fn into_error(self, heap: &JsHeap<'a>) -> Error<'a> { + (*self).into_error(heap) + } +} impl<'a> IntoError<'a> for core::convert::Infallible { fn into_error(self, _: &JsHeap<'a>) -> Error<'a> { match self {} @@ -210,10 +223,10 @@ impl<'a, T> JsLocal<'a, T> { } /// # Safety - /// `value` is a live, non-empty `T` belonging to the call `'a`. + /// `value` is a non-empty `T` that something holds for the call `heap` belongs to. #[doc(hidden)] #[inline(always)] - pub unsafe fn from_extern(value: JSValue) -> Self { + pub unsafe fn from_extern(value: JSValue, _: &JsHeap<'a>) -> Self { Self::from_raw(value) } @@ -238,7 +251,7 @@ impl<'a> JsLocal<'a> { pub const UNDEFINED: Self = JsLocal(JSValue::UNDEFINED, PhantomData, PhantomData); pub const NULL: Self = JsLocal(JSValue::NULL, PhantomData, PhantomData); - /// `value` is alive now, so it is for the rest of the call that `heap` belongs to. + /// Where a [`JSValue`] of the older API comes in: one this call was given or made, not one kept from an earlier call. #[inline(always)] pub fn new(_: &JsHeap<'a>, value: JSValue) -> Self { debug_assert!(!value.is_empty()); @@ -312,7 +325,7 @@ pub unsafe trait JsPayload: Sized + 'static { /// A new JS object that owns `self`. #[inline] - fn to_js<'a>(self, heap: &JsHeap<'a>) -> JsWrapper<'a, Self> { + fn into_js<'a>(self, heap: &JsHeap<'a>) -> JsWrapper<'a, Self> { // SAFETY: a null structure is always valid. unsafe { JsWrapper::create_with_structure(heap, core::ptr::null_mut(), self) } } @@ -335,10 +348,10 @@ impl Copy for JsWrapper<'_, T> {} impl<'a, T: JsPayload> JsWrapper<'a, T> { /// # Safety - /// `cell` is a live wrapper of `T` belonging to the call `'a`. + /// `cell` is a wrapper of `T` that something holds for the call `heap` belongs to. #[doc(hidden)] #[inline(always)] - pub unsafe fn from_extern(cell: NonNull) -> Self { + pub unsafe fn from_extern(cell: NonNull, _: &JsHeap<'a>) -> Self { JsWrapper(cell, PhantomData, PhantomData) } @@ -353,7 +366,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { ) -> Self { // SAFETY: nothing runs between the allocation and the write, which cannot panic. unsafe { - let this = Self::from_extern(T::create_cell(heap.global_ptr(), structure)); + let this = Self::from_extern(T::create_cell(heap.global_ptr(), structure), heap); this.payload().write(payload); this } @@ -391,7 +404,7 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { /// Lends the payload. `f` cannot run JavaScript: that takes the `&mut JsScope` the `JsPayloads` is borrowed from. #[inline(always)] pub fn with(self, _: &JsPayloads<'a>, f: impl FnOnce(&T) -> R) -> R { - // SAFETY: every `&mut T` holds `JsPayloads` exclusively. + // SAFETY: a `&mut T` is lent by `with_mut`, which holds `JsPayloads` exclusively, or to a body that has no `JsPayloads`. let result = f(unsafe { &*self.payload() }); self.as_value().0.ensure_still_alive(); result @@ -454,10 +467,13 @@ impl<'a, T: JsPayload> JsWrapper<'a, T> { pub struct JsWrapperStrong(Strong, PhantomData<*const T>); impl JsWrapperStrong { #[inline] - pub fn get<'a>(&self, _: &JsHeap<'a>) -> JsWrapper<'a, T> { + pub fn get<'a>(&self, heap: &JsHeap<'a>) -> JsWrapper<'a, T> { // SAFETY: made from a `JsWrapper`; a cell value is its pointer. unsafe { - JsWrapper::from_extern(NonNull::new_unchecked(self.0.get().encoded() as *mut c_void)) + JsWrapper::from_extern( + NonNull::new_unchecked(self.0.get().encoded() as *mut c_void), + heap, + ) } } } @@ -481,10 +497,10 @@ pub mod to_js { u8: u8 => |n| JSValue::js_number_from_int32(i32::from(n)); u16: u16 => |n| JSValue::js_number_from_int32(i32::from(n)); i32: i32 => JSValue::js_number_from_int32; - u32: u32 => |n| JSValue::js_number(f64::from(n)); + u32: u32 => |n| JSValue::js_number_from_uint64(u64::from(n)); u64: u64 => JSValue::js_number_from_uint64; usize: usize => |n| JSValue::js_number_from_uint64(n as u64); - f64: f64 => JSValue::js_number; + f64: f64 => |n| JSValue::js_number(JSValue::purify_nan(n)); } #[inline(always)] @@ -566,14 +582,17 @@ pub mod to_js { } /// An empty value is a member that is absent. + /// + /// # Safety + /// `values` are the members of `Tables::objectStructures[structure_index]`. #[doc(hidden)] #[inline] - pub fn object_with_structure<'a>( + pub unsafe fn object_with_structure<'a>( heap: &JsHeap<'a>, structure_index: u32, values: &[JSValue], ) -> Result<'a, JsLocal<'a>> { - // SAFETY: `values` is on the caller's stack, where the GC sees it; this cannot throw. + // SAFETY: forwarded; this cannot throw. Ok(JsLocal::from_raw(unsafe { extern_types::Bun__Bindgen__createObject( heap.global_ptr(), @@ -584,10 +603,12 @@ pub mod to_js { })) } + /// # Safety + /// `index` is one into `Tables::enumerationValues`. #[doc(hidden)] #[inline] - pub fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsLocal<'a>> { - // SAFETY: a generated index; this cannot throw. + pub unsafe fn enumeration_value<'a>(heap: &JsHeap<'a>, index: u32) -> Result<'a, JsLocal<'a>> { + // SAFETY: forwarded; this cannot throw. Ok(JsLocal::from_raw(unsafe { extern_types::Bun__Bindgen__enumerationValueToJS(heap.global_ptr(), index) })) @@ -601,12 +622,20 @@ pub fn host_fn_return<'a, T, E: IntoError<'a>>( result: core::result::Result, convert: impl FnOnce(&JsHeap<'a>, T) -> Result<'a, JsLocal<'a>>, ) -> JSValue { - let error = match result { - Ok(value) => match convert(&cx.heap, value) { - Ok(value) => return value.0, - Err(error) => error, - }, - Err(error) => error.into_error(&cx.heap), + let value = 'value: { + let error = match result { + Ok(value) => match convert(&cx.heap, value) { + Ok(value) => break 'value value.0, + Err(error) => error, + }, + Err(error) => error.into_error(&cx.heap), + }; + error.into_host_fn_return(cx) }; - error.into_host_fn_return(cx) + debug_assert_eq!( + value.is_empty(), + cx.heap.global().has_exception(), + "a body fails exactly when an exception is pending", + ); + value } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 7750b0b0e997..7602faf5efbc 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -2,6 +2,7 @@ use core::ffi::{c_uint, c_void}; +use super::JsHeap; use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; /// `WTF::StringImpl*` carrying one ref, or null. @@ -91,21 +92,21 @@ pub struct ExternBytes { } impl ExternBytes { /// # Safety - /// `'a` ends before the host function that passed this returns. + /// The host function that passed this is the call `heap` belongs to. #[inline(always)] - pub unsafe fn get<'a>(self) -> &'a [u8] { + pub unsafe fn get<'a>(self, _: &JsHeap<'a>) -> &'a [u8] { // SAFETY: C++ never passes a null pointer for a present value. unsafe { core::slice::from_raw_parts(self.data, self.length) } } /// # Safety /// As [`get`](Self::get). #[inline(always)] - pub unsafe fn get_optional<'a>(self) -> Option<&'a [u8]> { + pub unsafe fn get_optional<'a>(self, heap: &JsHeap<'a>) -> Option<&'a [u8]> { if self.data.is_null() { return None; } // SAFETY: forwarded. - Some(unsafe { self.get() }) + Some(unsafe { self.get(heap) }) } } diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index be1942c685bf..082152268c70 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -184,14 +184,18 @@ export const TestInner = b.dictionary({ name: b.string.utf8.default("inner"), }); export const TestChoice = b.union({ none: b.null, flag: b.bool }); +/** A string is a color. What `bytes` refuses, and everything else, is kept as it is. */ +export const TestColorOrRest = b.union({ color: TestColor, bytes: b.bufferSource, rest: b.any }); export const TestOuter = b.dictionary({ inner: TestInner.nullable, value: b.any, choice: TestChoice, + rest: TestColorOrRest, counter: TestCounter.nullable, }); /** What a default written in the schema gives each member, the nested ones included. */ export const describeDefaults = b.fn({ args: { options: TestOuter.default({ inner: {} }) }, returns: b.string.utf8, + capability: "heap", }); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index e1c3de004e1e..8acc7e313320 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -7,18 +7,18 @@ use crate::bindgen::{ JsWrapperStrong, WithValue, }; use crate::generated_bindgen::bindgen_test::{ - TestAnything, TestChoice, TestColor, TestCounterClass, TestCounterInternalFields as _, - TestFailure, TestHolderClass, TestNames, TestOptions, TestOptionsEcho, TestOuter, - TestTokenClass, TestValue, + TestAnything, TestChoice, TestColor, TestColorOrRest, TestCounterClass, + TestCounterInternalFields as _, TestFailure, TestHolderClass, TestNames, TestOptions, + TestOptionsEcho, TestOuter, TestTokenClass, TestValue, }; // This example should be kept in sync with bindgen's documentation -pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { +pub(crate) 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"))) } -pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { +pub(crate) fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { let Some(b_nonnull) = b else { return 123456i32 .wrapping_add(c) @@ -36,27 +36,27 @@ pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option &[u8] { +pub(crate) fn utf8_echo(text: &[u8]) -> &[u8] { text } -pub fn utf8_echo_loose(text: &[u8]) -> &[u8] { +pub(crate) fn utf8_echo_loose(text: &[u8]) -> &[u8] { text } -pub fn utf8_optional(text: Option<&[u8]>) -> Option<&[u8]> { +pub(crate) fn utf8_optional(text: Option<&[u8]>) -> Option<&[u8]> { text } -pub fn utf8_owned(text: bun_core::Utf8Bytes<'static>) -> bun_core::Utf8Bytes<'static> { +pub(crate) fn utf8_owned(text: bun_core::Utf8Bytes<'static>) -> bun_core::Utf8Bytes<'static> { text } -pub fn string_echo(text: bun_core::String) -> bun_core::String { +pub(crate) fn string_echo(text: bun_core::String) -> bun_core::String { text } -pub fn next_color(color: TestColor) -> TestColor { +pub(crate) fn next_color(color: TestColor) -> TestColor { match color { TestColor::Red => TestColor::Green, TestColor::Green => TestColor::Blue, @@ -64,7 +64,7 @@ pub fn next_color(color: TestColor) -> TestColor { } } -pub fn options_echo<'a>(_: &JsHeap<'a>, options: TestOptions<'a>) -> TestOptionsEcho<'a> { +pub(crate) fn options_echo<'a>(_: &JsHeap<'a>, options: TestOptions<'a>) -> TestOptionsEcho<'a> { TestOptionsEcho { count: options.count, flag: options.flag, @@ -76,7 +76,7 @@ pub fn options_echo<'a>(_: &JsHeap<'a>, options: TestOptions<'a>) -> TestOptions } } -pub fn describe_value(value: TestValue<'_>) -> Vec { +pub(crate) fn describe_value(value: TestValue<'_>) -> Vec { match value { TestValue::Number(number) => format!("number {number}").into_bytes(), TestValue::Flag(flag) => format!("flag {flag}").into_bytes(), @@ -85,7 +85,7 @@ pub fn describe_value(value: TestValue<'_>) -> Vec { } } -pub fn describe_anything<'a>(cx: &mut JsScope<'a>, value: TestAnything<'a>) -> Vec { +pub(crate) fn describe_anything<'a>(cx: &mut JsScope<'a>, value: TestAnything<'a>) -> Vec { match value { TestAnything::Nothing => b"nothing".to_vec(), TestAnything::Flag(flag) => format!("flag {flag}").into_bytes(), @@ -110,7 +110,7 @@ pub fn describe_anything<'a>(cx: &mut JsScope<'a>, value: TestAnything<'a>) -> V } } -pub fn describe_defaults(options: TestOuter<'_>) -> Vec { +pub(crate) fn describe_defaults<'a>(heap: &JsHeap<'a>, options: TestOuter<'a>) -> Vec { let inner = match options.inner { Some(inner) => { let name = bstr::BStr::new(inner.name); @@ -127,15 +127,22 @@ pub fn describe_defaults(options: TestOuter<'_>) -> Vec { TestChoice::None => "none".to_owned(), TestChoice::Flag(flag) => flag.to_string(), }; + let rest = match options.rest { + TestColorOrRest::Color(color) => format!("{color:?}"), + TestColorOrRest::Bytes(bytes) => bytes.with_bytes(heap, |bytes| format!("{bytes:?}")), + TestColorOrRest::Rest(rest) if rest.is_undefined() => "undefined".to_owned(), + TestColorOrRest::Rest(_) => "given".to_owned(), + }; let counter = options.counter.is_some(); - format!("inner: {inner}; value: {value}; choice: {choice}; counter: {counter}").into_bytes() + format!("inner: {inner}; value: {value}; choice: {choice}; rest: {rest}; counter: {counter}") + .into_bytes() } -pub fn overloaded_text_times(text: &[u8], times: u32) -> Vec { +pub(crate) fn overloaded_text_times(text: &[u8], times: u32) -> Vec { [b"text times ", text.repeat(times as usize).as_slice()].concat() } -pub fn overloaded_text_upper(text: &[u8], upper: bool) -> Vec { +pub(crate) fn overloaded_text_upper(text: &[u8], upper: bool) -> Vec { let text = if upper { text.to_ascii_uppercase() } else { @@ -144,19 +151,19 @@ pub fn overloaded_text_upper(text: &[u8], upper: bool) -> Vec { [b"text upper ", text.as_slice()].concat() } -pub fn overloaded_two_numbers(a: f64, b: f64) -> Vec { +pub(crate) fn overloaded_two_numbers(a: f64, b: f64) -> Vec { format!("two numbers {a} {b}").into_bytes() } -pub fn overloaded_number(number: f64) -> Vec { +pub(crate) fn overloaded_number(number: f64) -> Vec { format!("number {number}").into_bytes() } -pub fn overloaded_list(list: Vec) -> Vec { +pub(crate) fn overloaded_list(list: Vec) -> Vec { format!("list {:?}", list.into_boxed_slice()).into_bytes() } -pub fn overloaded_counter<'a>( +pub(crate) fn overloaded_counter<'a>( cx: &mut JsScope<'a>, counter: JsWrapper<'a, TestCounter>, ) -> Vec { @@ -164,17 +171,17 @@ pub fn overloaded_counter<'a>( format!("counter {count}").into_bytes() } -pub fn sum_array(numbers: Vec) -> f64 { +pub(crate) fn sum_array(numbers: Vec) -> f64 { numbers .into_iter() .fold(0.0, |sum, number| sum + f64::from(number)) } -pub fn range(length: u32) -> core::ops::Range { +pub(crate) fn range(length: u32) -> core::ops::Range { 0..length } -pub fn names(text: &[u8]) -> TestNames { +pub(crate) fn names(text: &[u8]) -> TestNames { let all: Vec> = words(text).map(<[u8]>::to_vec).collect(); TestNames { first: all.first().cloned().unwrap_or_default(), @@ -183,7 +190,7 @@ pub fn names(text: &[u8]) -> TestNames { } } -pub fn words(text: &[u8]) -> impl Iterator { +pub(crate) fn words(text: &[u8]) -> impl Iterator { bun_core::strings::tokenize(text, b" ") } @@ -191,19 +198,19 @@ fn sum(bytes: &[u8]) -> u32 { bytes.iter().map(|&byte| u32::from(byte)).sum() } -pub fn sum_bytes(bytes: &[u8]) -> u32 { +pub(crate) fn sum_bytes(bytes: &[u8]) -> u32 { sum(bytes) } -pub fn sum_bytes_optional(bytes: Option<&[u8]>) -> Option { +pub(crate) fn sum_bytes_optional(bytes: Option<&[u8]>) -> Option { bytes.map(sum) } -pub fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { +pub(crate) fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { sum(bytes) + sum(text) } -pub fn sum_before_and_after_callback<'a>( +pub(crate) fn sum_before_and_after_callback<'a>( cx: &mut JsScope<'a>, buffer: JsLocal<'a, BufferSource>, callback: JsLocal<'a>, @@ -218,14 +225,14 @@ pub fn sum_before_and_after_callback<'a>( Ok([before, buffer.with_bytes(&cx.heap, sum)]) } -pub fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsLocal<'a> { +pub(crate) fn with_value_echo<'a>(_: &JsHeap<'a>, text: WithValue<'a, &'a [u8]>) -> JsLocal<'a> { if text.converted.is_empty() { return JsLocal::UNDEFINED; } text.value } -pub fn fail<'a>( +pub(crate) fn fail<'a>( heap: &JsHeap<'a>, how: TestFailure, value: JsLocal<'a>, @@ -243,7 +250,7 @@ pub fn fail<'a>( static ALIVE: AtomicUsize = AtomicUsize::new(0); -pub struct TestCounter { +pub(crate) struct TestCounter { count: i32, label: Vec, } @@ -316,7 +323,7 @@ impl TestCounterClass for TestCounter { } fn starting<'a>(heap: &JsHeap<'a>, at: i32) -> JsWrapper<'a, Self> { - Self::new(at).to_js(heap) + Self::new(at).into_js(heap) } fn alive() -> usize { @@ -324,7 +331,7 @@ impl TestCounterClass for TestCounter { } } -pub struct TestHolder { +pub(crate) struct TestHolder { counter: JsWrapperStrong, } @@ -345,7 +352,7 @@ impl TestHolderClass for TestHolder { } } -pub struct TestToken { +pub(crate) struct TestToken { id: u32, } @@ -355,6 +362,6 @@ impl TestTokenClass for TestToken { } } -pub fn make_token<'a>(heap: &JsHeap<'a>, id: u32) -> JsWrapper<'a, TestToken> { - TestToken { id }.to_js(heap) +pub(crate) fn make_token<'a>(heap: &JsHeap<'a>, id: u32) -> JsWrapper<'a, TestToken> { + TestToken { id }.into_js(heap) } diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp index cd55f05f0195..b1205391d4ed 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp @@ -2,20 +2,29 @@ #include "Bindgen/DictionaryMemberReader.h" #include "ObjectBindings.h" #include -#include namespace Bun::Bindgen { using namespace JSC; +// `get` stops before Object.prototype. +static ALWAYS_INLINE bool canSkipPrototypeChain(JSGlobalObject* globalObject, JSObject* object) +{ + if (object->type() != FinalObjectType) + return false; + JSValue prototype = object->getPrototypeDirect(); + return prototype == globalObject->objectPrototype() || prototype.isNull(); +} + JSValue DictionaryMemberReader::get(unsigned index) { - if (canSkipPrototypeChain()) [[likely]] { + // Asked per member: converting one can run JavaScript. + if (canSkipPrototypeChain(m_globalObject, m_object)) [[likely]] { unsigned attributes = 0; - PropertyOffset offset = m_object->structure()->get(m_vm, m_memberNames[index], attributes); - if (offset == invalidOffset) + JSValue value = m_object->getDirect(m_vm, m_memberNames[index], attributes); + if (!value) return jsUndefined(); if (!(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) [[likely]] - return m_object->getDirect(offset); + return value; } return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); } diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index ecea97954cb2..34674efd16a1 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -20,18 +20,6 @@ class DictionaryMemberReader { NEVER_INLINE JSC::JSValue get(unsigned index); private: - // `get` stops before Object.prototype. Asked per member: converting one can run JavaScript. - ALWAYS_INLINE bool canSkipPrototypeChain() const - { - if (m_object->type() != JSC::FinalObjectType) - return false; - JSC::Structure* structure = m_object->structure(); - if (structure->hasPolyProto()) - return false; - JSC::JSValue prototype = structure->storedPrototype(); - return prototype == m_globalObject->objectPrototype() || prototype.isNull(); - } - JSC::VM& m_vm; JSC::JSGlobalObject* m_globalObject; JSC::JSObject* m_object; diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 0b7a25951cbf..7687ec2859a8 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -7,7 +7,7 @@ #include #include #include -#include "JSStringUTF8View.h" +#include "Bindgen/JSStringUTF8View.h" namespace Bun::Bindgen { diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 87884bc252ee..771db7cc58c5 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -25,6 +25,11 @@ struct IDLHumanReadableName> : BaseIDLHumanReadableName { = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); }; +// A union with this alternative takes every value, so nothing reports it. +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("any value"); +}; + template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); }; @@ -37,7 +42,6 @@ template<> struct IDLHumanReadableName : BaseIDLHu static constexpr auto humanReadableName = std::to_array("string"); }; template<> struct IDLHumanReadableName : IDLHumanReadableName {}; -template<> struct IDLHumanReadableName : IDLHumanReadableName {}; template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { static constexpr auto humanReadableName = std::to_array("ArrayBuffer or ArrayBufferView"); }; @@ -310,6 +314,6 @@ template<> struct WebCore::Converter JSC::ThrowScope& scope, Ctx& ctx) { - ctx.throwNotBufferSource(globalObject, scope); + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); } }; diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h index f593631a2c6a..76efbce3c2c2 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -3,6 +3,9 @@ #include "Bindgen/IDLTypes.h" #include "ErrorCode.h" #include "JSDOMConvertBase.h" +#include +#include +#include namespace Bun::Bindgen { @@ -45,7 +48,7 @@ struct WebCore::Converter> : WebCo return 0; } double number = value.asNumber(); - if (!std::isfinite(number) || number != std::round(number)) { + if (!JSC::isInteger(number)) { Bun::ERR::OUT_OF_RANGE(scope, &globalObject, name, "an integer"_s, value); return 0; } diff --git a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h index 53f309394745..e1d2c82283cc 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h @@ -1,7 +1,8 @@ #pragma once #include "Bindgen/IDLTypes.h" #include "BunIDLConvertBase.h" -#include +#include +#include #include #include #include @@ -71,7 +72,6 @@ struct WebCore::Converter> { static constexpr auto minInt = std::numeric_limits::min(); static constexpr auto maxInt = std::numeric_limits::max(); - static constexpr auto maxSafeInteger = 9007199254740991LL; auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -102,7 +102,7 @@ struct WebCore::Converter> double number = value.asNumber(); // Written so that NaN is caught here: casting it to an integer is undefined. - if (!(number <= maxSafeInteger && number >= -maxSafeInteger)) { + if (!(number <= JSC::maxSafeInteger() && number >= JSC::minSafeInteger())) { ctx.throwNumberNotInteger(globalObject, scope, number); return {}; } @@ -111,7 +111,7 @@ struct WebCore::Converter> ctx.throwNumberNotInteger(globalObject, scope, number); return {}; } - if constexpr (maxInt >= static_cast(maxSafeInteger)) { + if constexpr (maxInt >= JSC::maxSafeIntegerAsUInt64()) { if (std::signed_integral || intVal >= 0) { return static_cast(intVal); } diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp index 013ed367844e..6840fb7bbeb4 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp @@ -1,6 +1,7 @@ #include "root.h" #include "Bindgen/JSStringUTF8View.h" #include +#include namespace Bun::Bindgen { using namespace JSC; @@ -25,9 +26,8 @@ bool JSStringUTF8View::convert() return true; } // simdutf refuses unpaired surrogates; each becomes U+FFFD. - auto lenient = view.utf8(WTF::ConversionMode::StrictConversionReplacingUnpairedSurrogatesWithFFFD); - m_converted.shrink(0); - return m_converted.tryAppend(byteCast(lenient.span())); + m_converted.shrink(WTF::Unicode::convertReplacingInvalidSequences(span, byteCast(m_converted.mutableSpan())).buffer.size()); + return true; } } diff --git a/src/jsc/bindings/Bindgen/JSWrapper.cpp b/src/jsc/bindings/Bindgen/JSWrapper.cpp index 67a6d5a4826e..68af1c2966de 100644 --- a/src/jsc/bindings/Bindgen/JSWrapper.cpp +++ b/src/jsc/bindings/Bindgen/JSWrapper.cpp @@ -7,6 +7,10 @@ namespace Bun::Bindgen { using namespace JSC; +// It shares a subspace with the constructors of `.classes.ts` classes. +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSClassConstructor, InternalFunction); +STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSClassConstructor); + const ClassInfo JSClassConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSClassConstructor) }; static JSC_DECLARE_HOST_FUNCTION(callJSClassConstructor); @@ -29,7 +33,9 @@ JSClassConstructor* JSClassConstructor::create(VM& vm, JSGlobalObject* globalObj auto* structure = createStructure(vm, globalObject, globalObject->functionPrototype()); auto* constructor = new (NotNull, allocateCell(vm)) JSClassConstructor(vm, structure, definition.construct); constructor->finishCreation(vm, definition.constructorLength, definition.info->className, PropertyAdditionMode::WithoutStructureTransition); - Bun::reifyStaticPropertyTable(vm, info(), definition.constructorTableValues, *constructor); + // Reifying makes the structure a dictionary, even for no properties. + if (!definition.constructorTableValues.empty()) + Bun::reifyStaticPropertyTable(vm, info(), definition.constructorTableValues, *constructor); constructor->putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); return constructor; } @@ -46,7 +52,7 @@ void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIn definition.finishPrototypeCreation(vm, init.global, prototype); Bun::putToStringTagWithoutTransition(vm, prototype, definition.info); init.setPrototype(prototype); - init.setStructure(definition.createStructure(vm, init.global, prototype)); + init.setStructure(Bun::createClassStructure(vm, init.global, prototype, TypeInfo(static_cast(WebCore::JSDOMWrapperType), JSDestructibleObject::StructureFlags), definition.info)); if (definition.construct) init.setConstructor(JSClassConstructor::create(vm, init.global, definition, prototype)); } @@ -54,13 +60,13 @@ void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIn Structure* structureForClass(JSGlobalObject* globalObject, unsigned classIndex) { auto* global = defaultGlobalObject(globalObject); - return global->m_bindgenAdditions.m_classes[classIndex].getInitializedOnMainThread(global); + return global->m_bindgenAdditions.m_classes[classIndex].get(global); } JSObject* constructorForClass(JSGlobalObject* globalObject, unsigned classIndex) { auto* global = defaultGlobalObject(globalObject); - return global->m_bindgenAdditions.m_classes[classIndex].constructorInitializedOnMainThread(global); + return global->m_bindgenAdditions.m_classes[classIndex].constructor(global); } Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFrame, unsigned classIndex) diff --git a/src/jsc/bindings/Bindgen/JSWrapper.h b/src/jsc/bindings/Bindgen/JSWrapper.h index 266815595846..bfec6088beb3 100644 --- a/src/jsc/bindings/Bindgen/JSWrapper.h +++ b/src/jsc/bindings/Bindgen/JSWrapper.h @@ -14,24 +14,35 @@ namespace Bun::Bindgen { template class JSWrapper; } +namespace JSC { +namespace JSCastingHelpers { + // A cell whose type is JSDOMWrapperType is a JSDestructibleObject, so its ClassInfo is in the cell: no Structure load. template -struct JSC::JSCastingHelpers::InheritsTraits> { +struct InheritsTraits> { static constexpr std::optional typeRange { std::nullopt }; template static inline bool inherits(From* from) { - return from->type() == static_cast(WebCore::JSDOMWrapperType) - && uncheckedDowncast(from)->classInfo() == Bun::Bindgen::JSWrapper::info(); + using To = Bun::Bindgen::JSWrapper; + if (from->type() != static_cast(WebCore::JSDOMWrapperType)) { + ASSERT(!from->JSCell::inheritsSlow(To::info())); + return false; + } + const ClassInfo* classInfo = uncheckedDowncast(from)->classInfo(); + ASSERT(classInfo == from->JSCell::classInfo()); + return classInfo == To::info(); } }; +} // namespace JSCastingHelpers +} // namespace JSC + namespace Bun::Bindgen { // One per class, defined by generated code. struct ClassDefinition { const JSC::ClassInfo* info; - JSC::Structure* (*createStructure)(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); std::span prototypeTableValues; std::span constructorTableValues; // Properties a static table cannot describe (private names). May be null. @@ -48,8 +59,6 @@ class JSWrapper final : public JSC::JSDestructibleObject { using Base = JSC::JSDestructibleObject; static constexpr unsigned StructureFlags = Base::StructureFlags; static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; - // A payload is not one of a few sizes shared with other cells, so a precise-allocation tier buys nothing. - static constexpr uint8_t numberOfLowerTierPreciseCells = 0; static constexpr unsigned numberOfInternalFields = Traits::numberOfInternalFields; DECLARE_INFO; @@ -71,11 +80,6 @@ class JSWrapper final : public JSC::JSDestructibleObject { nullptr); } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(static_cast(WebCore::JSDOMWrapperType), StructureFlags), info()); - } - // The payload is uninitialized: the caller writes it before anything else can run. static JSWrapper* create(JSC::VM& vm, JSC::Structure* structure) { @@ -86,6 +90,7 @@ class JSWrapper final : public JSC::JSDestructibleObject { static void destroy(JSC::JSCell* cell) { + STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSWrapper); Traits::finalize(static_cast(cell)->payload()); } @@ -94,7 +99,11 @@ class JSWrapper final : public JSC::JSDestructibleObject { static size_t allocationSize() { return offsetOfPayload() + Traits::payloadSize(); } void* payload() { return reinterpret_cast(this) + offsetOfPayload(); } - JSC::WriteBarrier& internalField(unsigned index) { return internalFields()[index]; } + JSC::WriteBarrier& internalField(unsigned index) + { + ASSERT(index < numberOfInternalFields); + return internalFields()[index]; + } std::span, numberOfInternalFields> internalFields() { return std::span, numberOfInternalFields> { reinterpret_cast*>(reinterpret_cast(this) + offsetOfInternalFields()), numberOfInternalFields }; @@ -112,6 +121,8 @@ class JSWrapper final : public JSC::JSDestructibleObject { field.setWithoutWriteBarrier(JSC::jsUndefined()); } } + + DECLARE_DEFAULT_FINISH_CREATION; }; template @@ -121,8 +132,7 @@ void JSWrapper::visitChildrenImpl(JSC::JSCell* cell, Visitor& visitor) auto* thisObject = uncheckedDowncast(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); - for (auto& field : thisObject->internalFields()) - visitor.append(field); + visitor.appendValues(thisObject->internalFields().data(), numberOfInternalFields); } DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template, JSWrapper); diff --git a/src/jsc/bindings/Bindgen/SchemaStrings.cpp b/src/jsc/bindings/Bindgen/SchemaStrings.cpp index 38bc67803647..dc1954a17a03 100644 --- a/src/jsc/bindings/Bindgen/SchemaStrings.cpp +++ b/src/jsc/bindings/Bindgen/SchemaStrings.cpp @@ -40,7 +40,7 @@ template void SchemaStrings::visit(SlotVisitor&); SchemaStrings& schemaStrings(VM& vm) { - return static_cast(vm.clientData)->bindgenStrings; + return static_cast(vm.clientData)->schemaStrings; } } diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp index 846da14c8b2f..99ea8cdc76e8 100644 --- a/src/jsc/bindings/Bindgen/ToJS.cpp +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -31,18 +31,19 @@ Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) { VM& vm = getVM(globalObject); + auto* global = defaultGlobalObject(globalObject); ASSERT(count == Tables::objectStructures[structureIndex].memberCount); bool hasEveryMember = true; for (size_t i = 0; i < count; ++i) hasEveryMember &= !!values[i]; if (hasEveryMember) [[likely]] { - JSObject* object = constructEmptyObject(vm, defaultGlobalObject(globalObject)->m_bindgenAdditions.m_objectStructures[structureIndex].getInitializedOnMainThread(globalObject)); + JSObject* object = constructEmptyObject(vm, global->m_bindgenAdditions.m_objectStructures[structureIndex].get(global)); for (size_t i = 0; i < count; ++i) object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); return JSValue::encode(object); } const Identifier* memberNames = schemaStrings(vm).memberNames(Tables::objectStructures[structureIndex].firstMemberNameIndex, count); - JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), count); + JSObject* object = constructEmptyObject(global, global->objectPrototype(), count); for (size_t i = 0; i < count; ++i) { if (values[i]) object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index cb90640a0ea5..b0793827744f 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -57,7 +57,7 @@ JSHeapData::~JSHeapData() = default; JSVMClientData::JSVMClientData(VM& vm, RefPtr sourceProvider) : commonStrings(vm) - , bindgenStrings(vm) + , schemaStrings(vm) , m_builtinNames(vm) , m_builtinFunctions(makeUnique(vm, sourceProvider, m_builtinNames)) , m_heapData(JSHeapData::ensureHeapData(vm.heap)) @@ -184,7 +184,7 @@ void JSVMClientData::create(VM* vm, void* bunVM, WorkerMessagingProxy* worker) MAKE_MARKING_CONSTRAINT_EXECUTOR_PAIR(([clientData](auto& visitor) { JSC::SetRootMarkReasonScope rootScope(visitor, JSC::RootMarkReason::StrongHandles); clientData->commonStrings.visit(visitor); - clientData->bindgenStrings.visit(visitor); + clientData->schemaStrings.visit(visitor); })), JSC::ConstraintVolatility::GreyedByExecution)); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 7cb7b4efbb63..4e4fafbcfb5d 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -246,7 +246,7 @@ class JSVMClientData : public JSC::VM::ClientData { // Public so Bun::commonStrings(vm) below is a static_cast and a member load. Bun::CommonStrings commonStrings; - Bun::Bindgen::SchemaStrings bindgenStrings; + Bun::Bindgen::SchemaStrings schemaStrings; WebCore::DOMURLBaseCache& urlBaseCache() { return m_urlBaseCache; } Bun::NodeHTTPRequestHeaderValues& nodeHTTPRequestHeaderValues() { return m_nodeHTTPRequestHeaderValues; } diff --git a/src/jsc/bindings/BunIDLConvertContext.h b/src/jsc/bindings/BunIDLConvertContext.h index b4f5af7d367d..817b2527f6e5 100644 --- a/src/jsc/bindings/BunIDLConvertContext.h +++ b/src/jsc/bindings/BunIDLConvertContext.h @@ -87,11 +87,6 @@ struct IDLConversionContextBase : Detail::IDLConversionContextMarker { derived().throwTypeMustBe(global, scope, "an object"_s); } - void throwNotBufferSource(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) - { - derived().throwTypeMustBe(global, scope, "an ArrayBuffer or TypedArray"_s); - } - void throwNotBlob(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { derived().throwTypeMustBe(global, scope, "a Blob"_s); diff --git a/src/jsc/bindings/IDLTypes.h b/src/jsc/bindings/IDLTypes.h index bd32cb4064f2..d76028ad88bb 100644 --- a/src/jsc/bindings/IDLTypes.h +++ b/src/jsc/bindings/IDLTypes.h @@ -71,7 +71,7 @@ struct IDLType { struct IDLNull : IDLType { }; -// See also: Bun::IDLRawAny +// See also: Bun::Bindgen::IDLRawAny struct IDLAny : IDLType> { // SequenceStorageType must be left as JSC::Strong; otherwise // IDLSequence would yield a Vector, whose contents diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index 388f53f3e2f6..3874694dee7b 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -6,13 +6,13 @@ use crate::JsError; use bun_core::fmt; use bun_core::{MutableString, String}; -pub mod js_bindings { +pub(crate) mod js_bindings { use super::*; use crate::generated_bindgen::fmt_jsc::Formatter; /// Internal function for testing in highlighter.test.ts - pub fn fmt_string(code: &[u8], formatter_id: Formatter) -> Result { + pub(crate) fn fmt_string(code: &[u8], formatter_id: Formatter) -> Result { let mut buffer = MutableString::init_empty(); let writer = buffer.writer(); diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 907fc49d520b..f1c0ed0ba700 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -405,7 +405,7 @@ pub mod btjs; #[path = "CppTask.rs"] pub mod cpp_task; #[path = "fmt_jsc.rs"] -pub mod fmt_jsc; +pub(crate) mod fmt_jsc; #[path = "HTTPServerAgent.rs"] pub mod http_server_agent; #[path = "JSSecrets.rs"] @@ -460,7 +460,7 @@ pub mod async_module; pub mod bindgen; #[cfg(debug_assertions)] #[path = "bindgen_test.rs"] -pub mod bindgen_test; +pub(crate) mod bindgen_test; #[path = "BunCPUProfiler.rs"] pub mod bun_cpu_profiler; #[path = "BunHeapProfiler.rs"] @@ -1581,4 +1581,4 @@ unsafe extern "C" { pub mod generated; #[path = "generated_bindgen.rs"] -pub mod generated_bindgen; +pub(crate) mod generated_bindgen; diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index 1595ba03b367..41b35810522f 100644 --- a/src/jsc/modules/NodeModuleModule.cpp +++ b/src/jsc/modules/NodeModuleModule.cpp @@ -988,7 +988,7 @@ _pathCache getPathCacheObject PropertyCallback _preloadModules jsFunctionPreloadModules Function 0 _resolveFilename nodeModuleResolveFilename CustomAccessor _resolveLookupPaths jsFunctionResolveLookupPaths Function 2 -_stat &Bun::Bindgen::Generated::NodeModuleModule::js_stat Function 1 +_stat &Bun::Bindgen::Generated::NodeModuleModule::jsStat Function 1 builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index 49e300ec54c4..404fa19f018f 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -21,7 +21,7 @@ export const loadavg = b.fn({ returns: b.array(b.f64) }); export const networkInterfaces = b.fn({ returns: b.any, capability: "js", throws: true }); export const release = b.fn({ returns: b.string }); export const totalmem = b.fn({ returns: b.u64 }); -export const uptime = b.fn({ returns: b.f64, capability: "js", throws: true }); +export const uptime = b.fn({ returns: b.f64, throws: true }); export const UserInfoOptions = b.dictionary( { encoding: b.string.loose.utf8.default("") }, diff --git a/src/runtime/node/node_os.rs b/src/runtime/node/node_os.rs index 5770b3bd7a66..e1fdd1673c03 100644 --- a/src/runtime/node/node_os.rs +++ b/src/runtime/node/node_os.rs @@ -1318,28 +1318,25 @@ mod _impl { } } - pub(crate) fn uptime(cx: &mut JsScope<'_>) -> JsResult { - let global = cx.global(); + pub(crate) fn uptime() -> Result> { #[cfg(windows)] { let mut uptime_value: f64 = 0.0; // SAFETY: valid out-pointer let err = unsafe { libuv::uv_uptime(&mut uptime_value) }; if err != 0 { - let sys_err = SystemError { + return Err(Box::new(SystemError { message: BunString::static_("failed to get system uptime").into(), code: BunString::static_("ERR_SYSTEM_ERROR").into(), errno: err, syscall: BunString::static_("uv_uptime").into(), ..Default::default() - }; - return Err(global.throw_value(sys_err.to_error_instance(global))); + })); } return Ok(uptime_value); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { - let _ = global; let mut boot_time: bun_sys::posix::timeval = bun_core::ffi::zeroed(); if bun_sys::posix::sysctl_read(c"kern.boottime", &mut boot_time).is_err() { return Ok(0.0); @@ -1348,7 +1345,6 @@ mod _impl { } #[cfg(any(target_os = "linux", target_os = "android"))] { - let _ = global; if let Ok(info) = bun_sys::posix::sysinfo() { return Ok(info.uptime as f64); } diff --git a/src/runtime/socket/SSLConfig.bind.ts b/src/runtime/socket/SSLConfig.bind.ts index ee2439f7df31..40debf1cd1ac 100644 --- a/src/runtime/socket/SSLConfig.bind.ts +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -37,7 +37,7 @@ export const TLSOptions = b.dictionary( keyFile: b.string.nullable, certFile: b.string.nullable, caFile: b.string.nullable, - ALPNProtocols: { type: ALPNProtocols, rustName: "alpn_protocols" }, + ALPNProtocols, ciphers: b.string.nullable, clientRenegotiationLimit: b.u32.default(0), clientRenegotiationWindow: b.u32.default(0), diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index 63e38560e90b..9b0bcbbab85a 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -145,9 +145,8 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { - generated::TLSOptions::from_js(global, value, |generated| { - Self::from_generated(vm, global, &generated, is_server) - }) + let generated = generated::TLSOptions::from_js(global, value)?; + Self::from_generated(vm, global, &generated, is_server) } fn from_generated( diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index fdafa969ba0d..d422fd56ce3a 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -67,6 +67,37 @@ it("optional arguments / default arguments", () => { expect(bindgen.requiredAndOptionalArg(true, null, 5, 2)).toBe(123463); }); +it("length counts the arguments up to the last that is not optional, or is what the schema says", () => { + expect(bindgen.utf8EchoLoose.length).toBe(1); + expect(bindgen.utf8Optional.length).toBe(0); + expect(bindgen.utf8Echo.length).toBe(3); + // These take undefined, and are still arguments a caller writes. + expect(bindgen.describeAnything.length).toBe(1); + expect(bindgen.sumBeforeAndAfterCallback.length).toBe(2); +}); + +itAlways("mistakes in a schema are errors when it loads", () => { + expect(() => b.i32.enforceRange(0, 5).loose).toThrow(".loose is another way"); + expect(() => b.i32.validateInteger().wrap).toThrow(".wrap is another way"); + expect(() => b.string.utf8.owned.default("x")).toThrow("cannot have a default"); + expect(() => b.array(b.null)).toThrow("an array of nothing but null"); + expect(() => b.dictionary({ fooBar: b.bool, foo_bar: b.bool })).toThrow("two fields are both foo_bar"); + expect(() => b.union({ "a-b": b.bool })).toThrow("an alternative is named in camelCase"); + expect(() => b.enumeration(["self"])).toThrow("Self cannot name anything in Rust"); + expect(() => b.fn({ args: { fooBar: b.bool, foo_bar: b.bool } })).toThrow("two arguments are both foo_bar"); + expect(() => b.fn({ length: 1.5 })).toThrow("1.5 is not a count"); + expect(() => b.overloads({ args: { a: b.bool }, length: 1 }, { args: { a: b.f64 }, length: 2 })).toThrow( + "length is given once", + ); + const label = { getter: b.string, setter: true } as const; + expect(() => b.class({ rustPath: "T", proto: { label, setLabel: {} } })).toThrow("are both set_label"); + b.class({ rustPath: "T", proto: { label: { getter: b.string }, setLabel: {}, getLabel: {} } }); + expect(() => b.class({ rustPath: "T", proto: { value: { getter: b.any } } })).toThrow('takes capability: "heap"'); + expect(() => b.class({ rustPath: "T", internalFields: ["a", "a"] })).toThrow("two internal fields are both a"); + // What takes undefined can come before what does not. + b.fn({ args: { encoding: b.string.nullable, callback: b.any, length: b.f64 } }); +}); + it("custom enforceRange boundaries", () => { expect(bindgen.requiredAndOptionalArg(false, 0, 5)).toBe(5); expect(() => bindgen.requiredAndOptionalArg(false, 0, -1)).toThrow("Value -1 is outside the range [0, 100]"); @@ -165,7 +196,7 @@ describe("strings", () => { expect(bindgen.utf8EchoLoose(12)).toBe("12"); expect(bindgen.utf8EchoLoose(null)).toBe("null"); expect(bindgen.utf8EchoLoose({ toString: () => "custom" })).toBe("custom"); - expect(() => bindgen.utf8EchoLoose(Symbol("s"))).toThrow(TypeError); + expect(() => bindgen.utf8EchoLoose(Symbol("s"))).toThrow(new TypeError("Cannot convert a symbol to a string")); expect(() => bindgen.utf8EchoLoose({ toString() { @@ -182,12 +213,6 @@ describe("strings", () => { expect(bindgen.utf8Optional("")).toBe(""); }); - it("length is the number of required arguments, or what the schema says", () => { - expect(bindgen.utf8EchoLoose.length).toBe(1); - expect(bindgen.utf8Optional.length).toBe(0); - expect(bindgen.utf8Echo.length).toBe(3); - }); - it("an argument that was not passed is undefined", () => { for (const args of [[], [undefined]]) { expect(() => bindgen.utf8Echo(...args)).toThrow( @@ -207,8 +232,13 @@ describe("enumerations", () => { }); it("rejects anything else", () => { - for (const value of ["Red", "", "purple", 0, null, undefined, {}]) { - expect(() => bindgen.nextColor(value)).toThrow(TypeError); + const invalid = (message: string) => + expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE", message }); + for (const value of ["Red", "", "purple"]) { + expect(() => bindgen.nextColor(value)).toThrow(invalid('color must be "red", "green", or "blue"')); + } + for (const value of [0, null, undefined, {}]) { + expect(() => bindgen.nextColor(value)).toThrow(invalid("color must be a string")); } }); }); @@ -246,19 +276,22 @@ describe("dictionaries", () => { }); it("a default written in the schema reaches every member, the nested ones included", () => { - const defaults = "inner: a=true b=7 name=inner; value: undefined; choice: none; counter: false"; + const defaults = "inner: a=true b=7 name=inner; value: undefined; choice: none; rest: undefined; counter: false"; expect(bindgen.describeDefaults()).toBe(defaults); expect(bindgen.describeDefaults(undefined)).toBe(defaults); expect(bindgen.describeDefaults({ inner: {} })).toBe(defaults); - expect(bindgen.describeDefaults({})).toBe("inner: none; value: undefined; choice: none; counter: false"); + expect(bindgen.describeDefaults({})).toBe( + "inner: none; value: undefined; choice: none; rest: undefined; counter: false", + ); expect( bindgen.describeDefaults({ inner: { a: false, name: "n" }, value: null, choice: true, + rest: "lime", counter: new (bindgen as any).TestCounter(), }), - ).toBe("inner: a=false b=7 name=n; value: given; choice: true; counter: true"); + ).toBe("inner: a=false b=7 name=n; value: given; choice: true; rest: Green; counter: true"); }); it("a member that is absent is absent, whatever was on the stack before", () => { @@ -513,9 +546,11 @@ describe("unions", () => { it("rejects what fits none", () => { for (const value of [null, undefined, {}, Symbol("s"), 1n]) { - expect(() => bindgen.describeValue(value)).toThrow(TypeError); + expect(() => bindgen.describeValue(value)).toThrow( + "value must be of type number, boolean, string, or array of integer", + ); } - expect(() => bindgen.describeValue(["not a number"])).toThrow(TypeError); + expect(() => bindgen.describeValue(["not a number"])).toThrow("element of value must be a number"); }); it("sends each kind of value to the alternative that takes it", () => { @@ -606,6 +641,14 @@ describe("unions", () => { ); }); + it("an enumeration takes strings, and b.any what nothing before it took", () => { + const rest = (value: unknown) => bindgen.describeDefaults({ rest: value }).match(/rest: (.*);/)![1]; + expect(rest("blue")).toBe("Blue"); + expect(() => rest("purple")).toThrow('TestOuter.rest must be "red", "green", or "blue"'); + expect(rest(new Uint8Array([1, 2]))).toBe("[1, 2]"); + for (const value of [new SharedArrayBuffer(2), null, 5, {}, new String("blue")]) expect(rest(value)).toBe("given"); + }); + itAlways("an alternative that cannot be chosen is an error in the schema", () => { expect(() => b.union({ anything: b.any, text: b.string })).toThrow("text is never chosen"); expect(() => b.union({ text: b.string.loose, flag: b.bool })).toThrow("flag is never chosen"); @@ -614,8 +657,16 @@ describe("unions", () => { expect(() => b.union({ none: b.null, inner: b.union({ text: b.string }) })).toThrow("list the alternatives"); expect(() => b.union({ text: b.string.nullable, flag: b.bool })).toThrow("add b.null as an alternative"); expect(() => b.union({ tag: b.bool, text: b.string })).toThrow("has a member of that name"); - // What one refuses, the next can take. - b.union({ bytes: b.bufferSource, reference: b.bufferSource.arrayBuffer }); + const mode = b.enumeration(["on", "off"]); + expect(() => b.union({ text: b.string, mode })).toThrow("mode is never chosen"); + b.union({ mode, options: b.dictionary({ flag: b.bool }) }); + // What one refuses, the next can take, unless it refuses the same. + b.union({ bytes: b.bufferSource, rest: b.any }); + expect(() => b.union({ bytes: b.bufferSource, reference: b.bufferSource.arrayBuffer })).toThrow( + "reference is never chosen", + ); + expect(() => b.union({ one: b.blob, other: b.blob })).toThrow("other is never chosen"); + expect(() => b.union({ none: b.null, flag: b.bool }).nullable).toThrow("already takes undefined"); b.union({ text: b.string, boxed: b.string.utf8 }); }); }); @@ -679,8 +730,8 @@ describe("arrays", () => { expect(bindgen.sumArray([1, 2, 3])).toBe(6); expect(bindgen.sumArray(Array.from({ length: 1000 }, (_, i) => i))).toBe(499500); expect(() => bindgen.sumArray(new Set([4, 5]))).toThrow("numbers must be an array of integer"); - expect(() => bindgen.sumArray([1, "2"])).toThrow(TypeError); - expect(() => bindgen.sumArray(5)).toThrow(TypeError); + expect(() => bindgen.sumArray([1, "2"])).toThrow("element of numbers must be a number"); + expect(() => bindgen.sumArray(5)).toThrow("numbers must be an array of integer"); }); it("out", () => { @@ -730,7 +781,9 @@ describe("buffers", () => { expect(() => bindgen.sumBytes(value)).toThrow( "bytes must be an ArrayBuffer or ArrayBufferView that is not shared", ); - expect(() => bindgen.sumBeforeAndAfterCallback(value, () => {})).toThrow(TypeError); + expect(() => bindgen.sumBeforeAndAfterCallback(value, () => {})).toThrow( + "buffer must be an ArrayBuffer or ArrayBufferView that is not shared", + ); } }); @@ -846,7 +899,7 @@ describe("classes", () => { it("a constructor that throws makes nothing", () => { const before = TestCounter.alive(); expect(() => new TestCounter(-1)).toThrow(new RangeError("start must not be negative")); - expect(() => new TestCounter("1")).toThrow(TypeError); + expect(() => new TestCounter("1")).toThrow("start must be a number"); // A collection in between can only lower it. expect(TestCounter.alive()).toBeLessThanOrEqual(before); }); @@ -863,7 +916,7 @@ describe("classes", () => { expect(() => { "use strict"; counter.count = 1; - }).toThrow(TypeError); + }).toThrow(new TypeError("Attempted to assign to readonly property.")); }); it("a cached getter is computed once per object", () => { @@ -895,12 +948,22 @@ describe("classes", () => { new Headers(), 10n ** 30n, ]) { - expect(() => increment.call(receiver)).toThrow(TypeError); - expect(() => count.call(receiver)).toThrow(TypeError); - expect(() => setLabel.call(receiver, "x")).toThrow(TypeError); + expect(() => increment.call(receiver)).toThrow( + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_THIS", + message: expect.stringStartingWith("Expected this to be instanceof TestCounter"), + }), + ); + expect(() => count.call(receiver)).toThrow( + new TypeError("The TestCounter.count getter can only be used on instances of TestCounter"), + ); + expect(() => setLabel.call(receiver, "x")).toThrow( + new TypeError("The TestCounter.label setter can only be used on instances of TestCounter"), + ); } expect(() => new TestCounter().plus({})).toThrow("other must be an instance of TestCounter"); - expect(() => new TestCounter().plus(new Bun.Glob("*"))).toThrow(TypeError); + expect(() => new TestCounter().plus(new Bun.Glob("*"))).toThrow("other must be an instance of TestCounter"); }); it("can be subclassed and made with Reflect.construct", () => { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 8862b84fb875..825685fcab06 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6553,6 +6553,8 @@ it("socket and tls options are not read from Object.prototype", async () => { hostname: "256.256.256.256", unix: "/nonexistent/socket", tls: true, + passphrase: 5, + ca: 5, }; Object.assign(Object.prototype, polluted); try { @@ -6562,6 +6564,8 @@ it("socket and tls options are not read from Object.prototype", async () => { using server = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } }); expect(server.port).toBeGreaterThan(1); expect(server.hostname).toBe("127.0.0.1"); + using secure = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} }, tls: { ...tls } }); + expect(secure.port).toBeGreaterThan(1); } finally { for (const name of Object.keys(polluted)) delete (Object.prototype as any)[name]; } diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index ba3e3030cff3..7acc69af443a 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -26,6 +26,15 @@ describe.concurrent("node-module-module", () => { expect(isBuiltin("test")).toBe(false); // "test" does not alias to "node:test" }); + test("_stat() says what is at a path", () => { + expect(Module._stat(import.meta.path)).toBe(0); + expect(Module._stat(import.meta.dir)).toBe(1); + expect(Module._stat(path.join(import.meta.dir, "does-not-exist"))).toBe(-1); + // The path is converted to a string, an argument that was not passed included. + expect(Module._stat()).toBe(-1); + expect(Module._stat(undefined)).toBe(-1); + }); + test("module.globalPaths exists", () => { expect(Array.isArray(require("module").globalPaths)).toBe(true); }); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index 394ac2b119dc..d65d6b722483 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -348,9 +348,16 @@ it("functions have Node's length", () => { it("userInfo ignores options that are not an object, like Node", () => { const info = os.userInfo(); - for (const options of [undefined, null, 5, "utf8", true, Symbol("s"), [], ["utf8"], () => {}]) { + const encoding = { + get() { + throw new Error("encoding was read"); + }, + }; + const callable = Object.defineProperty(() => {}, "encoding", encoding); + for (const options of [undefined, null, 5, "utf8", true, Symbol("s"), [], ["utf8"], callable]) { expect(os.userInfo(options)).toEqual(info); } + expect(() => os.userInfo(Object.defineProperty({}, "encoding", encoding))).toThrow("encoding was read"); }); it("setPriority with an undefined priority takes the first argument as the priority", () => { From ef741e450af470a50c935aa7b923d255cd7c8f45 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 01:43:17 +0000 Subject: [PATCH 39/44] Match the receiver error's message with a matcher that exists --- test/internal/bindgen.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index d422fd56ce3a..19e881159ab8 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -952,7 +952,7 @@ describe("classes", () => { expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_THIS", - message: expect.stringStartingWith("Expected this to be instanceof TestCounter"), + message: expect.stringMatching(/^Expected this to be instanceof TestCounter/), }), ); expect(() => count.call(receiver)).toThrow( From b952b542e25d91088d29e51e2bd3fd97a391a8c4 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 01:55:42 +0000 Subject: [PATCH 40/44] Build a returned dictionary with putDirect instead of a prebuilt Structure, and keep the lazy classes in an array in the global object --- docs/project/bindgen.mdx | 5 +-- src/codegen/bindgen-emit.ts | 27 +++---------- src/codegen/bindgen-lib-internal.ts | 5 +-- src/jsc/bindgen.rs | 8 ++-- src/jsc/bindgen/extern_types.rs | 2 +- .../Bindgen/AdditionsToGlobalObject.cpp | 19 ++------- .../Bindgen/AdditionsToGlobalObject.h | 13 ++----- src/jsc/bindings/Bindgen/ToJS.cpp | 39 ++----------------- src/jsc/bindings/Bindgen/ToJS.h | 9 ----- .../bindings/webcore/DOMClientIsoSubspaces.h | 2 +- src/jsc/bindings/webcore/DOMIsoSubspaces.h | 2 +- 11 files changed, 29 insertions(+), 102 deletions(-) delete mode 100644 src/jsc/bindings/Bindgen/ToJS.h diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 33af3cb1c085..f5f5880621cd 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -219,9 +219,8 @@ 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 a prebuilt `Structure`: one -allocation and one store per member. An `.optional` member that is `None` is -left out, and the object is then built one property at a time. +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: diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index f6d6eb840211..3a5220b85f26 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -43,8 +43,6 @@ export interface Tables { memberNames: string[]; /** Made into `JSString`s per VM on first use. */ enumerationValues: string[]; - /** Returned dictionaries: a run of `memberNames`, made into a `Structure` per global object on first use. */ - objectStructures: { firstMemberNameIndex: number; memberCount: number }[]; classes: ClassType[]; } @@ -66,7 +64,7 @@ function walk(type: Type, visit: (type: Type) => void, seen = new Set()): /** Marks which direction each dictionary travels and gives every table entry its index. */ export function analyze(files: readonly SchemaFile[]): Tables { - const tables: Tables = { memberNames: [], enumerationValues: [], objectStructures: [], classes: [] }; + const tables: Tables = { memberNames: [], enumerationValues: [], classes: [] }; const markAccepted = (type: Type) => walk(type, t => void (t instanceof DictionaryType && (t.accepted = true))); const markReturned = (type: Type) => walk(type, t => void ((t instanceof DictionaryType || t instanceof EnumerationType) && (t.returned = true))); @@ -109,11 +107,7 @@ export function analyze(files: readonly SchemaFile[]): Tables { tables.memberNames.push(...type.names); } if (type.returned) { - type.structureIndex = tables.objectStructures.length; - tables.objectStructures.push({ - firstMemberNameIndex: tables.memberNames.length, - memberCount: type.members.length, - }); + type.firstReturnedMemberNameIndex = tables.memberNames.length; tables.memberNames.push(...type.members.map(member => member.name)); const scope = type.origin!; for (const member of type.members) { @@ -863,7 +857,8 @@ export function cppCounts(tables: Tables): string { return `${banner(null)}#pragma once namespace Bun::Bindgen::Counts { -inline constexpr unsigned numberOfClassSubspaces = ${Math.max(tables.classes.length, 1)}; +// At least 1: an array cannot be empty. +inline constexpr unsigned numberOfClasses = ${Math.max(tables.classes.length, 1)}; } `; } @@ -891,16 +886,6 @@ ${array("WTF::ASCIILiteral", "memberNames", tables.memberNames.map(asciiLiteral) ${array("WTF::ASCIILiteral", "enumerationValues", tables.enumerationValues.map(asciiLiteral))} -struct ObjectStructure { - unsigned firstMemberNameIndex; - unsigned memberCount; -}; -${array( - "ObjectStructure", - "objectStructures", - tables.objectStructures.map(structure => `{ ${structure.firstMemberNameIndex}, ${structure.memberCount} }`), -)} - ${array( "const ClassDefinition*", "classes", @@ -1116,8 +1101,8 @@ impl ${type.name}${lifetime ? "<'_>" : ""} { let values = [ ${type.members.map(member => ` ${value(member)},`).join("\n")} ]; - // SAFETY: structure ${type.structureIndex} has these members, in this order. - unsafe { bindgen::to_js::object_with_structure(heap, ${type.structureIndex}, &values) } + // SAFETY: the names of these members start there in the table. + unsafe { bindgen::to_js::object(heap, ${type.firstReturnedMemberNameIndex}, &values) } } } `; diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 795705b59bb9..ac04e60ea25d 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1199,10 +1199,9 @@ export interface DictionaryOptions { export class DictionaryType extends NamedType { readonly options: DictionaryOptions; readonly members: readonly Member[]; - /** Index of the first member name in `Tables::memberNames`. Set by the generator. */ + /** Index in `Tables::memberNames` of the first name read, and of the first name written. Set by the generator. */ firstMemberNameIndex = -1; - /** Index in `Tables::objectStructures`. Set by the generator. */ - structureIndex = -1; + firstReturnedMemberNameIndex = -1; /** Reachable from an argument or `fromJS` / from a return type. Set by the generator. */ accepted = false; returned = false; diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 4361b15d8b1f..d47d8a0699f1 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -584,19 +584,19 @@ pub mod to_js { /// An empty value is a member that is absent. /// /// # Safety - /// `values` are the members of `Tables::objectStructures[structure_index]`. + /// `Tables::memberNames` names `values`, from `first_member_name_index`. #[doc(hidden)] #[inline] - pub unsafe fn object_with_structure<'a>( + pub unsafe fn object<'a>( heap: &JsHeap<'a>, - structure_index: u32, + first_member_name_index: u32, values: &[JSValue], ) -> Result<'a, JsLocal<'a>> { // SAFETY: forwarded; this cannot throw. Ok(JsLocal::from_raw(unsafe { extern_types::Bun__Bindgen__createObject( heap.global_ptr(), - structure_index, + first_member_name_index, values.as_ptr(), values.len(), ) diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 7602faf5efbc..f68ec1dc01e6 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -129,7 +129,7 @@ unsafe extern "C" { ) -> JSValue; pub(super) fn Bun__Bindgen__createObject( global: *mut JSGlobalObject, - structure_index: u32, + first_member_name_index: u32, values: *const JSValue, count: usize, ) -> JSValue; diff --git a/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp index 4d21145cad1f..cb1f366fb550 100644 --- a/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp @@ -1,30 +1,19 @@ #include "root.h" #include "Bindgen/AdditionsToGlobalObject.h" #include "Bindgen/JSWrapper.h" -#include "Bindgen/ToJS.h" #include "GeneratedBindings+Tables.h" #include "ZigGlobalObject.h" namespace Bun::Bindgen { -AdditionsToGlobalObject::AdditionsToGlobalObject() - : m_objectStructures(Tables::objectStructures.size()) - , m_classes(Tables::classes.size()) -{ -} - void AdditionsToGlobalObject::initialize() { - for (auto& structure : m_objectStructures) { - structure.initLater([](const JSC::LazyProperty::Initializer& init) { - auto& additions = defaultGlobalObject(init.owner)->m_bindgenAdditions; - init.set(createObjectStructure(init.vm, init.owner, &init.property - additions.m_objectStructures.begin())); - }); - } + // One callback for every class, which it tells apart by where the LazyClassStructure is, as in GlobalObject::initGeneratedLazyClasses. for (auto& lazyClass : m_classes) { lazyClass.initLater([](JSC::LazyClassStructure::Initializer& init) { - auto& additions = defaultGlobalObject(init.global)->m_bindgenAdditions; - setupClassStructure(init, &init.classStructure - additions.m_classes.begin()); + size_t classIndex = &init.classStructure - defaultGlobalObject(init.global)->m_bindgenAdditions.m_classes; + RELEASE_ASSERT(classIndex < Tables::classes.size()); + setupClassStructure(init, classIndex); }); } } diff --git a/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h index ec44591965cf..8e9a530d8b76 100644 --- a/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h @@ -1,28 +1,23 @@ #pragma once #include "root.h" +#include "GeneratedBindings+Counts.h" #include -#include -#include namespace Bun::Bindgen { -// The per-global-object state of generated bindings, by index into Tables. +// The per-global-object state of generated bindings. struct AdditionsToGlobalObject { - AdditionsToGlobalObject(); void initialize(); template void visit(Visitor& visitor) { - for (auto& structure : m_objectStructures) - structure.visit(visitor); for (auto& lazyClass : m_classes) lazyClass.visit(visitor); } - // Sized by the constructor, so the marker never sees them change. - WTF::FixedVector> m_objectStructures; - WTF::FixedVector m_classes; + // By index into Tables::classes. + JSC::LazyClassStructure m_classes[Counts::numberOfClasses]; }; } diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp index 99ea8cdc76e8..4c5ec95dad51 100644 --- a/src/jsc/bindings/Bindgen/ToJS.cpp +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -1,49 +1,18 @@ #include "root.h" -#include "Bindgen/ToJS.h" #include "Bindgen/SchemaStrings.h" -#include "GeneratedBindings+Tables.h" -#include "ZigGlobalObject.h" #include #include #include -#include -#include namespace Bun::Bindgen { using namespace JSC; -static_assert(std::ranges::all_of(Tables::objectStructures, [](const auto& structure) { return structure.memberCount <= JSFinalObject::maxInlineCapacity; }), "a returned dictionary has more members than an object holds inline"); - -Structure* createObjectStructure(VM& vm, JSGlobalObject* globalObject, unsigned index) -{ - auto [firstMemberNameIndex, memberCount] = Tables::objectStructures[index]; - const Identifier* memberNames = schemaStrings(vm).memberNames(firstMemberNameIndex, memberCount); - Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), memberCount); - for (unsigned i = 0; i < memberCount; ++i) { - PropertyOffset offset; - structure = Structure::addPropertyTransition(vm, structure, memberNames[i], 0, offset); - RELEASE_ASSERT(offset == static_cast(i)); - } - return structure; -} - -// An empty value is a member that is absent. -extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned structureIndex, const EncodedJSValue* values, size_t count) +// The members are named by Tables::memberNames, from `firstMemberNameIndex`. An empty value is a member that is absent. +extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned firstMemberNameIndex, const EncodedJSValue* values, size_t count) { VM& vm = getVM(globalObject); - auto* global = defaultGlobalObject(globalObject); - ASSERT(count == Tables::objectStructures[structureIndex].memberCount); - bool hasEveryMember = true; - for (size_t i = 0; i < count; ++i) - hasEveryMember &= !!values[i]; - if (hasEveryMember) [[likely]] { - JSObject* object = constructEmptyObject(vm, global->m_bindgenAdditions.m_objectStructures[structureIndex].get(global)); - for (size_t i = 0; i < count; ++i) - object->putDirectOffset(vm, static_cast(i), JSValue::decode(values[i])); - return JSValue::encode(object); - } - const Identifier* memberNames = schemaStrings(vm).memberNames(Tables::objectStructures[structureIndex].firstMemberNameIndex, count); - JSObject* object = constructEmptyObject(global, global->objectPrototype(), count); + const Identifier* memberNames = schemaStrings(vm).memberNames(firstMemberNameIndex, count); + JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(count, JSFinalObject::maxInlineCapacity)); for (size_t i = 0; i < count; ++i) { if (values[i]) object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); diff --git a/src/jsc/bindings/Bindgen/ToJS.h b/src/jsc/bindings/Bindgen/ToJS.h deleted file mode 100644 index 2e40facb936a..000000000000 --- a/src/jsc/bindings/Bindgen/ToJS.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include "root.h" - -namespace Bun::Bindgen { - -// The Structure of a returned dictionary, by index into Tables::objectStructures. -JSC::Structure* createObjectStructure(JSC::VM&, JSC::JSGlobalObject*, unsigned index); - -} diff --git a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h index 33cc60ba36e5..581c6f39097d 100644 --- a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h @@ -80,7 +80,7 @@ class DOMClientIsoSubspaces { GCClient::IsoSubspace* m_clientSubspaceForJSWebView { nullptr }; #include "ZigGeneratedClasses+DOMClientIsoSubspaces.h" - GCClient::IsoSubspace* m_clientSubspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClassSubspaces] {}; + GCClient::IsoSubspace* m_clientSubspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClasses] {}; /* --- bun --- */ GCClient::IsoSubspace* m_clientSubspaceForDOMException { nullptr }; diff --git a/src/jsc/bindings/webcore/DOMIsoSubspaces.h b/src/jsc/bindings/webcore/DOMIsoSubspaces.h index 4e4d706ad9cd..dd37885d99fc 100644 --- a/src/jsc/bindings/webcore/DOMIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMIsoSubspaces.h @@ -77,7 +77,7 @@ class DOMIsoSubspaces { IsoSubspace* m_subspaceForWasmStreamingCompiler { nullptr }; IsoSubspace* m_subspaceForJSWebView { nullptr }; #include "ZigGeneratedClasses+DOMIsoSubspaces.h" - IsoSubspace* m_subspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClassSubspaces] {}; + IsoSubspace* m_subspaceForBindgenClass[Bun::Bindgen::Counts::numberOfClasses] {}; /*-- BUN --*/ IsoSubspace* m_subspaceForFetchHeaders { nullptr }; From 9af4c090a3cbefbb13db2fff0e82197d2f471797 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 02:01:03 +0000 Subject: [PATCH 41/44] Use JSC's, WTF's and the tree's own helpers where generated bindings had their own, and Node's wording for an integer out of range --- src/codegen/bindgen-emit.ts | 33 +++++++-------- src/codegen/bindgen-lib-internal.ts | 2 +- src/jsc/bindgen.rs | 23 ----------- src/jsc/bindgen/extern_types.rs | 5 --- .../Bindgen/DictionaryMemberReader.cpp | 2 +- .../bindings/Bindgen/DictionaryMemberReader.h | 2 + src/jsc/bindings/Bindgen/ExternTraits.h | 30 +++++++------- src/jsc/bindings/Bindgen/ExternVectorTraits.h | 35 +++++++--------- src/jsc/bindings/Bindgen/IDLConvert.h | 37 +++++++---------- src/jsc/bindings/Bindgen/IDLConvertBase.h | 2 +- src/jsc/bindings/Bindgen/IDLConvertBlob.h | 3 +- .../bindings/Bindgen/IDLConvertIntegerRange.h | 35 +++++----------- src/jsc/bindings/Bindgen/IDLConvertNumbers.h | 40 +++++++------------ src/jsc/bindings/Bindgen/IDLTypes.h | 6 +-- src/jsc/bindings/Bindgen/JSStringUTF8View.cpp | 8 ++-- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 11 ++--- src/jsc/bindings/Bindgen/JSWrapper.cpp | 19 +++++---- src/jsc/bindings/Bindgen/JSWrapper.h | 31 ++++++-------- src/jsc/bindings/Bindgen/SchemaStrings.cpp | 10 ++--- src/jsc/bindings/Bindgen/SchemaStrings.h | 5 ++- src/jsc/bindings/Bindgen/ToJS.cpp | 9 +---- src/jsc/bindings/BunIDLConvertBase.h | 2 +- .../bindings/webcore/JSDOMConvertNumbers.cpp | 2 +- .../bindings/webcore/JSDOMConvertNumbers.h | 3 ++ test/js/node/os/os.test.js | 2 +- 25 files changed, 138 insertions(+), 219 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 3a5220b85f26..e6b20a68f283 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -138,18 +138,18 @@ const externType = (type: Type): string => : `Bun::Bindgen::ExternOf<${type.idlType}>`; const toExtern = (variable: string) => `Bun::Bindgen::toExtern(${variable})`; -/** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` from `type`, the cell's type byte. */ +/** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` of `cell`, by its type byte. */ const kindTests: Record, { ofValue?: (value: string) => string; ofCell?: string }> = { undefined: { ofValue: value => `${value}.isUndefined()` }, null: { ofValue: value => `${value}.isNull()` }, boolean: { ofValue: value => `${value}.isBoolean()` }, number: { ofValue: value => `${value}.isNumber()` }, - bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "type == JSC::HeapBigIntType" }, - string: { ofCell: "type == JSC::StringType" }, - stringObject: { ofCell: "type == JSC::StringObjectType || type == JSC::DerivedStringObjectType" }, - array: { ofCell: "type == JSC::ArrayType" }, - arrayBuffer: { ofCell: "type == JSC::ArrayBufferType" }, - arrayBufferView: { ofCell: "JSC::isTypedArrayTypeIncludingDataView(type)" }, + bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "cell->isHeapBigInt()" }, + string: { ofCell: "cell->isString()" }, + stringObject: { ofCell: "cell->inherits()" }, + array: { ofCell: "JSC::isJSArray(cell)" }, + arrayBuffer: { ofCell: "cell->inherits()" }, + arrayBufferView: { ofCell: "cell->inherits()" }, }; /** @@ -191,7 +191,6 @@ function cppChooseByKind(value: string, byKind: ReadonlyMap 0) { lines.push(`if (${value}.isCell()) {`, ` auto* cell = ${value}.asCell();`); - if (ofType.size > 0) lines.push(" JSC::JSType type = cell->type();"); lines.push(indent(cppChain(ofType, ofClass), 4), ofValue.size > 0 ? `} else ${cppChain(ofValue)}` : "}"); } else if (ofValue.size > 0) { lines.push(cppChain(ofValue)); @@ -212,7 +211,8 @@ function humanReadableEnumeration(type: EnumerationType): string { } /** Value-initialization zeroes a struct whose constructor is implicit, and these hold the buffers of string views. */ -const cppDefaultConstructor = (name: string) => ` ${name}() {}`; +const cppDefaultConstructor = (type: NamedType) => + `${type.needsLifetime ? " WTF_FORBID_HEAP_ALLOCATION;\n\npublic:\n" : ""} ${type.name}() {}`; /** C++ that offers `value` to `candidates`, indices of alternatives, in order. All but the last may refuse it. */ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]): string { @@ -222,7 +222,7 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) const member = `result.${name}`; if (alternative.refuses) return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { - ${member} = std::move(*converted); + ${member} = WTF::move(*converted); result.tag = ${i}; return; } @@ -256,15 +256,15 @@ function cppHeaderForUnion(type: UnionType): string { return ` namespace Bun::Bindgen::Generated { struct ${name} { -${cppDefaultConstructor(name)} +${cppDefaultConstructor(type)} ${held.map(a => ` ${a.type.convertsInPlace ? `std::optional<${implementationType(a.type)}> ${a.name}` : `${implementationType(a.type)} ${a.name} {}`};`).join("\n")} - std::uint8_t tag; + uint8_t tag; }; struct Extern${name} { union { ${alternatives.map(a => ` ${externType(a.type)} ${a.name};`).join("\n")} } data; - std::uint8_t tag; + uint8_t tag; }; ${layoutAssert(`Extern${name}`, type)} } @@ -321,7 +321,7 @@ function cppHeaderForType(type: NamedType): string { if (type instanceof EnumerationType) { return ` namespace Bun::Bindgen::Generated { -enum class ${name} : std::uint32_t { +enum class ${name} : uint32_t { ${type.variants.map(variant => ` ${variant},`).join("\n")} }; } @@ -339,7 +339,7 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC return ` namespace Bun::Bindgen::Generated { struct ${name} { -${cppDefaultConstructor(name)} +${cppDefaultConstructor(type)} ${type.members.map(member => ` ${implementationType(member.type)} ${member.fieldName};`).join("\n")} }; struct Extern${name} { @@ -403,7 +403,7 @@ function cppSourceForEnum(type: EnumerationType): string { template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) { static constexpr WTF::SortedArrayMap mapping { std::to_array<${pair}>({ -${pairs.map(([spelling, variant]) => ` ${pair} { ${asciiLiteral(spelling)}, ${type.cpp}::${variant} },`).join("\n")} +${pairs.map(([spelling, variant]) => ` { ${asciiLiteral(spelling)}, ${type.cpp}::${variant} },`).join("\n")} }) }; if (auto* value = mapping.tryGet(string)) [[likely]] return *value; @@ -733,6 +733,7 @@ ${privateMethods } const Bun::Bindgen::ClassDefinition Bun::Bindgen::Generated::JS${name}ClassDefinition = { .info = ${type.cpp}::info(), + .createStructure = ${type.cpp}::createStructure, .prototypeTableValues = ${span(`JS${name}PrototypeTableValues`, prototypeRows)}, .constructorTableValues = ${span(`JS${name}ConstructorTableValues`, constructorRows)}, .finishPrototypeCreation = ${privateMethods.length > 0 ? `finish${name}PrototypeCreation` : "nullptr"}, diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index ac04e60ea25d..dcf6cd2077ee 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -414,7 +414,7 @@ export class RangeType extends IntegerType { } const integer = (rust: string, cpp: string, size: number, webidl: string, min: number | bigint, max: number | bigint) => - new IntegerType({ rust, cpp: `std::${cpp}`, size, webidl, min, max }); + new IntegerType({ rust, cpp, size, webidl, min, max }); export const integers = { u8: integer("u8", "uint8_t", 1, "IDLOctet", 0, 255), diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index d47d8a0699f1..1ec3009d79a6 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -547,30 +547,7 @@ pub mod to_js { items: I, mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, ) -> Result<'a, JsLocal<'a>> { - // The inline capacity of a `MarkedArgumentBuffer`. - const INLINE_CAPACITY: usize = 8; - let mut inline = [JSValue::ZERO; INLINE_CAPACITY]; - let mut items = items.into_iter(); - for count in 0..inline.len() { - let Some(item) = items.next() else { - // SAFETY: `count` initialized values. - return JsLocal::from_js_result(crate::call_zero_is_throw( - heap.global(), - || unsafe { - extern_types::Bun__Bindgen__constructArrayFromValues( - heap.global_ptr(), - inline.as_ptr(), - count, - ) - }, - )); - }; - inline[count] = convert(heap, item)?.0; - } MarkedArgumentBuffer::new(|values| { - for value in inline { - values.append(value); - } for item in items { values.append(convert(heap, item)?.0); } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index f68ec1dc01e6..227d9423926b 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -122,11 +122,6 @@ unsafe extern "C" { global: *mut JSGlobalObject, values: *mut MarkedArgumentBuffer, ) -> JSValue; - pub(super) fn Bun__Bindgen__constructArrayFromValues( - global: *mut JSGlobalObject, - values: *const JSValue, - count: usize, - ) -> JSValue; pub(super) fn Bun__Bindgen__createObject( global: *mut JSGlobalObject, first_member_name_index: u32, diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp index b1205391d4ed..75330505f49b 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp @@ -23,7 +23,7 @@ JSValue DictionaryMemberReader::get(unsigned index) JSValue value = m_object->getDirect(m_vm, m_memberNames[index], attributes); if (!value) return jsUndefined(); - if (!(attributes & (PropertyAttribute::Accessor | PropertyAttribute::CustomAccessorOrValue))) [[likely]] + if (!(attributes & PropertyAttribute::AccessorOrCustomAccessorOrValue)) [[likely]] return value; } return getIfPropertyExistsPrototypePollutionMitigation(m_vm, m_globalObject, m_object, m_memberNames[index]); diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 34674efd16a1..92316d5d44dd 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -7,6 +7,8 @@ namespace Bun::Bindgen { // Reads the members of a dictionary, by index into its run of Tables::memberNames. class DictionaryMemberReader { + WTF_FORBID_HEAP_ALLOCATION; + public: DictionaryMemberReader(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* object, unsigned firstMemberNameIndex, unsigned memberNameCount) : m_vm(vm) diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 7687ec2859a8..f1d52b4c3914 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -21,7 +21,7 @@ using ExternOf = typename ExternTraits::Extern template ALWAYS_INLINE auto toExtern(T& value) { - return ExternTraits::convertToExtern(std::move(value)); + return ExternTraits::convertToExtern(WTF::move(value)); } template @@ -30,19 +30,19 @@ struct TrivialExtern { static ExternType convertToExtern(T&& cppValue) { - return std::move(cppValue); + return WTF::move(cppValue); } }; template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; -template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; template<> struct ExternTraits : TrivialExtern {}; -enum ExternNullPtr : std::uint8_t {}; +enum ExternNullPtr : uint8_t {}; template<> struct ExternTraits { using ExternType = ExternNullPtr; @@ -59,7 +59,7 @@ struct ExternOptional { ExternNullPtr none; T some; } data; - std::uint8_t tag; + uint8_t tag; }; template @@ -71,7 +71,7 @@ struct ExternTraits> { ExternType result; result.tag = cppValue.has_value(); if (cppValue) - result.data.some = ExternTraits::convertToExtern(std::move(*cppValue)); + result.data.some = ExternTraits::convertToExtern(WTF::move(*cppValue)); else result.data.none = ExternNullPtr {}; return result; @@ -97,14 +97,14 @@ template<> struct ExternTraits { }; struct ExternBytes { - const std::uint8_t* data; - std::size_t length; + const uint8_t* data; + size_t length; }; -template<> struct ExternTraits> { +template<> struct ExternTraits> { using ExternType = ExternBytes; - static ExternType convertToExtern(std::span cppValue) + static ExternType convertToExtern(std::span cppValue) { return { cppValue.data(), cppValue.size() }; } @@ -116,7 +116,7 @@ template<> struct ExternTraits { // The storage stays with `cppValue`, which outlives the call this is an argument of. static ExternType convertToExtern(JSStringUTF8View&& cppValue) { - return ExternTraits>::convertToExtern(cppValue.bytes()); + return ExternTraits>::convertToExtern(cppValue.bytes()); } }; diff --git a/src/jsc/bindings/Bindgen/ExternVectorTraits.h b/src/jsc/bindings/Bindgen/ExternVectorTraits.h index 7bbe9f632365..3ca7ff9a8b73 100644 --- a/src/jsc/bindings/Bindgen/ExternVectorTraits.h +++ b/src/jsc/bindings/Bindgen/ExternVectorTraits.h @@ -20,14 +20,14 @@ template struct ExternVector { T* data; // WTF::Vector stores the length and capacity as `unsigned`. We can save space by using that - // instead of `std::size_t` here. + // instead of `size_t` here. unsigned length; unsigned capacity; }; namespace Detail { template -void asanSetBufferSizeToFullCapacity(T* buffer, std::size_t length, std::size_t capacity) +void asanSetBufferSizeToFullCapacity(T* buffer, size_t length, size_t capacity) { #if ASAN_ENABLED // Without this, ASan will complain if the Rust side touches memory in the range @@ -42,7 +42,7 @@ void asanSetBufferSizeToFullCapacity(T* buffer, std::size_t length, std::size_t } } -template +template struct ExternTraits> { private: using CPPType = WTF::Vector; @@ -54,19 +54,12 @@ struct ExternTraits) { - // We can reuse the allocation. - alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; - // This prevents the contents from being freed or destructed. - CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; - T* const buffer = vec->mutableSpan().data(); - const std::size_t length = vec->size(); - const std::size_t capacity = vec->capacity(); - Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); - + unsigned capacity = cppValue.capacity(); + auto span = cppValue.releaseBuffer().leakSpan(); return ExternType { - .data = vec->mutableSpan().data(), - .length = static_cast(length), - .capacity = static_cast(capacity), + .data = span.data(), + .length = static_cast(span.size()), + .capacity = capacity, }; } else { static_assert(sizeof(ExternElement) <= sizeof(T) && alignof(ExternElement) <= MimallocMalloc::maxAlign); @@ -74,20 +67,20 @@ struct ExternTraitssize(); - const std::size_t capacity = vec->capacity(); + CPPType* const vec = new (cppStorage) CPPType { WTF::move(cppValue) }; + const size_t length = vec->size(); + const size_t capacity = vec->capacity(); T* const buffer = vec->mutableSpan().data(); Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); std::byte* storage = reinterpret_cast(buffer); // Convert the elements. - for (std::size_t i = 0; i < length; ++i) { + for (size_t i = 0; i < length; ++i) { T* oldPtr = std::launder(reinterpret_cast(storage + i * sizeof(T))); - ExternElement newElem { ExternTraits::convertToExtern(std::move(*oldPtr)) }; + ExternElement newElem { ExternTraits::convertToExtern(WTF::move(*oldPtr)) }; oldPtr->~T(); - new (storage + i * sizeof(ExternElement)) ExternElement { std::move(newElem) }; + new (storage + i * sizeof(ExternElement)) ExternElement { WTF::move(newElem) }; } #if __cpp_lib_start_lifetime_as >= 202207L diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 771db7cc58c5..4c310538de00 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -60,18 +60,15 @@ void throwNotUnsharedBufferSource(JSC::JSGlobalObject& globalObject, JSC::ThrowS } // std::nullopt for anything else, a shared buffer included. A detached or out-of-bounds view is empty. -inline std::optional> unsharedBufferSourceSpan(JSC::JSValue value) +inline std::optional> unsharedBufferSourceSpan(JSC::VM& vm, JSC::JSValue value) { if (auto* view = dynamicDowncast(value)) { if (view->isShared()) return std::nullopt; return view->span(); } - if (auto* buffer = dynamicDowncast(value)) { - if (buffer->isShared()) - return std::nullopt; - return buffer->impl()->span(); - } + if (auto* buffer = JSC::toUnsharedArrayBuffer(vm, value)) + return buffer->span(); return std::nullopt; } @@ -129,12 +126,12 @@ template<> struct WebCore::Converter struct WebCore::Converter : Bun::DefaultTryConverter { template - static std::optional> tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + static std::optional> tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) { - auto bytes = Bun::Bindgen::unsharedBufferSourceSpan(value); + auto bytes = Bun::Bindgen::unsharedBufferSourceSpan(JSC::getVM(&globalObject), value); // Null data is the null value. if (bytes && !bytes->data()) - return std::span { reinterpret_cast(""), 0 }; + return byteCast(WTF::nonNullEmptyUTF8Span()); return bytes; } @@ -147,9 +144,9 @@ template<> struct WebCore::Converter : Bun:: template<> struct WebCore::Converter : Bun::DefaultTryConverter { template - static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) { - if (!Bun::Bindgen::unsharedBufferSourceSpan(value)) + if (!Bun::Bindgen::unsharedBufferSourceSpan(JSC::getVM(&globalObject), value)) return std::nullopt; return value; } @@ -211,9 +208,8 @@ template<> struct WebCore::Converter JSC::JSValue value, Ctx& ctx) { - if (value.isBoolean()) { + if (value.isBoolean()) return value.asBoolean(); - } return std::nullopt; } @@ -238,9 +234,8 @@ template<> struct WebCore::Converter JSC::JSValue value, Ctx& ctx) { - if (value.isString()) { + if (value.isString()) return value.toWTFString(&globalObject); - } return std::nullopt; } @@ -262,9 +257,8 @@ struct WebCore::Converter> : Bun::DefaultTryConverte JSC::JSValue value, Ctx& ctx) { - if (JSC::isJSArray(value)) { + if (JSC::isJSArray(value)) return Bun::convertIDL::Base>(globalObject, value, ctx); - } return std::nullopt; } @@ -290,17 +284,14 @@ template<> struct WebCore::Converter Ctx& ctx) { auto& vm = JSC::getVM(&globalObject); - if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { + if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) return jsBuffer; - } if (auto* jsView = dynamicDowncast(value)) { - if (jsView->isShared()) { + if (jsView->isShared()) return std::nullopt; - } // A copy: the view's buffer can hold more than the view shows. - if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) { + if (auto bytes = JSC::ArrayBuffer::tryCreate(jsView->span())) return bytes; - } // Declared where it throws: at the top, every caller would owe a check for the paths that cannot. auto scope = DECLARE_THROW_SCOPE(vm); JSC::throwOutOfMemoryError(&globalObject, scope); diff --git a/src/jsc/bindings/Bindgen/IDLConvertBase.h b/src/jsc/bindings/Bindgen/IDLConvertBase.h index 7e7a86284d2d..2f37772a3079 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertBase.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBase.h @@ -37,7 +37,7 @@ struct ElementOf : ContextBase> { using ElementContext = ElementOf>; explicit ElementOf(Parent parent) - : m_parent(std::move(parent)) + : m_parent(WTF::move(parent)) { } diff --git a/src/jsc/bindings/Bindgen/IDLConvertBlob.h b/src/jsc/bindings/Bindgen/IDLConvertBlob.h index ed2132208e41..a52c15f5a8d5 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertBlob.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBlob.h @@ -18,9 +18,8 @@ template<> struct WebCore::Converter : Bun::DefaultTry Ctx& ctx) { if (auto* jsBlob = dynamicDowncast(value)) { - if (void* wrapped = jsBlob->wrapped()) { + if (void* wrapped = jsBlob->wrapped()) return static_cast(wrapped); - } } return std::nullopt; } diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h index 76efbce3c2c2..fee5a5007319 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -1,11 +1,8 @@ #pragma once #include "root.h" #include "Bindgen/IDLTypes.h" -#include "ErrorCode.h" -#include "JSDOMConvertBase.h" -#include -#include -#include +#include "JSDOMConvertNumbers.h" +#include "NodeValidator.h" namespace Bun::Bindgen { @@ -27,13 +24,7 @@ struct WebCore::Converter> : WebCore: auto scope = DECLARE_THROW_SCOPE(vm); double number = value.toNumber(&globalObject); RETURN_IF_EXCEPTION(scope, 0); - if (std::isfinite(number)) { - number = std::trunc(number); - if (number >= Min && number <= Max) - return static_cast(number); - } - throwTypeError(&globalObject, scope, makeString("Value "_s, number, " is outside the range ["_s, static_cast(Min), ", "_s, static_cast(Max), ']')); - return 0; + RELEASE_AND_RETURN(scope, static_cast(WebCore::enforceRange(globalObject, number, Min, Max))); } }; @@ -43,19 +34,11 @@ struct WebCore::Converter> : WebCo { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if (!value.isNumber()) { - Bun::ERR::INVALID_ARG_TYPE(scope, &globalObject, name, "number"_s, value); - return 0; - } - double number = value.asNumber(); - if (!JSC::isInteger(number)) { - Bun::ERR::OUT_OF_RANGE(scope, &globalObject, name, "an integer"_s, value); - return 0; - } - if (number < Min || number > Max) { - Bun::ERR::OUT_OF_RANGE(scope, &globalObject, name, Min, Max, value); - return 0; - } - return static_cast(number); + // The widths it is instantiated for. + using Validated = std::conditional_t, int32_t, std::conditional_t>; + static_assert(sizeof(T) <= sizeof(Validated)); + Validated result = 0; + Bun::V::validateInteger(scope, &globalObject, value, name, JSC::jsNumber(Min), JSC::jsNumber(Max), &result); + return static_cast(result); } }; diff --git a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h index e1d2c82283cc..992dc5f11b65 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h @@ -11,8 +11,8 @@ namespace Bun::Bindgen::Detail { template std::optional tryBigIntToInt(JSC::JSValue value) { - static constexpr std::int64_t minInt = std::numeric_limits::min(); - static constexpr std::int64_t maxInt = std::numeric_limits::max(); + static constexpr int64_t minInt = std::numeric_limits::min(); + static constexpr int64_t maxInt = std::numeric_limits::max(); using ComparisonResult = JSC::JSBigInt::ComparisonResult; if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { @@ -24,8 +24,8 @@ std::optional tryBigIntToInt(JSC::JSValue value) template std::optional tryBigIntToInt(JSC::JSValue value) { - static constexpr std::uint64_t minInt = 0; - static constexpr std::uint64_t maxInt = std::numeric_limits::max(); + static constexpr uint64_t minInt = 0; + static constexpr uint64_t maxInt = std::numeric_limits::max(); using ComparisonResult = JSC::JSBigInt::ComparisonResult; if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { @@ -36,7 +36,7 @@ std::optional tryBigIntToInt(JSC::JSValue value) } template - requires(sizeof(T) <= sizeof(std::uint64_t)) + requires(sizeof(T) <= sizeof(uint64_t)) struct WebCore::Converter> : Bun::DefaultTryConverter> { @@ -48,9 +48,8 @@ struct WebCore::Converter> JSC::JSValue value, Ctx& ctx) { - if (value.isInt32() && std::in_range(value.asInt32())) [[likely]] { + if (value.isInt32() && std::in_range(value.asInt32())) [[likely]] return static_cast(value.asInt32()); - } return tryConvertSlow(globalObject, value, ctx); } @@ -81,11 +80,10 @@ struct WebCore::Converter> return {}; } - using Largest = std::conditional_t, std::int64_t, std::uint64_t>; + using Largest = std::conditional_t, int64_t, uint64_t>; if (value.isBigInt()) { - if (auto result = Bun::Bindgen::Detail::tryBigIntToInt(value)) { + if (auto result = Bun::Bindgen::Detail::tryBigIntToInt(value)) return *result; - } if constexpr (maxInt < std::numeric_limits::max()) { if (auto result = Bun::Bindgen::Detail::tryBigIntToInt(value)) { ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); @@ -96,27 +94,20 @@ struct WebCore::Converter> return {}; } - if (!value.isNumber()) { + if (!value.isNumber()) return std::nullopt; - } double number = value.asNumber(); - // Written so that NaN is caught here: casting it to an integer is undefined. - if (!(number <= JSC::maxSafeInteger() && number >= JSC::minSafeInteger())) { - ctx.throwNumberNotInteger(globalObject, scope, number); - return {}; - } - auto intVal = static_cast(number); - if (intVal != number) { + if (!JSC::isSafeInteger(number)) { ctx.throwNumberNotInteger(globalObject, scope, number); return {}; } + auto intVal = static_cast(number); if constexpr (maxInt >= JSC::maxSafeIntegerAsUInt64()) { - if (std::signed_integral || intVal >= 0) { + if (std::signed_integral || intVal >= 0) return static_cast(intVal); - } - } else if (intVal >= static_cast(minInt) - && intVal <= static_cast(maxInt)) { + } else if (intVal >= static_cast(minInt) + && intVal <= static_cast(maxInt)) { return static_cast(intVal); } ctx.throwIntegerOutOfRange(globalObject, scope, intVal, minInt, maxInt); @@ -134,9 +125,8 @@ struct WebCore::Converter : Bun::DefaultTryConver JSC::JSValue value, Ctx& ctx) { - if (value.isNumber()) { + if (value.isNumber()) return value.asNumber(); - } return std::nullopt; } diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 36bfd8130d82..62797087a62a 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -7,10 +7,6 @@ #include #include -namespace WTF { -struct CrashOnOverflow; -} - namespace Bun { struct MimallocMalloc; } @@ -72,7 +68,7 @@ struct IDLArrayBufferRef : IDLRef {}; // Defined in IDLConvertBlob.h struct IDLBlobRef; -// Dictionaries that hold a raw `JSValue` or a `JSStringUTF8View` must live on the stack. +// A dictionary that holds a raw `JSValue` or a `JSStringUTF8View`. Its struct is WTF_FORBID_HEAP_ALLOCATION. template struct IDLStackOnlyDictionary : WebCore::IDLType { using SequenceStorageType = void; diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp index 6840fb7bbeb4..0994e14509cf 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp @@ -14,19 +14,19 @@ bool JSStringUTF8View::convert() auto span = view.span8(); if (!m_converted.tryGrow(span.size() * 2)) return false; - m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span.data()), span.size(), byteCast(m_converted.mutableSpan().data()))); + m_converted.shrink(simdutf::convert_latin1_to_utf8(byteCast(span), m_converted.mutableSpan())); return true; } auto span = view.span16(); if (!m_converted.tryGrow(span.size() * 3)) return false; - size_t length = simdutf::convert_utf16le_to_utf8(span.data(), span.size(), byteCast(m_converted.mutableSpan().data())); - if (length) [[likely]] { + size_t length = StringImpl::tryConvertUTF16ToUTF8(span, m_converted.mutableSpan()); + if (length != notFound) [[likely]] { m_converted.shrink(length); return true; } // simdutf refuses unpaired surrogates; each becomes U+FFFD. - m_converted.shrink(WTF::Unicode::convertReplacingInvalidSequences(span, byteCast(m_converted.mutableSpan())).buffer.size()); + m_converted.shrink(WTF::Unicode::convertReplacingInvalidSequences(span, m_converted.mutableSpan()).buffer.size()); return true; } diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index 4548c32ece8a..03d4a6a9462b 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -7,8 +7,9 @@ namespace Bun::Bindgen { -// Bun::UTF8View for an argument of a host call. Lives in the host function's frame. +// Bun::UTF8View for an argument of a host call. class JSStringUTF8View { + WTF_FORBID_HEAP_ALLOCATION; WTF_MAKE_NONCOPYABLE(JSStringUTF8View); WTF_MAKE_NONMOVABLE(JSStringUTF8View); @@ -39,13 +40,13 @@ class JSStringUTF8View { } // Null data only for a view that was not initialized. - std::span bytes() const + std::span bytes() const LIFETIME_BOUND { if (m_isNull) return {}; - auto span = m_isConverted ? m_converted.span() : byteCast(m_view->span8()); + auto span = byteCast(m_isConverted ? m_converted.span() : byteCast(m_view->span8())); if (!span.data()) - return { reinterpret_cast(""), 0 }; + return byteCast(WTF::nonNullEmptyUTF8Span()); return span; } @@ -54,7 +55,7 @@ class JSStringUTF8View { // The owner is not always the string: the characters of a substring are its base's. JSC::GCOwnedDataScope m_view; - WTF::Vector m_converted; + WTF::Vector m_converted; bool m_isConverted { false }; bool m_isNull { true }; }; diff --git a/src/jsc/bindings/Bindgen/JSWrapper.cpp b/src/jsc/bindings/Bindgen/JSWrapper.cpp index 68af1c2966de..6c708f2a8f0c 100644 --- a/src/jsc/bindings/Bindgen/JSWrapper.cpp +++ b/src/jsc/bindings/Bindgen/JSWrapper.cpp @@ -7,7 +7,6 @@ namespace Bun::Bindgen { using namespace JSC; -// It shares a subspace with the constructors of `.classes.ts` classes. STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSClassConstructor, InternalFunction); STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSClassConstructor); @@ -19,8 +18,7 @@ JSC_DEFINE_HOST_FUNCTION(callJSClassConstructor, (JSGlobalObject * globalObject, VM& vm = getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); auto* constructor = uncheckedDowncast(callFrame->jsCallee()); - Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, makeString(constructor->name(), " constructor cannot be invoked without 'new'"_s)); - return {}; + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_ILLEGAL_CONSTRUCTOR, makeString(constructor->name(), " constructor cannot be invoked without 'new'"_s)); } JSClassConstructor::JSClassConstructor(VM& vm, Structure* structure, NativeFunction construct) @@ -52,7 +50,7 @@ void setupClassStructure(LazyClassStructure::Initializer& init, unsigned classIn definition.finishPrototypeCreation(vm, init.global, prototype); Bun::putToStringTagWithoutTransition(vm, prototype, definition.info); init.setPrototype(prototype); - init.setStructure(Bun::createClassStructure(vm, init.global, prototype, TypeInfo(static_cast(WebCore::JSDOMWrapperType), JSDestructibleObject::StructureFlags), definition.info)); + init.setStructure(definition.createStructure(vm, init.global, prototype)); if (definition.construct) init.setConstructor(JSClassConstructor::create(vm, init.global, definition, prototype)); } @@ -71,14 +69,15 @@ JSObject* constructorForClass(JSGlobalObject* globalObject, unsigned classIndex) Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFrame, unsigned classIndex) { - VM& vm = getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); + // JSC_GET_DERIVED_STRUCTURE, for a Structure that is not a member of the global object. JSObject* newTarget = asObject(callFrame->newTarget()); - if (newTarget == constructorForClass(globalObject, classIndex)) [[likely]] + if (newTarget == callFrame->jsCallee()) [[likely]] return structureForClass(globalObject, classIndex); - // ShadowRealm functions belong to a different global object. - auto* functionGlobalObject = getFunctionRealm(globalObject, newTarget); - RETURN_IF_EXCEPTION(scope, nullptr); + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* functionGlobalObject = tryGetFunctionRealm(newTarget); + if (!functionGlobalObject) + functionGlobalObject = globalObject; RELEASE_AND_RETURN(scope, InternalFunction::createSubclassStructure(globalObject, newTarget, structureForClass(functionGlobalObject, classIndex))); } diff --git a/src/jsc/bindings/Bindgen/JSWrapper.h b/src/jsc/bindings/Bindgen/JSWrapper.h index bfec6088beb3..ba2fcd31c219 100644 --- a/src/jsc/bindings/Bindgen/JSWrapper.h +++ b/src/jsc/bindings/Bindgen/JSWrapper.h @@ -35,14 +35,15 @@ struct InheritsTraits> { } }; -} // namespace JSCastingHelpers -} // namespace JSC +} +} namespace Bun::Bindgen { // One per class, defined by generated code. struct ClassDefinition { const JSC::ClassInfo* info; + JSC::Structure* (*createStructure)(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); std::span prototypeTableValues; std::span constructorTableValues; // Properties a static table cannot describe (private names). May be null. @@ -58,7 +59,6 @@ class JSWrapper final : public JSC::JSDestructibleObject { public: using Base = JSC::JSDestructibleObject; static constexpr unsigned StructureFlags = Base::StructureFlags; - static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; static constexpr unsigned numberOfInternalFields = Traits::numberOfInternalFields; DECLARE_INFO; @@ -80,6 +80,11 @@ class JSWrapper final : public JSC::JSDestructibleObject { nullptr); } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(static_cast(WebCore::JSDOMWrapperType), StructureFlags), info()); + } + // The payload is uninitialized: the caller writes it before anything else can run. static JSWrapper* create(JSC::VM& vm, JSC::Structure* structure) { @@ -98,7 +103,7 @@ class JSWrapper final : public JSC::JSDestructibleObject { static constexpr size_t offsetOfPayload() { return offsetOfInternalFields() + numberOfInternalFields * sizeof(JSC::WriteBarrier); } static size_t allocationSize() { return offsetOfPayload() + Traits::payloadSize(); } - void* payload() { return reinterpret_cast(this) + offsetOfPayload(); } + void* payload() { return std::bit_cast(this) + offsetOfPayload(); } JSC::WriteBarrier& internalField(unsigned index) { ASSERT(index < numberOfInternalFields); @@ -106,7 +111,7 @@ class JSWrapper final : public JSC::JSDestructibleObject { } std::span, numberOfInternalFields> internalFields() { - return std::span, numberOfInternalFields> { reinterpret_cast*>(reinterpret_cast(this) + offsetOfInternalFields()), numberOfInternalFields }; + return std::span, numberOfInternalFields> { std::bit_cast*>(std::bit_cast(this) + offsetOfInternalFields()), numberOfInternalFields }; } private: @@ -116,9 +121,10 @@ class JSWrapper final : public JSC::JSDestructibleObject { // Rust reads the schema's `internalFields`, which start as `undefined`. The rest hold cached getters: empty until computed. unsigned index = 0; for (auto& field : internalFields()) { - new (NotNull, &field) JSC::WriteBarrier; if (index++ < numberOfInternalFields - Traits::numberOfCachedGetters) - field.setWithoutWriteBarrier(JSC::jsUndefined()); + new (NotNull, &field) JSC::WriteBarrier(JSC::UndefinedWriteBarrierTag); + else + new (NotNull, &field) JSC::WriteBarrier; } } @@ -146,19 +152,8 @@ template struct ExternTraits*> : TrivialExter class JSClassConstructor final : public JSC::InternalFunction { public: using Base = JSC::InternalFunction; - static constexpr unsigned StructureFlags = Base::StructureFlags; - static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return WebCore::subspaceForImpl( - vm, BUN_SUBSPACE_SLOTS(m_clientSubspaceForBunClassConstructor, m_subspaceForBunClassConstructor)); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { return Bun::createClassStructure(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); diff --git a/src/jsc/bindings/Bindgen/SchemaStrings.cpp b/src/jsc/bindings/Bindgen/SchemaStrings.cpp index dc1954a17a03..3accf9a3edcd 100644 --- a/src/jsc/bindings/Bindgen/SchemaStrings.cpp +++ b/src/jsc/bindings/Bindgen/SchemaStrings.cpp @@ -9,8 +9,8 @@ using namespace JSC; SchemaStrings::SchemaStrings(VM& vm) : m_vm(vm) - , m_memberNames(std::make_unique(Tables::memberNames.size())) - , m_enumerationValues(std::make_unique(Tables::enumerationValues.size())) + , m_memberNames(Tables::memberNames.size()) + , m_enumerationValues(WTF::FillWith {}, Tables::enumerationValues.size(), nullptr) { } @@ -30,10 +30,8 @@ void SchemaStrings::initializeEnumerationValue(unsigned index) template void SchemaStrings::visit(Visitor& visitor) { - for (unsigned i = 0; i < Tables::enumerationValues.size(); ++i) { - if (auto* string = m_enumerationValues[i]) - visitor.appendUnbarriered(string); - } + for (auto* string : m_enumerationValues) + visitor.appendUnbarriered(string); } template void SchemaStrings::visit(AbstractSlotVisitor&); template void SchemaStrings::visit(SlotVisitor&); diff --git a/src/jsc/bindings/Bindgen/SchemaStrings.h b/src/jsc/bindings/Bindgen/SchemaStrings.h index 8f48ee5475f0..f82c6c60658c 100644 --- a/src/jsc/bindings/Bindgen/SchemaStrings.h +++ b/src/jsc/bindings/Bindgen/SchemaStrings.h @@ -2,6 +2,7 @@ #include "root.h" #include #include +#include namespace Bun::Bindgen { @@ -38,8 +39,8 @@ class SchemaStrings { JSC::VM& m_vm; // Not arrays sized by the tables: most of the C++ includes this through BunClientData.h. - const std::unique_ptr m_memberNames; - const std::unique_ptr m_enumerationValues; + WTF::FixedVector m_memberNames; + WTF::FixedVector m_enumerationValues; }; SchemaStrings& schemaStrings(JSC::VM&); diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp index 4c5ec95dad51..3eba3f1727da 100644 --- a/src/jsc/bindings/Bindgen/ToJS.cpp +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -8,14 +8,14 @@ namespace Bun::Bindgen { using namespace JSC; // The members are named by Tables::memberNames, from `firstMemberNameIndex`. An empty value is a member that is absent. -extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned firstMemberNameIndex, const EncodedJSValue* values, size_t count) +extern "C" EncodedJSValue Bun__Bindgen__createObject(JSGlobalObject* globalObject, unsigned firstMemberNameIndex, const JSValue* values, size_t count) { VM& vm = getVM(globalObject); const Identifier* memberNames = schemaStrings(vm).memberNames(firstMemberNameIndex, count); JSObject* object = constructEmptyObject(globalObject, globalObject->objectPrototype(), std::min(count, JSFinalObject::maxInlineCapacity)); for (size_t i = 0; i < count; ++i) { if (values[i]) - object->putDirect(vm, memberNames[i], JSValue::decode(values[i])); + object->putDirect(vm, memberNames[i], values[i]); } return JSValue::encode(object); } @@ -25,11 +25,6 @@ extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* glo return JSValue::encode(schemaStrings(getVM(globalObject)).enumerationValue(index)); } -extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) -{ - return JSValue::encode(constructArray(globalObject, static_cast(nullptr), values, count)); -} - extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) { VM& vm = getVM(globalObject); diff --git a/src/jsc/bindings/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index 8ccff9dc3078..1c5ab8bb478a 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -49,7 +49,7 @@ struct DefaultTryConverter : DefaultContextConverter { auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); if (result.has_value()) [[likely]] { scope.release(); - return std::move(*result); + return WTF::move(*result); } RETURN_IF_EXCEPTION(scope, {}); throwConversionFailedSlow(globalObject, scope, ctx); diff --git a/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp b/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp index e4a4ea5bc8d1..03e89e41a08e 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp +++ b/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp @@ -49,7 +49,7 @@ static String rangeErrorString(double value, double min, double max) return makeString("Value "_s, value, " is outside the range ["_s, min, ", "_s, max, ']'); } -static double enforceRange(JSGlobalObject& lexicalGlobalObject, double x, double minimum, double maximum) +double enforceRange(JSGlobalObject& lexicalGlobalObject, double x, double minimum, double maximum) { VM& vm = lexicalGlobalObject.vm(); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/jsc/bindings/webcore/JSDOMConvertNumbers.h b/src/jsc/bindings/webcore/JSDOMConvertNumbers.h index d7c3330d194a..a7d94d91f869 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertNumbers.h +++ b/src/jsc/bindings/webcore/JSDOMConvertNumbers.h @@ -47,6 +47,9 @@ template<> WEBCORE_EXPORT uint32_t convertToInteger(JSC::JSGlobalObjec template<> WEBCORE_EXPORT int64_t convertToInteger(JSC::JSGlobalObject&, JSC::JSValue); template<> WEBCORE_EXPORT uint64_t convertToInteger(JSC::JSGlobalObject&, JSC::JSValue); +// `x` truncated, or a TypeError if that is not in the range. +double enforceRange(JSC::JSGlobalObject&, double x, double minimum, double maximum); + template T convertToIntegerEnforceRange(JSC::JSGlobalObject&, JSC::JSValue); template<> WEBCORE_EXPORT int8_t convertToIntegerEnforceRange(JSC::JSGlobalObject&, JSC::JSValue); template<> WEBCORE_EXPORT uint8_t convertToIntegerEnforceRange(JSC::JSGlobalObject&, JSC::JSValue); diff --git a/test/js/node/os/os.test.js b/test/js/node/os/os.test.js index d65d6b722483..6f91022ca26c 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -372,7 +372,7 @@ it("setPriority with an undefined priority takes the first argument as the prior expect.objectContaining({ name: "RangeError", code: "ERR_OUT_OF_RANGE", - message: 'The value of "priority" is out of range. It must be >= -20 and <= 19. Received 20', + message: 'The value of "priority" is out of range. It must be >= -20 && <= 19. Received 20', }), ); const priority = os.getPriority(); From afdb6fa0b42c0ed01ab41f362536b9858bbf4b87 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 02:11:12 +0000 Subject: [PATCH 42/44] Refuse a type used from another crate and a field named by a C++ keyword when the schema loads, and escape Rust's reserved words --- src/codegen/bindgen-lib-internal.ts | 17 ++++++++++++++--- test/internal/bindgen.test.ts | 13 +++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index dcf6cd2077ee..14922d4547c0 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -59,7 +59,7 @@ export function pascalCase(name: string): string { } const rustKeywords = new Set( - "as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn gen".split( + "as break const continue crate else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while async await dyn gen abstract become box do final macro override priv try typeof unsized virtual yield".split( " ", ), ); @@ -68,6 +68,13 @@ export function rustIdent(name: string): string { return rustKeywords.has(name) ? `r#${name}` : name; } +/** C++ has no way to spell one as a name. */ +const cppKeywords = new Set( + "alignas alignof and and_eq asm auto bitand bitor bool break case catch char char8_t char16_t char32_t class compl concept const consteval constexpr constinit const_cast continue co_await co_return co_yield decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept not not_eq nullptr operator or or_eq private protected public register reinterpret_cast requires return short signed sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while xor xor_eq".split( + " ", + ), +); + function assertDistinct(names: readonly string[], what: string): void { const repeated = names.find((name, i) => names.indexOf(name) !== i); assert(repeated === undefined, `two ${what} are both ${repeated}`); @@ -1090,8 +1097,10 @@ export abstract class NamedType extends Type { } protected path(scope: RustScope, name: string): string { assert(this.origin, `${this.constructor.name}: used but not exported from a .bind.ts file`); - if (this.origin.crate !== scope.crate) - return `${this.origin.crate}::generated_bindgen::${this.origin.rustModule}::${name}`; + assert( + this.origin.crate === scope.crate, + `${this.name} is declared in ${this.origin.crate} and used in ${scope.crate}: what is generated is private to its crate`, + ); if (this.origin.rustModule !== scope.rustModule) return `super::${this.origin.rustModule}::${name}`; return name; } @@ -1177,6 +1186,7 @@ export class Member { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a member is named by an identifier`); this.fieldName = options.rustName ?? snakeCase(name); assert(/^[a-z_][a-z0-9_]*$/.test(this.fieldName), `${this.fieldName}: rustName is written in snake_case`); + assert(!cppKeywords.has(this.fieldName), `${this.fieldName} is a C++ keyword: name the field with rustName`); this.type = options.type; } get name(): string { @@ -1328,6 +1338,7 @@ export class UnionType extends NamedType { assert(!(type instanceof WithValueType), `${name}: .withValue is only for an argument`); assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); + assert(!cppKeywords.has(name), `${name} is a C++ keyword, and JavaScript never sees the name: pick another`); assert( !(type instanceof RangeType), `${name}: a range reports the name of its argument, which a union does not know`, diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 19e881159ab8..eff47df67d59 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -1,5 +1,6 @@ import { bindgen as bindings } from "bun:internal-for-testing"; import { describe as describeAlways, expect, it as itAlways } from "bun:test"; +import { rustIdent } from "../../src/codegen/bindgen-lib-internal.ts"; import * as b from "../../src/codegen/bindgen-lib.ts"; // bindgen_test.bind.ts is only in a build with assertions: a debug build, CI's ASAN lane. @@ -84,6 +85,18 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.dictionary({ fooBar: b.bool, foo_bar: b.bool })).toThrow("two fields are both foo_bar"); expect(() => b.union({ "a-b": b.bool })).toThrow("an alternative is named in camelCase"); expect(() => b.enumeration(["self"])).toThrow("Self cannot name anything in Rust"); + expect(["match", "final", "try", "name"].map(rustIdent)).toEqual(["r#match", "r#final", "r#try", "name"]); + expect(() => b.dictionary({ default: b.bool })).toThrow("default is a C++ keyword: name the field with rustName"); + b.dictionary({ default: { type: b.bool, rustName: "default_value" } }); + expect(() => b.union({ int: b.i32, text: b.string })).toThrow("int is a C++ keyword"); + const elsewhere = Object.assign(b.dictionary({ flag: b.bool }), { + name: "Elsewhere", + origin: { stem: "a", namespace: "A", rustModule: "a", crate: "bun_jsc" }, + }); + expect(elsewhere.rustType({ crate: "bun_jsc", rustModule: "b" })).toBe("super::a::Elsewhere"); + expect(() => elsewhere.rustType({ crate: "bun_runtime", rustModule: "b" })).toThrow( + "Elsewhere is declared in bun_jsc and used in bun_runtime", + ); expect(() => b.fn({ args: { fooBar: b.bool, foo_bar: b.bool } })).toThrow("two arguments are both foo_bar"); expect(() => b.fn({ length: 1.5 })).toThrow("1.5 is not a count"); expect(() => b.overloads({ args: { a: b.bool }, length: 1 }, { args: { a: b.f64 }, length: 2 })).toThrow( From 52f5c7a0e559e91d9f6de2ab8ef2f829d3d8c2ac Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 02:37:12 +0000 Subject: [PATCH 43/44] Validate an int32 in range inline, and make a short array with one call --- .../implementing-jsc-classes-rust/SKILL.md | 2 ++ src/jsc/bindgen.rs | 22 +++++++++++++++++++ src/jsc/bindgen/extern_types.rs | 5 +++++ .../bindings/Bindgen/IDLConvertIntegerRange.h | 12 ++++++++++ src/jsc/bindings/Bindgen/ToJS.cpp | 5 +++++ 5 files changed, 46 insertions(+) diff --git a/.claude/skills/implementing-jsc-classes-rust/SKILL.md b/.claude/skills/implementing-jsc-classes-rust/SKILL.md index 087c4b3b9f8c..26e7aca01215 100644 --- a/.claude/skills/implementing-jsc-classes-rust/SKILL.md +++ b/.claude/skills/implementing-jsc-classes-rust/SKILL.md @@ -7,6 +7,8 @@ description: Creates JavaScript classes using Bun's Rust bindings generator (.cl Bridge JavaScript and Rust through `.classes.ts` definitions and Rust implementations. +`src/codegen/bindgen.ts` (`*.bind.ts`, `docs/project/bindgen.mdx`) is replacing this generator: typed arguments, and the struct stored inside the JS object. Use it for a class whose struct only the JS object owns. It does not do ref-counted or shared structs, `hasPendingActivity`, `estimatedSize` or writing to a cached getter's slot yet. + ## Architecture 1. **JavaScript Interface Definition** (`.classes.ts` files) diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 1ec3009d79a6..b2011d51be68 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -547,7 +547,29 @@ pub mod to_js { items: I, mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, ) -> Result<'a, JsLocal<'a>> { + // A short array is made with one call. A longer one takes a call per element. + let mut short = [JSValue::ZERO; 8]; + let mut items = items.into_iter(); + for count in 0..short.len() { + let Some(item) = items.next() else { + // SAFETY: `count` initialized values. + return JsLocal::from_js_result(crate::call_zero_is_throw( + heap.global(), + || unsafe { + extern_types::Bun__Bindgen__constructArrayFromValues( + heap.global_ptr(), + short.as_ptr(), + count, + ) + }, + )); + }; + short[count] = convert(heap, item)?.0; + } MarkedArgumentBuffer::new(|values| { + for value in short { + values.append(value); + } for item in items { values.append(convert(heap, item)?.0); } diff --git a/src/jsc/bindgen/extern_types.rs b/src/jsc/bindgen/extern_types.rs index 227d9423926b..f68ec1dc01e6 100644 --- a/src/jsc/bindgen/extern_types.rs +++ b/src/jsc/bindgen/extern_types.rs @@ -122,6 +122,11 @@ unsafe extern "C" { global: *mut JSGlobalObject, values: *mut MarkedArgumentBuffer, ) -> JSValue; + pub(super) fn Bun__Bindgen__constructArrayFromValues( + global: *mut JSGlobalObject, + values: *const JSValue, + count: usize, + ) -> JSValue; pub(super) fn Bun__Bindgen__createObject( global: *mut JSGlobalObject, first_member_name_index: u32, diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h index fee5a5007319..13809dc499a7 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -30,7 +30,19 @@ struct WebCore::Converter> : WebCore: template struct WebCore::Converter> : WebCore::DefaultConverter> { + // Inlined so that an int32 in range costs its tests and nothing else. static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) + { + if (value.isInt32()) [[likely]] { + int64_t number = value.asInt32(); + if (number >= static_cast(Min) && number <= static_cast(Max)) [[likely]] + return static_cast(number); + } + return convertSlow(globalObject, value, name); + } + +private: + NEVER_INLINE static T convertSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp index 3eba3f1727da..77aefc32b773 100644 --- a/src/jsc/bindings/Bindgen/ToJS.cpp +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -25,6 +25,11 @@ extern "C" EncodedJSValue Bun__Bindgen__enumerationValueToJS(JSGlobalObject* glo return JSValue::encode(schemaStrings(getVM(globalObject)).enumerationValue(index)); } +extern "C" EncodedJSValue Bun__Bindgen__constructArrayFromValues(JSGlobalObject* globalObject, const JSValue* values, size_t count) +{ + return JSValue::encode(constructArray(globalObject, static_cast(nullptr), values, count)); +} + extern "C" EncodedJSValue Bun__Bindgen__constructArray(JSGlobalObject* globalObject, MarkedArgumentBuffer* values) { VM& vm = getVM(globalObject); From 24567b6f46579493efc3a634de47e5bf490c2270 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 16 Oct 2026 03:06:01 +0000 Subject: [PATCH 44/44] Refuse an array whose elements hold JS values as a result too, not only as an argument --- src/codegen/bindgen-lib-internal.ts | 5 ++++- test/internal/bindgen.test.ts | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 14922d4547c0..e565e7bdd68d 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1030,6 +1030,10 @@ export class ArrayType extends Type { "b.bufferSource.bytes is only for an argument: it is read after everything else is converted", ); assert(!(element instanceof UnitType), "an array of nothing but null"); + assert( + !element.holdsJSValue, + "an array is a Vec, where the GC does not see JS values: take or return b.any and go through the array in Rust", + ); this.element = element; const markConvertedByValue = (type: Type) => { if (type instanceof DictionaryType) type.convertedByValue = true; @@ -1038,7 +1042,6 @@ export class ArrayType extends Type { markConvertedByValue(element); } get idlType(): string { - assert(!this.element.holdsJSValue, "an array argument cannot hold JS values: the GC does not scan it"); assert( !this.element.borrowsFrame, "an array argument cannot hold borrowed views; use b.string or b.string.utf8.owned", diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index eff47df67d59..1fc49046079e 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -82,6 +82,9 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.i32.validateInteger().wrap).toThrow(".wrap is another way"); expect(() => b.string.utf8.owned.default("x")).toThrow("cannot have a default"); expect(() => b.array(b.null)).toThrow("an array of nothing but null"); + for (const element of [b.any, b.bufferSource, b.dictionary({ value: b.any }), b.class({ rustPath: "T" })]) { + expect(() => b.array(element)).toThrow("an array is a Vec, where the GC does not see JS values"); + } expect(() => b.dictionary({ fooBar: b.bool, foo_bar: b.bool })).toThrow("two fields are both foo_bar"); expect(() => b.union({ "a-b": b.bool })).toThrow("an alternative is named in camelCase"); expect(() => b.enumeration(["self"])).toThrow("Self cannot name anything in Rust");