diff --git a/.claude/skills/implementing-jsc-classes-rust/SKILL.md b/.claude/skills/implementing-jsc-classes-rust/SKILL.md index 6dd76be69442..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) @@ -18,7 +20,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 +47,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 +90,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); ``` @@ -124,7 +126,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..2a9f21326dc7 100644 --- a/.github/workflows/rust-lints.yml +++ b/.github/workflows/rust-lints.yml @@ -10,6 +10,7 @@ on: - "src/**/*.rs" - "src/**/Cargo.toml" - "src/**/*.classes.ts" + - "src/**/*.bind.ts" - "src/codegen/**" - "scripts/build/**" - "scripts/build.ts" diff --git a/.prettierrc b/.prettierrc index b389721f9995..206e2ea810e4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -20,12 +20,6 @@ "printWidth": 80 } }, - { - "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], - "options": { - "printWidth": 100 - } - }, { "files": ["src/runtime/cli/init/**"], "options": { 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 7593fe0f2ab4..f5f5880621cd 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -5,281 +5,426 @@ 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. -- "Classes generator", converting `*.classes.ts` for custom classes. -- "JS2Native", allowing ad-hoc calls from `src/js` to native code. +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`. -## Creating JS Functions in Rust +The body of a binding is always Rust. These do overlapping work: -Given a file implementing a function, such as `add`: +- "Classes generator", converting `*.classes.ts` for custom classes. +- "JS2Native", allowing ad-hoc calls from `src/js` to native code (`$rust`, `$cpp`). +- `#[bun_jsc::host_fn]` functions, which take a `CallFrame`. -```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"))) - } - } -} -``` +## Functions -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.error(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. `length` sets the function's `.length`, +which is otherwise the number of arguments up to the last that is not written +`.optional`, `.nullable`, `.falsyAsNull` or `.default()`. The ones after that are +the ones TypeScript lets a caller leave out. -```rust -use crate::r#gen::bindgen_test as generated; +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. -let js_fn: JSValue = generated::create_add_callback(global); -``` +To reach the function: -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`. +- 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`. -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`). +### What a function may do: `capability` -## Strings +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. -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. +| `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 | -When in doubt, use DOMString. +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. -`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. +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. -TLDRs from the WebIDL spec: +`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`, `SystemError`, `JsError` and a `Box` of +one do), which lets a `capability: "none"` function fail too. -- 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. +`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`. -## Function Variants +### Overloads -The `variants` key declares multiple variants (also known as overloads) of a 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 -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 read = b.overloads( + { args: { path: b.string.utf8, options: ReadOptions.optional }, rustPath: "crate::read_path" }, + { args: { fd: b.i32, buffer: b.bufferSource.bytes, offset: b.u32.default(0) }, rustPath: "crate::read_fd" }, +); ``` -Each variant gets a numbered Rust function: - -```rust -pub fn action1(a: i32) -> i32 { - a -} +Use it when the type of one argument says what the others are. A union for each +argument would accept combinations that mean nothing, and leave the body to sort them out. + +The generated function starts with every signature as a candidate and looks at the +arguments from left to right: + +1. It keeps the candidates whose type at that position takes the [kind](#unions-and-kinds) + of the argument. +2. An argument that was not passed is `undefined`. So `f(x)` and `f(x, undefined)` are the + same call. +3. A candidate with no argument at that position stays. +4. If no candidate takes the argument, all of them stay, and the next argument decides. +5. When the arguments run out, the candidate written first wins. Its conversion reports + whatever is wrong. So of two signatures that start the same, the longer one goes first. + +Choosing converts nothing and runs no JavaScript. A signature that no call can reach is +an error when the schema is loaded. + +## Types + +Conversions are strict unless the schema says otherwise: `b.i32` takes a number +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 | +| ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- | +| `b.bool` | `bool` | a boolean; `.loose` is `ToBoolean` | +| `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` | `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` | `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 | +| `b.dictionary(members)` | a generated `struct` | an object that is not an array | +| `b.union(alternatives)` | a generated `enum` | see [Unions and kinds](#unions-and-kinds) | +| `b.class(options)` | `JsWrapper<'a, T>` | an instance | + +One JS type has one name; a modifier picks the Rust form. The last four are named +types: each is called what it is exported as, and has to be exported. + +These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.withValue` applies to it): + +- `.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). Not with `.default()`. + +As results, `b.string.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.string.utf8` is the one to reach for. When the JS string is 8-bit and ASCII the +slice points at the string's own bytes; otherwise it is transcoded into a buffer +in the generated function's frame, which holds 128 bytes and is sized for the worst +case up front: beyond 64 Latin-1 characters or 42 UTF-16 code units it allocates. +Nothing needs freeing. An unpaired surrogate becomes U+FFFD. `.loose` comes before +`.utf8`. A `String` object, which `b.string` refuses, is converted with `ToString`, so +its `toString` runs. + +`b.string.utf8.owned` is for a body that stores the string: a +`bun_core::Utf8Bytes<'static>`, which shares the JS string's buffer when it is 8-bit +and ASCII and is a transcoded copy otherwise. + +`b.string` shares the JS string's storage and can be stored. It belongs to the +JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending +one anywhere. + +### Dictionaries -pub fn action2(a: bun_core::String) -> bun_core::String { - a -} +```ts +export const BracesOptions = b.dictionary({ + tokenize: b.bool.loose.default(false), + parse: b.bool.loose.default(false), +}); ``` -## `t.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, rustName, altNames }` to rename its field or to accept other spellings. +A dictionary whose members can all be absent implements `Default`. -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. +A member is looked up on the object and its prototype chain, **stopping before +the calling realm's `Object.prototype`**, so a polluted `Object.prototype` cannot +configure a Bun API. For a plain object, where the own properties are therefore +the whole answer, the member is read straight out of the object's property table. The two paths are indistinguishable to JavaScript; +`test/internal/bindgen.test.ts` holds them to that. -## Enumerations +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. -`t.stringEnum` creates a [WebIDL enumeration](https://webidl.spec.whatwg.org/#idl-enums) and generates a new enum type for it. +The second argument of `b.dictionary` takes: -An example of `stringEnum` from `fmt_jsc.bind.ts` / `bun:internal-for-testing`: +| Key | What it does | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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)`, for a `#[host_fn]` body; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | -```ts -export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell"); +### Enumerations -export const fmtString = fn({ - args: { - global: t.globalObject, - code: t.UTF8String, - formatter: Formatter, - }, - ret: t.DOMString, -}); +```ts +export const Formatter = b.enumeration(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]); ``` -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: +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. -```rust -#[repr(u8)] -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum Formatter { - EscapePowershell = 0, - HighlightJavascript = 1, - HighlightJavascriptRedacted = 2, -} +### Unions and kinds -pub fn fmt_string( - global: &JSGlobalObject, - code: &[u8], - formatter_id: Formatter, -) -> JsResult { - // ... -} -``` +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 | -WebIDL strongly encourages kebab-case for enumeration values, to be consistent with existing Web APIs. +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`, a dictionary. -## `t.oneOf` +**A value goes to the first alternative, in written order, that takes its kind.** That +alternative then converts it, and reports what is wrong with it: `1.5` given to +`b.union({ integer: b.i32, text: b.string })` "must be an integer", not "must be of type integer or string". +The buffer types and `b.blob` refuse some values of their kinds (a shared buffer, a +detached `Blob`); those go on to the next alternative that takes the kind and does not +refuse the same. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. -A `oneOf` is a union of two or more types. It is represented as a Rust -`enum` with one variant per member type. +So the order only matters between alternatives that overlap, and one that takes every +kind goes last. An alternative that can never be chosen is an error when the schema is +loaded. Choosing tests whether the value is a cell once, loads the type byte once and +repeats no test. Strings as UTF-8, dictionaries and unions are then converted in the +place they end up in. -## Attributes +### Buffers -You can chain attributes onto `t.*` types. On all types: +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: -- `.required`, in dictionary parameters only -- `.optional`, in function arguments only -- `.default(T)` +```rust +let before = buffer.with_bytes(&cx.heap, sum); +callback.call(cx, JsLocal::UNDEFINED, &[])?; +let after = buffer.with_bytes(&cx.heap, sum); +``` -When a value is `.optional`, it is lowered to a Rust `Option`: +Calling `callback.call(cx, ..)` inside the closure does not compile. A detached or +out-of-bounds view is empty. + +### 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 `internalFields`, a `Strong`, another JS call, the result), ask +for both: ```ts -export const requiredAndOptionalArg = fn({ - args: { - a: t.boolean, - b: t.usize.optional, - c: t.i32.enforceRange(0, 100).default(42), - d: t.u8.optional, - }, - ret: t.i32, -}); +args: { name: b.string.utf8.withValue }, ``` ```rust -pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option) -> i32 { - // ... +fn set_name<'a>(&mut self, this: JsWrapper<'a, Self>, heap: &JsHeap<'a>, name: WithValue<'a, &'a [u8]>) { + this.set_name_field(heap, name.value); // the JS string itself, not a copy + self.name_length = name.converted.len(); } ``` -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. +## Values and lifetimes -### Integer Attributes +A JS value in Rust is a `JsLocal<'a>`: a `JSValue` tied to the host call it belongs +to. It is `Copy`, one register, and not rooted, because JavaScriptCore scans the +stack. The lifetime is what stops it from being stored in a struct, captured by a +task or sent to a thread. `JsLocal<'a, T>` is one known to be a `JSString`, +`JSObject`, `Callable` or `BufferSource`. `value.to_js()` gives the `JSValue`; +`JsLocal::new(heap, value)` goes the other way. -Integer types take `clamp` or `enforceRange` to customize overflow behavior: +What outlives a call: -```ts -import { fn, t } from "bindgen"; +- 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. -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, +A list of values goes in a `MarkedArgumentBuffer`, never a `Vec` or a `Box`: the GC does +not look inside heap allocations. The compiler does not check this one. + +## Classes + +The keys are mostly those of `.classes.ts`, with types. `klass` is `static` and `values` is `internalFields`. + +```ts +export const Glob = b.class({ + rustPath: "crate::api::glob::Glob", + construct: { + args: { pattern: b.string.utf8.owned }, + }, + proto: { + match: { + args: { str: b.string.utf8 }, + returns: b.bool, + }, }, - ret: t.i32, }); ``` -Node.js validator functions such as `validateInteger` and `validateNumber` -are also available. Use these when implementing Node.js APIs so the error -messages match Node exactly. +generates a trait, which the struct implements: -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. +```rust +pub(crate) struct Glob { + pattern: bun_core::Utf8Bytes<'static>, +} -```ts -import { fn, t } from "bindgen"; +impl GlobClass for Glob { + fn constructor(pattern: bun_core::Utf8Bytes<'static>) -> Self { + Glob { pattern } + } -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, -}); + fn r#match<'a>(&self, str: &'a [u8]) -> bool { + bun_glob::r#match(self.pattern.slice(), str).matches() + } +} +``` + +The struct, the _payload_, is stored **inside** the JS object, the _wrapper_, so the +two have one lifetime and one allocation. It is dropped when the object is +collected. `Drop` gets no capability, so it cannot touch the JS heap, and no Rust +runs on the collector's threads. The collector is not told of memory the payload owns +outside the object: there is no `estimatedSize` yet. + +| Key | What it declares | +| ---------------- | ---------------------------------------------------------------------------------------------- | +| `rustPath` | the struct | +| `construct` | `new Name(..)`. Without it JavaScript cannot make one; Rust can, with `payload.into_js(heap)`. | +| `proto` | properties of the prototype | +| `static` | functions on the constructor | +| `internalFields` | JS values the object holds | +| `configurable` | `false` makes the native properties of the prototype non-configurable | + +A property of `proto` is one of: + +| Property | What it is | +| ------------------------------------------- | ---------------------------------------------------------------------------- | +| `{ args, returns, capability, throws, .. }` | a native function; `privateSymbol` keys it by a private name of `src/js` | +| `{ getter: T, capability, throws }` | an accessor; with `setter: true` it can be assigned | +| `{ getter: T, cache: true, .. }` | an accessor whose value is computed once per object; it cannot have a setter | +| `{ builtin: true, length }` | the export of that name in `src/js/builtins/.ts` | + +If any member has `throws: true`, the trait has a +`type Error<'a>: bindgen::IntoError<'a>` that they all fail with. `b.self` is the +class itself as an argument, a result or a getter of its own members. `Glob::get_constructor(heap)` is the +constructor function. + +### The receiver + +| The method is | It receives | +| -------------------------------- | --------------------------- | +| `capability: "none"` or `"heap"` | `&self` | +| …with `mutates: true` | `&mut self` | +| `capability: "js"` | `this: JsWrapper<'a, Self>` | + +A method that can run JavaScript can be re-entered, by a callback that calls +another method of the same object. A `&mut self` held across that would be +undefined behaviour, so it gets the wrapper instead and borrows the payload for as +long as nothing else can run: + +```rust +fn notify<'a>(this: JsWrapper<'a, Self>, cx: &mut JsScope<'a>) -> bindgen::Result<'a, i32> { + if let Some(listener) = this.listener_field(&cx.heap).as_callable() { + listener.call(cx, this.as_value(), &[])?; + } + Ok(this.with(&cx.payloads, |counter| counter.count)) +} ``` -## 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 +### `internalFields` -## Classes +`internalFields: ["listener"]` generates `this.listener_field(heap)` and +`this.set_listener_field(heap, value)` on `JsWrapper<'a, T>`. A `&self` or +`&mut self` method that needs them asks for the wrapper too, with `passThis: true`. +The wrapper reaches the internal fields and not the payload. + +### Staying alive + +To keep an object alive while native work is pending, hold a +`JsWrapperStrong` (`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`: 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/mordant-baseline.toml b/mordant-baseline.toml index f235a589bfa0..05258568deab 100644 --- a/mordant-baseline.toml +++ b/mordant-baseline.toml @@ -67,7 +67,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/CLAUDE.md b/scripts/build/CLAUDE.md index 841ead6db8e3..43f7d8a3563e 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/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 b7eaa050761f..6c4a646223ce 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,7 +18,7 @@ * 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 + * 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, @@ -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); @@ -347,7 +337,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; } @@ -880,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]), + args: shJoin(cfg, ["run", script, debugFlag(cfg), cfg.buildDir, cfg.typesDir, typesOnlyFlag(cfg, sources)]), }, }); @@ -931,99 +921,85 @@ 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}`]), - }, - }); +/** A `*_test.bind.ts` binds what exists for tests. Only a build where Rust has `debug_assertions` has it, and none ships. */ +function isCompiled(cfg: CodegenFields, src: string): boolean { + return cfg.debug || cfg.assertions || !src.endsWith("_test.bind.ts"); +} - o.all.push(...allOutputs); - o.bindgenV2Cpp.push(...cppOutputs); - o.cppHeaders.push(...headerOutputs); +function typesOnlyFlag(cfg: CodegenFields, sources: Sources): string { + return `--types-only=${sources.bindgen + .filter(src => !isCompiled(cfg, src)) + .map(src => basename(src)) + .join(",")}`; } +/** 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`), + const crates = new Map(sources.bindgen.map(src => [src, crateOf(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"))}`); + 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 = outputNames.map(name => resolve(cfg.codegenDir, `${name}.cpp`)); + const headers = [ + ...outputNames.map(name => resolve(cfg.codegenDir, `${name}.h`)), + resolve(cfg.codegenDir, "GeneratedBindings+Tables.h"), + resolve(cfg.codegenDir, "GeneratedBindings+Counts.h"), + ]; + 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"); - // 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: [...sources.bindgen, ...sources.bindgenCodegen], 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, + // 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(",")}`, + typesOnlyFlag(cfg, sources), + `--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`. */ +/** `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[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(""); +} + +/** 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 9d7546b13aa8..1a96331213cc 100644 --- a/scripts/glob-sources.ts +++ b/scripts/glob-sources.ts @@ -67,17 +67,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 */ + bindgenCodegen: { + paths: ["src/codegen/bindgen*.ts", "src/codegen/helpers.ts"], }, /** * all `*.rs` + workspace manifests — implicit inputs to the cargo step. @@ -101,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/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 new file mode 100644 index 000000000000..e6b20a68f283 --- /dev/null +++ b/src/codegen/bindgen-emit.ts @@ -0,0 +1,1433 @@ +// Turns the loaded schema into text. + +import assert from "node:assert"; +import { + asciiLiteral, + ClassType, + cppEmplace, + DefaultType, + DictionaryType, + EnumerationType, + type FunctionDefinition, + type Getter, + indent, + type JSClass, + type Method, + type ModuleDefinition, + NamedType, + OptionalType, + type Origin, + type OverloadDispatch, + rustIdent, + type RustScope, + Signature, + snakeCase, + type Type, + UnionType, + UnitType, + type ValueKind, + WithValueType, +} from "./bindgen-lib-internal.ts"; +import { cap, low } from "./helpers.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[]; + classes: ClassType[]; +} + +const banner = (origin: Origin | null) => + `// Generated by src/codegen/bindgen.ts${origin ? ` from ${origin.stem}.bind.ts` : ""}. Do not edit.\n`; + +// ───────────────────────────── analysis ───────────────────────────── + +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); +} + +/** 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: [], 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))); + 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 EnumerationType) { + if (!type.returned) continue; + 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.firstReturnedMemberNameIndex = tables.memberNames.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) : [])), + ]); + const repeated = symbols.filter((symbol, i) => symbols.indexOf(symbol) !== i); + assert(repeated.length === 0, `two bindings make the same symbol: ${repeated.join(", ")}`); + 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::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` 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: "cell->isHeapBigInt()" }, + string: { ofCell: "cell->isString()" }, + stringObject: { ofCell: "cell->inherits()" }, + array: { ofCell: "JSC::isJSArray(cell)" }, + arrayBuffer: { ofCell: "cell->inherits()" }, + arrayBufferView: { ofCell: "cell->inherits()" }, +}; + +/** + * 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(); + 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, `cell->inherits<${kind.cpp}>()`); + } else { + add(ofValue, kindTests[kind].ofValue?.(value)); + add(ofType, kindTests[kind].ofCell); + } + } + const nullish = [`${value}.isUndefined()`, `${value}.isNull()`]; + const chain = (...tests: Map[]) => + tests + .flatMap(map => [...map]) + .map(([outcome, conditions]) => { + const merged = nullish.every(test => conditions.includes(test)) + ? [`${value}.isUndefinedOrNull()`, ...conditions.filter(test => !nullish.includes(test))] + : conditions; + 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;`]; + if (ofType.size + ofClass.size > 0) { + lines.push(`if (${value}.isCell()) {`, ` auto* cell = ${value}.asCell();`); + lines.push(indent(cppChain(ofType, ofClass), 4), ofValue.size > 0 ? `} else ${cppChain(ofValue)}` : "}"); + } else if (ofValue.size > 0) { + lines.push(cppChain(ofValue)); + } + 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 { + const { size, align } = type.layout(); + return `static_assert(sizeof(${cppType}) == ${size} && alignof(${cppType}) == ${align});`; +} + +function humanReadableEnumeration(type: EnumerationType): string { + 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)}`; +} + +/** Value-initialization zeroes a struct whose constructor is implicit, and these hold the buffers of string views. */ +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 { + return candidates + .map(i => { + const { name, type: alternative } = type.alternatives[i]; + const member = `result.${name}`; + if (alternative.refuses) + return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { + ${member} = WTF::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}; +scope.release(); +${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} +return;`; + }) + .join("\n"); +} + +function cppHeaderForUnion(type: UnionType): string { + const { name, alternatives } = type; + /** 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.refuses; + const cases = outcomes.flatMap((candidates, index) => + candidates.length === 0 + ? [] + : `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::Bindgen::Generated { +struct ${name} { +${cppDefaultConstructor(type)} +${held.map(a => ` ${a.type.convertsInPlace ? `std::optional<${implementationType(a.type)}> ${a.name}` : `${implementationType(a.type)} ${a.name} {}`};`).join("\n")} + uint8_t tag; +}; +struct Extern${name} { + union { +${alternatives.map(a => ` ${externType(a.type)} ${a.name};`).join("\n")} + } data; + uint8_t tag; +}; +${layoutAssert(`Extern${name}`, type)} +} +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; + switch (value.tag) { +${held + .map( + a => ` case ${alternatives.indexOf(a)}: + result.data.${a.name} = ${toExtern(`${a.type.convertsInPlace ? "*" : ""}value.${a.name}`)}; + break;`, + ) + .join("\n")} + } + return result; + } +}; +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::IDLHumanReadableName> {}; +template<> struct WebCore::Converter<${type.idlType}> : Bun::DefaultContextConverter<${type.idlType}> {${ + type.needsLifetime + ? "" + : ` + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + ReturnType result; + convertInto(globalObject, value, ctx, result); + return result; + } +` + } + template + static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) + { + auto* globalObject = &lexicalGlobalObject; + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); +${indent(code, 8)} + switch (choice) { +${indent(cases.join("\n"), 8)} + }${outcomes.some(mayMatchNone) ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);` : ""} + } +}; +`; +} + +function cppHeaderForType(type: NamedType): string { + const { name } = type; + if (type instanceof ClassType) return cppHeaderForClass(type); + if (type instanceof EnumerationType) { + return ` +namespace Bun::Bindgen::Generated { +enum class ${name} : uint32_t { +${type.variants.map(variant => ` ${variant},`).join("\n")} +}; +} +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); +`; + } + if (type instanceof UnionType) return cppHeaderForUnion(type); + assert(type instanceof DictionaryType); + if (!type.accepted) return ""; + return ` +namespace Bun::Bindgen::Generated { +struct ${name} { +${cppDefaultConstructor(type)} +${type.members.map(member => ` ${implementationType(member.type)} ${member.fieldName};`).join("\n")} +}; +struct Extern${name} { +${type.members.map(member => ` ${externType(member.type)} ${member.fieldName};`).join("\n")} +}; +${layoutAssert(`Extern${name}`, type)} +} +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.fieldName} = ${toExtern(`value.${member.fieldName}`)},`).join("\n")} + }; + } +}; +template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("${type.name}"); +}; +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}> {};` + : type.convertedByValue + ? `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/JSWrapper.h"); + headers.delete(`Generated${file.origin.namespace}.h`); + return `${banner(file.origin)}#pragma once +#include "root.h" +#include "Bindgen/ExternTraits.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${cap(fn.name.replace(/^_+/, ""))}`; + +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`; + 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]) => ` { ${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 [first, ...others] = member.names.map( + () => `members.get(${nameIndex++});\nRETURN_IF_EXCEPTION(scope, void());`, + ); + const qualified = `${type.name}.${member.name}`; + 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.takesUndefined) + lines.push(`if (value${i}.isUndefined()) [[unlikely]] { + ctx${i}.throwRequired(*globalObject, scope); + return; +}`); + 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; + return ` +template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) +{ + auto* globalObject = &lexicalGlobalObject; + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + [[maybe_unused]] auto ctx = Bun::Bindgen::LiteralConversionContext { ${asciiLiteral(type.name)} }; + auto* object = value.getObject(); +${ + type.nonObjectAsEmpty + ? ` if (!object || object->isCallable()) { +${indent(type.cppAssign("result", {}), 8)} + return; + }` + : ` if (!object) [[unlikely]] { + ctx.throwNotObject(*globalObject, scope); + return; + }` +} +${ + type.allowArray + ? "" + : ` bool isArray = JSC::isArray(globalObject, value); + RETURN_IF_EXCEPTION(scope, void()); + if (isArray) [[unlikely]] { + ctx.throwNotObject(*globalObject, scope); + return; + } +` +} Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); +${members.join("\n")} +} +${ + type.needsLifetime || !type.convertedByValue + ? "" + : ` +template<> ${type.cpp} WebCore::convertDictionary<${type.cpp}>(JSC::JSGlobalObject& globalObject, JSC::JSValue value) +{ + ${type.cpp} 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) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ${type.cpp} converted; + Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted); + RETURN_IF_EXCEPTION(scope, false); + *result = ${toExtern("converted")}; + return true; +} +` + : "" + }`; +} + +const overloadSuffix = (fn: FunctionDefinition, signature: Signature) => + 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[] { + 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. */ +function cppCallBody(signature: Signature, symbol: string, leading: readonly string[] = []): string { + const lines: string[] = []; + 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)} };`); + // Converting a later argument can run JavaScript that detaches or resizes the buffer. + if (type.borrowsHeap && i < signature.args.length - 1) + rereads.push( + `converted${i} = ${type.cppConvert(`argument${i}.value()`, `ctx${i}`, name)};`, + `scope.assertNoException();`, + ); + lines.push(...cppDeclareConverted(type, `converted${i}`, `argument${i}.value()`, `ctx${i}`, name)); + if (type.conversionThrows) lines.push(`RETURN_IF_EXCEPTION(scope, {});`); + }); + lines.push(...rereads); + const args = signature.args.map(({ type }, i) => { + return type instanceof WithValueType + ? `${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"); +} + +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 ───────────────────────────── + +/** 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) => + `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)), + ...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.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); } +}; +using JS${name} = JSWrapper<${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.cached + ? ` 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}`)} }; + ${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")})); +} +`; + } + 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::${low(name)}${cap(builtin.name)}CodeGenerator, ${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::Private), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum);`, + ) + .join("\n")} +} +` + : "" + } +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"}, + .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})); +} + +${ + 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.numberOfInternalFields; + +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 cppDispatch = (dispatch: OverloadDispatch): string => { + if (dispatch instanceof Signature) + return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, dispatch)}(globalObject, callFrame));`; + 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}) { +${[...cases.slice(1), cases[0]].join("\n")} +}`; + }; + definition = `${overloads.join("\n")} +JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ +${cppPrologue} +${indent(cppDispatch(fn.dispatch), 4)} +}`; + } + 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(JSC::getVM(globalObject), 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`); + 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 "ZigGlobalObject.h" +${[...headers].map(header => `#include "${header}"\n`).join("")}${systemHeaders.map(header => `#include <${header}>\n`).join("")}${file.types + .map(type => + type instanceof EnumerationType + ? 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 { +// At least 1: an array cannot be empty. +inline constexpr unsigned numberOfClasses = ${Math.max(tables.classes.length, 1)}; +} +`; +} + +export function cppTables(tables: Tables): string { + 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 +#include + +namespace Bun::Bindgen { +struct ClassDefinition; +} + +namespace Bun::Bindgen::Generated { +${tables.classes.map(type => `extern const ClassDefinition JS${type.name}ClassDefinition;`).join("\n")} +} + +namespace Bun::Bindgen::Tables { + +${array("WTF::ASCIILiteral", "memberNames", tables.memberNames.map(asciiLiteral))} + +${array("WTF::ASCIILiteral", "enumerationValues", tables.enumerationValues.map(asciiLiteral))} + +${array( + "const ClassDefinition*", + "classes", + tables.classes.map(type => `&Generated::JS${type.name}ClassDefinition`), +)} + +} +`; +} + +// ───────────────────────────── Rust ───────────────────────────── + +const jsValueResult = "bindgen::Result<'a, bindgen::JsLocal<'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) => 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: [], + heap: ["heap: &bindgen::JsHeap<'a>"], + js: ["cx: &mut bindgen::JsScope<'a>"], +}; +const capabilityArgument = { none: [], heap: ["&cx.heap"], js: ["cx"] }; + +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)] +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::JsScope::with(global, |cx| { +${indent(body, 12)} + }) + } +} +`; + +function rustForEnumeration(type: EnumerationType): 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")} +} +${ + type.returned + ? `impl ${type.name} { + #[inline] + pub fn to_js<'a>(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + // SAFETY: the values of this enumeration start there in the table. + unsafe { 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)} +${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++${lifetime && " for the call `heap` belongs to"} and is read once. + #[doc(hidden)] + 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 { +${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 = ` +${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")} +} +`; + 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++${lifetime && " for the call `heap` belongs to"} and is read once. + #[doc(hidden)] + pub unsafe fn from_extern(value: &Extern${type.name}${lifetime && ", heap: &bindgen::JsHeap<'a>"}) -> 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 ${type.name}${lifetime ? "<'_>" : ""} { + ${ + lifetime + ? `/// Converts \`value\` and lends it to \`f\`. + pub fn from_js( + global: &JSGlobalObject, + value: JSValue, + 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${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()) })` + } + } +} +`; + } + if (type.returned) { + const value = (member: (typeof type.members)[number]) => { + const field = `self.${member.rustName}`; + 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()`; + }; + text += `impl${lifetime} ${type.name}${lifetime} { + pub fn into_js${lifetime ? "" : "<'a>"}(self, heap: &bindgen::JsHeap<'a>) -> ${jsValueResult} { + let values = [ +${type.members.map(member => ` ${value(member)},`).join("\n")} + ]; + // SAFETY: the names of these members start there in the table. + unsafe { bindgen::to_js::object(heap, ${type.firstReturnedMemberNameIndex}, &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), &cx.heap);`; + 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)].join(", ")})`; + thunks += rustThunk( + classSymbolName(type, "construct"), + ["structure: *mut core::ffi::c_void", ...rustExternParameters(signature, scope)], + "JSValue", + `${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()), heap) } + }`); + } + + 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), + ].join(", ")})`; + thunks += rustThunk( + methodSymbolName(type, method), + [...(isStatic ? [] : ["this: *mut core::ffi::c_void"]), ...rustExternParameters(method, scope)], + "JSValue", + `${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)})`, + ); + } + + 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 core::ffi::c_void"], + "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 core::ffi::c_void", `value: ${getter.type.rustExternType(scope)}`], + "bool", + `${takeThis} +${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()`, + ); + } + + const fieldDeclarations = type.internalFields + .map(field => { + const id = snakeCase(field); + 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::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::JsLocal<'a, T>) { + // SAFETY: internal field ${index} of ${type.numberOfInternalFields}. + 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;${ + 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) } + } +} +// 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}>(); +#[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.internalFields.length > 0 + ? `/// The JS values a \`${name}\` holds. +pub trait ${name}InternalFields<'a>: Copy { +${fieldDeclarations} +} +impl<'a> ${name}InternalFields<'a> for bindgen::JsWrapper<'a, ${rustPath}> { +${fieldDefinitions} +} +` + : "" +}${thunks}`; +} + +function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string { + const scope = file.origin; + return fn.signatures + .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)].join(", ")})`; + return rustThunk( + thunkSymbol(fn, signature), + rustExternParameters(signature, scope), + "JSValue", + `${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)})`, + ); + }) + .join(""); +} + +export function rustForCrate(files: readonly SchemaFile[]): string { + return ( + banner(null) + + files + .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, + clippy::redundant_closure +)] +pub mod ${file.origin.rustModule} { + use bun_jsc::{JSGlobalObject, JSValue, bindgen}; +${indent( + file.types + .map(type => + type instanceof EnumerationType + ? rustForEnumeration(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, 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 => + file.types.map(type => { + if (type instanceof DictionaryType) { + 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} { +${[ + ...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};`, + ), + ...type.builtins.map(builtin => member(builtin.name, false, "(...args: any[]) => any")), +].join("\n")} + }${ + type.construct + ? ` + interface ${type.name}Constructor { +${[ + ` 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 \`$bindgen(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..e565e7bdd68d 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1,1061 +1,1754 @@ -// 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"; +// The type model behind `bindgen-lib.ts`. + 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; - -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(); - -/** 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)); - -// Return symbol names of extern values (must be equivalent on both sides of the FFI boundary) - -/** 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}`; - -interface TypeDataDefs { - /** The name */ - ref: string; - - sequence: { - element: TypeImpl; - repr: "slice"; - }; - record: { - value: TypeImpl; - repr: "kv-slices"; - }; - stringEnum: string[]; - oneOf: TypeImpl[]; - dictionary: DictionaryField[]; +import util from "node:util"; + +export interface Layout { + size: number; + align: number; } -type TypeData = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any; -export const enum NodeValidator { - validateInteger = "validateInteger", +/** Where a named type or function was declared. */ +export interface Origin { + /** `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; } -interface Flags { - nodeValidator?: NodeValidator; - optional?: boolean; - required?: boolean; - nonNull?: boolean; - default?: any; - range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"]; - finite?: boolean; +/** Where Rust code is being emitted, so paths to named types can be spelled from there. */ +export type RustScope = Pick; + +const roundUp = (n: number, align: number) => Math.ceil(n / align) * align; + +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 }; } -export interface DictionaryField { - key: string; - type: TypeImpl; +/** `{ union data; uint8_t tag; }`. */ +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 }]); } -export declare const isType: unique symbol; +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(); +} -const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]); +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(""); +} -/** - * 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; +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 abstract become box do final macro override priv try typeof unsized virtual yield".split( + " ", + ), +); +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; +} - declare [isType]: true; +/** 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( + " ", + ), +); - constructor(kind: K, data: TypeData, flags: Flags = {}) { - this.kind = kind; - this.data = data; - this.flags = flags; - this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts"); - } +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}`); +} - isVirtualArgument() { - return this.kind === "globalObject" || this.kind === "virtualMachine"; - } +export function asciiLiteral(value: string): string { + assert(!/[^\x20-\x7e]/.test(value), `must be printable ASCII: ${util.inspect(value)}`); + return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"_s`; +} - 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; - } - - /** - * If this type lowers to a named type (struct, union, enum) - */ - 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; - } - } +/** 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" + | "boolean" + | "number" + | "bigint" + | "string" + | "stringObject" + | "array" + | "arrayBuffer" + | "arrayBufferView" + | JSClass; + +/** The instances of a C++ `JSCell` class. */ +export interface JSClass { + readonly cpp: string; +} - 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; - - 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)); +type ValueKinds = readonly ValueKind[] | "any"; + +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, in order: all but the last refuse some. Equal lists are one array. */ +function alternativesByKind(alternatives: readonly Type[]): Map { + const lists = new Map(); + return new Map( + distinguishingKinds(alternatives).map(kind => { + 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}`)!]; + }), + ); +} - 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}`); - } - - 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; - } +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"); +} - 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; - } +/** 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`; + return `auto& ${target} = ${optional}.emplace();\n${fill(target)}`; +} - 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; +export abstract class Type { + /** `undefined` means absent. */ + get optional(): OptionalType { + return new OptionalType(this, "optional"); } - - #generateName() { - return `bindgen_${this.ownerFile}_${this.hash()}`; + /** `null` or `undefined` means absent. */ + get nullable(): OptionalType { + return new OptionalType(this, "nullable"); } - - /** - * 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; - } - - 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); - - 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; - } + /** Every falsy value means absent. */ + get falsyAsNull(): OptionalType { + return new OptionalType(this, "falsyAsNull"); + } + /** Converted for use now, and the value itself for whatever holds it longer. Only for arguments. */ + get withValue(): WithValueType { + return new WithValueType(this); + } + /** Value to use when absent. Only for arguments and dictionary members. */ + default(value: unknown): DefaultType { + return new DefaultType(this, value); } - #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"); - } + /** The C++ tag type whose `WebCore::Converter` turns a JSValue into a C++ value. */ + abstract readonly idlType: string; + /** TypeScript spelling. */ + abstract readonly tsType: string; - // 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); + /** How `IDLNullable::ImplementationType` says "null". */ + get nullRepresentation(): "optional" | "pointer" { + return "optional"; + } + /** Converting `undefined` succeeds, so a dictionary member of this type is not required. */ + 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"; + } + /** 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); + } + /** 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); + } + /** 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); + } + /** `rustType` is `Copy`. */ + get rustIsCopy(): boolean { + return this.dependencies.every(type => type.rustIsCopy); + } + 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); + } - 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)}`); - } - } + /** A C++ expression of `idlType::ImplementationType` for a default value written in the schema. */ + 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)};`; + } + /** 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})`; + } + /** 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 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)};`; + } - return new TypeImpl(this.kind, this.data, { - ...this.flags, - range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max], - }); + 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`. What has a lifetime takes it from `heap`, in scope there. */ + rustFromExtern(expr: string, scope: RustScope): string { + return expr; } - 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}`); - } + /** 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::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`); } +} - 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}`); +// ───────────────────────────── primitives ───────────────────────────── + +class PrimitiveType extends Type { + readonly idlType: string; + readonly tsType: string; + readonly rustName: string; + readonly 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", `${util.inspect(value)} is not a boolean`); + 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} }`; } - - 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"); + toRust(value: unknown): string { + return this.rustName === "f64" ? `${value}_f64` : 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})`; } +} - isIgnoredUndefinedType() { - return this.kind === "undefined"; +export class BoolType extends PrimitiveType { + constructor(idlType = "Bun::Bindgen::IDLStrictBoolean", valueKinds: ValueKinds = ["boolean"]) { + super(idlType, "boolean", "bool", 1, valueKinds); + } + /** `ToBoolean`: any value is accepted. */ + get loose(): BoolType { + return new BoolType("WebCore::IDLBoolean", "any"); + } + get conversionThrows(): boolean { + return this.valueKinds !== "any"; } +} + +interface IntegerInfo { + rust: string; + cpp: string; + size: number; + /** The WebIDL integer type of this width. */ + webidl: string; + min: number | bigint; + max: number | bigint; +} - isStringType() { - return ( - this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String" +export class IntegerType extends PrimitiveType { + readonly info: IntegerInfo; + /** Only the strict conversion is given no `idlType`. */ + constructor(info: IntegerInfo, idlType?: string) { + super( + idlType ?? `Bun::Bindgen::IDLStrictInteger<${info.cpp}>`, + "number", + info.rust, + info.size, + idlType ? "any" : ["number", "bigint"], ); + this.info = info; + } + 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::Bindgen::IDLLooseInteger<${this.info.cpp}>`); + } + /** WebIDL conversion: `ToNumber`, then wrap modulo the width. */ + get wrap(): IntegerType { + 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, "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, "validateInteger"); + } +} - isNumberType() { - return numericTypes.has(this.kind); - } - - isObjectType() { - return this.kind === "externalClass" || this.kind === "dictionary"; - } - - [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") - : "") +export class RangeType extends IntegerType { + readonly modifier: "enforceRange" | "validateInteger"; + readonly #range: readonly [number, number]; + 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}`); + const tag = modifier === "enforceRange" ? "IDLEnforceRange" : "IDLValidateInteger"; + super(info, `Bun::Bindgen::${tag}<${info.cpp}, ${min}, ${max}>`); + this.modifier = modifier; + this.#range = [min, max]; } - - // 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, - }); + protected get range() { + return this.#range; } - - 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, - }); + 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"); + } + cppConvert(value: string, ctx: string, name: string): string { + const reported = this.modifier === "validateInteger" ? `, ${asciiLiteral(name)}` : ""; + return `WebCore::Converter<${this.idlType}>::convert(*globalObject, ${value}${reported})`; + } +} - 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, - }); +const integer = (rust: string, cpp: string, size: number, webidl: string, min: number | bigint, max: number | bigint) => + new IntegerType({ rust, cpp, size, webidl, min, max }); + +export const integers = { + u8: integer("u8", "uint8_t", 1, "IDLOctet", 0, 255), + 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), + 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() { + super("Bun::Bindgen::IDLStrictDouble", "number", "f64", 8, ["number"]); } +} - 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, - }); +/** 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::Bindgen::IDLStrictNull" : ""; + } + get valueKinds(): ValueKinds { + return this.tsType === "null" ? ["undefined", "null"] : ["undefined"]; + } + get takesUndefined(): 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 }; + } + rustExternType(): string { + return "u8"; + } + rustType(): string { + return "()"; + } + rustFromExtern(): string { + return "()"; + } + rustToJS(value: string): string { + assert(this.tsType === "undefined", "leave `returns` out, or return a nullable type"); + return `bindgen::to_js::undefined(heap, ${value})`; + } +} - clamp(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "clamp"); +// ───────────────────────────── 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"; + readonly #valueKinds: ValueKinds; + constructor(idlType = "Bun::Bindgen::IDLStrictString", valueKinds: ValueKinds = ["string"]) { + super(); + this.idlType = idlType; + this.#valueKinds = valueKinds; + } + get valueKinds(): ValueKinds { + return this.#valueKinds; + } + get rustIsCopy(): boolean { + return false; + } + /** `ToString`: any value is accepted. */ + get loose(): StringType { + 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 { + return new Utf8Type(this.valueKinds === "any"); + } + get nullRepresentation() { + return "pointer" as const; + } + toCpp(value: unknown): string { + 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)})`; + } + 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})`; + } +} - enforceRange(min?: number | bigint, max?: number | bigint) { - return this.#rangeModifier(min, max, "enforce"); +/** 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"); + } + toCpp(): never { + throw new TypeError("b.string.utf8.owned cannot have a default"); + } + rustType(): string { + return "bun_core::Utf8Bytes<'static>"; + } + rustFromExtern(expr: string): string { + return `${super.rustFromExtern(expr)}.into_utf8()`; + } + rustToJS(): string { + throw new TypeError("return b.string.utf8"); } +} - 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: `Bun::Bindgen::JSStringUTF8View`. */ +export class Utf8Type extends Type { + readonly isLoose: boolean; + readonly tsType = "string"; + constructor(isLoose: boolean) { + super(); + 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 { + return this.isLoose + ? new OwnedUtf8Type("WebCore::IDLDOMString", "any") + : new OwnedUtf8Type("Bun::Bindgen::IDLStrictStringOrStringObject", this.valueKinds); + } + get valueKinds(): ValueKinds { + return this.isLoose ? "any" : ["string", "stringObject"]; + } + 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});`; + } + cppAssign(target: string, value: unknown): string { + assert(typeof value === "string", `${util.inspect(value)} is not a string`); + return `${target}.initialize(${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(heap)`; + } + 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]); +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 { + readonly idlType = "Bun::Bindgen::IDLBufferSourceBytes"; + readonly tsType = "ArrayBuffer | ArrayBufferView"; + get valueKinds(): ValueKinds { + return bufferSourceKinds; + } + get borrowsHeap(): boolean { + return true; + } + get nullRepresentation() { + return "pointer" as const; + } + 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(heap)`; + } +} - 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 valueKinds(): ValueKinds { + return bufferSourceKinds; + } + get refuses() { + return sharedBuffers; + } + /** `&[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 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::Bindgen::IDLArrayBufferRef", + this.tsType, + "bun_jsc::JSCArrayBuffer", + null, + bufferSourceKinds, + sharedBuffers, + ); + } + get holdsJSValue(): boolean { + return true; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "JSValue"; } + rustType(): string { + return "bindgen::JsLocal<'a, bindgen::BufferSource>"; + } + rustFromExtern(expr: string): string { + return `bindgen::JsLocal::from_extern(${expr}, heap)`; + } + 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 host function passes the argument itself along. */ +export class WithValueType extends Type { + readonly inner: Type; + constructor(inner: Type) { + super(); + assert(!(inner instanceof DefaultType), ".withValue cannot be combined with .default()"); + assert(!(inner instanceof WithValueType), "already .withValue"); + this.inner = inner; + } + get idlType(): string { + return this.inner.idlType; + } + get tsType(): string { + return this.inner.tsType; + } + 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; + } + 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); + } + 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::JsLocal::from_extern(with_value.value, heap) } }`; } } -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::Bindgen::IDLRawAny"; + readonly tsType = "any"; + get holdsJSValue(): boolean { + return true; + } + 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()"; + } + toRust(): string { + return "bindgen::JsLocal::UNDEFINED"; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "JSValue"; + } + rustType(): string { + return "bindgen::JsLocal<'a>"; + } + rustFromExtern(expr: string): string { + return `bindgen::JsLocal::from_extern(${expr}, heap)`; + } + 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; + readonly #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 refuses() { + return this.#refuses; + } + get rustIsCopy(): boolean { + return false; + } + get nullRepresentation() { + return "pointer" as const; + } + getHeaders(out: Set): void { + if (this.header) out.add(this.header); + } + 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 inner: Type; + readonly modifier: "optional" | "nullable" | "falsyAsNull"; + constructor(inner: Type, modifier: "optional" | "nullable" | "falsyAsNull") { + super(); + 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"); + assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); + this.inner = inner; + this.modifier = modifier; + } + get idlType(): string { + assert(!(this.inner instanceof RangeType), "a range cannot be absent; give it a .default()"); + const template = { + optional: "WebCore::IDLOptional", + nullable: "WebCore::IDLNullable", + falsyAsNull: "Bun::Bindgen::IDLFalsyAsNull", + }[this.modifier]; + return `${template}<${this.inner.idlType}>`; + } + get tsType(): string { + return `${this.inner.tsType} | ${this.modifier === "optional" ? "undefined" : "null | undefined"}`; + } + 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) : [])]; + } + get dependencies(): readonly Type[] { + return [this.inner]; + } + get convertsInPlace(): boolean { + return this.inner.convertsInPlace; + } + /** 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 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 (${present}) {\n${indent(body, 4)}\n}`; + } + toCpp(value: unknown): string { + 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.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.inner.toRust(value, scope)})`; + } + layout(): Layout { + return this.inner.nullRepresentation === "optional" + ? taggedUnionLayout([{ size: 1, align: 1 }, this.inner.layout()]) + : this.inner.layout(); + } + rustExternType(scope: RustScope): string { + return this.inner.nullRepresentation === "optional" + ? `bindgen::extern_types::ExternOptional<${this.inner.rustExternType(scope)}>` + : this.inner.rustExternType(scope); + } + rustType(scope: RustScope): string { + return `Option<${this.inner.rustType(scope)}>`; + } + rustFromExtern(expr: string, scope: RustScope): string { + if (this.inner.nullRepresentation === "optional") { + 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(heap)`; + return `{ let value = ${expr}; if value.is_null() { None } else { Some(${this.inner.rustFromExtern("value", scope)}) } }`; + } + rustResultFieldType(scope: RustScope): string { + return `Option<${this.inner.rustResultFieldType(scope)}>`; + } + rustReturnType(scope: RustScope): string { + return `Option<${this.inner.rustReturnType(scope)}>`; + } + 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::JsLocal::${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 inner: Type; + readonly value: unknown; + /** `null` also takes the default. */ + readonly onNull: boolean; + constructor(inner: Type, value: unknown) { + super(); + 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 { - 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.inner = inner; + this.onNull = false; } + this.value = value; + // For what it asserts about `value`. + this.inner.cppAssign("", value); + } + get idlType(): string { + return this.inner.idlType; + } + get tsType(): string { + return `${this.inner.tsType} | ${this.onNull ? "null | undefined" : "undefined"}`; + } + get takesUndefined(): boolean { + return true; + } + get isOptional(): boolean { + return true; + } + get conversionThrows(): boolean { + return this.inner.conversionThrows; + } + get dependencies(): readonly Type[] { + return [this.inner]; + } + toCpp(value: unknown): string { + return this.inner.toCpp(value); + } + toRust(value: unknown, scope: RustScope): string { + return this.inner.toRust(value, scope); + } + get valueKinds(): ValueKinds { + 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.inner.cppConvert(value, ctx, name)}`; + } + get convertsInPlace(): boolean { + 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.inner.cppConvertInto(target, value, ctx, name), 4)} +}`; + } + cppAssign(target: string, value: unknown): string { + return this.inner.cppAssign(target, value); + } + layout(): Layout { + return this.inner.layout(); + } + rustExternType(scope: RustScope): string { + return this.inner.rustExternType(scope); + } + rustType(scope: RustScope): string { + return this.inner.rustType(scope); + } + rustFromExtern(expr: string, scope: RustScope): string { + return this.inner.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 { + get rustIsCopy(): boolean { + return false; + } + readonly element: 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", + ); + 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; + else if (!(type instanceof NamedType)) type.dependencies.forEach(markConvertedByValue); + }; + markConvertedByValue(element); + } + get idlType(): string { + assert( + !this.element.borrowsFrame, + "an array argument cannot hold borrowed views; use b.string or b.string.utf8.owned", + ); + return `Bun::Bindgen::IDLArray<${this.element.idlType}>`; + } + get tsType(): string { + return `(${this.element.tsType})[]`; + } + get valueKinds(): ValueKinds { + return ["array"]; + } + get dependencies(): readonly Type[] { + return [this.element]; + } + getHeaders(out: Set): void { + out.add("Bindgen/ExternVectorTraits.h"); + super.getHeaders(out); + } + 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 { + const element = this.element.rustFromExtern("value", scope); + return element === "value" ? `${expr}.into_vec()` : `${expr}.into_vec_with(|value| ${element})`; + } + 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 { + /** The name it is exported under, and where. Set when the generator finds it among a file's exports. */ + name = ""; + origin: Origin | null = null; + get cpp(): string { + 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`); + out.add(`Generated${this.origin.namespace}.h`); + } + protected path(scope: RustScope, name: string): string { + assert(this.origin, `${this.constructor.name}: used but not exported from a .bind.ts file`); + 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; + } + 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 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. */ + 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 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, `a value is listed twice: ${all.join(", ")}`); + 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(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}>`; + } + 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 one of ${this.tsType}`); + 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; + /** 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[]; } -/** - * 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[]; + /** 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.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 { + return this.names[0]; + } + get rustName(): string { + return rustIdent(this.fieldName); + } } -export interface TypeDef { - name: string; - type: TypeImpl; +export interface DictionaryOptions { + /** 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. */ + allowArray?: boolean; + /** A value whose `typeof` is not "object" gives every member its default: Node's `if (typeof options !== "object") options = null`. */ + nonObjectAsEmpty?: 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 options: DictionaryOptions; + readonly members: readonly Member[]; + /** Index in `Tables::memberNames` of the first name read, and of the first name written. Set by the generator. */ + firstMemberNameIndex = -1; + firstReturnedMemberNameIndex = -1; + /** 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( + !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; 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 fromJS(): boolean { + return !!this.options.fromJS; + } + get allowArray(): boolean { + return !!this.options.allowArray; + } + get nonObjectAsEmpty(): boolean { + return !!this.options.nonObjectAsEmpty; + } + get idlType(): string { + return `WebCore::IDLDictionary<${this.cpp}>`; + } + 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::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, + `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( + 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.takesUndefined, `missing key: ${member.name}`); + return member.type.cppAssign(`${target}.${member.fieldName}`, field); }); + return fields.filter(text => text.length > 0).join("\n"); + } + /** Every member can be absent. */ + get hasDefault(): boolean { + return this.members.every(member => member.type.takesUndefined); } + toRust(value: unknown, scope: RustScope): string { + const given = value as Record; + 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())); + } + 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}${this.needsLifetime ? ", heap" : ""})`; + } + rustToJS(value: string, scope: RustScope): string { + return `${value}.into_js(heap)`; + } +} - const func: Func = { - [isFunc]: true, - name: "", - snapshot, - sourceFile, - variants, - }; - file.functions.push(func); - return func; +/** 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) { + 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", + ); + 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 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`, + ); + } + 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. Move it earlier or remove it`, + ), + ); + } + get alternativesByKind() { + return alternativesByKind(this.alternatives.map(a => a.type)); + } + 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::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});`; + } + get tsType(): string { + return this.alternatives.map(a => a.type.tsType).join(" | "); + } + get takesUndefined(): boolean { + return this.alternatives.some(a => a.type.takesUndefined); + } + get dependencies(): readonly Type[] { + return this.alternatives.map(a => a.type); + } + /** 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 { + 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 { + 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())); + } + 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}${this.needsLifetime ? ", heap" : ""})`; + } } -function validateVariant(variant: any) { - let minRequiredArgs = 0; - let seenOptionalArgument = false; - let i = 0; +// ───────────────────────────── functions ───────────────────────────── + +/** 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 { + 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; +} - 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; - } - 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; +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; + 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", + `${name}: JavaScript can detach a buffer; take b.bufferSource without .bytes and use with_bytes`, + ); } + 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"', + ); + } + /** The arguments up to the last that is not optional: what a caller writes at least. */ + get requiredCount(): number { + return this.args.findLastIndex(arg => !arg.type.isOptional) + 1; } - - 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]; +/** Call a signature, or look at the kind of one more argument. */ +export type OverloadDispatch = Signature | { argument: number; byKind: Map }; + +export class FunctionDefinition { + readonly signatures: readonly Signature[]; + readonly dispatch: OverloadDispatch; + name = ""; + origin: Origin | null = null; + 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()); + this.signatures.forEach((signature, i) => + assert( + chosen.has(signature), + `overload ${i + 1} is never chosen: the ones before it take every call it would. Move it earlier or remove it`, + ), + ); + } + /** "Overloads" in the docs. Equal inputs give the same object of `nodes`: nodes are compared by identity. */ + private dispatchOn( + candidates: readonly Signature[], + argument: number, + nodes: Map, + ): OverloadDispatch { + const key = `${candidates.map(signature => this.signatures.indexOf(signature))} ${argument}`; + const existing = nodes.get(key); + if (existing) return existing; + 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") => { + const takers = candidates.filter((signature, i) => { + if (!types[i]) return true; + return kind === "undefined" ? types[i].takesUndefined : takes(types[i], kind); + }); + return this.dispatchOn(takers.length > 0 ? takers : candidates, argument + 1, nodes); + }; + 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; + } + 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 { + /** `&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. */ + 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 export of this name in `src/js/builtins/.ts`. */ +export interface BuiltinField { + builtin: true; + 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. */ + 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; } -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 = rustIdent(snakeCase(name.replace(/^_+/, ""))); + 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; + readonly cached: boolean; + /** Where a cached one keeps the value. Set by its class. */ + 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`); + assert( + !this.type.holdsJSValue || this.capability !== "none", + `${name}: returning a JS value takes capability: "heap" or "js"`, + ); + this.cached = !!options.cache; } - - 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 an argument, a result or a getter of the class's own members, as it is: not .nullable, not in an array", + ); } - - 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; 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(options: ClassOptions) { + super(); + 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; 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) { + 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; + this.builtins = builtins; + this.staticMethods = Object.entries(options.static ?? {}).map( + ([name, field]) => new Method(name, resolve(field), true), + ); + 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 === this), + `${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 numberOfInternalFields(): number { + return this.internalFields.length + this.cachedGetters.length; } - - add(s: string) { - this.buffer += (this.buffer.endsWith("\n") ? " ".repeat(this.level) : "") + s; + get cpp(): string { + return `Bun::Bindgen::Generated::JS${this.name}`; } - - indent() { - this.level += 1; + get idlType(): string { + return `Bun::Bindgen::IDLRawInterface<${this.cpp}>`; } - - dedent() { - this.level -= 1; + get valueKinds(): ValueKinds { + return [this]; } - - trimLastNewline() { - this.buffer = this.buffer.trimEnd(); + get holdsJSValue(): boolean { + return true; } - - resetTemporaries() { - this.temporaries.clear(); + get nullRepresentation() { + return "pointer" as const; + } + /** 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), + ]; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "*mut core::ffi::c_void"; } + 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}), heap)`; + } + 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..1ae732912544 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -1,339 +1,85 @@ -/** - * 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 as `b`: 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; -} - -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; - -export type AcceptedDictionaryTypeKind = Exclude; - -function builtinType() { - return (kind: K) => new TypeImpl(kind, undefined as any, {}) as Type as Type; -} - -/** 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"); - - /** - * 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"); - - /** - * Equivalent to IDL's `unrestricted double`, allowing NaN and Infinity. - * To restrict to finite values, use `f64.finite`. - */ - export const f64 = builtinType()("f64"); - - 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 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"); - - /** An array or iterable type of T */ - export function sequence(itemType: Type): Type, "sequence"> { - return new TypeImpl("sequence", { - element: itemType as TypeImpl, - repr: "slice", - }); - } - - /** 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", - }); - } - - /** - * 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); - } - - /** - * 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); - } - - 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 function dictionary>>( - fields: R, - ): Type< - { - [K in keyof R]?: R[K] extends Type ? T : never; - }, - "dictionary" - > { - return dictionaryImpl(fields as Record); - } - - /** 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()); - } -} - -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; - -export type FuncReference = { [isFunc]: true }; - -export interface FuncVariant { - /** Ordered record. Cannot include ".required" types since required is the default. */ - args: Record>; - ret: Type; -} - -export function fn(opts: FuncOptions) { - return registerFunction(opts) as FuncReference; -} + AnyType, + ArrayType, + BoolType, + BufferSourceType, + type ClassOptions, + ClassType, + type DictionaryOptions, + DictionaryType, + DoubleType, + EnumerationType, + FunctionDefinition, + integers, + type MemberOptions, + ModuleDefinition, + RefType, + type SignatureOptions, + StringType, + type Type, + UnionType, + UnitType, +} from "./bindgen-lib-internal.ts"; + +export type { Capability, Type } from "./bindgen-lib-internal.ts"; +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 { u8, u16, i32, u32, u64, usize } = integers; + +/** Strict: only a number. */ +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 `JsLocal<'a>`. */ +export const any = new AnyType(); + +/** 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`. */ +export const blob = new RefType( + "Bun::Bindgen::IDLBlobRef", + "Blob", + "bun_jsc::webcore_types::Blob", + "Bindgen/IDLConvertBlob.h", + [{ cpp: "WebCore::JSBlob" }], + "detached Blobs", +); + +const null_ = new UnitType("null"); + +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 EnumerationType(values); + +export const dictionary = (members: Record, options?: DictionaryOptions) => + new DictionaryType(members, options); + +/** 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. */ +const class_ = (options: ClassOptions) => new ClassType(options); +export { class_ as class }; + +/** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */ +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 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. */ +export const module = (options: { rustPath: string }) => new ModuleDefinition(options); diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index c89c1579bab2..80bc8cf72b35 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -1,1283 +1,110 @@ -// The binding generator to rule them all. -// Converts binding definition files (.bind.ts) into C++ and native binding code. -// -// 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::`). +#!/usr/bin/env bun +// Usage: bindgen.ts --sources==,.. --types-only=,.. --codegen-path= --types-path= (emitBindgen in scripts/build/codegen.ts) + import assert from "node:assert"; -import * as path from "node:path"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; 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); + 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"; + +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 = { stem, namespace: pascalCase(stem), rustModule: snakeCase(stem), crate }; + const result: SchemaFile = { origin, moduleDefinition: null, types: [], functions: [] }; + 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; + } 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(/^[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 { - 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; + throw new TypeError(`${stem}.bind.ts: export ${name} is not a type, a function or b.module()`); } } - - 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", +/** 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); }; - 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); - } else { - externLayout.emitCpp(w, type.name()); - w.line(); - } - 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}`); - } -} - -/** 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); - }; - - 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(`}`); - } - } -} - -// BEGIN MAIN CODE GENERATION - -// Search for all .bind.ts files -const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["node_modules", ".git"], [".bind.ts"]); - -// 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(); -} - -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); -} + file.types.forEach(visit); + file.types = sorted; +} + +const args = argParse(["sources", "types-only", "codegen-path", "types-path"]); +const codegenPath: string = args["codegen-path"]; +const sources = (args.sources as string) + .split(",") + .filter(source => source.length > 0) + .sort(); + +const allFiles: SchemaFile[] = []; +for (const source of sources) allFiles.push(await load(source)); +allFiles.forEach(sortTypes); +const typesOnly = ((args["types-only"] as string | undefined) ?? "").split(","); +/** What this build compiles. The types describe every schema. */ +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) { + 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); + +/** 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`), 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)); +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(allFiles)); 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/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 487c66281fb1..ab54aac0fa0e 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -54,7 +54,6 @@ const rustIdentifierPaths: Record = { "SecureContext.rs": "runtime/api/bun/SecureContext.rs", "SemverObject.rs": "semver_jsc/SemverObject.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", @@ -78,7 +77,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", @@ -128,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 3aaf4f3132c0..23a73fccbb88 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -129,6 +129,8 @@ export const enums = { // These are passed to --define to the bundler const debug = process.argv[2] === "--debug=ON"; +/** 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), @@ -171,7 +173,7 @@ export const function_replacements = [ "$isPromiseFulfilled", "$isPromiseRejected", "$isPromisePending", - "$bindgenFn", + "$bindgen", ]; const function_regexp = new RegExp(`__intrinsic__(${function_replacements.join("|").replaceAll("$", "")})`); @@ -267,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 { @@ -286,6 +288,8 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } + 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); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; 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 75cd53e615fb..22edbb91820f 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/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 a24ddf662bff..fa6de0bb5c6a 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"); @@ -242,9 +242,34 @@ 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; +// 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"), + 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"); @@ -580,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 3326d8093d54..39ca250ef850 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -102,16 +102,29 @@ function bound(binding) { : $bundleError("TODO: endianness"); }, freemem: binding.freemem, - getPriority: binding.getPriority, - homedir: binding.homedir, - hostname: binding.hostname, - loadavg: binding.loadavg, - networkInterfaces: binding.networkInterfaces, + // 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); + }, + 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 binding.setPriority(pid, priority); + }, get tmpdir() { return tmpdir; }, @@ -127,8 +140,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) — @@ -152,7 +169,22 @@ function bound(binding) { }; } -const out = bound($rust("node_os.rs", "createNodeOsBinding")); +const out = bound({ + 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"); symbolToStringify(out, "availableParallelism"); diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 5bfc1139d1b0..53ffd831248e 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -273,13 +273,17 @@ 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). + * `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 function to call. + * @param name - The name of the export. */ -declare function $bindgenFn any>(filename: string, symbol: string): T; +declare function $bindgen( + filename: File, + name: Name, +): $Bindgen.Exports[File][Name]; // NOTE: $debug, $assert, and $isPromiseFulfilled omitted declare module "node:stream" { 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/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/NodeModuleModule.bind.ts b/src/jsc/NodeModuleModule.bind.ts index 64e43a9ecad6..cce88d018b08 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.string.loose.utf8, }, - ret: t.i32, + returns: b.i32, + rustPath: "crate::node_module_module::stat", }); 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/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..b2011d51be68 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -1,293 +1,640 @@ -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: `docs/project/bindgen.mdx`. + +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, JSGlobalObject, JSObject, JSString, JSValue, JsError, JsThread, + MarkedArgumentBuffer, Strong, +}; + +#[path = "bindgen/extern_types.rs"] +pub mod extern_types; -// `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. +/// Makes `'a` invariant, so two calls' lifetimes never unify. +type InvariantLifetime<'a> = PhantomData &'a ()>; +type NotSend = PhantomData<*mut ()>; +/// As `bun_opaque::opaque_ffi!`: a reference to it is a bare pointer to LLVM (no `noalias`). +type OpaqueFfi = (UnsafeCell<[u8; 0]>, PhantomPinned); -// ────────────────────────────────────────────────────────────────────────── -// A `Bindgen` adapter supplies associated `ZigType`/`ExternType` plus -// `convert_from_extern`. -// ────────────────────────────────────────────────────────────────────────── +// ───────────────────────────── capabilities ───────────────────────────── -pub trait Bindgen { - type ZigType; - type ExternType; +/// 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); - /// `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; +/// What reaches 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; +/// `&mut JsScope` is what runs JavaScript. It is the `JSGlobalObject*`: both fields are zero-sized at offset 0. +#[repr(C)] +pub struct JsScope<'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 `JsScope::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; + #[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, 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 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 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 range_error(&self, args: core::fmt::Arguments<'_>) -> Error<'a> { + Error::unthrown(self.global().create_range_error_instance(args)) + } +} -pub struct BindgenStrongAny; +impl<'a> JsScope<'a> { + /// # Safety + /// `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, + f: impl for<'call> FnOnce(&mut JsScope<'call>) -> R, + ) -> R { + // SAFETY: `JsScope` is zero-sized and `global` is non-null. + f(unsafe { &mut *global.cast::>() }) + } -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>; + /// Anything can be done with a `&JSGlobalObject`, so it borrows the whole `JsScope`. + #[inline(always)] + pub fn global(&mut self) -> &JSGlobalObject { + self.heap.global() + } - 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")) } + /// 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() } } -// ────────────────────────────────────────────────────────────────────────── +// ───────────────────────────── errors ───────────────────────────── -/// This represents both `IDLNull` and `IDLMonostateUndefined`. -pub struct BindgenNull; +#[derive(Debug)] +enum ErrorState { + Pending(JsError), + /// Thrown once the body has returned: `VM::throwException` notifies the debugger, which can run code. + ToThrow(JSValue), +} -impl Bindgen for BindgenNull { - type ZigType = (); - type ExternType = u8; +/// 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); +pub type Result<'a, T> = core::result::Result>; - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - let _ = extern_value; +impl<'a> Error<'a> { + #[inline(always)] + 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) + } -pub struct BindgenOptional(PhantomData); + #[inline(always)] + fn pending(error: JsError) -> Self { + Error(ErrorState::Pending(error), PhantomData, PhantomData) + } -// Wrap in `ExternTaggedUnion<(u8, Child::ExternType)>` and produce -// `Option`. -impl Bindgen for BindgenOptional { - type ZigType = Option; - type ExternType = ExternTaggedUnion2; + #[doc(hidden)] + #[cold] + #[inline(never)] + 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), + ErrorState::ToThrow(value) => { + let _ = global.throw_value(value); + JSValue::ZERO + } + } + } - fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { - if extern_value.tag == 0 { - return None; + /// 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 { + ErrorState::Pending(error) => error, + ErrorState::ToThrow(value) => global.throw_value(value), } - 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) - })) } } -// ────────────────────────────────────────────────────────────────────────── +/// 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::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 {} + } +} + +// ───────────────────────────── values ───────────────────────────── + +/// A [`JSValue`] held by a host call, known to be a `T`. Lists of them go in a [`MarkedArgumentBuffer`]. +#[repr(transparent)] +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 JsLocal<'_, T> {} + +impl<'a, T> JsLocal<'a, T> { + #[inline(always)] + 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::from_raw).map_err(Error::pending) + } -pub struct BindgenString; + /// # Safety + /// `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, _: &JsHeap<'a>) -> Self { + Self::from_raw(value) + } -impl Bindgen for BindgenString { - type ZigType = WTFString; - // `?bun.string.WTFStringImpl` — `Option>` for single-word FFI layout. - type ExternType = Option>; + #[inline(always)] + pub fn to_js(self) -> JSValue { + self.0 + } - 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()) } + #[inline(always)] + pub fn as_value(self) -> JsLocal<'a> { + JsLocal::from_raw(self.0) + } + + /// Keeps the value past the call. + #[inline] + pub fn to_strong(self, heap: &JsHeap<'a>) -> Strong { + Strong::create(self.0, heap.global()) } } -// ────────────────────────────────────────────────────────────────────────── -// BindgenUnion / ExternTaggedUnion / ExternUnion -// -// These are generated per arity (or by a proc-macro from the bindgen codegen). -// ────────────────────────────────────────────────────────────────────────── +impl<'a> JsLocal<'a> { + pub const UNDEFINED: Self = JsLocal(JSValue::UNDEFINED, PhantomData, PhantomData); + pub const NULL: Self = JsLocal(JSValue::NULL, PhantomData, PhantomData); + + /// 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()); + Self::from_raw(value) + } + + #[inline(always)] + pub fn is_undefined(self) -> bool { + self.0.is_undefined() + } -// 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. + #[inline] + pub fn as_callable(self) -> Option> { + self.0.is_callable().then(|| JsLocal::from_raw(self.0)) + } +} -/// `extern struct { data: ExternUnion(field_types), tag: u8 }` +/// Marks a [`JsLocal`] that can be called: a function, or a proxy of one. +pub enum Callable {} + +impl<'a> JsLocal<'a, Callable> { + /// Runs JavaScript. + pub fn call( + self, + 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]) }; + JsLocal::from_js_result(self.0.call(cx.heap.global(), this.0, args)) + } +} + +/// 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 { + 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(); + result + } +} + +/// 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, + pub value: JsLocal<'a>, +} + +// ───────────────────────────── classes ───────────────────────────── + +/// The Rust struct of a generated class, stored inside its JS wrapper and dropped by the sweeper. /// -/// 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; + /// # Safety + /// 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; + + /// A new JS object that owns `self`. + #[inline] + 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) } + } } -/// 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`. 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> { + /// # Safety + /// `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, _: &JsHeap<'a>) -> Self { + JsWrapper(cell, PhantomData, PhantomData) + } + + /// # 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), heap); + this.payload().write(payload); + this + } + } -// ────────────────────────────────────────────────────────────────────────── + /// # 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() } + } -pub struct BindgenArray(PhantomData); + /// # 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() } + } -impl Bindgen for BindgenArray { - type ZigType = bun_collections::ArrayListDefault; - type ExternType = ExternArrayList; + #[inline(always)] + fn payload(self) -> *mut T { + // SAFETY: generated offset inside the cell. + unsafe { self.0.as_ptr().byte_add(T::PAYLOAD_OFFSET).cast() } + } - 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"); + #[inline(always)] + pub fn as_value(self) -> JsLocal<'a, JSObject> { + JsLocal::from_raw(JSValue::from_cell(self.0.as_ptr())) + } - if extern_value.data.is_null() { - return Self::ZigType::init(); + /// 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: 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 + } + + /// 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. + let result = f(unsafe { &mut *self.payload() }); + self.as_value().0.ensure_still_alive(); + result + } + + /// 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) + } + + /// # Safety + /// `index` is one of the class's `internalFields`. + #[doc(hidden)] + #[inline(always)] + pub unsafe fn internal_field(self, _: &JsHeap<'a>, index: usize) -> JsLocal<'a> { + // SAFETY: generated offset inside the cell; they are initialized to `undefined`. + JsLocal::from_raw(unsafe { + self.0 + .as_ptr() + .byte_add(T::INTERNAL_FIELDS_OFFSET) + .cast::() + .add(index) + .read() + }) + } + + /// # Safety + /// As [`internal_field`](Self::internal_field). + #[doc(hidden)] + #[inline(always)] + pub unsafe fn set_internal_field( + self, + heap: &JsHeap<'a>, + index: usize, + value: JsLocal<'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 * size_of::()) 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, 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), + heap, + ) + } + } +} + +// ───────────────────────────── results ───────────────────────────── + +/// 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::*; + + macro_rules! number { + ($($name:ident: $t:ty => $f:expr;)*) => {$( + #[inline(always)] + pub fn $name<'a>(_: &JsHeap<'a>, value: $t) -> Result<'a, JsLocal<'a>> { + Ok(JsLocal::from_raw($f(value))) } + )*}; + } + number! { + bool: bool => JSValue::from; + 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_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 => |n| JSValue::js_number(JSValue::purify_nan(n)); + } - 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 - }; + #[inline(always)] + pub fn undefined<'a>(_: &JsHeap<'a>, (): ()) -> Result<'a, JsLocal<'a>> { + Ok(JsLocal::UNDEFINED) + } - 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); + /// 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>) -> JsLocal<'a>; + } + impl<'a, T> Any<'a> for JsLocal<'a, T> { + #[inline(always)] + 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>) -> JsLocal<'a> { + JsLocal::new(heap, self) } + } + + #[inline(always)] + pub fn any<'a>(heap: &JsHeap<'a>, value: impl Any<'a>) -> Result<'a, JsLocal<'a>> { + Ok(value.into_value(heap)) + } - // 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) - })); + #[inline] + pub fn utf8<'a>(heap: &JsHeap<'a>, value: impl AsRef<[u8]>) -> Result<'a, JsLocal<'a>> { + heap.create_utf8_string(value.as_ref()) + .map(JsLocal::as_value) + } + + #[inline] + 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, 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; } - // SAFETY: `data` is the live `mi_malloc`'d block from `ExternVectorTraits::convertToExtern`. - unsafe { bun_alloc::mimalloc::mi_free(data.cast()) }; - result + MarkedArgumentBuffer::new(|values| { + for value in short { + values.append(value); + } + for item in items { + values.append(convert(heap, item)?.0); + } + // SAFETY: `values` is live for the call. + JsLocal::from_js_result(crate::call_zero_is_throw(heap.global(), || unsafe { + extern_types::Bun__Bindgen__constructArray(heap.global_ptr(), values) + })) + }) + } + + /// An empty value is a member that is absent. + /// + /// # Safety + /// `Tables::memberNames` names `values`, from `first_member_name_index`. + #[doc(hidden)] + #[inline] + pub unsafe fn object<'a>( + heap: &JsHeap<'a>, + 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(), + first_member_name_index, + values.as_ptr(), + values.len(), + ) + })) + } + + /// # Safety + /// `index` is one into `Tables::enumerationValues`. + #[doc(hidden)] + #[inline] + 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) + })) } } -#[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, +#[doc(hidden)] +#[inline(always)] +pub fn host_fn_return<'a, T, E: IntoError<'a>>( + cx: &mut JsScope<'a>, + result: core::result::Result, + convert: impl FnOnce(&JsHeap<'a>, T) -> Result<'a, JsLocal<'a>>, +) -> JSValue { + 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) + }; + 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 new file mode 100644 index 000000000000..f68ec1dc01e6 --- /dev/null +++ b/src/jsc/bindgen/extern_types.rs @@ -0,0 +1,146 @@ +//! 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}; + +use super::JsHeap; +use crate::{JSGlobalObject, JSValue, MarkedArgumentBuffer}; + +/// `WTF::StringImpl*` carrying one ref, or null. +pub type ExternString = *mut bun_core::WTFStringImplStruct; + +/// 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); + if string.is_empty() { + return bun_core::String::EMPTY; + } + string +} + +/// 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 { + if self.tag == 0 { + return None; + } + // SAFETY: C++ initialized the alternative `tag` names. + Some(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 { + /// The buffer becomes the `Vec`, where the global allocator is the one C++ used. + /// + /// # Safety + /// No copy of `self` is used again. + #[inline] + pub unsafe fn into_vec(self) -> Vec { + if !bun_alloc::USE_MIMALLOC || self.data.is_null() { + // 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 unsafe fn into_vec_with(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 + /// The host function that passed this is the call `heap` belongs to. + #[inline(always)] + 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, heap: &JsHeap<'a>) -> Option<&'a [u8]> { + if self.data.is_null() { + return None; + } + // SAFETY: forwarded. + Some(unsafe { self.get(heap) }) + } +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ExternWithValue { + pub converted: T, + pub value: JSValue, +} + +unsafe extern "C" { + pub(super) fn Bun__Bindgen__constructArray( + 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, + values: *const JSValue, + count: usize, + ) -> JSValue; + 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, + value: JSValue, + ); +} diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index ee61e0733c09..082152268c70 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -1,20 +1,201 @@ -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.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 }); +export const stringEcho = b.fn({ args: { text: b.string }, returns: b.string }); + +export const TestColor = b.enumeration(["red", ["green", "lime"], "blue"]); +export const nextColor = b.fn({ args: { color: TestColor }, returns: TestColor }); + +export const TestOptions = b.dictionary({ + count: b.i32.default(1), + flag: b.bool.loose.default(false), + 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({ + count: b.i32, + flag: b.bool, + name: b.string.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 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, + text: b.string.utf8, + list: b.array(b.i32), +}); +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.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 }, + 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), + capability: "js", + throws: true, +}); + +/** The original value if the converted text is not empty. */ +export const withValueEcho = b.fn({ + args: { text: b.string.loose.utf8.withValue }, + returns: b.any, + capability: "heap", +}); + +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({ + rustPath: "crate::bindgen_test::TestCounter", + construct: { args: { start: b.i32.default(0) }, capability: "heap", throws: true }, + internalFields: ["listener"], + proto: { + count: { getter: b.i32 }, + label: { getter: b.string.utf8, setter: 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. */ + notify: { returns: b.i32, capability: "js", throws: true }, + /** `other` may be `this`. */ + plus: { args: { other: b.self }, returns: b.i32, capability: "js" }, + }, + static: { + starting: { args: { at: b.i32 }, returns: b.self, capability: "heap" }, + alive: { returns: b.usize }, + }, +}); + +/** 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" }, }, - ret: t.i32, +}); + +/** Only native code makes one. */ +export const TestToken = b.class({ + rustPath: "crate::bindgen_test::TestToken", + proto: { + id: { getter: b.u32 }, + }, +}); +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 first argument, then 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", + }, +); + +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 }); +/** 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 cab573ad8b66..8acc7e313320 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -1,50 +1,24 @@ //! 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::bindgen::{ + self, BufferSource, IntoError as _, JsHeap, JsLocal, JsPayload as _, JsScope, JsWrapper, + JsWrapperStrong, WithValue, +}; +use crate::generated_bindgen::bindgen_test::{ + 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(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(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) @@ -61,3 +35,333 @@ pub fn required_and_optional_arg(a: bool, b: Option, c: i32, d: Option &[u8] { + text +} + +pub(crate) fn utf8_echo_loose(text: &[u8]) -> &[u8] { + text +} + +pub(crate) fn utf8_optional(text: Option<&[u8]>) -> Option<&[u8]> { + text +} + +pub(crate) fn utf8_owned(text: bun_core::Utf8Bytes<'static>) -> bun_core::Utf8Bytes<'static> { + text +} + +pub(crate) fn string_echo(text: bun_core::String) -> bun_core::String { + text +} + +pub(crate) fn next_color(color: TestColor) -> TestColor { + match color { + TestColor::Red => TestColor::Green, + TestColor::Green => TestColor::Blue, + TestColor::Blue => TestColor::Red, + } +} + +pub(crate) 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(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(), + TestValue::Text(text) => [b"text ", text].concat(), + TestValue::List(list) => format!("list {list:?}").into_bytes(), + } +} + +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(), + 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: crate::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(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); + 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(), + }; + 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}; rest: {rest}; counter: {counter}") + .into_bytes() +} + +pub(crate) fn overloaded_text_times(text: &[u8], times: u32) -> Vec { + [b"text times ", text.repeat(times as usize).as_slice()].concat() +} + +pub(crate) 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(crate) fn overloaded_two_numbers(a: f64, b: f64) -> Vec { + format!("two numbers {a} {b}").into_bytes() +} + +pub(crate) fn overloaded_number(number: f64) -> Vec { + format!("number {number}").into_bytes() +} + +pub(crate) fn overloaded_list(list: Vec) -> Vec { + format!("list {:?}", list.into_boxed_slice()).into_bytes() +} + +pub(crate) 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() +} + +pub(crate) fn sum_array(numbers: Vec) -> f64 { + numbers + .into_iter() + .fold(0.0, |sum, number| sum + f64::from(number)) +} + +pub(crate) fn range(length: u32) -> core::ops::Range { + 0..length +} + +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(), + nickname: all.get(1).cloned(), + all, + } +} + +pub(crate) 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(crate) fn sum_bytes(bytes: &[u8]) -> u32 { + sum(bytes) +} + +pub(crate) fn sum_bytes_optional(bytes: Option<&[u8]>) -> Option { + bytes.map(sum) +} + +pub(crate) fn sum_bytes_then_text(bytes: &[u8], text: &[u8]) -> u32 { + sum(bytes) + sum(text) +} + +pub(crate) fn sum_before_and_after_callback<'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 + .type_error(format_args!("callback must be a function"))); + }; + callback.call(cx, JsLocal::UNDEFINED, &[])?; + Ok([before, buffer.with_bytes(&cx.heap, sum)]) +} + +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(crate) fn fail<'a>( + heap: &JsHeap<'a>, + how: TestFailure, + value: JsLocal<'a>, +) -> bindgen::Result<'a, ()> { + Err(match how { + 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 => bindgen::Error::from_value(value), + }) +} + +static ALIVE: AtomicUsize = AtomicUsize::new(0); + +pub(crate) struct TestCounter { + count: i32, + label: Vec, +} + +impl TestCounter { + fn new(count: i32) -> Self { + ALIVE.fetch_add(1, Ordering::SeqCst); + TestCounter { + count, + label: Vec::new(), + } + } +} + +impl Drop for TestCounter { + fn drop(&mut self) { + ALIVE.fetch_sub(1, Ordering::SeqCst); + } +} + +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.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>) -> bindgen::Result<'a, JsLocal<'a>> { + bindgen::to_js::array(heap, [self.count], bindgen::to_js::i32) + } + + 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: JsLocal<'a>) { + this.set_listener_field(heap, listener); + } + + 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 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)) + } + + fn starting<'a>(heap: &JsHeap<'a>, at: i32) -> JsWrapper<'a, Self> { + Self::new(at).into_js(heap) + } + + fn alive() -> usize { + ALIVE.load(Ordering::SeqCst) + } +} + +pub(crate) 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 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)) + } +} + +pub(crate) struct TestToken { + id: u32, +} + +impl TestTokenClass for TestToken { + fn id(&self) -> u32 { + self.id + } +} + +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.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/AdditionsToGlobalObject.cpp b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp new file mode 100644 index 000000000000..cb1f366fb550 --- /dev/null +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.cpp @@ -0,0 +1,21 @@ +#include "root.h" +#include "Bindgen/AdditionsToGlobalObject.h" +#include "Bindgen/JSWrapper.h" +#include "GeneratedBindings+Tables.h" +#include "ZigGlobalObject.h" + +namespace Bun::Bindgen { + +void AdditionsToGlobalObject::initialize() +{ + // 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) { + 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 new file mode 100644 index 000000000000..8e9a530d8b76 --- /dev/null +++ b/src/jsc/bindings/Bindgen/AdditionsToGlobalObject.h @@ -0,0 +1,23 @@ +#pragma once +#include "root.h" +#include "GeneratedBindings+Counts.h" +#include + +namespace Bun::Bindgen { + +// The per-global-object state of generated bindings. +struct AdditionsToGlobalObject { + void initialize(); + + template + void visit(Visitor& visitor) + { + for (auto& lazyClass : m_classes) + lazyClass.visit(visitor); + } + + // By index into Tables::classes. + JSC::LazyClassStructure m_classes[Counts::numberOfClasses]; +}; + +} diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp new file mode 100644 index 000000000000..75330505f49b --- /dev/null +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.cpp @@ -0,0 +1,32 @@ +#include "root.h" +#include "Bindgen/DictionaryMemberReader.h" +#include "ObjectBindings.h" +#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) +{ + // Asked per member: converting one can run JavaScript. + if (canSkipPrototypeChain(m_globalObject, m_object)) [[likely]] { + unsigned attributes = 0; + JSValue value = m_object->getDirect(m_vm, m_memberNames[index], attributes); + if (!value) + return jsUndefined(); + 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 new file mode 100644 index 000000000000..92316d5d44dd --- /dev/null +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -0,0 +1,31 @@ +#pragma once +#include "root.h" +#include "Bindgen/SchemaStrings.h" +#include + +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) + , m_globalObject(globalObject) + , m_object(object) + , m_memberNames(schemaStrings(vm).memberNames(firstMemberNameIndex, memberNameCount)) + { + } + + // May throw. Out of line: inlined, it is most of the size of every generated dictionary converter. + NEVER_INLINE JSC::JSValue get(unsigned index); + +private: + JSC::VM& m_vm; + JSC::JSGlobalObject* m_globalObject; + JSC::JSObject* m_object; + const JSC::Identifier* m_memberNames; +}; + +} diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 0bac90fdf14f..f1d52b4c3914 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -1,46 +1,48 @@ #pragma once #include #include -#include #include -#include +#include #include -#include #include #include -#include #include -#include -#include "ExternUnion.h" +#include "Bindgen/JSStringUTF8View.h" 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(WTF::move(value)); +} + template struct TrivialExtern { using ExternType = T; 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 {}; +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; @@ -51,56 +53,28 @@ template<> struct ExternTraits { } }; -template<> struct ExternTraits { - using ExternType = ExternNullPtr; - - static ExternType convertToExtern(std::monostate cppValue) - { - return ExternType { 0 }; - } -}; - -template -struct ExternVariant { - ExternUnion data; - std::uint8_t tag; - - 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())) - { - } -}; - -template -struct ExternTraits> { - using ExternType = ExternVariant::ExternType...>; - - 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)) }; - } +template +struct ExternOptional { + union { + ExternNullPtr none; + T some; + } data; + uint8_t tag; }; template struct ExternTraits> { - using ExternType = ExternVariant::ExternType>; + using ExternType = ExternOptional::ExternType>; 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.some = ExternTraits::convertToExtern(WTF::move(*cppValue)); + else + result.data.none = ExternNullPtr {}; + return result; } }; @@ -122,24 +96,37 @@ template<> struct ExternTraits { } }; -template<> struct ExternTraits { - using ExternType = Bun::StrongRefImpl*; +struct ExternBytes { + const uint8_t* data; + size_t length; +}; + +template<> struct ExternTraits> { + using ExternType = ExternBytes; - static ExternType convertToExtern(Bun::StrongRef&& cppValue) + static ExternType convertToExtern(std::span cppValue) { - return cppValue.release(); + return { cppValue.data(), cppValue.size() }; } }; -template struct ExternTraits> { - using ExternType = T*; +template<> struct ExternTraits { + using ExternType = ExternBytes; - static ExternType convertToExtern(WTF::Ref&& cppValue) + // The storage stays with `cppValue`, which outlives the call this is an argument of. + static ExternType convertToExtern(JSStringUTF8View&& cppValue) { - return &cppValue.leakRef(); + 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/ExternUnion.h b/src/jsc/bindings/Bindgen/ExternUnion.h deleted file mode 100644 index f7552605fa71..000000000000 --- a/src/jsc/bindings/Bindgen/ExternUnion.h +++ /dev/null @@ -1,88 +0,0 @@ -#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; -} - -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); -} diff --git a/src/jsc/bindings/Bindgen/ExternVectorTraits.h b/src/jsc/bindings/Bindgen/ExternVectorTraits.h index ad609c7781d5..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,94 +54,53 @@ 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 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)]; // Prevent the memory from being freed. - CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; - const std::size_t length = vec->size(); - const std::size_t capacity = vec->capacity(); - const std::size_t allocSize = capacity * sizeof(T); + 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) }; - } - - std::size_t newCapacity {}; - std::size_t newAllocSize {}; - - static constexpr bool newSizeIsMultiple = sizeof(T) % sizeof(ExternElement) == 0; - if (newSizeIsMultiple) { - newCapacity = capacity * (sizeof(T) / sizeof(ExternElement)); - newAllocSize = allocSize; - } else { - newCapacity = allocSize / sizeof(ExternElement); - newAllocSize = newCapacity * sizeof(ExternElement); - if (newAllocSize != allocSize) { - static_assert(std::is_trivially_copyable_v); - storage = static_cast( - MimallocMalloc::realloc(storage, newCapacity * sizeof(ExternElement))); - } + new (storage + i * sizeof(ExternElement)) ExternElement { WTF::move(newElem) }; } #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 669b74c2d893..4c310538de00 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -1,19 +1,310 @@ #pragma once -#include -#include "IDLTypes.h" -#include "IDLConvertBase.h" +#include "Bindgen/IDLConvertBase.h" +#include "Bindgen/IDLConvertNumbers.h" +#include "Bindgen/IDLTypes.h" +#include "JSDOMConvert.h" +#include +#include +#include +#include +#include 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("null"); }; + +template +struct IDLHumanReadableName> + : IDLHumanReadableName> {}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool hasPreposition = true; + static constexpr auto humanReadableName + = 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"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("Blob"); +}; + +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 { + +// 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) +{ + 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. +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 = JSC::toUnsharedArrayBuffer(vm, value)) + return buffer->span(); + return std::nullopt; +} + +} } -template<> struct WebCore::Converter - : WebCore::DefaultConverter { +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)); + convertIntoSlow(globalObject, value, ctx, result); + } + +private: + 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 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() || value.inherits()) [[likely]] + 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> tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) + { + auto bytes = Bun::Bindgen::unsharedBufferSourceSpan(JSC::getVM(&globalObject), value); + // Null data is the null value. + if (bytes && !bytes->data()) + return byteCast(WTF::nonNullEmptyUTF8Span()); + return bytes; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); + } +}; + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + template + static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) + { + if (!Bun::Bindgen::unsharedBufferSourceSpan(JSC::getVM(&globalObject), value)) + return std::nullopt; + return value; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (auto* object = dynamicDowncast(value)) + return object; + return std::nullopt; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + 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; + } - 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)) }; + Bun::Bindgen::throwNotUnsharedBufferSource(globalObject, scope, ctx); } }; diff --git a/src/jsc/bindings/Bindgen/IDLConvertBase.h b/src/jsc/bindings/Bindgen/IDLConvertBase.h index e73a664c1fb6..2f37772a3079 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 { @@ -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/BunIDLConvertBlob.h b/src/jsc/bindings/Bindgen/IDLConvertBlob.h similarity index 63% rename from src/jsc/bindings/BunIDLConvertBlob.h rename to src/jsc/bindings/Bindgen/IDLConvertBlob.h index 2cb5a229756d..a52c15f5a8d5 100644 --- a/src/jsc/bindings/BunIDLConvertBlob.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBlob.h @@ -1,26 +1,25 @@ #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) { 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 new file mode 100644 index 000000000000..13809dc499a7 --- /dev/null +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -0,0 +1,56 @@ +#pragma once +#include "root.h" +#include "Bindgen/IDLTypes.h" +#include "JSDOMConvertNumbers.h" +#include "NodeValidator.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); + RELEASE_AND_RETURN(scope, static_cast(WebCore::enforceRange(globalObject, number, Min, Max))); + } +}; + +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); + // 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/BunIDLConvertNumbers.h b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h similarity index 55% rename from src/jsc/bindings/BunIDLConvertNumbers.h rename to src/jsc/bindings/Bindgen/IDLConvertNumbers.h index 9f32f7928fd4..992dc5f11b65 100644 --- a/src/jsc/bindings/BunIDLConvertNumbers.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h @@ -1,18 +1,18 @@ #pragma once -#include "BunIDLTypes.h" +#include "Bindgen/IDLTypes.h" #include "BunIDLConvertBase.h" -#include -#include +#include +#include #include #include #include -namespace Bun::Detail { +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,86 +36,87 @@ std::optional tryBigIntToInt(JSC::JSValue value) } template - requires(sizeof(T) <= sizeof(std::uint64_t)) -struct WebCore::Converter> - : Bun::DefaultTryConverter> { + requires(sizeof(T) <= sizeof(uint64_t)) +struct WebCore::Converter> + : Bun::DefaultTryConverter> { 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); + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } + +private: + template + NEVER_INLINE static std::optional tryConvertSlow( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) { 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); 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 {}; } - 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::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 {}; } } ctx.throwBigIntOutOfRange(globalObject, scope, minInt, maxInt); return {}; } - if (!value.isNumber()) { + if (!value.isNumber()) return std::nullopt; - } double number = value.asNumber(); - if (number > maxSafeInteger || number < -maxSafeInteger) { - ctx.throwNumberNotInteger(globalObject, scope, number); - return {}; - } - auto intVal = static_cast(number); - if (intVal != number) { + if (!JSC::isSafeInteger(number)) { ctx.throwNumberNotInteger(globalObject, scope, number); return {}; } - if constexpr (maxInt >= static_cast(maxSafeInteger)) { - if (std::signed_integral || intVal >= 0) { + auto intVal = static_cast(number); + if constexpr (maxInt >= JSC::maxSafeIntegerAsUInt64()) { + 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); return {}; } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - ctx.throwNotNumber(globalObject, scope); - } }; template<> -struct WebCore::Converter : Bun::DefaultTryConverter { +struct WebCore::Converter : Bun::DefaultTryConverter { static constexpr bool conversionHasSideEffects = false; template @@ -124,9 +125,8 @@ struct WebCore::Converter : Bun::DefaultTryConverter : 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> { +struct WebCore::Converter> + : Bun::DefaultContextConverter> { template static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -186,6 +153,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 93089efbe6e6..62797087a62a 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -1,23 +1,74 @@ #pragma once -#include -#include +#include "Bindgen/JSStringUTF8View.h" +// WebCore's. In quotes it would be this file. +#include +#include +#include +#include +#include + +namespace Bun { +struct MimallocMalloc; +} namespace Bun::Bindgen { -// See also: Bun::IDLRawAny -struct IDLStrongAny : WebCore::IDLType { - using NullableType = Bun::StrongRef; +// 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; } - 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. +struct IDLArrayBufferRef : IDLRef {}; + +// Defined in IDLConvertBlob.h +struct IDLBlobRef; + +// 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; @@ -25,4 +76,42 @@ struct IDLStackOnlyDictionary : WebCore::IDLType { using NullableParameterType = const T&; }; +// 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 {}; + +// 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 {}; } +}; +// `ToString`. +struct IDLLooseUTF8String : IDLStrictUTF8String {}; +// The values IDLStrictUTF8String takes, as the WTF::String. +struct IDLStrictStringOrStringObject : WebCore::IDLDOMString {}; + +// 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; + using NullableInnerParameterType = std::span; + static NullableType nullValue() { return {}; } +}; + +// The same values, as the `JSValue` itself. See 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; } +}; + } diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp new file mode 100644 index 000000000000..0994e14509cf --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.cpp @@ -0,0 +1,33 @@ +#include "root.h" +#include "Bindgen/JSStringUTF8View.h" +#include +#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), m_converted.mutableSpan())); + return true; + } + auto span = view.span16(); + if (!m_converted.tryGrow(span.size() * 3)) + return false; + 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, m_converted.mutableSpan()).buffer.size()); + return true; +} + +} diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h new file mode 100644 index 000000000000..03d4a6a9462b --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -0,0 +1,63 @@ +#pragma once +#include "root.h" +#include +#include +#include +#include + +namespace Bun::Bindgen { + +// Bun::UTF8View for an argument of a host call. +class JSStringUTF8View { + WTF_FORBID_HEAP_ALLOCATION; + WTF_MAKE_NONCOPYABLE(JSStringUTF8View); + WTF_MAKE_NONMOVABLE(JSStringUTF8View); + +public: + // Not `= default`: value-initialization would zero the buffer first. + JSStringUTF8View() {} + + // 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); + 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); + } + + void initialize(WTF::ASCIILiteral literal) + { + ASSERT(m_isNull); + m_view = { nullptr, literal }; + m_isNull = false; + } + + // Null data only for a view that was not initialized. + std::span bytes() const LIFETIME_BOUND + { + if (m_isNull) + return {}; + auto span = byteCast(m_isConverted ? m_converted.span() : byteCast(m_view->span8())); + if (!span.data()) + return byteCast(WTF::nonNullEmptyUTF8Span()); + return span; + } + +private: + bool convert(); + + // 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/Bindgen/JSWrapper.cpp b/src/jsc/bindings/Bindgen/JSWrapper.cpp new file mode 100644 index 000000000000..6c708f2a8f0c --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSWrapper.cpp @@ -0,0 +1,90 @@ +#include "root.h" +#include "Bindgen/JSWrapper.h" +#include "GeneratedBindings+Tables.h" +#include +#include + +namespace Bun::Bindgen { +using namespace JSC; + +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); +JSC_DEFINE_HOST_FUNCTION(callJSClassConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + 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) + : Base(vm, structure, callJSClassConstructor, construct) +{ +} + +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)) JSClassConstructor(vm, structure, definition.construct); + constructor->finishCreation(vm, definition.constructorLength, definition.info->className, PropertyAdditionMode::WithoutStructureTransition); + // 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; +} + +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(JSClassConstructor::create(vm, init.global, definition, prototype)); +} + +Structure* structureForClass(JSGlobalObject* globalObject, unsigned classIndex) +{ + auto* global = defaultGlobalObject(globalObject); + 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].constructor(global); +} + +Structure* structureForNewTarget(JSGlobalObject* globalObject, CallFrame* callFrame, unsigned classIndex) +{ + // JSC_GET_DERIVED_STRUCTURE, for a Structure that is not a member of the global object. + JSObject* newTarget = asObject(callFrame->newTarget()); + if (newTarget == callFrame->jsCallee()) [[likely]] + return structureForClass(globalObject, classIndex); + 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))); +} + +} + +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/Bindgen/JSWrapper.h b/src/jsc/bindings/Bindgen/JSWrapper.h new file mode 100644 index 000000000000..ba2fcd31c219 --- /dev/null +++ b/src/jsc/bindings/Bindgen/JSWrapper.h @@ -0,0 +1,175 @@ +#pragma once +#include "root.h" +#include "BunClientData.h" +#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 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 InheritsTraits> { + static constexpr std::optional typeRange { std::nullopt }; + template + static inline bool inherits(From* from) + { + 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 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. + 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; +}; + +// JSDestructibleObject | WriteBarrier internalFields[numberOfInternalFields] | the Rust payload +template +class JSWrapper final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + 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; + RELEASE_ASSERT(allocationSize() <= JSC::MarkedSpace::largeCutoff); + 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(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) + { + auto* object = new (NotNull, JSC::allocateCell(vm, allocationSize())) JSWrapper(vm, structure); + object->finishCreation(vm); + return object; + } + + static void destroy(JSC::JSCell* cell) + { + STATIC_ASSERT_IS_TRIVIALLY_DESTRUCTIBLE(JSWrapper); + 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 std::bit_cast(this) + offsetOfPayload(); } + JSC::WriteBarrier& internalField(unsigned index) + { + ASSERT(index < numberOfInternalFields); + return internalFields()[index]; + } + std::span, numberOfInternalFields> internalFields() + { + return std::span, numberOfInternalFields> { std::bit_cast*>(std::bit_cast(this) + offsetOfInternalFields()), numberOfInternalFields }; + } + +private: + 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. + unsigned index = 0; + for (auto& field : internalFields()) { + if (index++ < numberOfInternalFields - Traits::numberOfCachedGetters) + new (NotNull, &field) JSC::WriteBarrier(JSC::UndefinedWriteBarrierTag); + else + new (NotNull, &field) JSC::WriteBarrier; + } + } + + DECLARE_DEFAULT_FINISH_CREATION; +}; + +template +template +void JSWrapper::visitChildrenImpl(JSC::JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.appendValues(thisObject->internalFields().data(), numberOfInternalFields); +} + +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template, JSWrapper); + +template +const JSC::ClassInfo JSWrapper::s_info = { Traits::className, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWrapper) }; + +template struct ExternTraits*> : TrivialExtern*> {}; + +// The constructor object of every generated class that has one. +class JSClassConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + DECLARE_INFO; + + 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 JSClassConstructor* create(JSC::VM&, JSC::JSGlobalObject*, const ClassDefinition&, JSC::JSObject* prototype); + +private: + 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/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/Bindgen/SchemaStrings.cpp b/src/jsc/bindings/Bindgen/SchemaStrings.cpp new file mode 100644 index 000000000000..3accf9a3edcd --- /dev/null +++ b/src/jsc/bindings/Bindgen/SchemaStrings.cpp @@ -0,0 +1,44 @@ +#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(Tables::memberNames.size()) + , m_enumerationValues(WTF::FillWith {}, Tables::enumerationValues.size(), nullptr) +{ +} + +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 (auto* string : m_enumerationValues) + visitor.appendUnbarriered(string); +} +template void SchemaStrings::visit(AbstractSlotVisitor&); +template void SchemaStrings::visit(SlotVisitor&); + +SchemaStrings& schemaStrings(VM& vm) +{ + return static_cast(vm.clientData)->schemaStrings; +} + +} diff --git a/src/jsc/bindings/Bindgen/SchemaStrings.h b/src/jsc/bindings/Bindgen/SchemaStrings.h new file mode 100644 index 000000000000..f82c6c60658c --- /dev/null +++ b/src/jsc/bindings/Bindgen/SchemaStrings.h @@ -0,0 +1,48 @@ +#pragma once +#include "root.h" +#include +#include +#include + +namespace Bun::Bindgen { + +// 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 SchemaStrings(JSC::VM&); + + // `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]] + initializeMemberNames(first, count); + return &m_memberNames[first]; + } + + // Tables::enumerationValues[index]. + ALWAYS_INLINE JSC::JSString* enumerationValue(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. + 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 new file mode 100644 index 000000000000..77aefc32b773 --- /dev/null +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -0,0 +1,44 @@ +#include "root.h" +#include "Bindgen/SchemaStrings.h" +#include +#include +#include + +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 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], 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/BindgenCustomEnforceRange.h b/src/jsc/bindings/BindgenCustomEnforceRange.h deleted file mode 100644 index a97f5aeda7e2..000000000000 --- a/src/jsc/bindings/BindgenCustomEnforceRange.h +++ /dev/null @@ -1,110 +0,0 @@ -#pragma once -#include "root.h" -#include "IDLTypes.h" -#include "JSDOMConvertBase.h" -#include "ErrorCode.h" - -namespace Bun { - -enum class BindgenCustomEnforceRangeKind { - Node, - 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() -template< - typename 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< - typename 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(); - - // 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; - } - } - } 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. - 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); - } - } - - 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/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index a7040df327de..b0793827744f 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) + , schemaStrings(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->schemaStrings.visit(visitor); })), JSC::ConstraintVolatility::GreyedByExecution)); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 05e23676d425..4e4fafbcfb5d 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/SchemaStrings.h" #include "DOMURLBaseCache.h" #include "NodeHTTPRequestHeaderValues.h" #include "NodeVMOptionNames.h" @@ -245,6 +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 schemaStrings; 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 7ac1d80c88b8..000000000000 --- a/src/jsc/bindings/BunIDLConvert.h +++ /dev/null @@ -1,306 +0,0 @@ -#pragma once -#include "BunIDLTypes.h" -#include "BunIDLConvertBase.h" -#include "BunIDLConvertNumbers.h" -#include "BunIDLHumanReadable.h" -#include "JSDOMConvert.h" -#include -#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::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> { - - 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) - { - 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)) { - return jsView->unsharedBuffer(); - } - if (auto* jsDataView = dynamicDowncast(value)) { - return jsDataView->unsharedBuffer(); - } - return std::nullopt; - } - - template - static void throwConversionFailed( - JSC::JSGlobalObject& globalObject, - JSC::ThrowScope& scope, - Ctx& ctx) - { - 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/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index 7d53546df067..1c5ab8bb478a 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -3,13 +3,12 @@ #include "JSDOMConvertBase.h" #include #include -#include #include namespace Bun { template -typename WebCore::Converter::ReturnType convertIDL( +ALWAYS_INLINE typename WebCore::Converter::ReturnType convertIDL( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) @@ -22,16 +21,12 @@ typename WebCore::Converter::ReturnType convertIDL( } template -std::optional::ReturnType> tryConvertIDL( +ALWAYS_INLINE std::optional::ReturnType> tryConvertIDL( JSC::JSGlobalObject& globalObject, 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,38 +34,33 @@ 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 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()) { - return std::move(*result); + if (result.has_value()) [[likely]] { + scope.release(); + return WTF::move(*result); } - WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + RETURN_IF_EXCEPTION(scope, {}); + throwConversionFailedSlow(globalObject, scope, ctx); return ReturnType {}; } - static std::optional tryConvert( - JSC::JSGlobalObject& globalObject, - JSC::JSValue value) +private: + template + NEVER_INLINE static void throwConversionFailedSlow(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { - auto ctx = DefaultConversionContext {}; - return WebCore::Converter::tryConvert(globalObject, value, ctx); + WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); } }; diff --git a/src/jsc/bindings/BunIDLConvertContext.h b/src/jsc/bindings/BunIDLConvertContext.h index 5dde10a73045..817b2527f6e5 100644 --- a/src/jsc/bindings/BunIDLConvertContext.h +++ b/src/jsc/bindings/BunIDLConvertContext.h @@ -13,23 +13,6 @@ struct IDLConversionContextMarker {}; template concept IDLConversionContext = std::derived_from; -namespace Detail { -template -struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; -}; - -template -struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; -}; - -template -struct IDLUnionForDiagnostic { - using Type = IDLOrderedUnion; -}; -} - template struct IDLConversionContextBase : Detail::IDLConversionContextMarker { void throwRequired(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) @@ -37,14 +20,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,21 +87,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); - } - void throwNotBlob(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { derived().throwTypeMustBe(global, scope, "a Blob"_s); @@ -147,14 +107,8 @@ 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) + void throwBadEnumerationValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) { derived().throwTypeMustBe(global, scope, idlHumanReadableName()); } @@ -163,11 +117,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 239087715098..a2e2198b532b 100644 --- a/src/jsc/bindings/BunIDLHumanReadable.h +++ b/src/jsc/bindings/BunIDLHumanReadable.h @@ -1,15 +1,19 @@ #pragma once -#include "BunIDLTypes.h" +#include "IDLTypes.h" +#include #include "ConcatCStrings.h" #include #include -#include namespace Bun { template struct IDLHumanReadableName; +// A list of types, named "a, b, or c". +template +struct IDLAlternatives {}; + template concept HasIDLHumanReadableName = requires { IDLHumanReadableName::humanReadableName; }; @@ -49,14 +53,6 @@ static constexpr auto separatorForHumanReadableBinaryDisjunction() } } -template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { - 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 { @@ -105,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 { @@ -123,21 +108,13 @@ 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()...); }; -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 17a2d9f83e0d..000000000000 --- a/src/jsc/bindings/BunIDLTypes.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once -#include "IDLTypes.h" -#include -#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; - 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 {}; - -template -struct IDLStrictInteger : WebCore::IDLInteger {}; -struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; -struct IDLFiniteDouble : WebCore::IDLDouble {}; -struct IDLStrictBoolean : WebCore::IDLBoolean {}; -struct IDLStrictString : WebCore::IDLDOMString {}; - -// Converts to a number first. -template -struct IDLLooseInteger : IDLStrictInteger {}; - -template -struct IDLOrderedUnion : WebCore::IDLType> {}; - -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; } - template static inline bool isNullValue(U&& value) { return !value; } - template static inline U&& extractValueFromNullable(U&& value) - { - return std::forward(value); - } -}; - -struct IDLArrayBufferRef : IDLBunInterface {}; - -// Defined in BunIDLConvertBlob.h -struct IDLBlobRef; - -} 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..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, Bun::Bindgen::IDLStrongAny +// 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/bindings/JSDOMWrapper.h b/src/jsc/bindings/JSDOMWrapper.h index 31a2b19edf27..eb56032214ab 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::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/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/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 2daaea3a024e..c49d75a6fcbd 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" @@ -2147,6 +2146,7 @@ void GlobalObject::finishCreation(VM& vm) setEvalEnabled(false, Bun::codeGenerationFromStringsDisallowedMessage); m_bakeAdditions.initialize(); + m_bindgenAdditions.initialize(); m_markdownTagStrings.initialize(); static const LazyClassStructureInit lazyClassStructureInits[] = { diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 883e2035037f..0c563bf2e734 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -71,6 +71,7 @@ struct node_module; #include #include #include "BakeAdditionsToGlobalObject.h" +#include "Bindgen/AdditionsToGlobalObject.h" #include "NativeModuleList.h" #include "streams/JSStreamsRuntime.h" @@ -499,6 +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) \ \ /* TODO: these should use LazyProperty */ \ \ diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 9f6a0f2e4747..eb6b829383e8 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6445,15 +6445,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/bindings/webcore/DOMClientIsoSubspaces.h b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h index ee30ad6b1a8d..581c6f39097d 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::numberOfClasses] {}; /* --- bun --- */ GCClient::IsoSubspace* m_clientSubspaceForDOMException { nullptr }; diff --git a/src/jsc/bindings/webcore/DOMIsoSubspaces.h b/src/jsc/bindings/webcore/DOMIsoSubspaces.h index 3860de3220bd..dd37885d99fc 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::numberOfClasses] {}; /*-- BUN --*/ IsoSubspace* m_subspaceForFetchHeaders { nullptr }; 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..2d149d0aa54b 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) @@ -72,7 +59,7 @@ template struct Converter> : DefaultConverter>(lexicalGlobalObject, throwScope); + ctx.template throwBadEnumerationValue>(lexicalGlobalObject, throwScope); return {}; } 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/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/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) { diff --git a/src/jsc/fmt_jsc.bind.ts b/src/jsc/fmt_jsc.bind.ts index 79a7a2aa3daa..39a766e9eaf4 100644 --- a/src/jsc/fmt_jsc.bind.ts +++ b/src/jsc/fmt_jsc.bind.ts @@ -1,12 +1,14 @@ -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(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]); + +export const fmtString = b.fn({ args: { - global: t.globalObject, - code: t.UTF8String, + code: b.string.loose.utf8, formatter: Formatter, }, - ret: t.DOMString, + returns: b.string, + throws: true, }); diff --git a/src/jsc/fmt_jsc.rs b/src/jsc/fmt_jsc.rs index 3d65201fc24c..3874694dee7b 100644 --- a/src/jsc/fmt_jsc.rs +++ b/src/jsc/fmt_jsc.rs @@ -1,35 +1,18 @@ -//! 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. +//! 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 _; -use crate::{JSGlobalObject, JsResult}; +use crate::JsError; use bun_core::fmt; use bun_core::{MutableString, String}; -pub mod js_bindings { +pub(crate) 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, - } + use crate::generated_bindgen::fmt_jsc::Formatter; /// Internal function for testing in highlighter.test.ts - pub fn fmt_string( - global: &JSGlobalObject, - code: &[u8], - formatter_id: Formatter, - ) -> JsResult { + pub(crate) fn fmt_string(code: &[u8], formatter_id: Formatter) -> Result { let mut buffer = MutableString::init_empty(); let writer = buffer.writer(); @@ -43,7 +26,7 @@ pub mod js_bindings { ..Default::default() }, ); - write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| JsError::OutOfMemory)?; } Formatter::HighlightJavascriptRedacted => { let formatter = fmt::fmt_javascript( @@ -54,11 +37,11 @@ pub mod js_bindings { redact_sensitive_information: true, }, ); - write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?; + write!(writer, "{}", formatter).map_err(|_| JsError::OutOfMemory)?; } Formatter::EscapePowershell => { write!(writer, "{}", fmt::escape_powershell(code)) - .map_err(|_| global.throw_out_of_memory())?; + .map_err(|_| JsError::OutOfMemory)?; } } diff --git a/src/jsc/generated.rs b/src/jsc/generated.rs index 6e7e53843139..85b889c120cb 100644 --- a/src/jsc/generated.rs +++ b/src/jsc/generated.rs @@ -1,711 +1,12 @@ -//! 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}`). +//! 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` //! `${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/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/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 c15ddb5406fa..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"] @@ -458,8 +458,9 @@ 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; +pub(crate) mod bindgen_test; #[path = "BunCPUProfiler.rs"] pub mod bun_cpu_profiler; #[path = "BunHeapProfiler.rs"] @@ -1575,12 +1576,9 @@ 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; -/// `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; +#[path = "generated_bindgen.rs"] +pub(crate) mod generated_bindgen; diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index 99a097fea1b4..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 &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/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 14bfbea46095..c3a335af3e27 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -1,23 +1,30 @@ -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( + { + tokenize: b.bool.loose.default(false), + parse: b.bool.loose.default(false), + }, + { allowArray: true }, +); -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..c9ae697f5a35 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::JsScope; // ─── wrap_static_method adapters ─────────────────────────────────────────── // `#[bun_jsc::host_fn(static)]` is not yet emitted, @@ -387,19 +386,27 @@ fn shell_escape(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult Ok(jsval) } +/// `Bun.gc(force)`: heap size after the collection. +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( - global: &JSGlobalObject, - brace_str: &BunString, - opts: r#gen::BracesOptions, + cx: &mut JsScope<'_>, + brace_str: BunString, + opts: BracesOptions, ) -> JsResult { - let brace_slice = brace_str.to_utf8(); + 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; 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( @@ -409,7 +416,7 @@ pub(crate) fn braces( }; } - 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( @@ -444,7 +451,7 @@ pub(crate) fn braces( } 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, core::slice::from_ref(&brace_str)); } // Hard cap before preallocation: `calculate_expanded_amount` saturates to @@ -1738,7 +1745,12 @@ 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 `JsScope` borrowed. + unsafe { + JsScope::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.bind.ts b/src/runtime/api/Glob.bind.ts new file mode 100644 index 000000000000..46a16f703b82 --- /dev/null +++ b/src/runtime/api/Glob.bind.ts @@ -0,0 +1,54 @@ +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), + }, + { allowArray: true }, +); + +export const GlobScanArgument = b.union({ + cwd: b.string.utf8, + options: GlobScanOptions, +}); + +export const Glob = b.class({ + rustPath: "crate::api::glob::Glob", + configurable: false, + construct: { + args: { pattern: b.string.utf8.owned }, + }, + proto: { + scan: { + builtin: true, + length: 1, + }, + scanSync: { + builtin: true, + length: 1, + }, + __scan: { + privateSymbol: "pull", + args: { options: GlobScanArgument.nullable }, + returns: b.any, + capability: "js", + throws: true, + }, + __scanSync: { + privateSymbol: "resolveSync", + args: { options: GlobScanArgument.nullable }, + returns: b.any, + capability: "heap", + throws: true, + }, + match: { + args: { str: b.string.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 c66d3a1c6f2c..325fe832984a 100644 --- a/src/runtime/api/glob.rs +++ b/src/runtime/api/glob.rs @@ -1,26 +1,41 @@ -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, JsHeap, JsLocal, JsScope, 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 { /// An ASCII pattern is the JavaScript string's own buffer, not a copy. pattern: bun_core::Utf8Bytes<'static>, shape: bun_glob::matcher::Shape, - 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.error(format_args!( + "{fn_name}: invalid `cwd`, longer than {MAX_PATH_BYTES} bytes" + )), + ScanError::Js(err) => err, + } + } } struct ScanOpts { @@ -33,164 +48,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, + }) } } @@ -349,10 +262,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, JsLocal<'a>> { + fn to_js<'a, A: walk::Accessor>( + walker: &walk::GlobWalker, + heap: &JsHeap<'a>, + ) -> bindgen::Result<'a, JsLocal<'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), } } } @@ -366,31 +285,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 { @@ -434,48 +331,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)), + 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))), }; - promise.resolve(global, js_strings) + // SAFETY: a job's completion, entered from the event loop with no `JsScope` borrowed. + unsafe { JsScope::with(global.as_mut_ptr(), settle) } } } -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); - } - - 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, @@ -497,10 +373,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 @@ -519,171 +395,63 @@ impl Glob { }; if in_standalone_graph { - return Ok(Some(AnyGlobWalker::Standalone(init_walker::< - StandaloneAccessor, - >( - global_this, - self.pattern.slice(), - &match_opts, - )?))); + return Ok(AnyGlobWalker::Standalone( + init_walker::(self.pattern.slice(), &match_opts)?, + )); } - Ok(Some(AnyGlobWalker::Fs( - init_walker::(global_this, self.pattern.slice(), &match_opts)?, - ))) + Ok(AnyGlobWalker::Fs(init_walker::( + self.pattern.slice(), + &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(pattern: bun_core::Utf8Bytes<'static>) -> Self { + Glob { + shape: bun_glob::matcher::Shape::of(pattern.slice()), + pattern, } - - let pat_str = pat_arg.to_utf8(global_this)?; - - Ok(Box::new(Glob { - shape: bun_glob::matcher::Shape::of(pat_str.slice()), - 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::matcher::match_prepared::<{ cfg!(windows) }>( + self.pattern.slice(), + &self.shape, + str, + bun_glob::Dots::Match, + ) + .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 JsScope<'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) + Ok(JsLocal::new(&cx.heap, value)) } - #[bun_jsc::host_fn(method)] - pub(crate) fn __scan_sync( + 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 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) - } - - #[bun_jsc::host_fn(method)] - pub(crate) fn r#match( - &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)?; - - let matched = bun_glob::matcher::match_prepared::<{ cfg!(windows) }>( - self.pattern.slice(), - &self.shape, - str.slice(), - bun_glob::Dots::Match, - ); - Ok(JSValue::from(matched.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 9aa52f73c0c5..30676941b365 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/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/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..94988537961e 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -64,6 +64,7 @@ pub(crate) mod napi; pub(crate) mod run_main; pub(crate) mod timer; +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.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..404fa19f018f 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -1,94 +1,60 @@ // 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 const userInfo = fn({ +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 }); +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, +}); +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, throws: true }); + +export const UserInfoOptions = b.dictionary( + { encoding: b.string.loose.utf8.default("") }, + { allowArray: true, nonObjectAsEmpty: true }, +); +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, }); +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::set_priority", + }, + { + args: { + priority: b.i32.validateInteger(PRI_MIN, PRI_MAX), }, - ], -}); + capability: "js", + throws: true, + 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 f5abb8887154..e1fdd1673c03 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::JsScope; use bun_jsc::bun_string_jsc; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; @@ -29,13 +30,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 +74,8 @@ 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; - } - - // 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; - } - - /// `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 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) + pub(crate) fn host_cpu_count() -> i32 { + bun_sysconf__SC_NPROCESSORS_ONLN().max(1) } impl CPUTimes { @@ -216,7 +110,8 @@ mod _impl { } } - pub(crate) fn cpus(global: &JSGlobalObject) -> 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); #[cfg(target_os = "macos")] @@ -635,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 JsScope<'_>, pid: i32) -> JsResult { + let global = cx.global(); let result = get_process_priority(pid); if result == i32::MAX { let err = SystemError { @@ -653,7 +549,8 @@ mod _impl { Ok(result) } - pub(crate) fn homedir(global: &JSGlobalObject) -> 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)] { @@ -756,7 +653,8 @@ mod _impl { } } - pub(crate) fn hostname(global: &JSGlobalObject) -> JsResult { + pub(crate) fn hostname(cx: &mut JsScope<'_>) -> JsResult { + let global = cx.global(); #[cfg(windows)] { let mut name_buffer: [u16; 130] = [0; 130]; // [129:0]u16 → 130 u16s with NUL at [129] @@ -781,7 +679,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 +729,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)] @@ -847,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 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(); // SAFETY: valid out-pointer @@ -1140,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 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; // SAFETY: valid out-pointers @@ -1372,7 +1265,15 @@ 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_of_current_process( + cx: &mut JsScope<'_>, + priority: i32, + ) -> JsResult<()> { + set_priority(cx, 0, priority) + } + + 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 { return Ok(()); @@ -1385,10 +1286,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")] { @@ -1421,27 +1318,25 @@ mod _impl { } } - pub(crate) fn uptime(global: &JSGlobalObject) -> JsResult { + 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); @@ -1450,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); } @@ -1459,15 +1353,15 @@ mod _impl { } pub(crate) fn user_info( - global_this: &JSGlobalObject, - options: &gen_::UserInfoOptions, + cx: &mut JsScope<'_>, + 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 1b737bb33f1a..74802e4be6d7 100644 --- a/src/runtime/socket/Handlers.rs +++ b/src/runtime/socket/Handlers.rs @@ -2,11 +2,11 @@ use core::cell::Cell; use core::ptr::NonNull; use std::rc::Rc; +use crate::generated_bindgen::socket_config::{ + SocketHandler, SocketHandlerBinaryType, SocketOptions, SocketOptionsTLS, +}; use bun_core::Utf8Bytes; use bun_jsc::array_buffer::BinaryType; -use bun_jsc::generated::{ - SocketConfig as GeneratedSocketConfig, SocketConfigHandlers as GeneratedSocketConfigHandlers, -}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{GlobalRef, JSGlobalObject, JSValue, JsResult, Strong}; use bun_sys::Fd; @@ -75,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, } } @@ -310,13 +310,14 @@ impl Handlers { opts: JSValue, mode: SocketMode, ) -> JsResult> { - let generated = GeneratedSocketConfigHandlers::from_js(global_object, opts)?; - Self::from_generated(global_object, &generated, mode) + 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)?; @@ -343,11 +344,11 @@ 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) => {{ - let value = generated.$field; + let value = generated.$field.to_js(); if value.is_undefined_or_null() { JSValue::ZERO } else if !value.is_callable() { @@ -406,11 +407,12 @@ impl Handlers { global_object: &JSGlobalObject, opts: JSValue, ) -> JsResult { - let generated = GeneratedSocketConfigHandlers::from_js(global_object, opts)?; - let callbacks = Self::validate_callbacks(global_object, &generated)?; - Ok(ReloadedHandlers { - callbacks, - binary_type: binary_type_from_generated(generated.binary_type), + SocketHandler::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), + }) }) } @@ -450,8 +452,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,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())? } }; @@ -530,7 +530,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 +550,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 +564,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 +601,8 @@ impl SocketConfig { global_object: &JSGlobalObject, mode: SocketMode, ) -> JsResult { - let generated = GeneratedSocketConfig::from_js(global_object, opts)?; - Self::from_generated(vm, global_object, generated, mode) + SocketOptions::from_js(global_object, opts, |generated| { + Self::from_generated(vm, global_object, generated, mode) + }) } } - -use bun_jsc::generated::SocketConfigTls as GeneratedTls; diff --git a/src/runtime/socket/SSLConfig.bind.ts b/src/runtime/socket/SSLConfig.bind.ts new file mode 100644 index 000000000000..40debf1cd1ac --- /dev/null +++ b/src/runtime/socket/SSLConfig.bind.ts @@ -0,0 +1,51 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export const TLSOptionsSingleFile = b.union({ + string: b.string, + buffer: b.bufferSource.arrayBuffer, + file: b.blob, +}); + +export const TLSOptionsFile = b.union({ + none: b.null, + string: b.string, + buffer: b.bufferSource.arrayBuffer, + file: b.blob, + array: b.array(TLSOptionsSingleFile), +}); + +export const ALPNProtocols = b.union({ + none: b.null, + string: b.string, + buffer: b.bufferSource.arrayBuffer, +}); + +export const TLSOptions = b.dictionary( + { + passphrase: b.string.nullable, + 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: TLSOptionsFile, + cert: TLSOptionsFile, + key: TLSOptionsFile, + secureOptions: b.u32.default(0), + 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, + ciphers: b.string.nullable, + clientRenegotiationLimit: b.u32.default(0), + clientRenegotiationWindow: b.u32.default(0), + crl: TLSOptionsFile, + allowPartialTrustChain: b.bool.default(false), + sessionTimeout: b.i32.default(0), + sigalgs: b.string.nullable, + ecdhCurve: b.string.nullable, + }, + { fromJS: true }, +); diff --git a/src/runtime/socket/SSLConfig.bindv2.ts b/src/runtime/socket/SSLConfig.bindv2.ts deleted file mode 100644 index caee0e9bb506..000000000000 --- a/src/runtime/socket/SSLConfig.bindv2.ts +++ /dev/null @@ -1,116 +0,0 @@ -import * as b from "../../codegen/bindgenv2/lib.ts"; - -export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { - 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), -}); - -export const ALPNProtocols = b.union("ALPNProtocols", { - none: b.null, - string: b.String, - buffer: b.ArrayBuffer, -}); - -export const SSLConfig = b.dictionary( - { - name: "SSLConfig", - userFacingName: "TLSOptions", - generateConversionFunction: true, - }, - { - 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", - }, - 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", - }, - ciphers: b.String.nullable, - clientRenegotiationLimit: { - type: b.u32, - default: 0, - internalName: "client_renegotiation_limit", - }, - clientRenegotiationWindow: { - type: b.u32, - default: 0, - internalName: "client_renegotiation_window", - }, - crl: SSLConfigFile, - allowPartialTrustChain: { - type: b.bool, - default: false, - internalName: "allow_partial_trust_chain", - }, - sessionTimeout: { - type: b.i32, - default: 0, - internalName: "session_timeout", - }, - sigalgs: b.String.nullable, - ecdhCurve: { - type: b.String.nullable, - internalName: "ecdh_curve", - }, - }, -); diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index 970aee2095d2..9b0bcbbab85a 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::TLSOptions, is_server: bool, ) -> JsResult>; } @@ -144,15 +145,14 @@ impl SSLConfigFromJs for SSLConfig { value: JSValue, is_server: bool, ) -> JsResult> { - let generated = jsc::generated::SSLConfig::from_js(global, value)?; - // `generated` dropped at scope exit + let generated = generated::TLSOptions::from_js(global, value)?; Self::from_generated(vm, global, &generated, is_server) } fn from_generated( vm: &VirtualMachine, global: &JSGlobalObject, - generated: &jsc::generated::SSLConfig, + generated: &generated::TLSOptions, is_server: bool, ) -> JsResult> { let mut result = SSLConfig::zero(); @@ -245,13 +245,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 +274,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 +367,7 @@ fn handle_path( fn handle_file_for_field( global: &JSGlobalObject, field: &'static str, - file: &jsc::generated::SSLConfigFile, + file: &generated::TLSOptionsFile, ) -> JsResult { match handle_file(global, file) { Ok(v) => Ok(v), @@ -389,25 +386,23 @@ fn handle_file_for_field( fn handle_file( global: &JSGlobalObject, - file: &jsc::generated::SSLConfigFile, + file: &generated::TLSOptionsFile, ) -> 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::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() }) } - 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::TLSOptionsFile::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::TLSOptionsFile::Array(list) => { + return handle_file_array(global, list); } }, )?; @@ -418,7 +413,7 @@ fn handle_file( fn handle_file_array( global: &JSGlobalObject, - elements: &[jsc::generated::SSLConfigSingleFile], + elements: &[generated::TLSOptionsSingleFile], ) -> Result { if elements.is_empty() { return Ok(None); @@ -436,18 +431,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::TLSOptionsSingleFile::String(val) => SingleFile::String(val), + generated::TLSOptionsSingleFile::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::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 new file mode 100644 index 000000000000..e1ccfcda5bc4 --- /dev/null +++ b/src/runtime/socket/SocketConfig.bind.ts @@ -0,0 +1,55 @@ +import * as b from "../../codegen/bindgen-lib.ts"; +import { TLSOptions } from "./SSLConfig.bind.ts"; + +export const SocketHandlerBinaryType = b.enumeration([ + ["arraybuffer", "ArrayBuffer"], + ["buffer", "Buffer"], + ["uint8array", "Uint8Array"], +]); + +export const SocketHandler = b.dictionary( + { + 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 SocketOptionsTLS = b.union({ + none: b.null, + boolean: b.bool, + object: TLSOptions, +}); + +export const SocketOptions = b.dictionary( + { + 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: 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, + rustName: "unix_", // The C++ struct has the same field, and `unix` is a predefined C macro. + }, + fd: b.i32.optional, + }, + { fromJS: true }, +); 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-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 4beefdead8f8..1fc49046079e 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -1,4 +1,12 @@ -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"; +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. +const bindgen = bindings ?? ({} as NonNullable); +const describe = describeAlways.skipIf(!bindings); +const it = itAlways.skipIf(!bindings); it("bindgen add example", () => { // Simple cases @@ -46,7 +54,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"); }); @@ -61,6 +68,52 @@ 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"); + 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"); + 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( + "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]"); @@ -69,7 +122,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() {} } }); @@ -86,3 +140,931 @@ it("unsigned integer dictionary fields reject negative values", () => { expect(caught.message).toBe(`TLSOptions.${field} must be in the range [0, 4294967295] (received -1)`); } }); + +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 = [ + "", + "ascii", + "latin1 éÿ", + "utf16 你好 \u{1f600}", + 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", + ]; + + 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.utf8Owned(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"); + expect(bindgen.utf8Owned(new String("boxed"))).toBe("boxed"); + expect(bindgen.utf8Owned(new Path("derived"))).toBe("derived"); + for (const value of [1, true, null, undefined, {}, [], Symbol("s"), { toString: () => "no" }]) { + for (const echo of [bindgen.utf8Echo, bindgen.utf8Owned]) { + expect(() => echo(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(new TypeError("Cannot convert a symbol to a string")); + 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("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"); + } + }); +}); + +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", () => { + 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")); + } + }); +}); + +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("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; 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; 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; rest: Green; 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 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); + 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" }); + }); + + 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", () => { + 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"); + 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( + "value must be of type number, boolean, string, or array of integer", + ); + } + 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", () => { + 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"], + [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"], + [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( + 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"); + }); + + 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 number, boolean, string, or array of integer", + ); + expect(() => + bindgen.describeValue({ toString: () => calls.push("toString"), valueOf: () => calls.push("valueOf") }), + ).toThrow("value must be of type number, boolean, string, or array of integer"); + 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", + ); + }); + + 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"); + 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"); + expect(() => b.union({ tag: b.bool, text: b.string })).toThrow("has a member of that name"); + 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 }); + }); +}); + +describe("overloads", () => { + it("are told apart by the kinds of their arguments", () => { + 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, undefined], "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 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("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", () => { + 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", + ); + // 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 } }); + }); +}); + +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("element of numbers must be a number"); + expect(() => bindgen.sumArray(5)).toThrow("numbers must be an array of integer"); + }); + + 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)); + 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); + }); +}); + +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( + "buffer must be an ArrayBuffer or ArrayBufferView that is not shared", + ); + } + }); + + 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") }; + 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]); + 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, TestHolder } = bindgen as any; + + it("looks like 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", () => { + const before = TestCounter.alive(); + expect(() => new TestCounter(-1)).toThrow(new RangeError("start must not be negative")); + expect(() => new TestCounter("1")).toThrow("start must be a number"); + // A collection in between can only lower it. + expect(TestCounter.alive()).toBeLessThanOrEqual(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(new TypeError("Attempted to assign to readonly property.")); + }); + + 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("*"), + bindgen.makeToken(1), + new Blob([]), + new URL("http://localhost/"), + new Headers(), + 10n ** 30n, + ]) { + expect(() => increment.call(receiver)).toThrow( + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_THIS", + message: expect.stringMatching(/^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("other must be an instance of TestCounter"); + }); + + 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 an internal field 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("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); + Bun.gc(true); + 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 7da469bb0c1a..29c3469a9a13 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, bindgenCodegen: [] }), 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,30 @@ describe("emitBindgen", () => { const bindgen = repoBindFiles(cfg); expect(bindgen).not.toBeEmpty(); - emitBindgen({ n, cfg, sources: sourceLists({ bindgen }), o, dirStamp }); + emitBindgen({ n, cfg, sources: sourceLists({ bindgen, bindgenCodegen: [] }), 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"); + const schemas = n + .toString() + .replaceAll("$:", ":") + .match(/--(sources|types-only)=[^\s'"]*/g)!; + expect( + await bunRun([ + "run", + script, + ...new Set(schemas), + `--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. diff --git a/test/js/bun/glob/proto.test.ts b/test/js/bun/glob/proto.test.ts index f6ed1bff81e8..3d2c6de3fcc1 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 () => { @@ -44,3 +44,76 @@ 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 glob = new Bun.Glob("*"); + 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(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")); + 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 read like any other object. + 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"); + // Over MAX_PATH_BYTES everywhere: Windows has the largest, 98302. + return Buffer.alloc(100_000, "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"]); +}); + +// 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([]); +}); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index c672579e18cf..825685fcab06 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, @@ -6545,3 +6546,76 @@ 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, + passphrase: 5, + ca: 5, + }; + Object.assign(Object.prototype, polluted); + try { + 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"); + 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]; + } +}); + +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), + ]) { + for (const key of [view, [view]]) { + using listener = listen(key); + expect(listener.port).toBeGreaterThan(0); + } + } +}); + +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)]]; + 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" })); + } + } +}); diff --git a/test/js/bun/shell/brace.test.ts b/test/js/bun/shell/brace.test.ts index c4c29140c79e..eecddfced8a0 100644 --- a/test/js/bun/shell/brace.test.ts +++ b/test/js/bun/shell/brace.test.ts @@ -333,3 +333,34 @@ 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("an argument that was not passed is undefined", () => { + expect(($.braces as any)()).toEqual(["undefined"]); + expect($.braces(undefined as any)).toEqual(["undefined"]); + }); +}); 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 007f92805a64..6f91022ca26c 100644 --- a/test/js/node/os/os.test.js +++ b/test/js/node/os/os.test.js @@ -309,3 +309,105 @@ 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(); + 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", () => { + expect(() => os.setPriority()).toThrow( + 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 && <= 19. Received 20', + }), + ); + const priority = os.getPriority(); + os.setPriority(priority, undefined); + expect(os.getPriority()).toBe(priority); +}); + +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"); + } +});