diff --git a/bench/ffi/bun.js b/bench/ffi/bun.js index 5ef13e234ada..5800b53b2466 100644 --- a/bench/ffi/bun.js +++ b/bench/ffi/bun.js @@ -8,20 +8,31 @@ const { ffi_noop: { native: ffi_noop }, ffi_hash: { native: ffi_hash }, ffi_string: { native: ffi_string }, + ffi_strlen: { native: ffi_strlen }, }, } = dlopen(import.meta.dir + "/src/ffi_napi_bench.node", { ffi_noop: { args: [], returns: "void" }, ffi_string: { args: [], returns: "ptr" }, ffi_hash: { args: ["ptr", "u32"], returns: "u32" }, + ffi_strlen: { args: ["cstring"], returns: "u32" }, }); const bytes = new Uint8Array(64); +const str36 = "550e8400-e29b-41d4-a716-446655440000"; +const strBuf = Buffer.from(str36 + "\0", "utf8"); +const strPtr = ptr(strBuf); +const cachedCString = new CString(strPtr); group("bun:ffi", () => { bench("noop", () => ffi_noop()); bench("hash", () => ffi_hash(ptr(bytes), bytes.byteLength)); bench("c string", () => new CString(ffi_string())); + + bench("string arg: JS string", () => ffi_strlen(str36)); + bench("string arg: cached CString", () => ffi_strlen(cachedCString)); + bench("string arg: raw pointer", () => ffi_strlen(strPtr)); + bench("string arg: TypedArray", () => ffi_strlen(strBuf)); }); if (process.env.SHOW_NAPI) diff --git a/bench/ffi/src/src/lib.rs b/bench/ffi/src/src/lib.rs index dd0f80fcfd5f..168fdba25897 100644 --- a/bench/ffi/src/src/lib.rs +++ b/bench/ffi/src/src/lib.rs @@ -15,8 +15,6 @@ fn hash(buf: &[u8]) -> u32 { return hash; } - - #[cfg(feature="enable-napi")] #[napi] pub fn napi_noop() { // do nothing @@ -26,19 +24,22 @@ fn hash(buf: &[u8]) -> u32 { // do nothing } - - #[cfg(feature="enable-napi")] #[napi] pub fn napi_string() -> &'static str { return &STRING[0..(STRING.len() - 1)]; } +#[no_mangle] unsafe extern "C" fn ffi_strlen(p: *const u8) -> u32 { + if p.is_null() { return 0; } + let mut n = 0u32; + while *p.add(n as usize) != 0 { n += 1; } + n +} + #[no_mangle] unsafe extern "C" fn ffi_string() -> *const u8 { return STRING.as_ptr(); } - - #[cfg(feature="enable-napi")] #[napi] pub fn napi_hash(buffer: Buffer) -> u32 { return hash(&buffer); diff --git a/docs/runtime/ffi.mdx b/docs/runtime/ffi.mdx index 867dd9483a1a..a5fc6766bd5e 100644 --- a/docs/runtime/ffi.mdx +++ b/docs/runtime/ffi.mdx @@ -49,7 +49,7 @@ According to [our benchmark](https://github.com/oven-sh/bun/tree/main/bench/ffi) -Bun generates and just-in-time compiles C bindings that efficiently convert values between JavaScript types and native types. To compile C, Bun embeds [TinyCC](https://github.com/TinyCC/tinycc), a small and fast C compiler. +`dlopen`, `linkSymbols`, `CFunction`, and `JSCallback` are implemented natively by Bun's JavaScript engine (JavaScriptCore): argument conversion, arity handling, and result boxing happen in-engine, and hot call sites compile down through the DFG/FTL JIT tiers into direct native calls with no per-argument JavaScript shim. [TinyCC](https://github.com/TinyCC/tinycc), a small and fast C compiler, is embedded only for [`cc()`](/runtime/c-compiler), which compiles C source you provide at runtime. --- @@ -129,31 +129,57 @@ clang++ -dynamiclib add.cpp -o libadd.dylib The following `FFIType` values are supported. -| `FFIType` | C Type | Aliases | -| ---------- | -------------- | --------------------------- | -| buffer | `char*` | | -| cstring | `char*` | | -| function | `(void*)(*)()` | `fn`, `callback` | -| ptr | `void*` | `pointer`, `void*`, `char*` | -| i8 | `int8_t` | `int8_t` | -| i16 | `int16_t` | `int16_t` | -| i32 | `int32_t` | `int32_t`, `int` | -| i64 | `int64_t` | `int64_t` | -| i64_fast | `int64_t` | | -| u8 | `uint8_t` | `uint8_t` | -| u16 | `uint16_t` | `uint16_t` | -| u32 | `uint32_t` | `uint32_t` | -| u64 | `uint64_t` | `uint64_t` | -| u64_fast | `uint64_t` | | -| f32 | `float` | `float` | -| f64 | `double` | `double` | -| bool | `bool` | | -| char | `char` | | -| napi_env | `napi_env` | | -| napi_value | `napi_value` | | +| `FFIType` | C Type | Aliases | +| ------------- | --------------------- | ------------------------------- | +| buffer | `char*` | | +| cstring | `char*` | | +| function | `(void*)(*)()` | `fn`, `callback` | +| ptr | `void*` | `pointer`, `void*`, `char*` | +| i8 | `int8_t` | `int8_t` | +| i16 | `int16_t` | `int16_t` | +| i32 | `int32_t` | `int32_t`, `int` | +| i64 | `int64_t` | `int64_t` | +| i64_fast | `int64_t` | | +| u8 | `uint8_t` | `uint8_t` | +| u16 | `uint16_t` | `uint16_t` | +| u32 | `uint32_t` | `uint32_t` | +| u64 | `uint64_t` | `uint64_t` | +| u64_fast | `uint64_t` | | +| f32 | `float` | `float` | +| f64 | `double` | `double` | +| bool | `bool` | | +| char | `char` | | +| napi_env | `napi_env` | `cc()` only | +| napi_value | `napi_value` | `cc()` only | +| buffer_length | `uint64_t` / `size_t` | engine-native only (not `cc()`) | `buffer` arguments must be a `TypedArray` or `DataView`. +`buffer_length` is `buffer`'s length twin: pass the **same** `TypedArray`/`DataView` you passed +for the `buffer` parameter, and the callee receives that view's **byte length** as an unsigned +64-bit integer. The engine reads the pointer and the length off the same object at the moment of +the call, so the two always agree — an atomic snapshot you can't get by passing +`view.byteLength` yourself (a length read in JavaScript beforehand can go stale against a +resizable, growable, or transferred buffer). It's argument-only and, like the napi types, not +available inside `cc()`. + +```ts +const { + symbols: { write_all }, +} = dlopen(path, { + // C: size_t write_all(int fd, const void *buf, size_t len) + write_all: { args: ["i32", "buffer", "buffer_length"], returns: "u64" }, +}); +const chunk = new TextEncoder().encode("hello"); +write_all(1, chunk, chunk); // buf and len both come from `chunk` +``` + +`napi_env` and `napi_value` are only valid in [`cc()`](/runtime/c-compiler) source, where a +`napi_env` parameter is filled in with the module's environment by the compiled trampoline (the +JavaScript argument passed at that position is a placeholder and is ignored) and `napi_value` +passes the JavaScript value through unchanged. Using either type in a `dlopen`, `linkSymbols`, `JSCallback`, +or `CFunction` descriptor throws a `TypeError`. + --- ## Strings @@ -175,26 +201,10 @@ C strings: -To solve this, `bun:ffi` exports `CString` which extends JavaScript's built-in `String` to support null-terminated strings and add a few extras: +To solve this, `bun:ffi` exports `CString`, which reads a UTF-8 C string at a pointer and returns a plain JavaScript string: ```ts -class CString extends String { - /** - * Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary. - */ - constructor(ptr: number, byteOffset?: number, byteLength?: number): string; - - /** - * The ptr to the C string - * - * This `CString` instance is a clone of the string, so it - * is safe to continue using this instance after the `ptr` has been - * freed. - */ - ptr: number; - byteOffset?: number; - byteLength?: number; -} +CString(ptr: number, byteOffset?: number, byteLength?: number): string; ``` To convert from a null-terminated string pointer to a JavaScript string: @@ -209,16 +219,30 @@ To convert from a pointer with a known length to a JavaScript string: const myString = new CString(ptr, 0, byteLength); ``` -The `new CString()` constructor clones the C string, so it is safe to continue using `myString` after `ptr` has been freed. +`new CString()` returns a normal string (`typeof myString === "string"`, `myString === "hello"` works) that is a clone of the C string, so it is safe to continue using it after `ptr` has been freed. ```ts -my_library_free(myString.ptr); +const myString = new CString(ptr); +my_library_free(ptr); // this is safe because myString is a clone console.log(myString); ``` -When used in `returns`, `FFIType.cstring` coerces the pointer to a JavaScript `string`. When used in `args`, `FFIType.cstring` is identical to `ptr`. +When used in `returns`, `FFIType.cstring` coerces the pointer to a JavaScript `string`. When used in `args`, `FFIType.cstring` accepts everything `ptr` does **and** additionally accepts a JavaScript string directly — the engine transcodes it to a null-terminated UTF-8 buffer that lives for the duration of the call, so you don't need to encode it into a `Buffer` yourself: + +```ts +symbols.puts("Hello, world!"); // args: ["cstring"] — pass the string directly +``` + +**Lifetime of a `cstring` return.** The pointer is whatever the C function returned — memory +owned by the native side (a static, a buffer it manages, or heap it allocated); the engine copies +nothing on return, and the JavaScript string is cloned out of it. The one aliasing case is a C +function that hands back a pointer _derived from a `cstring` argument you passed as a JavaScript +string_: that argument was transcoded into the engine's call-scoped buffer, so treat such a +returned pointer as valid only until your next FFI call reuses that buffer (the usual C rule for +functions that return their input). Clone it (via the returned string, or `new CString`) rather +than holding the raw address. --- @@ -317,7 +341,7 @@ When you're done with a `JSCallback`, call `close()` to free the memory. `JSCallback` has experimental support for thread-safe callbacks. You need this if you pass a callback function into a different thread from the one that created it. Enable it with the optional `threadsafe` parameter. -Thread-safe callbacks work best when run from another thread that is running JavaScript code, that is, a [`Worker`](/runtime/workers). A future version of Bun will enable them to be called from any thread, such as new threads spawned by your native library that Bun is not aware of. +Thread-safe callbacks can be invoked from **any thread** — including threads spawned by your native library that Bun is not otherwise aware of. The engine copies the C arguments on the calling thread and marshals the invocation onto the JavaScript thread, where the arguments are converted (64-bit integers and pointers arrive as exact BigInts) and your function runs. Because the invocation is asynchronous from C's point of view, the value returned to the C caller is unspecified: you may declare a non-`void` `returns` (the example below uses `"bool"`), but the C side must treat a thread-safe callback as returning `void` and ignore its return value. ```ts const searchIterator = new JSCallback((ptr, length) => /hello/.test(new CString(ptr, length)), { diff --git a/packages/bun-types/ffi.d.ts b/packages/bun-types/ffi.d.ts index 8c365189a310..93584ffc457d 100644 --- a/packages/bun-types/ffi.d.ts +++ b/packages/bun-types/ffi.d.ts @@ -9,9 +9,6 @@ * }); * ``` * - * Bun uses [tinycc](https://github.com/TinyCC/tinycc) to just-in-time compile - * C wrappers that convert JavaScript types to C types and back. - * * @category FFI */ declare module "bun:ffi" { @@ -306,9 +303,11 @@ declare module "bun:ffi" { void = 13, /** - * When used as a `returns`, the value becomes a {@link CString}. + * When used as a `returns`, the value becomes a `string` (a NULL pointer + * becomes `null`). * - * When used in `args`, it is equivalent to {@link FFIType.pointer} + * When used in `args`, it is equivalent to {@link FFIType.pointer} and + * additionally accepts a JavaScript string. */ cstring = 14, @@ -334,6 +333,14 @@ declare module "bun:ffi" { napi_env = 18, napi_value = 19, buffer = 20, + /** + * A TypedArray or DataView whose **byte length** is passed to the callee as an + * unsigned 64-bit integer. Pass the *same* view you passed for a `buffer` parameter; + * the engine reads pointer and length off the same object at call time, so the two + * always agree (an atomic snapshot -- unlike passing `view.byteLength` yourself). + * Engine-native only; not supported inside `cc()`. + */ + buffer_length = 21, } type Pointer = number & { __pointer__: null }; @@ -357,15 +364,16 @@ declare module "bun:ffi" { [FFIType.double]: number; [FFIType.float]: number; [FFIType.bool]: boolean; - [FFIType.ptr]: NodeJS.TypedArray | Pointer | CString | null; + [FFIType.ptr]: NodeJS.TypedArray | Pointer | bigint | null; [FFIType.void]: undefined; - [FFIType.cstring]: NodeJS.TypedArray | Pointer | CString | null; + [FFIType.cstring]: string | NodeJS.TypedArray | Pointer | bigint | null; [FFIType.i64_fast]: number | bigint; [FFIType.u64_fast]: number | bigint; [FFIType.function]: Pointer | JSCallback; // cannot be null [FFIType.napi_env]: unknown; [FFIType.napi_value]: unknown; [FFIType.buffer]: NodeJS.TypedArray | DataView; + [FFIType.buffer_length]: NodeJS.TypedArray | DataView; } interface FFITypeToReturnsType { [FFIType.char]: number; @@ -380,15 +388,16 @@ declare module "bun:ffi" { [FFIType.double]: number; [FFIType.float]: number; [FFIType.bool]: boolean; - [FFIType.ptr]: Pointer | null; + [FFIType.ptr]: Pointer | bigint | null; [FFIType.void]: undefined; - [FFIType.cstring]: CString; + [FFIType.cstring]: string | null; [FFIType.i64_fast]: number | bigint; [FFIType.u64_fast]: number | bigint; - [FFIType.function]: Pointer | null; + [FFIType.function]: Pointer | bigint | null; [FFIType.napi_env]: unknown; [FFIType.napi_value]: unknown; [FFIType.buffer]: NodeJS.TypedArray | DataView; + [FFIType.buffer_length]: NodeJS.TypedArray | DataView; } interface FFITypeStringToType { ["char"]: FFIType.char; @@ -424,6 +433,8 @@ declare module "bun:ffi" { ["napi_env"]: FFIType.napi_env; ["napi_value"]: FFIType.napi_value; ["buffer"]: FFIType.buffer; + ["buffer_length"]: FFIType.buffer_length; + ["buffer_bytelength"]: FFIType.buffer_length; } type FFITypeOrString = FFIType | keyof FFITypeStringToType; @@ -570,9 +581,6 @@ declare module "bun:ffi" { * // "1.0.0" * ``` * - * Bun uses [tinycc](https://github.com/TinyCC/tinycc) to just-in-time - * compile C wrappers that convert JavaScript types to C types and back. - * * @category FFI */ function dlopen>( @@ -711,10 +719,8 @@ declare module "bun:ffi" { * getVersion.close(); * ``` * - * Bun uses [tinycc](https://github.com/TinyCC/tinycc) to just-in-time - * compile a C wrapper that converts JavaScript types to C types and back. */ - function CFunction(fn: FFIFunction & { ptr: Pointer }): CallableFunction & { + function CFunction(fn: FFIFunction & { ptr: Pointer | number | bigint }): CallableFunction & { /** * Free the memory allocated by the wrapping function */ @@ -767,8 +773,6 @@ declare module "bun:ffi" { * ]; * ``` * - * Bun uses [tinycc](https://github.com/TinyCC/tinycc) to just-in-time - * compile C wrappers that convert JavaScript types to C types and back. */ function linkSymbols>(symbols: Fns): Library; @@ -785,7 +789,7 @@ declare module "bun:ffi" { * @param byteOffset bytes to skip before reading * @param byteLength bytes to read */ - function toBuffer(ptr: Pointer, byteOffset?: number, byteLength?: number): Buffer; + function toBuffer(ptr: Pointer | number | bigint, byteOffset?: number, byteLength?: number): Buffer; /** * Read a pointer as an {@link ArrayBuffer} @@ -800,7 +804,7 @@ declare module "bun:ffi" { * @param byteOffset bytes to skip before reading * @param byteLength bytes to read */ - function toArrayBuffer(ptr: Pointer, byteOffset?: number, byteLength?: number): ArrayBuffer; + function toArrayBuffer(ptr: Pointer | number | bigint, byteOffset?: number, byteLength?: number): ArrayBuffer; /** * Read a value directly from a memory address, without creating a @@ -820,7 +824,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function u8(ptr: Pointer, byteOffset?: number): number; + function u8(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a signed 8-bit integer at `ptr + byteOffset` * @@ -834,7 +838,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function i8(ptr: Pointer, byteOffset?: number): number; + function i8(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read an unsigned 16-bit integer at `ptr + byteOffset` * @@ -848,7 +852,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function u16(ptr: Pointer, byteOffset?: number): number; + function u16(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a signed 16-bit integer at `ptr + byteOffset` * @@ -862,7 +866,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function i16(ptr: Pointer, byteOffset?: number): number; + function i16(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read an unsigned 32-bit integer at `ptr + byteOffset` * @@ -876,7 +880,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function u32(ptr: Pointer, byteOffset?: number): number; + function u32(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a signed 32-bit integer at `ptr + byteOffset` * @@ -890,7 +894,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function i32(ptr: Pointer, byteOffset?: number): number; + function i32(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a 32-bit float at `ptr + byteOffset` * @@ -904,7 +908,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function f32(ptr: Pointer, byteOffset?: number): number; + function f32(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read an unsigned 64-bit integer at `ptr + byteOffset`, as a `bigint` * @@ -918,7 +922,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function u64(ptr: Pointer, byteOffset?: number): bigint; + function u64(ptr: Pointer | number | bigint, byteOffset?: number): bigint; /** * Read a signed 64-bit integer at `ptr + byteOffset`, as a `bigint` * @@ -932,7 +936,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function i64(ptr: Pointer, byteOffset?: number): bigint; + function i64(ptr: Pointer | number | bigint, byteOffset?: number): bigint; /** * Read a 64-bit double at `ptr + byteOffset` * @@ -946,7 +950,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function f64(ptr: Pointer, byteOffset?: number): number; + function f64(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a pointer at `ptr + byteOffset` * @@ -960,7 +964,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function ptr(ptr: Pointer, byteOffset?: number): number; + function ptr(ptr: Pointer | number | bigint, byteOffset?: number): number; /** * Read a pointer-sized signed integer (`intptr_t`) at `ptr + byteOffset` * @@ -974,7 +978,7 @@ declare module "bun:ffi" { * @param ptr The memory address to read * @param byteOffset bytes to skip before reading */ - function intptr(ptr: Pointer, byteOffset?: number): number; + function intptr(ptr: Pointer | number | bigint, byteOffset?: number): number; } /** @@ -1007,34 +1011,19 @@ declare module "bun:ffi" { function ptr(view: NodeJS.TypedArray | ArrayBufferLike | DataView, byteOffset?: number): Pointer; /** - * Get a string from a UTF-8 encoded C string. - * - * If `byteLength` is not provided, the string is assumed to be null-terminated. - * - * Bun catches some invalid pointers, but not all. Passing an invalid - * pointer, or reading past the end of the memory it points to, can crash - * the program or cause undefined behavior. - * - * @example - * ```js - * var ptr = lib.symbols.getVersion(); - * console.log(new CString(ptr)); - * ``` - * - * @example - * ```js - * var ptr = lib.symbols.getVersion(); - * // print the first 4 characters - * console.log(new CString(ptr, 0, 4)); - * ``` + * A JavaScript string decoded from a UTF-8 encoded C string. * * @category FFI */ - class CString extends String { + type CString = string; + + interface CStringConstructor { /** - * Get a string from a UTF-8 encoded C string. + * Read a UTF-8 encoded C string into a JavaScript string. * * If `byteLength` is not provided, the string is assumed to be null-terminated. + * The result is a clone of the C string, so it is safe to keep using it after + * the memory at `ptr` has been freed. A falsy `ptr` yields an empty string. * * Bun catches some invalid pointers, but not all. Passing an invalid * pointer, or reading past the end of the memory it points to, can crash @@ -1057,26 +1046,17 @@ declare module "bun:ffi" { * @param byteOffset bytes to skip before reading * @param byteLength bytes to read */ - constructor(ptr: Pointer, byteOffset?: number, byteLength?: number); - - /** - * The pointer to the C string - * - * The `CString` is a clone of the string, so the instance stays safe to - * use after the memory at `ptr` has been freed. - */ - ptr: Pointer; - byteOffset?: number; - byteLength?: number; - - /** - * Get the {@link ptr} as an `ArrayBuffer` - * - * A `null` or empty `ptr` returns an `ArrayBuffer` with `byteLength` 0 - */ - get arrayBuffer(): ArrayBuffer; + new (ptr: Pointer | number | bigint | null, byteOffset?: number, byteLength?: number): string; + (ptr: Pointer | number | bigint | null, byteOffset?: number, byteLength?: number): string; } + /** + * Get a string from a UTF-8 encoded C string. + * + * @category FFI + */ + const CString: CStringConstructor; + /** * Pass a JavaScript function to FFI (Foreign Function Interface) */ diff --git a/scripts/build/ci.ts b/scripts/build/ci.ts index 9a91c4caad04..ac722cd2b222 100644 --- a/scripts/build/ci.ts +++ b/scripts/build/ci.ts @@ -8,7 +8,17 @@ */ import { spawn as nodeSpawn, spawnSync } from "node:child_process"; -import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { + chmodSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { basename, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { generateOrderFile } from "../orderfile/generate.ts"; @@ -16,6 +26,7 @@ import { generateOrderFile } from "../orderfile/generate.ts"; import * as utils from "../utils.mjs"; import { bunExeName, shouldStrip, type BunOutput } from "./bun.ts"; import type { Config } from "./config.ts"; +import { webkitTestFFIPath } from "./deps/webkit.ts"; import { BuildError } from "./error.ts"; import { crossFeaturesJson } from "./features-json.ts"; import { orderFilePath, usesOrderFile } from "./flags.ts"; @@ -274,6 +285,12 @@ export function uploadArtifacts(cfg: Config, output: BunOutput): void { upload(depPaths, cfg.buildDir); } + const testFFI = webkitTestFFIPath(cfg); + if (existsSync(testFFI)) { + console.log("Uploading testFFI..."); + upload([relative(cfg.buildDir, testFFI)], cfg.buildDir); + } + // ─── Phase 2: free disk, gzip (posix only), upload archive ─── // CI agents are disk-constrained. Free what we no longer need: codegen/ // (sources already compiled into the archive), obj/ (.o files archived), @@ -323,6 +340,7 @@ function upload(paths: string[], cwd: string): void { // ${bunTriplet}-profile.zip (plain release) // └── ${bunTriplet}-profile/ // ├── bun-profile[.exe] +// ├── testFFI[.exe] (WebKit FFI test binary, when shipped) // ├── features.json // ├── bun-profile.linker-map (linux/mac non-asan) // ├── bun-profile.pdb (windows) @@ -405,6 +423,11 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void { // Result: bun-linux-x64-profile, bun-linux-x64-asan, etc. const bunPath = exeName.replace(/^bun/, bunTriplet); const files: string[] = [basename(exe), "features.json"]; + const testFFI = webkitTestFFIPath(cfg); + if (existsSync(testFFI)) { + chmodSync(testFFI, 0o755); + files.push(testFFI); + } // Debug symbols / linker map — platform-specific extras. if (cfg.windows) { files.push(`${exeName}.pdb`); diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f84dd77e0e19..45502005f29a 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "549170099226f816a4b204ea1d8fa102fb79eefa"; +export const WEBKIT_VERSION = "34c01d13391e00c06862a3d2c5b7fff350ac87e0"; /** * WebKit (JavaScriptCore) — the JS engine. @@ -106,6 +106,11 @@ function prebuiltDestDir(cfg: Config): string { // Lib paths — relative to destDir (prebuilt) or buildDir (local) // ─────────────────────────────────────────────────────────────────────────── +export function webkitTestFFIPath(cfg: Config): string { + const root = cfg.webkit === "prebuilt" ? prebuiltDestDir(cfg) : depBuildDir(cfg, "WebKit"); + return resolve(root, "bin", cfg.windows ? "testFFI.exe" : "testFFI"); +} + /** Build a lib path under the WebKit install's lib/ dir. */ function wkLib(cfg: Config, name: string): string { return `lib/${cfg.libPrefix}${name}${cfg.libSuffix}`; diff --git a/src/bundler/analyze_transpiled_module.rs b/src/bundler/analyze_transpiled_module.rs index dcc222b89649..93d4e38c81f5 100644 --- a/src/bundler/analyze_transpiled_module.rs +++ b/src/bundler/analyze_transpiled_module.rs @@ -17,7 +17,7 @@ use bun_core; // the print boundary. // ────────────────────────────────────────────────────────────────────────── pub use bun_js_printer::analyze_transpiled_module::{ - FetchParameters, ModuleInfo, ModulePhase, StringID, VarKind, + FetchParameters, ModuleInfo, ModulePhase, StringID, }; /// Downstream name for `FetchParameters` — mirrors how @@ -50,31 +50,25 @@ unsafe impl bytemuck::Zeroable for RecordKind {} unsafe impl bytemuck::Pod for RecordKind {} impl RecordKind { - /// var_name - pub const DECLARED_VARIABLE: Self = Self(0); - /// let_name - pub const LEXICAL_VARIABLE: Self = Self(1); /// module_name, import_name, local_name - pub const IMPORT_INFO_SINGLE: Self = Self(2); + pub const IMPORT_INFO_SINGLE: Self = Self(0); /// module_name, import_name, local_name - pub const IMPORT_INFO_SINGLE_TYPE_SCRIPT: Self = Self(3); + pub const IMPORT_INFO_SINGLE_TYPE_SCRIPT: Self = Self(1); /// module_name, import_name = '*', local_name - pub const IMPORT_INFO_NAMESPACE: Self = Self(4); + pub const IMPORT_INFO_NAMESPACE: Self = Self(2); /// export_name, import_name, module_name - pub const EXPORT_INFO_INDIRECT: Self = Self(5); + pub const EXPORT_INFO_INDIRECT: Self = Self(3); /// export_name, local_name, padding (for local => indirect conversion) - pub const EXPORT_INFO_LOCAL: Self = Self(6); + pub const EXPORT_INFO_LOCAL: Self = Self(4); /// export_name, module_name - pub const EXPORT_INFO_NAMESPACE: Self = Self(7); + pub const EXPORT_INFO_NAMESPACE: Self = Self(5); /// module_name - pub const EXPORT_INFO_STAR: Self = Self(8); + pub const EXPORT_INFO_STAR: Self = Self(6); /// module_name, import_name = '*', local_name (ModulePhase::Defer) - pub const IMPORT_INFO_NAMESPACE_DEFER: Self = Self(9); + pub const IMPORT_INFO_NAMESPACE_DEFER: Self = Self(7); // PascalCase aliases — `bundler_jsc::analyze_jsc` pattern-matches on these // (the SCREAMING_CASE consts above are kept for intra-crate use). - pub const DeclaredVariable: Self = Self::DECLARED_VARIABLE; - pub const LexicalVariable: Self = Self::LEXICAL_VARIABLE; pub const ImportInfoSingle: Self = Self::IMPORT_INFO_SINGLE; pub const ImportInfoSingleTypeScript: Self = Self::IMPORT_INFO_SINGLE_TYPE_SCRIPT; pub const ImportInfoNamespace: Self = Self::IMPORT_INFO_NAMESPACE; @@ -86,7 +80,6 @@ impl RecordKind { pub fn len(self) -> crate::Result { match self { - Self::DECLARED_VARIABLE | Self::LEXICAL_VARIABLE => Ok(1), Self::IMPORT_INFO_SINGLE => Ok(3), Self::IMPORT_INFO_SINGLE_TYPE_SCRIPT => Ok(3), Self::IMPORT_INFO_NAMESPACE => Ok(3), diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 49d092acb299..848f2e05d949 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -17,8 +17,8 @@ pub use bv2_impl::bake_types; pub use bv2_impl::dispatch; pub use bv2_impl::{ CompileResult, CompileResultForSourceMap, CompileResultForSourceMapColumns, ContentHasher, - DeclInfo, DeclInfoKind, EventLoop, ImportTracker, PartRange, StableRef, WrapKind, - generic_path_with_pretty_initialized, target_from_hashbang, + EventLoop, ImportTracker, PartRange, StableRef, WrapKind, generic_path_with_pretty_initialized, + target_from_hashbang, }; pub use bv2_impl::{DevServerInput, DevServerOutput, ImportTrackerIterator, ImportTrackerStatus}; // Flatten the impl-body module into this file's namespace so external callers @@ -7271,23 +7271,10 @@ pub mod bv2_impl { pub import_ref: bun_ast::Ref, } - #[repr(u8)] - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum DeclInfoKind { - Declared, - Lexical, - } - #[derive(Clone)] - pub struct DeclInfo { - pub name: Box<[u8]>, - pub kind: DeclInfoKind, - } - pub enum CompileResult { Javascript { source_index: IndexInt, result: bun_js_printer::PrintResult, - decls: Box<[DeclInfo]>, }, Css { result: crate::Result>, @@ -7353,7 +7340,6 @@ pub mod bv2_impl { CompileResult::Javascript { source_index, result, - decls, } => CompileResult::Javascript { source_index: *source_index, result: match result { @@ -7367,7 +7353,6 @@ pub mod bv2_impl { } bun_js_printer::PrintResult::Err(e) => bun_js_printer::PrintResult::Err(*e), }, - decls: decls.clone(), }, CompileResult::Css { result, @@ -7399,7 +7384,6 @@ pub mod bv2_impl { code: Box::new([]), source_map: None, }), - decls: Box::new([]), } } } diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 59c1e82f34f9..4c8f3e4d6eba 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -46,9 +46,8 @@ pub use options_impl::PathTemplate; pub use HTMLImportManifest::html_import_manifest; pub use bun_core::cheap_prefix_normalizer; pub use bundle_v2::{ - CompileResult, CompileResultForSourceMap, ContentHasher, DeclInfo, DeclInfoKind, EventLoop, - ImportTracker, PartRange, StableRef, WrapKind, generic_path_with_pretty_initialized, - target_from_hashbang, + CompileResult, CompileResultForSourceMap, ContentHasher, EventLoop, ImportTracker, PartRange, + StableRef, WrapKind, generic_path_with_pretty_initialized, target_from_hashbang, }; pub use chunk::{ CrossChunkImport, CrossChunkImportItem, CrossChunkImportItemList, bun_renamer, diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index 3b34751567b3..6f5dfa72c6b3 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -10,7 +10,7 @@ use bun_js_printer::{self as js_printer, PrintResult, PrintResultSuccess}; use crate::generic_path_with_pretty_initialized; use crate::linker_context_mod::{StmtList, StmtListWhich}; use crate::options::Format as OutputFormat; -use crate::{Chunk, DeclInfo, DeclInfoKind, Index, LinkerContext, Part, PartRange, WrapKind}; +use crate::{Chunk, Index, LinkerContext, Part, PartRange, WrapKind}; use bun_ast::StoreRef; use bun_ast::binding::ToExprWrapper; @@ -33,7 +33,6 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( stmts: &mut StmtList, arena: &Bump, temp_arena: &Bump, - decl_collector: Option<&mut DeclCollector>, ) -> js_printer::PrintResult { let source_index = part_range.source_index.get() as usize; @@ -915,16 +914,6 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( }); } - // Collect top-level declarations from the converted statements. - // This is done here (after convertStmtsForChunk) rather than in - // postProcessJSChunk, because convertStmtsForChunk transforms the AST - // (e.g. export default expr → var, export stripping) and the converted - // statements reflect what actually gets printed. - let mut r = r; - if let Some(dc) = decl_collector { - dc.collect_from_stmts(out_stmts, &mut r, c); - } - // `get_source` returns `&'static Source` (parse_graph SoA is append-only and // outlives the link step), so it does not borrow `c` — no split-borrow needed // across the `&mut self` call below. @@ -944,112 +933,6 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( ) } -pub struct DeclCollector { - pub decls: Vec, - pub arena: *const Bump, -} - -impl Default for DeclCollector { - fn default() -> Self { - Self { - decls: Vec::new(), - arena: core::ptr::null(), - } - } -} - -impl DeclCollector { - /// Collect top-level declarations from **converted** statements (after - /// `convertStmtsForChunk`). At that point, export statements have already - /// been transformed: - /// - `s_export_default` → `s_local` / `s_function` / `s_class` - /// - `s_export_clause` → removed entirely - /// - `s_export_from` / `s_export_star` → removed or converted to `s_import` - /// - /// Remaining `s_import` statements (external, non-bundled) don't need - /// handling here; their bindings are recorded separately in - /// `postProcessJSChunk` by scanning the original AST import records. - pub fn collect_from_stmts( - &mut self, - stmts: &[Stmt], - r: &mut renamer::Renamer<'_, '_>, - c: &LinkerContext, - ) { - for stmt in stmts { - match stmt.data { - StmtData::SLocal(s) => { - let kind: DeclInfoKind = if s.kind == LocalKind::KVar { - DeclInfoKind::Declared - } else { - DeclInfoKind::Lexical - }; - for decl in s.decls.slice() { - self.collect_from_binding(decl.binding, kind, r, c); - } - } - StmtData::SFunction(s) => { - if let Some(name_loc_ref) = s.func.name { - if let Some(name_ref) = name_loc_ref.ref_.to_nullable() { - self.add_ref(name_ref, DeclInfoKind::Lexical, r, c); - } - } - } - StmtData::SClass(s) => { - if let Some(class_name) = s.class.class_name { - if let Some(name_ref) = class_name.ref_.to_nullable() { - self.add_ref(name_ref, DeclInfoKind::Lexical, r, c); - } - } - } - _ => {} - } - } - } - - fn collect_from_binding( - &mut self, - binding: Binding, - kind: DeclInfoKind, - r: &mut renamer::Renamer<'_, '_>, - c: &LinkerContext, - ) { - match binding.data { - BindingData::BIdentifier(b) => { - self.add_ref(b.r#ref, kind, r, c); - } - BindingData::BArray(b) => { - for item in b.items() { - self.collect_from_binding(item.binding, kind, r, c); - } - } - BindingData::BObject(b) => { - for prop in b.properties() { - self.collect_from_binding(prop.value, kind, r, c); - } - } - BindingData::BMissing(_) => {} - } - } - - fn add_ref( - &mut self, - ref_: Ref, - kind: DeclInfoKind, - r: &mut renamer::Renamer<'_, '_>, - c: &LinkerContext, - ) { - let followed = c.graph.symbols.follow(ref_); - let name = r.name_for_symbol(followed); - if name.is_empty() { - return; - } - self.decls.push(DeclInfo { - name: name.to_vec().into_boxed_slice(), - kind, - }); - } -} - fn merge_adjacent_local_stmts(stmts: &mut Vec, _arena: &Bump) { if stmts.is_empty() { return; @@ -1107,7 +990,5 @@ fn merge_adjacent_local_stmts(stmts: &mut Vec, _arena: &Bump) { } // Type aliases / re-imports for readability of match arms. -use bun_ast::LocalKind; -use bun_ast::binding::Data as BindingData; use bun_ast::expr::Data as ExprData; use bun_ast::stmt::Data as StmtData; diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.rs b/src/bundler/linker_context/generateCompileResultForJSChunk.rs index 26d0480c674f..a5ef7c459a42 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.rs @@ -10,9 +10,7 @@ use crate::options::OutputFormat; use crate::thread_pool::Worker; use crate::{Chunk, CompileResult, Index, PartRange}; -use super::generate_code_for_file_in_chunk_js::{ - DeclCollector, generate_code_for_file_in_chunk_js, -}; +use super::generate_code_for_file_in_chunk_js::generate_code_for_file_in_chunk_js; // CONCURRENCY: thread-pool callback — runs on worker threads, one task per // `PendingPartRange`. Writes: `chunk.compile_results_for_chunk[i]` (disjoint @@ -94,9 +92,8 @@ fn generate_compile_result_for_js_chunk_impl( // Client and server bundles for Bake must outlive the bundle task. // `BufferWriter::init()` output is allocated from the global heap and - // `DeclCollector.decls` from the worker heap (`worker.arena`, alive until - // bundle teardown) — both outlive the task's CompileResult consumption, - // so a per-dev-server arena would only be a perf optimization. + // outlives the task's CompileResult consumption, so a per-dev-server + // arena would only be a perf optimization. let _ = c.dev_server; // temporary_arena / stmt_list are initialized in Worker::create before any task runs. @@ -148,16 +145,6 @@ fn generate_compile_result_for_js_chunk_impl( ) }; - let collect_decls = c.options.generate_bytecode_cache - && c.options.output_format == OutputFormat::Esm - && c.options.compile; - // DeclCollector wants `*const Arena` and uses the worker heap (see - // the dev-server allocation note above). - let mut dc = DeclCollector { - arena: worker.arena.as_ptr(), - ..Default::default() - }; - // `worker.arena` (= `BackRef` to `worker.heap`) is a disjoint field from // `worker.temporary_arena` / `worker.stmt_list` borrowed `&mut` above, so // a direct shared borrow is fine. Heap is pinned; see `Worker::arena`. @@ -181,7 +168,6 @@ fn generate_compile_result_for_js_chunk_impl( stmt_list, worker_alloc, &**arena, - if collect_decls { Some(&mut dc) } else { None }, ); // Update bytesInOutput for this source in the chunk (for metafile) @@ -206,10 +192,5 @@ fn generate_compile_result_for_js_chunk_impl( CompileResult::Javascript { source_index: part_range.source_index.get(), result, - decls: if collect_decls { - dc.decls.into_boxed_slice() - } else { - Box::new([]) - }, } } diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 8a839aae1c91..28aa63c8e7d6 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -1,4 +1,3 @@ -use crate::DeclInfoKind; use crate::LinkerContext; use crate::analyze_transpiled_module::{self, ModuleInfo}; use crate::bundle_v2::bake_types::{HmrRuntimeSide, get_hmr_runtime}; @@ -220,28 +219,10 @@ pub fn post_process_js_chunk( ); } - // Populate ModuleInfo with declarations collected during parallel printing, - // external import records from the original AST, and wrapper refs. + // Populate ModuleInfo with the import.meta flag and the external + // import records from the original AST. if let Some(mi) = module_info.as_deref_mut() { - // 1. Add declarations collected by DeclCollector during parallel part printing. - // These come from the CONVERTED statements (after convertStmtsForChunk transforms - // export default → var, strips exports, etc.), so they match what's actually printed. - for cr in chunk.compile_results_for_chunk.iter() { - let decls = match cr { - CompileResult::Javascript { decls, .. } => decls, - _ => continue, - }; - for decl in decls.iter() { - let var_kind: analyze_transpiled_module::VarKind = match decl.kind { - DeclInfoKind::Declared => analyze_transpiled_module::VarKind::Declared, - DeclInfoKind::Lexical => analyze_transpiled_module::VarKind::Lexical, - }; - let string_id = mi.str(&decl.name); - mi.add_var(string_id, var_kind); - } - } - - // 1b. Check if any source in this chunk uses import.meta. The per-part + // Check if any source in this chunk uses import.meta. The per-part // parallel printer does not have module_info, so the printer cannot set // this flag during per-part printing. We derive it from the AST instead. // Note: the runtime source (index 0) also uses import.meta (e.g. @@ -338,10 +319,6 @@ pub fn post_process_js_chunk( let local_name = chunk.renamer.name_for_symbol(name_ref); mi.str(local_name) }; - mi.add_var( - local_name_id, - analyze_transpiled_module::VarKind::Lexical, - ); let default_id = mi.str(b"default"); mi.add_import_info_single( irp_id, @@ -359,10 +336,6 @@ pub fn post_process_js_chunk( let local_name = chunk.renamer.name_for_symbol(name_ref); mi.str(local_name) }; - mi.add_var( - local_name_id, - analyze_transpiled_module::VarKind::Lexical, - ); // SAFETY: ClauseItem.alias is an arena `*const [u8]`; never null. let alias_id = mi.str(item.alias.slice()); mi.add_import_info_single( @@ -382,10 +355,6 @@ pub fn post_process_js_chunk( let local_name = chunk.renamer.name_for_symbol(s.namespace_ref); mi.str(local_name) }; - mi.add_var( - local_name_id, - analyze_transpiled_module::VarKind::Lexical, - ); mi.add_import_info_namespace(irp_id, local_name_id); } } @@ -395,26 +364,6 @@ pub fn post_process_js_chunk( part_i += 1; } } - - // 3. Add wrapper-generated declarations (init_xxx, require_xxx) that are - // not in any part statement. - let all_wrapper_refs = c.graph.ast.items_wrapper_ref(); - for part_range in chunk.content.javascript().parts_in_chunk_in_order.iter() { - let source_index = part_range.source_index.get() as usize; - if all_flags[source_index].wrap != crate::WrapKind::None { - let wrapper_ref = all_wrapper_refs[source_index]; - if !wrapper_ref.is_empty() { - let string_id = { - let name = chunk.renamer.name_for_symbol(wrapper_ref); - if name.is_empty() { - continue; - } - mi.str(name) - }; - mi.add_var(string_id, analyze_transpiled_module::VarKind::Declared); - } - } - } } // Generate the exports for the entry point, if there are any. @@ -440,7 +389,6 @@ pub fn post_process_js_chunk( code: Box::default(), source_map: None, }), - decls: Box::default(), }; }; @@ -877,37 +825,6 @@ pub fn post_process_js_chunk( Ok(()) } -/// Recursively walk a binding and add all declared names to `ModuleInfo`. -/// Handles `b_identifier`, `b_array`, `b_object`, and `b_missing`. -fn add_binding_vars_to_module_info( - mi: &mut ModuleInfo, - binding: Binding, - var_kind: analyze_transpiled_module::VarKind, - r: &mut js_printer::renamer::Renamer<'_, '_>, - symbols: &bun_ast::symbol::Map, -) { - match binding.data { - B::B::BIdentifier(b) => { - let name = r.name_for_symbol(symbols.follow(b.r#ref)); - if !name.is_empty() { - let str_id = mi.str(name); - mi.add_var(str_id, var_kind); - } - } - B::B::BArray(b) => { - for item in b.items() { - add_binding_vars_to_module_info(mi, item.binding, var_kind, r, symbols); - } - } - B::B::BObject(b) => { - for prop in b.properties() { - add_binding_vars_to_module_info(mi, prop.value, var_kind, r, symbols); - } - } - B::B::BMissing(_) => {} - } -} - // `js_printer::print` ties bump/Options/import_records/renamer to a // single `'a`, and `Renamer<'r, 'src>` is invariant in `'src` — so the caller's // renamer lifetime fixes `'a`. All by-ref params that flow into `print` must @@ -919,8 +836,8 @@ pub fn generate_entry_point_tail_js<'a>( source_index: IndexInt, arena: &'a Arena, temp_arena: &Arena, - mut r: js_printer::renamer::Renamer<'a, 'a>, - mut module_info: Option<&'a mut ModuleInfo>, + r: js_printer::renamer::Renamer<'a, 'a>, + module_info: Option<&'a mut ModuleInfo>, ) -> CompileResult { let flags: crate::js_meta::Flags = c.graph.meta.items_flags()[source_index as usize]; let mut stmts: Vec = Vec::new(); @@ -1306,35 +1223,6 @@ pub fn generate_entry_point_tail_js<'a>( } } - // Add generated local declarations from entry point tail to module_info. - // This captures vars like `var export_foo = cjs.foo` for CJS export copies. - // Reshaped for borrowck — reborrow via as_deref_mut so module_info - // remains usable for print_options below. - if let Some(mi) = module_info.as_mut() { - let mi: &mut ModuleInfo = &mut **mi; - for stmt in stmts.iter() { - match &stmt.data { - StmtData::SLocal(s) => { - let var_kind: analyze_transpiled_module::VarKind = if s.kind == S::Kind::KVar { - analyze_transpiled_module::VarKind::Declared - } else { - analyze_transpiled_module::VarKind::Lexical - }; - for decl in s.decls.slice() { - add_binding_vars_to_module_info( - mi, - decl.binding, - var_kind, - &mut r, - &c.graph.symbols, - ); - } - } - _ => {} - } - } - } - if stmts.is_empty() { return CompileResult::Javascript { source_index, @@ -1342,7 +1230,6 @@ pub fn generate_entry_point_tail_js<'a>( code: Box::default(), source_map: None, }), - decls: Box::default(), }; } @@ -1388,6 +1275,5 @@ pub fn generate_entry_point_tail_js<'a>( r, ), source_index, - decls: Box::default(), } } diff --git a/src/bundler_jsc/analyze_jsc.rs b/src/bundler_jsc/analyze_jsc.rs index 7648b1b3763c..02062efe3c05 100644 --- a/src/bundler_jsc/analyze_jsc.rs +++ b/src/bundler_jsc/analyze_jsc.rs @@ -18,8 +18,6 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( vm: &VM, module_key: &IdentifierArray, source_code: &SourceCode, - declared_variables: &mut VariableEnvironment, - lexical_variables: &mut VariableEnvironment, res: &ModuleInfoDeserialized, ) -> *mut JSModuleRecord { // Ownership of `res` stays with the caller; this function only reads it. @@ -81,8 +79,6 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( return core::ptr::null_mut(); } match k { - RecordKind::DeclaredVariable => declared_variables.add(vm, identifiers, buffer[i]), - RecordKind::LexicalVariable => lexical_variables.add(vm, identifiers, buffer[i]), RecordKind::ImportInfoSingle | RecordKind::ImportInfoSingleTypeScript | RecordKind::ImportInfoNamespace @@ -102,8 +98,6 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( vm, module_key, source_code, - declared_variables, - lexical_variables, res.flags.contains_import_meta(), res.flags.is_typescript(), res.flags.has_tla(), @@ -159,7 +153,6 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( unreachable!(); // handled above } match k { - RecordKind::DeclaredVariable | RecordKind::LexicalVariable => {} RecordKind::ImportInfoSingle => module_record.add_import_entry_single( identifiers, buffer[i + 1], @@ -216,30 +209,6 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( // ─── opaque FFI types ───────────────────────────────────────────────────────── -bun_opaque::opaque_ffi! { pub struct VariableEnvironment; } -unsafe extern "C" { - fn JSC__VariableEnvironment__add( - environment: *mut VariableEnvironment, - vm: *const VM, - identifier_array: *mut IdentifierArray, - identifier_index: StringID, - ); -} -impl VariableEnvironment { - // Forwards `identifier_array` to C++ without dereferencing; not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - #[inline] - pub fn add( - &mut self, - vm: &VM, - identifier_array: *mut IdentifierArray, - identifier_index: StringID, - ) { - // SAFETY: self is a valid &mut VariableEnvironment from C++; identifier_array is live (scopeguard). - unsafe { JSC__VariableEnvironment__add(self, vm, identifier_array, identifier_index) } - } -} - bun_opaque::opaque_ffi! { pub struct IdentifierArray; } unsafe extern "C" { fn JSC__IdentifierArray__create(len: usize) -> *mut IdentifierArray; @@ -284,8 +253,6 @@ unsafe extern "C" { vm: *const VM, module_key: *const IdentifierArray, source_code: *const SourceCode, - declared_variables: *mut VariableEnvironment, - lexical_variables: *mut VariableEnvironment, has_import_meta: bool, is_typescript: bool, has_tla: bool, @@ -384,8 +351,6 @@ impl JSModuleRecord { vm: &VM, module_key: &IdentifierArray, source_code: &SourceCode, - declared_variables: &mut VariableEnvironment, - lexical_variables: &mut VariableEnvironment, has_import_meta: bool, is_typescript: bool, has_tla: bool, @@ -397,8 +362,6 @@ impl JSModuleRecord { vm, module_key, source_code, - declared_variables, - lexical_variables, has_import_meta, is_typescript, has_tla, diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index b406273a7d89..569d3a109b1e 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -58,43 +58,33 @@ const FFIType = { napi_env: 18, napi_value: 19, buffer: 20, + buffer_length: 21, + buffer_bytelength: 21, }; const suffix = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; -declare const __GlobalBunFFIPtrFunctionForWrapper: typeof ptr; -declare const __GlobalBunCString: typeof CString; - var ffi = globalThis.Bun.FFI; const ptr = (arg1, arg2) => (typeof arg2 === "undefined" ? ffi.ptr(arg1) : ffi.ptr(arg1, arg2)); const toBuffer = ffi.toBuffer; const toArrayBuffer = ffi.toArrayBuffer; const nativeViewSource = ffi.viewSource; -const BunCString = ffi.CString; const nativeLinkSymbols = ffi.linkSymbols; const nativeDLOpen = ffi.dlopen; const nativeCallback = ffi.callback; const closeCallback = ffi.closeCallback; +const nativeCFunction = ffi.cfunction; delete ffi.callback; delete ffi.closeCallback; +delete ffi.cfunction; class JSCallback { constructor(cb, options) { - const result = nativeCallback(options, cb); - if (Error.isError(result)) throw result; - const { ctx, ptr } = result; - this.#ctx = ctx; - this.ptr = ptr; - this.#threadsafe = !!options?.threadsafe; - } - - ptr; - #ctx; - #threadsafe; - - get threadsafe() { - return this.#threadsafe; + const cell = nativeCallback(options, cb); + if (Error.isError(cell)) throw cell; + Object.setPrototypeOf(cell, (new.target ?? JSCallback).prototype); + return cell; } [Symbol.toPrimitive]() { @@ -103,13 +93,10 @@ class JSCallback { } close() { - const ctx = this.#ctx; - this.ptr = null; - this.#ctx = null; - - if (ctx) { - closeCallback(ctx); + if (!(this instanceof JSCallback)) { + throw new TypeError("JSCallback.prototype.close called on an incompatible receiver"); } + closeCallback(this); } [Symbol.dispose]() { @@ -117,250 +104,15 @@ class JSCallback { } } -class CString extends String { - constructor(ptr, byteOffset?, byteLength?) { - super( - ptr - ? typeof byteLength === "number" && Number.isSafeInteger(byteLength) - ? BunCString(ptr, byteOffset || 0, byteLength) - : BunCString(ptr, byteOffset || 0) - : "", - ); - this.ptr = typeof ptr === "number" ? ptr : 0; - if (typeof byteOffset !== "undefined") { - this.byteOffset = byteOffset; - } - if (typeof byteLength !== "undefined") { - this.byteLength = byteLength; - } - } - - ptr; - byteOffset; - byteLength; - #cachedArrayBuffer; - - get arrayBuffer() { - if (this.#cachedArrayBuffer) { - return this.#cachedArrayBuffer; - } - - if (!this.ptr) { - return (this.#cachedArrayBuffer = new ArrayBuffer(0)); - } - - return (this.#cachedArrayBuffer = toArrayBuffer(this.ptr, this.byteOffset, this.byteLength)); - } -} -Object.defineProperty(globalThis, "__GlobalBunCString", { - value: CString, - enumerable: false, - configurable: false, -}); - -const ffiWrappers = new Array(21); - -var char = "val|0"; -ffiWrappers.fill(char); -ffiWrappers[FFIType.uint8_t] = "val<0?0:val>=255?255:val|0"; -ffiWrappers[FFIType.int16_t] = "val<=-32768?-32768:val>=32768?32768:val|0"; -ffiWrappers[FFIType.uint16_t] = "val<=0?0:val>=65536?65536:val|0"; -ffiWrappers[FFIType.int32_t] = "val|0"; -// https://github.com/oven-sh/bun/issues/7007 -// This cast with `|0` looks incorrect as it converts 0xffffffff into -1, but this misinterpretation -// of the integer is taken advantage of by a second misinterpretation of the bytes in the C binding -// The bitwise operator | forces a conversion to int32_t, but it will wrap to negative numbers -// when going above >0x7fffffff. -// -// What this |0 operatation also *seems to do* (citation needed) is convert the internal representation -// of JSC::JSValue to ALWAYS use Int32Tag, which is important as `JSValue::asInt32()` can only handle -// this encoding to properly deserialize this as an int32. -// -// tldr jsc internals: JSValue represents int32 as a tag value, then the int32 bytes. -// and all other integers are as tagged 64-bit floats. -// -// The trick to fixing the bug: after using |0 to misinterpret and force the integer into Int32Tag, -// when passing the value to the C ffi code, misinterpret it again, resulting in the correct uint32_t. -// -// To do this in native code, there is a spot in zig where uint32_t just prints int32_t. -ffiWrappers[FFIType.uint32_t] = "val<0?0:val>0xFFFFFFFF?-1:val|0"; -ffiWrappers[FFIType.i64_fast] = `{ - if (typeof val === "bigint") { - if (val <= BigInt(Number.MAX_SAFE_INTEGER) && val >= BigInt(-Number.MAX_SAFE_INTEGER)) { - return Number(val).valueOf() || 0; - } - - return val; - } - - return !val ? 0 : +val || 0; -}`; -ffiWrappers[FFIType.i64_fast] = `{ - if (typeof val === "bigint") { - if (val <= BigInt(Number.MAX_SAFE_INTEGER) && val >= BigInt(-Number.MAX_SAFE_INTEGER)) { - return Number(val).valueOf() || 0; - } - - return val; - } - - return !val ? 0 : +val || 0; -}`; - -ffiWrappers[FFIType.u64_fast] = `{ - if (typeof val === "bigint") { - if (val <= BigInt(Number.MAX_SAFE_INTEGER) && val >= 0) { - return Number(val).valueOf() || 0; - } +const CString = ffi.CString; - return val; - } - - return !val ? 0 : +val || 0; -}`; - -ffiWrappers[FFIType.int64_t] = `{ - if (typeof val === "bigint") { - return val; - } - - if (typeof val === "number") { - return BigInt(val || 0); - } - - return BigInt(+val || 0); -}`; - -ffiWrappers[FFIType.uint64_t] = `{ - if (typeof val === "bigint") { - return val; - } - - if (typeof val === "number") { - return val <= 0 ? BigInt(0) : BigInt(val || 0); - } - - return BigInt(+val || 0); -}`; - -ffiWrappers[FFIType.u64_fast] = `{ - if (typeof val === "bigint") { - if (val <= BigInt(Number.MAX_SAFE_INTEGER) && val >= BigInt(0)) return Number(val); - return val; - } - - return typeof val === "number" ? (val <= 0 ? 0 : +val || 0) : +val || 0; -}`; - -ffiWrappers[FFIType.uint16_t] = `{ - const ret = (typeof val === "bigint" ? Number(val) : val) | 0; - return ret <= 0 ? 0 : ret > 0xffff ? 0xffff : ret; -}`; - -// Plain numbers pass through untouched: NaN, -0.0, and every other double are -// already in the representation the compiled stub reads. Everything else -// (BigInt included) is converted with Number(). -ffiWrappers[FFIType.double] = `{ - if (typeof val === "number") { - return val; - } - - return Number(val); -}`; - -ffiWrappers[FFIType.float] = ffiWrappers[10] = `{ - return Math.fround(val); -}`; -ffiWrappers[FFIType.bool] = `{ - return !!val; -}`; - -// This prevents an extra property getter in potentially hot code -Object.defineProperty(globalThis, "__GlobalBunFFIPtrFunctionForWrapper", { - value: ptr, - enumerable: false, - configurable: true, -}); -Object.defineProperty(globalThis, "__GlobalBunFFIPtrArrayBufferViewFn", { - value: function isTypedArrayView(val) { - return $isTypedArrayView(val); - }, - enumerable: false, - configurable: true, -}); - -ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{ - if (typeof val === "number") return val; - if (!val) { - return null; - } - - if (__GlobalBunFFIPtrArrayBufferViewFn(val)) { - return val; - } - - if (val instanceof ArrayBuffer) { - return __GlobalBunFFIPtrFunctionForWrapper(val); - } - - if (typeof val === "string") { - throw new TypeError("To convert a string to a pointer, encode it as a buffer"); - } - - throw new TypeError(\`Unable to convert \${ val } to a pointer\`); -}`; - -ffiWrappers[FFIType.buffer] = `{ - if (!__GlobalBunFFIPtrArrayBufferViewFn(val)) { - throw new TypeError("Expected a TypedArray"); - } - - return val; -}`; - -ffiWrappers[FFIType.function] = `{ - if (typeof val === "number") { - return val; - } - - if (typeof val === "bigint") { - return Number(val); - } - - var ptr = val && val.ptr; - - if (!ptr) { - throw new TypeError("Expected function to be a JSCallback or a number"); - } - - return ptr; -}`; - -function FFIBuilder(params, returnType, functionToCall, name) { - const hasReturnType = typeof FFIType[returnType] === "number" && FFIType[returnType as string] !== FFIType.void; +function FFIBuilder(params, functionToCall, name) { var paramNames = new Array(params.length); - var args = new Array(params.length); - for (let i = 0; i < params.length; i++) { - paramNames[i] = `p${i}`; - const wrapper = ffiWrappers[FFIType[params[i]]]; - if (wrapper) { - // doing this inline benchmarked about 4x faster than referencing - args[i] = `(val=>${wrapper})(p${i})`; - } else { - throw new TypeError(`Unsupported type ${params[i]}. Must be one of: ${Object.keys(FFIType).sort().join(", ")}`); - } - } + for (let i = 0; i < params.length; i++) paramNames[i] = `p${i}`; - var code = `functionToCall(${args.join(", ")})`; - if (hasReturnType) { - if (FFIType[returnType as string] === FFIType.cstring) { - code = `return new __GlobalBunCString(${code})`; - } else { - code = `return ${code}`; - } - } + var code = `return (v=>v?new __CString(v):null)(functionToCall(${paramNames.join(", ")}))`; - var func = new Function("functionToCall", ...paramNames, code); + var func = new Function("functionToCall", "__CString", ...paramNames, code); Object.defineProperty(func, "name", { value: name, }); @@ -371,40 +123,40 @@ function FFIBuilder(params, returnType, functionToCall, name) { var wrap; switch (paramNames.length) { case 0: - wrap = () => func(functionToCall); + wrap = () => func(functionToCall, CString); break; case 1: - wrap = arg1 => func(functionToCall, arg1); + wrap = arg1 => func(functionToCall, CString, arg1); break; case 2: - wrap = (arg1, arg2) => func(functionToCall, arg1, arg2); + wrap = (arg1, arg2) => func(functionToCall, CString, arg1, arg2); break; case 3: - wrap = (arg1, arg2, arg3) => func(functionToCall, arg1, arg2, arg3); + wrap = (arg1, arg2, arg3) => func(functionToCall, CString, arg1, arg2, arg3); break; case 4: - wrap = (arg1, arg2, arg3, arg4) => func(functionToCall, arg1, arg2, arg3, arg4); + wrap = (arg1, arg2, arg3, arg4) => func(functionToCall, CString, arg1, arg2, arg3, arg4); break; case 5: - wrap = (arg1, arg2, arg3, arg4, arg5) => func(functionToCall, arg1, arg2, arg3, arg4, arg5); + wrap = (arg1, arg2, arg3, arg4, arg5) => func(functionToCall, CString, arg1, arg2, arg3, arg4, arg5); break; case 6: - wrap = (arg1, arg2, arg3, arg4, arg5, arg6) => func(functionToCall, arg1, arg2, arg3, arg4, arg5, arg6); + wrap = (arg1, arg2, arg3, arg4, arg5, arg6) => func(functionToCall, CString, arg1, arg2, arg3, arg4, arg5, arg6); break; case 7: wrap = (arg1, arg2, arg3, arg4, arg5, arg6, arg7) => - func(functionToCall, arg1, arg2, arg3, arg4, arg5, arg6, arg7); + func(functionToCall, CString, arg1, arg2, arg3, arg4, arg5, arg6, arg7); break; case 8: wrap = (arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) => - func(functionToCall, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + func(functionToCall, CString, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); break; case 9: wrap = (arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) => - func(functionToCall, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + func(functionToCall, CString, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); break; default: { - wrap = (...args) => func(functionToCall, ...args); + wrap = (...args) => func(functionToCall, CString, ...args); break; } } @@ -448,26 +200,6 @@ function dlopen(path, options) { const result = nativeDLOpen(path, options); if (Error.isError(result)) throw result; - for (let key in result.symbols) { - var symbol = result.symbols[key]; - if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) { - result.symbols[key] = FFIBuilder( - options[key].args ?? [], - options[key].returns ?? FFIType.void, - symbol, - // in stacktraces: - // instead of - // "/usr/lib/sqlite3.so" - // we want - // "sqlite3_get_version() - sqlit3.so" - path.includes("/") ? `${key} (${path.split("/").pop()})` : `${key} (${path})`, - ); - } else { - // consistentcy - result.symbols[key].native = result.symbols[key]; - } - } - // Bind it because it's a breaking change to not do so // Previously, it didn't need to be bound result.close = result.close.bind(result); @@ -498,10 +230,10 @@ function cc(options) { for (let key in result.symbols) { var symbol = result.symbols[key]; - if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) { + const desc = options.symbols?.[key]; + if (FFIType[desc?.returns as string] === FFIType.cstring) { result.symbols[key] = FFIBuilder( - options[key].args ?? [], - options[key].returns ?? FFIType.void, + desc.args ?? [], symbol, // in stacktraces: // instead of @@ -532,43 +264,18 @@ function viewSource(symbols, isCallback?) { function linkSymbols(options) { const result = nativeLinkSymbols(options); if (Error.isError(result)) throw result; - - for (let key in result.symbols) { - var symbol = result.symbols[key]; - if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) { - result.symbols[key] = FFIBuilder(options[key].args ?? [], options[key].returns ?? FFIType.void, symbol, key); - } else { - // consistentcy - result.symbols[key].native = result.symbols[key]; - } - } - return result; } var cFunctionI = 0; -var cFunctionRegistry; -function onCloseCFunction(close) { - close(); -} +function closeJSCFFICFunction() {} function CFunction(options) { const identifier = `CFunction${cFunctionI++}`; - var result = linkSymbols({ - [identifier]: options, - }); - var hasClosed = false; - var close = result.close.bind(result); - result.symbols[identifier].close = () => { - if (hasClosed || !close) return; - hasClosed = true; - close(); - close = undefined; - }; - - cFunctionRegistry ||= new FinalizationRegistry(onCloseCFunction); - cFunctionRegistry.register(result.symbols[identifier], result.symbols[identifier].close); - - return result.symbols[identifier]; + + const fn = nativeCFunction(options, identifier); + if (Error.isError(fn)) throw fn; + fn.close = closeJSCFFICFunction; + return fn; } const read = ffi.read; diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 028828a4722e..9ec72e7d995d 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -78,10 +78,6 @@ pub mod analyze_transpiled_module { #[repr(u8)] #[derive(Clone, Copy, PartialEq, Eq)] pub enum RecordKind { - /// var_name - DeclaredVariable, - /// let_name - LexicalVariable, /// module_name, import_name, local_name ImportInfoSingle, /// module_name, import_name, local_name @@ -106,7 +102,6 @@ pub mod analyze_transpiled_module { impl RecordKind { pub fn len(self) -> usize { match self { - Self::DeclaredVariable | Self::LexicalVariable => 1, Self::ImportInfoSingle => 3, Self::ImportInfoSingleTypeScript => 3, Self::ImportInfoNamespace => 3, @@ -172,12 +167,6 @@ pub mod analyze_transpiled_module { } } - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum VarKind { - Declared, - Lexical, - } - /// `AbstractModuleRecord::ModulePhase` — only `Evaluation` and `Defer` /// exist. Stored as a `u8` parallel to `requested_modules_keys` so the /// serialized format stays dense. @@ -351,25 +340,12 @@ pub mod analyze_transpiled_module { } } - pub fn add_var(&mut self, name: StringID, kind: VarKind) { - match kind { - VarKind::Declared => self.add_declared_variable(name), - VarKind::Lexical => self.add_lexical_variable(name), - } - } - fn add_record(&mut self, kind: RecordKind, data: &[StringID]) { debug_assert!(!self.finalized); debug_assert_eq!(data.len(), kind.len()); self.record_kinds.push(kind); self.buffer.extend_from_slice(data); } - pub fn add_declared_variable(&mut self, id: StringID) { - self.add_record(RecordKind::DeclaredVariable, &[id]); - } - pub fn add_lexical_variable(&mut self, id: StringID) { - self.add_record(RecordKind::LexicalVariable, &[id]); - } pub fn add_import_info_single( &mut self, module_name: StringID, @@ -1140,6 +1116,13 @@ impl<'a> Default for Options<'a> { use bun_ast::{Indentation, IndentationCharacter}; +// `is_export` gates whether printing a binding also records an export entry +// in ModuleInfo; dead-code elimination drops it when MAY_HAVE_MODULE_INFO is false. +#[derive(Clone, Copy, Default)] +pub struct TopLevelAndIsExport { + pub is_export: bool, +} + /// Downstream-compat: `print_json` callers pass this. Only the fields any caller actually sets are surfaced /// here and forwarded into `Options { .. }` inside `print_json`. #[derive(Clone, Copy, Default)] @@ -1286,43 +1269,6 @@ enum ClauseItemAs { ExportFrom, } -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum IsTopLevel { - Yes, - VarOnly, - No, -} - -// One shape; dead-code elimination removes the unused fields when -// MAY_HAVE_MODULE_INFO is false. -#[derive(Clone, Copy, Default)] -pub struct TopLevelAndIsExport { - pub is_export: bool, - pub is_top_level: Option, -} - -#[derive(Clone, Copy)] -pub struct TopLevel { - pub is_top_level: IsTopLevel, -} - -impl TopLevel { - #[inline] - pub fn init(is_top_level: IsTopLevel) -> Self { - Self { is_top_level } - } - pub fn sub_var(self) -> Self { - if self.is_top_level == IsTopLevel::No { - return Self::init(IsTopLevel::No); - } - Self::init(IsTopLevel::VarOnly) - } - #[inline] - pub fn is_top_level(self) -> bool { - self.is_top_level != IsTopLevel::No - } -} - // ─────────────────────────────────────────────────────────────────────────── // Printer (NewPrinter) — the impl body is the bulk of this crate and touches // nearly every bun_js_parser AST node type. @@ -1822,30 +1768,6 @@ pub mod __gated_printer { self.print_semicolon_after_statement(); } - - // Record var declarations for module_info. printGlobalBunImportStatement - // bypasses printDeclStmt/printBinding, so we must record vars explicitly. - // reshaped for borrowck — compute names before borrowing module_info. - if Self::MAY_HAVE_MODULE_INFO && self.module_info.is_some() { - if !import.star_name_loc.is_empty() { - let name = self.name_for_symbol(import.namespace_ref); - let mi = self.module_info().expect("infallible: module_info enabled"); - let id = mi.str(name); - mi.add_var(id, analyze_transpiled_module::VarKind::Declared); - } - if let Some(default) = &import.default_name { - let name = self.name_for_symbol(default.ref_); - let mi = self.module_info().expect("infallible: module_info enabled"); - let id = mi.str(name); - mi.add_var(id, analyze_transpiled_module::VarKind::Declared); - } - for item in slice_of(import.items).iter() { - let name = self.name_for_symbol(item.name.ref_); - let mi = self.module_info().expect("infallible: module_info enabled"); - let id = mi.str(name); - mi.add_var(id, analyze_transpiled_module::VarKind::Declared); - } - } } #[inline] @@ -1892,31 +1814,26 @@ pub mod __gated_printer { } } - pub fn print_body(&mut self, stmt: Stmt, tlmtlo: TopLevel) { + pub fn print_body(&mut self, stmt: Stmt) { match &stmt.data { StmtData::SBlock(block) => { self.print_space(); - self.print_block( - stmt.loc, - slice_of(block.stmts), - Some(block.close_brace_loc), - tlmtlo, - ); + self.print_block(stmt.loc, slice_of(block.stmts), Some(block.close_brace_loc)); self.print_newline(); } _ => { self.print_newline(); self.indent(); - self.print_stmt(stmt, tlmtlo).expect("unreachable"); + self.print_stmt(stmt).expect("unreachable"); self.unindent(); } } } - pub fn print_block_body(&mut self, stmts: &[Stmt], tlmtlo: TopLevel) { + pub fn print_block_body(&mut self, stmts: &[Stmt]) { for stmt in stmts { self.print_semicolon_if_needed(); - self.print_stmt(*stmt, tlmtlo).expect("unreachable"); + self.print_stmt(*stmt).expect("unreachable"); } } @@ -1925,14 +1842,13 @@ pub mod __gated_printer { loc: bun_ast::Loc, stmts: &[Stmt], close_brace_loc: Option, - tlmtlo: TopLevel, ) { self.add_source_mapping(loc); self.print(b"{"); if !stmts.is_empty() { self.print_newline(); self.indent(); - self.print_block_body(stmts, tlmtlo); + self.print_block_body(stmts); self.unindent(); self.print_indent(); } @@ -2213,12 +2129,7 @@ pub mod __gated_printer { false, ); self.print_space(); - self.print_block( - func.body.loc, - slice_of(func.body.stmts), - None, - TopLevel::init(IsTopLevel::No), - ); + self.print_block(func.body.loc, slice_of(func.body.stmts), None); } pub fn print_class(&mut self, class: &G::Class) { @@ -2243,12 +2154,7 @@ pub mod __gated_printer { self.print(b"static"); self.print_space(); let csb = item.class_static_block_ref().unwrap(); - self.print_block( - csb.loc, - csb.stmts.slice(), - None, - TopLevel::init(IsTopLevel::No), - ); + self.print_block(csb.loc, csb.stmts.slice(), None); self.print_newline(); continue; } @@ -3499,12 +3405,7 @@ pub mod __gated_printer { } if !was_printed { - self.print_block( - e.body.loc, - slice_of(e.body.stmts), - None, - TopLevel::init(IsTopLevel::No), - ); + self.print_block(e.body.loc, slice_of(e.body.stmts), None); } if wrap { @@ -4746,9 +4647,6 @@ pub mod __gated_printer { let local_name = self.name_for_symbol(b.r#ref); if let Some(mi) = self.module_info() { let name_id = mi.str(local_name); - if let Some(vk) = tlm.is_top_level { - mi.add_var(name_id, vk); - } if tlm.is_export { mi.add_export_info_local(name_id, name_id); } @@ -4864,9 +4762,6 @@ pub mod __gated_printer { if Self::MAY_HAVE_MODULE_INFO { if let Some(mi) = self.module_info() { let name_id = mi.str(str.slice8()); - if let Some(vk) = tlm.is_top_level { - mi.add_var(name_id, vk); - } if tlm.is_export { mi.add_export_info_local( name_id, name_id, @@ -4902,9 +4797,6 @@ pub mod __gated_printer { let str8 = str.slice(self.bump); if let Some(mi) = self.module_info() { let name_id = mi.str(str8); - if let Some(vk) = tlm.is_top_level { - mi.add_var(name_id, vk); - } if tlm.is_export { mi.add_export_info_local( name_id, name_id, @@ -4965,7 +4857,7 @@ pub mod __gated_printer { } } - pub fn print_stmt(&mut self, stmt: Stmt, tlmtlo: TopLevel) -> crate::Result<()> { + pub fn print_stmt(&mut self, stmt: Stmt) -> crate::Result<()> { if !self.stack_check.is_safe_to_recurse() { self.stack_overflowed = true; return Ok(()); @@ -5016,11 +4908,6 @@ pub mod __gated_printer { if Self::MAY_HAVE_MODULE_INFO { if let Some(mi) = self.module_info() { let name_id = mi.str(local_name); - // function declarations are lexical (block-scoped in modules); - // only record at true top-level, not inside blocks. - if tlmtlo.is_top_level == IsTopLevel::Yes { - mi.add_var(name_id, analyze_transpiled_module::VarKind::Lexical); - } if s.func.flags.contains(G::FnFlags::IsExport) { mi.add_export_info_local(name_id, name_id); } @@ -5052,9 +4939,6 @@ pub mod __gated_printer { if Self::MAY_HAVE_MODULE_INFO { if let Some(mi) = self.module_info() { let name_id = mi.str(name_str); - if tlmtlo.is_top_level == IsTopLevel::Yes { - mi.add_var(name_id, analyze_transpiled_module::VarKind::Lexical); - } if s.is_export { mi.add_export_info_local(name_id, name_id); } @@ -5093,10 +4977,6 @@ pub mod __gated_printer { default_id, analyze_transpiled_module::StringID::STAR_DEFAULT, ); - mi.add_var( - analyze_transpiled_module::StringID::STAR_DEFAULT, - analyze_transpiled_module::VarKind::Lexical, - ); } } self.prev_stmt_tag = new_tag; @@ -5138,10 +5018,6 @@ pub mod __gated_printer { }; let default_id = mi.str(b"default"); mi.add_export_info_local(default_id, local_name); - mi.add_var( - local_name, - analyze_transpiled_module::VarKind::Lexical, - ); } } @@ -5173,10 +5049,6 @@ pub mod __gated_printer { }; let default_id = mi.str(b"default"); mi.add_export_info_local(default_id, local_name); - mi.add_var( - local_name, - analyze_transpiled_module::VarKind::Lexical, - ); } } @@ -5419,35 +5291,27 @@ pub mod __gated_printer { self.add_source_mapping(stmt.loc); match s.kind { S::Kind::KConst => { - self.print_decl_stmt(s.is_export, b"const", s.decls.slice(), tlmtlo) - } - S::Kind::KLet => { - self.print_decl_stmt(s.is_export, b"let", s.decls.slice(), tlmtlo) - } - S::Kind::KVar => { - self.print_decl_stmt(s.is_export, b"var", s.decls.slice(), tlmtlo) + self.print_decl_stmt(s.is_export, b"const", s.decls.slice()) } + S::Kind::KLet => self.print_decl_stmt(s.is_export, b"let", s.decls.slice()), + S::Kind::KVar => self.print_decl_stmt(s.is_export, b"var", s.decls.slice()), S::Kind::KUsing => { - self.print_decl_stmt(s.is_export, b"using", s.decls.slice(), tlmtlo) + self.print_decl_stmt(s.is_export, b"using", s.decls.slice()) + } + S::Kind::KAwaitUsing => { + self.print_decl_stmt(s.is_export, b"await using", s.decls.slice()) } - S::Kind::KAwaitUsing => self.print_decl_stmt( - s.is_export, - b"await using", - s.decls.slice(), - tlmtlo, - ), } } StmtData::SIf(s) => { self.print_indent(); - self.print_if(s, stmt.loc, tlmtlo.sub_var()); + self.print_if(s, stmt.loc); } StmtData::SDoWhile(s) => { self.print_indent(); self.print_space_before_identifier(); self.add_source_mapping(stmt.loc); self.print(b"do"); - let sub_var = tlmtlo.sub_var(); match s.body.data { StmtData::SBlock(block) => { self.print_space(); @@ -5455,14 +5319,13 @@ pub mod __gated_printer { s.body.loc, slice_of(block.stmts), Some(block.close_brace_loc), - sub_var, ); self.print_space(); } _ => { self.print_newline(); self.indent(); - self.print_stmt(s.body, sub_var).expect("unreachable"); + self.print_stmt(s.body).expect("unreachable"); self.print_semicolon_if_needed(); self.unindent(); self.print_indent(); @@ -5490,7 +5353,7 @@ pub mod __gated_printer { self.print_space(); self.print_expr(s.value, Level::Lowest, ExprFlag::none()); self.print(b")"); - self.print_body(s.body, tlmtlo.sub_var()); + self.print_body(s.body); } StmtData::SForOf(s) => { self.print_indent(); @@ -5510,7 +5373,7 @@ pub mod __gated_printer { self.print_space(); self.print_expr(s.value, Level::Comma, ExprFlag::none()); self.print(b")"); - self.print_body(s.body, tlmtlo.sub_var()); + self.print_body(s.body); } StmtData::SWhile(s) => { self.print_indent(); @@ -5521,7 +5384,7 @@ pub mod __gated_printer { self.print(b"("); self.print_expr(s.test, Level::Lowest, ExprFlag::none()); self.print(b")"); - self.print_body(s.body, tlmtlo.sub_var()); + self.print_body(s.body); } StmtData::SWith(s) => { self.print_indent(); @@ -5532,7 +5395,7 @@ pub mod __gated_printer { self.print(b"("); self.print_expr(s.value, Level::Lowest, ExprFlag::none()); self.print(b")"); - self.print_body(s.body, tlmtlo.sub_var()); + self.print_body(s.body); } StmtData::SLabel(s) => { if !self.options.minify_whitespace && self.options.indent.count > 0 { @@ -5542,7 +5405,7 @@ pub mod __gated_printer { self.add_source_mapping(stmt.loc); self.print_symbol(s.name.ref_); self.print(b":"); - self.print_body(s.stmt, tlmtlo.sub_var()); + self.print_body(s.stmt); } StmtData::STry(s) => { self.print_indent(); @@ -5550,8 +5413,7 @@ pub mod __gated_printer { self.add_source_mapping(stmt.loc); self.print(b"try"); self.print_space(); - let sub_var_try = tlmtlo.sub_var(); - self.print_block(s.body_loc, slice_of(s.body), None, sub_var_try); + self.print_block(s.body_loc, slice_of(s.body), None); if let Some(catch) = &s.catch { self.print_space(); @@ -5564,14 +5426,14 @@ pub mod __gated_printer { self.print(b")"); } self.print_space(); - self.print_block(catch.body_loc, slice_of(catch.body), None, sub_var_try); + self.print_block(catch.body_loc, slice_of(catch.body), None); } if let Some(finally) = &s.finally { self.print_space(); self.print(b"finally"); self.print_space(); - self.print_block(finally.loc, slice_of(finally.stmts), None, sub_var_try); + self.print_block(finally.loc, slice_of(finally.stmts), None); } self.print_newline(); @@ -5602,7 +5464,7 @@ pub mod __gated_printer { } self.print(b")"); - self.print_body(s.body, tlmtlo.sub_var()); + self.print_body(s.body); } StmtData::SSwitch(s) => { self.print_indent(); @@ -5631,8 +5493,6 @@ pub mod __gated_printer { } self.print(b":"); - - let sub_var_case = tlmtlo.sub_var(); let c_body = slice_of(c.body); if c_body.len() == 1 { if let StmtData::SBlock(block) = &c_body[0].data { @@ -5641,7 +5501,6 @@ pub mod __gated_printer { c_body[0].loc, slice_of(block.stmts), Some(block.close_brace_loc), - sub_var_case, ); self.print_newline(); continue; @@ -5652,7 +5511,7 @@ pub mod __gated_printer { self.indent(); for st in c_body.iter() { self.print_semicolon_if_needed(); - self.print_stmt(*st, sub_var_case).expect("unreachable"); + self.print_stmt(*st).expect("unreachable"); } self.unindent(); } @@ -5969,7 +5828,6 @@ pub mod __gated_printer { let local_name = self.name_for_symbol(name.ref_); let mi = self.module_info().expect("infallible: module_info enabled"); let local_name_id = mi.str(local_name); - mi.add_var(local_name_id, analyze_transpiled_module::VarKind::Lexical); let default_id = mi.str(b"default"); mi.add_import_info_single(irp_id, default_id, local_name_id, false); } @@ -5978,7 +5836,6 @@ pub mod __gated_printer { let local_name = self.name_for_symbol(item.name.ref_); let mi = self.module_info().expect("infallible: module_info enabled"); let local_name_id = mi.str(local_name); - mi.add_var(local_name_id, analyze_transpiled_module::VarKind::Lexical); let alias_id = mi.str(item.alias.slice()); mi.add_import_info_single(irp_id, alias_id, local_name_id, false); } @@ -5990,7 +5847,6 @@ pub mod __gated_printer { let local_name = self.name_for_symbol(s.namespace_ref); let mi = self.module_info().expect("infallible: module_info enabled"); let local_name_id = mi.str(local_name); - mi.add_var(local_name_id, analyze_transpiled_module::VarKind::Lexical); if phase_defer { mi.add_import_info_namespace_defer(irp_id, local_name_id); } else { @@ -6001,12 +5857,7 @@ pub mod __gated_printer { } StmtData::SBlock(s) => { self.print_indent(); - self.print_block( - stmt.loc, - slice_of(s.stmts), - Some(s.close_brace_loc), - tlmtlo.sub_var(), - ); + self.print_block(stmt.loc, slice_of(s.stmts), Some(s.close_brace_loc)); self.print_newline(); } StmtData::SDebugger(_) => { @@ -6163,7 +6014,7 @@ pub mod __gated_printer { } } - pub fn print_if(&mut self, s: &S::If, loc: bun_ast::Loc, tlmtlo: TopLevel) { + pub fn print_if(&mut self, s: &S::If, loc: bun_ast::Loc) { // `else if` chains recurse here directly without passing through // `print_stmt`, so they need their own guard. if !self.stack_check.is_safe_to_recurse() { @@ -6186,7 +6037,6 @@ pub mod __gated_printer { s.yes.loc, slice_of(block.stmts), Some(block.close_brace_loc), - tlmtlo, ); if s.no.is_some() { self.print_space(); @@ -6201,7 +6051,7 @@ pub mod __gated_printer { self.print_newline(); self.indent(); - self.print_stmt(s.yes, tlmtlo).expect("unreachable"); + self.print_stmt(s.yes).expect("unreachable"); self.unindent(); self.needs_semicolon = false; @@ -6216,7 +6066,7 @@ pub mod __gated_printer { } else { self.print_newline(); self.indent(); - self.print_stmt(s.yes, tlmtlo).expect("unreachable"); + self.print_stmt(s.yes).expect("unreachable"); self.unindent(); if s.no.is_some() { @@ -6235,16 +6085,16 @@ pub mod __gated_printer { match &no_block.data { StmtData::SBlock(block) => { self.print_space(); - self.print_block(no_block.loc, slice_of(block.stmts), None, tlmtlo); + self.print_block(no_block.loc, slice_of(block.stmts), None); self.print_newline(); } StmtData::SIf(s_if) => { - self.print_if(s_if, no_block.loc, tlmtlo); + self.print_if(s_if, no_block.loc); } _ => { self.print_newline(); self.indent(); - self.print_stmt(*no_block, tlmtlo).expect("unreachable"); + self.print_stmt(*no_block).expect("unreachable"); self.unindent(); } } @@ -6331,30 +6181,12 @@ pub mod __gated_printer { is_export: bool, keyword: &'static [u8], decls: &[G::Decl], - tlmtlo: TopLevel, ) { if is_export { self.print(b"export "); } let tlm: TopLevelAndIsExport = if Self::MAY_HAVE_MODULE_INFO { - TopLevelAndIsExport { - is_export, - is_top_level: if keyword == b"var" { - if tlmtlo.is_top_level() { - Some(analyze_transpiled_module::VarKind::Declared) - } else { - None - } - } else { - // let/const are block-scoped: only record at true top-level, - // not inside blocks where subVar() downgrades to .var_only. - if tlmtlo.is_top_level == IsTopLevel::Yes { - Some(analyze_transpiled_module::VarKind::Lexical) - } else { - None - } - }, - } + TopLevelAndIsExport { is_export } } else { TopLevelAndIsExport::default() }; @@ -6732,7 +6564,7 @@ pub mod __gated_printer { ); self.print(b" => {\n"); self.indent(); - self.print_block_body(slice_of(func.body.stmts), TopLevel::init(IsTopLevel::No)); + self.print_block_body(slice_of(func.body.stmts)); self.unindent(); self.print_indent(); self.print(b"}, "); @@ -7519,15 +7351,13 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR if PrinterType::::MAY_HAVE_MODULE_INFO { if let Some(mi) = printer.module_info.as_deref_mut() { mi.flags.contains_import_meta = true; - let s = mi.str(b"require"); - mi.add_var(s, analyze_transpiled_module::VarKind::Declared); } } } for part in tree.parts.iter() { for stmt in slice_of(part.stmts).iter() { - printer.print_stmt(*stmt, TopLevel::init(IsTopLevel::Yes))?; + printer.print_stmt(*stmt)?; printer.writer.get_error()?; printer.print_semicolon_if_needed(); } @@ -7761,7 +7591,7 @@ pub fn print_with_writer_and_platform< for part in parts { for stmt in slice_of(part.stmts).iter() { - if let Err(err) = printer.print_stmt(*stmt, TopLevel::init(IsTopLevel::Yes)) { + if let Err(err) = printer.print_stmt(*stmt) { return PrintResult::Err(err); } if let Err(err) = printer.writer.get_error() { diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 9d035d3e8b60..cc56fa495e9d 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -323,17 +323,17 @@ impl JSValue { #[inline] pub fn is_big_int_in_int64_range(self, min: i64, max: i64) -> bool { unsafe extern "C" { - safe fn JSC__isBigIntInInt64Range(this: JSValue, min: i64, max: i64) -> bool; + safe fn JSC__isBigIntInInt64Range(this: JSValue, max: i64, min: i64) -> bool; } - JSC__isBigIntInInt64Range(self, min, max) + JSC__isBigIntInInt64Range(self, max, min) } /// `JSValue.isBigIntInUInt64Range`. #[inline] pub fn is_big_int_in_uint64_range(self, min: u64, max: u64) -> bool { unsafe extern "C" { - safe fn JSC__isBigIntInUInt64Range(this: JSValue, min: u64, max: u64) -> bool; + safe fn JSC__isBigIntInUInt64Range(this: JSValue, max: u64, min: u64) -> bool; } - JSC__isBigIntInUInt64Range(self, min, max) + JSC__isBigIntInUInt64Range(self, max, min) } /// `JSValue.isCallable()`. #[inline] diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 1f2447a9ac9e..5ecf69886846 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -43,7 +43,12 @@ bun_core::declare_scope!(cache, visible); /// path reinstates the bug for any previously-cached TLA module (#30887). /// Version 23: `jsx.runtime`/`jsx.development` participate in the features hash, /// and tsconfig `"jsx": "react-jsx"` now emits the production runtime (#4227). -const EXPECTED_VERSION: u32 = 23; +/// Version 24: ModuleInfo drops the DeclaredVariable/LexicalVariable records and +/// renumbers RecordKind (0 is now ImportInfoSingle). JSC derives module-scope +/// bindings from the compiled bytecode after the module-loader rewrite, so the +/// record no longer carries them; blobs written in the old numbering must not +/// be read back. +const EXPECTED_VERSION: u32 = 24; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/src/jsc/bindings/BunAnalyzeTranspiledModule.cpp b/src/jsc/bindings/BunAnalyzeTranspiledModule.cpp index 71538245abbd..ae9be6840aa9 100644 --- a/src/jsc/bindings/BunAnalyzeTranspiledModule.cpp +++ b/src/jsc/bindings/BunAnalyzeTranspiledModule.cpp @@ -39,7 +39,7 @@ Identifier getFromIdentifierArray(VM& vm, Identifier* identifierArray, uint32_t return identifierArray[n]; } -extern "C" JSModuleRecord* zig__ModuleInfoDeserialized__toJSModuleRecord(JSGlobalObject* globalObject, VM& vm, const Identifier& module_key, const SourceCode& source_code, VariableEnvironment& declared_variables, VariableEnvironment& lexical_variables, bun_ModuleInfoDeserialized* module_info); +extern "C" JSModuleRecord* zig__ModuleInfoDeserialized__toJSModuleRecord(JSGlobalObject* globalObject, VM& vm, const Identifier& module_key, const SourceCode& source_code, bun_ModuleInfoDeserialized* module_info); extern "C" void zig__renderDiff(const char* expected_ptr, size_t expected_len, const char* received_ptr, size_t received_len, JSGlobalObject* globalObject); extern "C" Identifier* JSC__IdentifierArray__create(size_t len) @@ -55,23 +55,9 @@ extern "C" void JSC__IdentifierArray__setFromUtf8(Identifier* identifierArray, s identifierArray[n] = Identifier::fromString(vm, AtomString::fromUTF8(std::span(str, len))); } -extern "C" void JSC__VariableEnvironment__add(VariableEnvironment& environment, VM& vm, Identifier* identifierArray, uint32_t index) +extern "C" JSModuleRecord* JSC_JSModuleRecord__create(JSGlobalObject* globalObject, VM& vm, const Identifier* moduleKey, const SourceCode& sourceCode, bool hasImportMeta, bool isTypescript, bool hasTLA) { - environment.add(getFromIdentifierArray(vm, identifierArray, index)); -} - -extern "C" VariableEnvironment* JSC_JSModuleRecord__declaredVariables(JSModuleRecord* moduleRecord) -{ - return const_cast(&moduleRecord->declaredVariables()); -} -extern "C" VariableEnvironment* JSC_JSModuleRecord__lexicalVariables(JSModuleRecord* moduleRecord) -{ - return const_cast(&moduleRecord->lexicalVariables()); -} - -extern "C" JSModuleRecord* JSC_JSModuleRecord__create(JSGlobalObject* globalObject, VM& vm, const Identifier* moduleKey, const SourceCode& sourceCode, const VariableEnvironment& declaredVariables, const VariableEnvironment& lexicalVariables, bool hasImportMeta, bool isTypescript, bool hasTLA) -{ - JSModuleRecord* result = JSModuleRecord::create(globalObject, vm, globalObject->moduleRecordStructure(), *moduleKey, sourceCode, declaredVariables, lexicalVariables, hasImportMeta ? ImportMetaFeature : 0); + JSModuleRecord* result = JSModuleRecord::create(globalObject, vm, globalObject->moduleRecordStructure(), *moduleKey, sourceCode, hasImportMeta ? ImportMetaFeature : 0); result->m_isTypeScript = isTypescript; result->setHasTLA(hasTLA); return result; @@ -173,9 +159,6 @@ extern "C" EncodedJSValue Bun__analyzeTranspiledModule(JSGlobalObject* globalObj return promise; }; - VariableEnvironment declaredVariables = VariableEnvironment(); - VariableEnvironment lexicalVariables = VariableEnvironment(); - auto provider = static_cast(sourceCode.provider()); if (provider->m_resolvedSource.module_info == nullptr) { @@ -184,7 +167,7 @@ extern "C" EncodedJSValue Bun__analyzeTranspiledModule(JSGlobalObject* globalObj } auto* moduleInfo = static_cast(provider->m_resolvedSource.module_info); - auto moduleRecord = zig__ModuleInfoDeserialized__toJSModuleRecord(globalObject, vm, moduleKey, sourceCode, declaredVariables, lexicalVariables, moduleInfo); + auto moduleRecord = zig__ModuleInfoDeserialized__toJSModuleRecord(globalObject, vm, moduleKey, sourceCode, moduleInfo); // Under --isolate the same SourceProvider is reused across globals via the // IsolatedModuleCache, so module_info must remain alive on the provider; // ~SourceProvider frees it. Otherwise, free now. @@ -220,7 +203,7 @@ static EncodedJSValue fallbackParse(JSGlobalObject* globalObject, const Identifi RELEASE_AND_RETURN(scope, JSValue::encode(rejectWithError(error.toErrorObject(globalObject, sourceCode)))); ASSERT(moduleProgramNode); - ModuleAnalyzer moduleAnalyzer(globalObject, moduleKey, sourceCode, moduleProgramNode->varDeclarations(), moduleProgramNode->lexicalVariables(), moduleProgramNode->features()); + ModuleAnalyzer moduleAnalyzer(globalObject, moduleKey, sourceCode, moduleProgramNode->features()); RETURN_IF_EXCEPTION(scope, JSValue::encode(promise->rejectWithCaughtException(vm, scope))); auto result = moduleAnalyzer.analyze(*moduleProgramNode); @@ -254,30 +237,6 @@ String dumpRecordInfo(JSModuleRecord* moduleRecord) { WTF::StringPrintStream stream; - { - Vector sortedVars; - for (const auto& pair : moduleRecord->declaredVariables()) - sortedVars.append(String(pair.key.get())); - std::sort(sortedVars.begin(), sortedVars.end(), [](const String& a, const String& b) { - return codePointCompare(a, b) < 0; - }); - stream.print(" varDeclarations:\n"); - for (const auto& name : sortedVars) - stream.print(" - ", name, "\n"); - } - - { - Vector sortedVars; - for (const auto& pair : moduleRecord->lexicalVariables()) - sortedVars.append(String(pair.key.get())); - std::sort(sortedVars.begin(), sortedVars.end(), [](const String& a, const String& b) { - return codePointCompare(a, b) < 0; - }); - stream.print(" lexicalVariables:\n"); - for (const auto& name : sortedVars) - stream.print(" - ", name, "\n"); - } - stream.print(" features: (not accessible)\n"); stream.print("\nAnalyzing ModuleRecord key(", moduleRecord->moduleKey().impl(), ")\n"); diff --git a/src/jsc/bindings/JSCFFIBridge.cpp b/src/jsc/bindings/JSCFFIBridge.cpp new file mode 100644 index 000000000000..1e9ff0b37c66 --- /dev/null +++ b/src/jsc/bindings/JSCFFIBridge.cpp @@ -0,0 +1,121 @@ + +#include "root.h" + +#include +#include +#include +#include +#include +#include +#include "ScriptExecutionContext.h" +#include +#include +#include + +#include "ZigGlobalObject.h" +#include "headers-handwritten.h" + +static_assert(static_cast(JSC::FFI::Type::Char) == 0, "FFI::Type tag drift"); +static_assert(static_cast(JSC::FFI::Type::Pointer) == 12, "FFI::Type tag drift"); +static_assert(static_cast(JSC::FFI::Type::JSValue) == 19, "FFI::Type tag drift"); +static_assert(static_cast(JSC::FFI::Type::Buffer) == 20, "FFI::Type tag drift"); +static_assert(static_cast(JSC::FFI::Type::BufferLength) == 21, "FFI::Type tag drift"); + +extern "C" JSC::EncodedJSValue Bun__CreateJSCFFIFunction( + Zig::GlobalObject* globalObject, + const ZigString* symbolName, + const uint8_t* argTypes, + unsigned argCount, + uint8_t returnType, + void* target, + JSC::EncodedJSValue ownerValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + Vector arguments; + arguments.reserveInitialCapacity(argCount); + for (unsigned i = 0; i < argCount; ++i) + arguments.append(static_cast(argTypes[i])); + + RefPtr signature = JSC::FFI::Signature::tryCreate(arguments.span(), static_cast(returnType)); + if (!signature) { + JSC::throwTypeError(globalObject, scope, "bun:ffi: unsupported signature"_s); + RELEASE_AND_RETURN(scope, {}); + } + + JSC::JSObject* owner = JSC::JSValue::decode(ownerValue).getObject(); + + WTF::String name = symbolName ? Zig::toStringCopy(*symbolName) : WTF::String(); + JSC::JSFFIFunction* function = JSC::JSFFIFunction::create(vm, globalObject, globalObject->ffiFunctionStructure(), signature.releaseNonNull(), target, name, owner, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + if (!function) + RELEASE_AND_RETURN(scope, {}); + + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(function)); +} + +static void Bun__jscFFIThreadsafeDispatch(JSC::FFI::ThreadsafeInvocation& invocation) +{ + static_assert(sizeof(WebCore::ScriptExecutionContextIdentifier) <= sizeof(void*)); + auto contextId = static_cast(reinterpret_cast(invocation.embedderContext())); + WebCore::ScriptExecutionContext::postTaskTo(contextId, [protectedInvocation = Ref { invocation }](WebCore::ScriptExecutionContext&) mutable { JSC::FFI::runThreadsafeInvocation(protectedInvocation.get()); }); +} + +extern "C" JSC::EncodedJSValue Bun__CreateJSCFFICallback( + Zig::GlobalObject* globalObject, + JSC::EncodedJSValue callableValue, + const uint8_t* argTypes, + unsigned argCount, + uint8_t returnType, + bool threadsafe) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (threadsafe) { + static std::once_flag registerDispatch; + std::call_once(registerDispatch, [] { + JSC::FFI::FFIContext::setThreadsafeDispatch(Bun__jscFFIThreadsafeDispatch); + }); + } + + JSC::JSObject* callable = JSC::JSValue::decode(callableValue).getObject(); + if (!callable || !callable->isCallable()) [[unlikely]] { + JSC::throwTypeError(globalObject, scope, "bun:ffi: JSCallback requires a function"_s); + RELEASE_AND_RETURN(scope, {}); + } + + Vector arguments; + arguments.reserveInitialCapacity(argCount); + for (unsigned i = 0; i < argCount; ++i) + arguments.append(static_cast(argTypes[i])); + + RefPtr signature = JSC::FFI::Signature::tryCreate(arguments.span(), static_cast(returnType)); + if (!signature) { + JSC::throwTypeError(globalObject, scope, "bun:ffi: unsupported callback signature"_s); + RELEASE_AND_RETURN(scope, {}); + } + + void* embedderContext = nullptr; + if (threadsafe) { + auto* scriptExecutionContext = globalObject->scriptExecutionContext(); + if (!scriptExecutionContext) [[unlikely]] { + JSC::throwTypeError(globalObject, scope, "bun:ffi: no script execution context for a threadsafe JSCallback"_s); + RELEASE_AND_RETURN(scope, {}); + } + embedderContext = reinterpret_cast(static_cast(scriptExecutionContext->identifier())); + } + JSC::JSFFICallback* callback = JSC::FFI::createCallback(globalObject, signature.releaseNonNull(), callable, threadsafe, embedderContext); + RETURN_IF_EXCEPTION(scope, {}); + if (!callback) + RELEASE_AND_RETURN(scope, {}); + + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(callback)); +} + +extern "C" void Bun__JSCFFICallbackClose(JSC::EncodedJSValue callbackValue) +{ + if (auto* callback = dynamicDowncast(JSC::JSValue::decode(callbackValue))) + callback->close(); +} diff --git a/src/jsc/bindings/JSFFICString.cpp b/src/jsc/bindings/JSFFICString.cpp new file mode 100644 index 000000000000..d0852d92aa37 --- /dev/null +++ b/src/jsc/bindings/JSFFICString.cpp @@ -0,0 +1,83 @@ +#include "root.h" + +#include "JSFFICString.h" + +#include "ZigGlobalObject.h" +#include +#include + +extern "C" JSC::EncodedJSValue Bun__FFI__CString__transcode(JSC::JSGlobalObject*, JSC::EncodedJSValue ptr, JSC::EncodedJSValue byteOffset, JSC::EncodedJSValue byteLength); + +namespace Bun { + +using namespace JSC; + +static JSC_DECLARE_HOST_FUNCTION(callFFICString); +static JSC_DECLARE_HOST_FUNCTION(constructFFICString); + +const ClassInfo JSFFICStringConstructor::s_info = { "CString"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFFICStringConstructor) }; + +JSFFICStringConstructor::JSFFICStringConstructor(VM& vm, Structure* structure) + : Base(vm, structure, callFFICString, constructFFICString) +{ +} + +JSFFICStringConstructor* JSFFICStringConstructor::create(VM& vm, JSGlobalObject* globalObject) +{ + auto* structure = createStructure(vm, globalObject, globalObject->functionPrototype()); + JSFFICStringConstructor* constructor = new (NotNull, allocateCell(vm)) JSFFICStringConstructor(vm, structure); + constructor->finishCreation(vm); + return constructor; +} + +void JSFFICStringConstructor::finishCreation(VM& vm) +{ + Base::finishCreation(vm, 3, "CString"_s, PropertyAdditionMode::WithoutStructureTransition); +} + +JSC_DEFINE_HOST_FUNCTION(callFFICString, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return constructFFICString(globalObject, callFrame); +} + +static inline bool isSafeIntegerValue(JSValue value) +{ + if (value.isInt32()) + return true; + if (!value.isDouble()) + return false; + double number = value.asDouble(); + return std::isfinite(number) && std::trunc(number) == number && std::abs(number) <= maxSafeInteger(); +} + +JSC_DEFINE_HOST_FUNCTION(constructFFICString, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + VM& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue ptrValue = callFrame->argument(0); + JSValue byteOffset = callFrame->argument(1); + JSValue byteLength = callFrame->argument(2); + + bool hasPointer = ptrValue.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (!hasPointer) + return JSValue::encode(jsEmptyString(vm)); + + JSValue offsetArgument = byteOffset.toBoolean(globalObject) ? byteOffset : jsNumber(0); + RETURN_IF_EXCEPTION(scope, {}); + JSValue lengthArgument = isSafeIntegerValue(byteLength) ? byteLength : jsUndefined(); + JSValue transcoded = JSValue::decode(Bun__FFI__CString__transcode(globalObject, JSValue::encode(ptrValue), JSValue::encode(offsetArgument), JSValue::encode(lengthArgument))); + RETURN_IF_EXCEPTION(scope, {}); + if (transcoded.isString()) [[likely]] + return JSValue::encode(transcoded); + throwException(globalObject, scope, transcoded); + return {}; +} + +} + +extern "C" JSC::EncodedJSValue Bun__FFI__CStringConstructor(JSC::JSGlobalObject* globalObject) +{ + return JSC::JSValue::encode(defaultGlobalObject(globalObject)->JSFFICStringConstructor()); +} diff --git a/src/jsc/bindings/JSFFICString.h b/src/jsc/bindings/JSFFICString.h new file mode 100644 index 000000000000..1fc546d312df --- /dev/null +++ b/src/jsc/bindings/JSFFICString.h @@ -0,0 +1,31 @@ +#pragma once + +#include "root.h" + +#include + +namespace Bun { + +using namespace JSC; + +class JSFFICStringConstructor final : public JSC::InternalFunction { +public: + using Base = JSC::InternalFunction; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + DECLARE_INFO; + + static JSFFICStringConstructor* create(JSC::VM&, JSC::JSGlobalObject*); + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + +private: + JSFFICStringConstructor(JSC::VM& vm, JSC::Structure* structure); + void finishCreation(JSC::VM&); +}; + +} diff --git a/src/jsc/bindings/JSFFIFunction.cpp b/src/jsc/bindings/JSFFIFunction.cpp index d5c2fd50dc3c..2dc7bd22005c 100644 --- a/src/jsc/bindings/JSFFIFunction.cpp +++ b/src/jsc/bindings/JSFFIFunction.cpp @@ -37,47 +37,6 @@ #include "DOMJITIDLTypeFilter.h" #include "DOMJITHelpers.h" -// Refcounted so FFI_Callback_threadsafe_call can keep the wrapper alive from a -// foreign thread; copying the JSC::Strong members is only safe on the JS thread. -class FFICallbackFunctionWrapper : public ThreadSafeRefCounted { - - WTF_DEPRECATED_MAKE_FAST_ALLOCATED(FFICallbackFunctionWrapper); - -public: - JSC::Strong m_function; - JSC::Strong globalObject; - // Cached on the JS thread at construction time so the foreign-thread - // trampoline never has to dereference a Strong to find the context. - WebCore::ScriptExecutionContextIdentifier m_contextId; - ~FFICallbackFunctionWrapper() = default; - - FFICallbackFunctionWrapper(JSC::JSFunction* function, Zig::GlobalObject* globalObject) - : m_function(globalObject->vm(), function) - , globalObject(globalObject->vm(), globalObject) - , m_contextId(globalObject->scriptExecutionContext()->identifier()) - { - } -}; -extern "C" void FFICallbackFunctionWrapper_destroy(FFICallbackFunctionWrapper* wrapper) -{ - // deref, not delete: pending event-loop tasks may still hold refs. - wrapper->deref(); -} - -extern "C" FFICallbackFunctionWrapper* Bun__createFFICallbackFunction( - Zig::GlobalObject* globalObject, - JSC::EncodedJSValue callbackFn) -{ - auto* vm = &globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(*vm); - - auto* callbackFunction = uncheckedDowncast(JSC::JSValue::decode(callbackFn)); - - auto* wrapper = new FFICallbackFunctionWrapper(callbackFunction, globalObject); - - return wrapper; -} - extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunctionWithData(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, void* data) { auto& vm = JSC::getVM(globalObject); @@ -183,125 +142,3 @@ JSFFIFunction* JSFFIFunction::createForFFI(VM& vm, Zig::GlobalObject* globalObje } } // namespace JSC - -// Shared tail for the FFI_Callback_* entry points: call back into JS and leave any exception -// pending on the VM, like any other host function. Never clear and re-throw here: re-installing -// the TerminationException once the termination request is retired trips VM::setException. -static JSC::EncodedJSValue invokeFFICallback(Zig::GlobalObject* globalObject, JSC::JSFunction* function, JSC::MarkedArgumentBuffer& arguments) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsNull())); - return JSC::JSValue::encode(result); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - for (size_t i = 0; i < argCount; ++i) - arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(args[i])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" void -FFI_Callback_threadsafe_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - // Runs on a foreign thread: do not touch the wrapper's JSC::Strong members here. - WTF::Vector argsVec; - for (size_t i = 0; i < argCount; ++i) - argsVec.append(args[i]); - - // Ref only once the context is found live (inside the map lock) and release via - // adoptRef in the task, so the last deref — destroying two JSC::Strong members — - // can only happen on the JS thread. On a dead/terminating context nothing is destroyed here. - WebCore::ScriptExecutionContext::postTaskTo(wrapper.m_contextId, [&wrapper] { wrapper.ref(); }, [argsVec = WTF::move(argsVec), wrapper = &wrapper](WebCore::ScriptExecutionContext& ctx) mutable { - auto protectedWrapper = adoptRef(*wrapper); - auto* globalObject = uncheckedDowncast(ctx.jsGlobalObject()); - JSC::MarkedArgumentBuffer arguments; - for (size_t i = 0; i < argsVec.size(); ++i) - arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(argsVec[i])); - invokeFFICallback(globalObject, protectedWrapper->m_function.get(), arguments); }); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call_0(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call_1(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call_2(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue FFI_Callback_call_3(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - arguments.append(JSC::JSValue::decode(args[2])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue FFI_Callback_call_4(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - arguments.append(JSC::JSValue::decode(args[2])); - arguments.append(JSC::JSValue::decode(args[3])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue FFI_Callback_call_5(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - arguments.append(JSC::JSValue::decode(args[2])); - arguments.append(JSC::JSValue::decode(args[3])); - arguments.append(JSC::JSValue::decode(args[4])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call_6(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - arguments.append(JSC::JSValue::decode(args[2])); - arguments.append(JSC::JSValue::decode(args[3])); - arguments.append(JSC::JSValue::decode(args[4])); - arguments.append(JSC::JSValue::decode(args[5])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} - -extern "C" JSC::EncodedJSValue -FFI_Callback_call_7(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(args[0])); - arguments.append(JSC::JSValue::decode(args[1])); - arguments.append(JSC::JSValue::decode(args[2])); - arguments.append(JSC::JSValue::decode(args[3])); - arguments.append(JSC::JSValue::decode(args[4])); - arguments.append(JSC::JSValue::decode(args[5])); - arguments.append(JSC::JSValue::decode(args[6])); - return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments); -} diff --git a/src/jsc/bindings/NodeVMSourceTextModule.cpp b/src/jsc/bindings/NodeVMSourceTextModule.cpp index 67b09a97e8a9..363ce0d9db9d 100644 --- a/src/jsc/bindings/NodeVMSourceTextModule.cpp +++ b/src/jsc/bindings/NodeVMSourceTextModule.cpp @@ -183,7 +183,7 @@ JSValue NodeVMSourceTextModule::createModuleRecord(JSGlobalObject* globalObject) return {}; } - ModuleAnalyzer analyzer(globalObject, Identifier::fromString(vm, m_identifier), m_sourceCode, node->varDeclarations(), node->lexicalVariables(), AllFeatures); + ModuleAnalyzer analyzer(globalObject, Identifier::fromString(vm, m_identifier), m_sourceCode, AllFeatures); RETURN_IF_EXCEPTION(scope, {}); ASSERT(node != nullptr); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index a45cf0016dfe..4590e7bce691 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -110,6 +110,7 @@ #include "JSEventTarget.h" #include "JSFetchHeaders.h" #include "JSFFIFunction.h" +#include "JSFFICString.h" #include "webcore/JSMIMEParams.h" #include "webcore/JSMIMEType.h" #include "JSMessageChannel.h" @@ -2738,6 +2739,10 @@ void GlobalObject::finishCreation(VM& vm) init.setConstructor(constructor); }); + m_JSFFICStringConstructor.initLater([](const Initializer& init) { + init.set(Bun::JSFFICStringConstructor::create(init.vm, init.owner)); + }); + m_JSDatabaseSyncClassStructure.initLater( [](LazyClassStructure::Initializer& init) { auto* prototype = Bun::JSDatabaseSyncPrototype::create( diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index ca759a74da9e..f58ed618a745 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -261,6 +261,8 @@ class GlobalObject : public Bun::GlobalScope { JSC::JSObject* JSStringDecoder() const { return m_JSStringDecoderClassStructure.constructorInitializedOnMainThread(this); } JSC::JSValue JSStringDecoderPrototype() const { return m_JSStringDecoderClassStructure.prototypeInitializedOnMainThread(this); } + JSC::JSObject* JSFFICStringConstructor() const { return m_JSFFICStringConstructor.getInitializedOnMainThread(this); } + JSC::Structure* NodeVMScriptStructure() const { return m_NodeVMScriptClassStructure.getInitializedOnMainThread(this); } JSC::JSObject* NodeVMScript() const { return m_NodeVMScriptClassStructure.constructorInitializedOnMainThread(this); } JSC::JSValue NodeVMScriptPrototype() const { return m_NodeVMScriptClassStructure.prototypeInitializedOnMainThread(this); } @@ -559,6 +561,7 @@ class GlobalObject : public Bun::GlobalScope { V(private, LazyClassStructure, m_JSH3ResponseSinkClassStructure) \ \ V(private, LazyClassStructure, m_JSStringDecoderClassStructure) \ + V(private, LazyPropertyOfGlobalObject, m_JSFFICStringConstructor) \ V(public, LazyClassStructure, m_JSDatabaseSyncClassStructure) \ V(public, LazyClassStructure, m_JSStatementSyncClassStructure) \ V(public, LazyClassStructure, m_JSStatementSyncIteratorClassStructure) \ diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 97fbbf7b30fd..327b310e8520 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5424,12 +5424,11 @@ extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInUInt64Range(JSC::EncodedJ return false; JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); - auto result = bigInt->compare(bigInt, min); - if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) { - return true; - } - result = bigInt->compare(bigInt, max); - return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal; + auto low = bigInt->compare(bigInt, min); + if (low != JSBigInt::ComparisonResult::GreaterThan && low != JSBigInt::ComparisonResult::Equal) + return false; + auto high = bigInt->compare(bigInt, max); + return high == JSBigInt::ComparisonResult::LessThan || high == JSBigInt::ComparisonResult::Equal; } extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJSValue value, int64_t max, int64_t min) @@ -5439,12 +5438,11 @@ extern "C" [[ZIG_EXPORT(nothrow)]] bool JSC__isBigIntInInt64Range(JSC::EncodedJS return false; JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); - auto result = bigInt->compare(bigInt, min); - if (result == JSBigInt::ComparisonResult::GreaterThan || result == JSBigInt::ComparisonResult::Equal) { - return true; - } - result = bigInt->compare(bigInt, max); - return result == JSBigInt::ComparisonResult::LessThan || result == JSBigInt::ComparisonResult::Equal; + auto low = bigInt->compare(bigInt, min); + if (low != JSBigInt::ComparisonResult::GreaterThan && low != JSBigInt::ComparisonResult::Equal) + return false; + auto high = bigInt->compare(bigInt, max); + return high == JSBigInt::ComparisonResult::LessThan || high == JSBigInt::ComparisonResult::Equal; } [[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) @@ -6047,7 +6045,7 @@ CPP_DECL void JSC__VM__setControlFlowProfiler(JSC::VM* vm, bool isEnabled) CPP_DECL void JSC__VM__performOpportunisticallyScheduledTasks(JSC::VM* vm, double until) { - vm->performOpportunisticallyScheduledTasks(MonotonicTime::now() + Seconds(until), {}); + vm->performOpportunisticallyScheduledTasks(ApproximateTime::now() + Seconds(until), {}); } extern "C" EncodedJSValue JSC__createError(JSC::JSGlobalObject* globalObject, const BunString* str) diff --git a/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp index c669f9966147..b8c6fe5c88e6 100644 --- a/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp +++ b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp @@ -839,7 +839,7 @@ static IterationRecord getIteratorAsync(JSC::VM& vm, JSGlobalObject* globalObjec } IterationRecord syncRecord = iteratorDirect(globalObject, syncIterator); RETURN_IF_EXCEPTION(scope, {}); - auto* asyncFromSyncIterator = JSAsyncFromSyncIterator::create(vm, globalObject->asyncFromSyncIteratorStructure(), syncRecord.iterator, syncRecord.nextMethod); + auto* asyncFromSyncIterator = JSAsyncFromSyncIterator::create(vm, globalObject->asyncFromSyncIteratorStructure(), asObject(syncRecord.iterator), syncRecord.nextMethod, IterationMode::Generic); RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN(scope, iteratorDirect(globalObject, asyncFromSyncIterator)); } diff --git a/src/runtime/ffi/FFI.h b/src/runtime/ffi/FFI.h index 5159a50a2761..6a4f733c3e17 100644 --- a/src/runtime/ffi/FFI.h +++ b/src/runtime/ffi/FFI.h @@ -6,9 +6,6 @@ // This file is only compatible with 64 bit CPUs // It must be kept in sync with JSCJSValue.h // https://github.com/oven-sh/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCJSValue.h -#ifdef IS_CALLBACK -#define INJECT_BEFORE int c = 500; // This is a callback, so we need to inject code before the call -#endif #define IS_BIG_ENDIAN 0 #define USE_JSVALUE64 1 #define USE_JSVALUE32_64 0 @@ -71,10 +68,6 @@ BUN_FFI_IMPORT extern struct NapiEnv Bun__thisFFIModuleNapiEnv; #endif -#ifdef INJECT_BEFORE -// #include -#endif -// #include // This value is 2^49, used to encode doubles such that the encoded value will // begin with a 15-bit pattern within the range 0x0002..0xFFFC. @@ -144,17 +137,6 @@ typedef void* JSContext; int64_t *argsPtr = (int64_t*)((size_t*)callFrame + Bun_FFI_PointerOffsetToArgumentsList) -#ifdef IS_CALLBACK -void* callback_ctx; -BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args); -// We wrap -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__)); -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) { - EncodedJSValue return_value; - return_value.asZigRepr = FFI_Callback_call(ctx, argCount, args); - return return_value; -} -#endif static bool JSVALUE_IS_CELL(EncodedJSValue val) __attribute__((__always_inline__)); static bool JSVALUE_IS_INT32(EncodedJSValue val) __attribute__((__always_inline__)); @@ -380,10 +362,7 @@ static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) { return INT64_TO_JSVALUE_SLOW(jsGlobalObject, val); } -#ifndef IS_CALLBACK BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); -#endif - // --- Generated Code --- diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index 8ce99b873fa3..76d7c1d0a0e6 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -90,6 +90,23 @@ pub(crate) fn new_cstring( } } +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn Bun__FFI__CString__transcode( + global: &JSGlobalObject, + ptr: JSValue, + byte_offset: JSValue, + byte_length: JSValue, +) -> JSValue { + jsc::to_js_host_fn_result( + global, + new_cstring(global, ptr, Some(byte_offset), Some(byte_length)), + ) +} + +unsafe extern "C" { + fn Bun__FFI__CStringConstructor(global: *const JSGlobalObject) -> JSValue; +} + // DOMJIT fast-path descriptor + slow-path host fn, represented here as a const // descriptor. The `DOMEffect.forRead(.TypedArrayProperties)` argument is consumed // by the C++ codegen, not the runtime descriptor; it lives in the generated @@ -103,35 +120,21 @@ pub(crate) const DOM_CALL: DomCall = DomCall { pub fn to_js(global_object: &JSGlobalObject) -> JSValue { // Unrolled manually; keep in sync with `FIELDS` below. let fields = FIELDS(); - let object = JSValue::create_empty_object(global_object, fields.len() + 2); + let object = JSValue::create_empty_object(global_object, fields.len() + 3); for &(name, func) in &fields { - if name == "CString" { - // CString needs to be callable as a constructor for backward compatibility. - // Pass the same function as the constructor so `new CString(ptr)` works. - object.put( - global_object, - name.as_bytes(), - JSFunction::create( - global_object, - name, - func, - 1, - jsc::js_function::CreateJSFunctionOptions { - constructor: Some(func), - ..Default::default() - }, - ), - ); - } else { - object.put( - global_object, - name.as_bytes(), - JSFunction::create(global_object, name, func, 1, Default::default()), - ); - } + object.put( + global_object, + name.as_bytes(), + JSFunction::create(global_object, name, func, 1, Default::default()), + ); } + // SAFETY: `global_object` is a live JSC handle for the duration of the call. + object.put(global_object, b"CString", unsafe { + Bun__FFI__CStringConstructor(global_object) + }); + // SAFETY: `put` is the C++-side `FFI__ptr__put` helper; global_object is live. unsafe { (DOM_CALL.put)(std::ptr::from_ref(global_object).cast_mut(), object) }; object.put(global_object, b"read", reader::to_js(global_object)); @@ -258,15 +261,31 @@ pub mod reader { #[inline(always)] fn addr_from_args(global_object: &JSGlobalObject, arguments: &[JSValue]) -> JsResult { - if arguments.is_empty() || !arguments[0].is_number() { - return Err(global_object.throw_invalid_arguments(format_args!("Expected a pointer"))); - } - let off = if arguments.len() > 1 { - usize::try_from(arguments[1].to_int32()).expect("int cast") + let base = if !arguments.is_empty() && arguments[0].is_number() { + arguments[0].as_ptr_address() + } else if !arguments.is_empty() + && arguments[0].is_big_int() + && arguments[0].is_big_int_in_uint64_range(1, usize::MAX as u64) + { + arguments[0].to_uint64_no_truncate() as usize } else { - 0usize + return Err(global_object.throw_invalid_arguments(format_args!("Expected a pointer"))); }; - Ok(arguments[0].as_ptr_address() + off) + let mut addr = base; + if let Some(off_value) = arguments.get(1) { + let off = off_value.to_int32(); + if off < 0 { + addr = addr.saturating_sub(off.unsigned_abs() as usize); + } else { + addr = addr.saturating_add(off as usize); + } + } + if addr == 0 { + return Err(global_object.throw_invalid_arguments(format_args!( + "ptr cannot be zero, that would segfault Bun :(" + ))); + } + Ok(addr) } /// Read a `T` from a user-supplied raw address (unaligned). @@ -451,9 +470,10 @@ fn ptr_(global_this: &JSGlobalObject, value: JSValue, byte_offset: Option array_buffer.ptr as usize + array_buffer.byte_len as usize { @@ -502,13 +522,21 @@ fn get_ptr_slice( byte_offset: Option, byte_length: Option, ) -> ValueOrError { - if !value.is_number() || value.as_number() < 0.0 || value.as_number() > usize::MAX as f64 { - return ValueOrError::Err( - global_this.to_invalid_arguments(format_args!("ptr must be a number.")), - ); - } - - let num = value.as_ptr_address(); + let num = if value.is_big_int() { + if !value.is_big_int_in_uint64_range(0, usize::MAX as u64) { + return ValueOrError::Err( + global_this.to_invalid_arguments(format_args!("ptr is out of range.")), + ); + } + value.to_uint64_no_truncate() as usize + } else { + if !value.is_number() || value.as_number() < 0.0 || value.as_number() > usize::MAX as f64 { + return ValueOrError::Err( + global_this.to_invalid_arguments(format_args!("ptr must be a number.")), + ); + } + value.as_ptr_address() + }; if num == 0 { return ValueOrError::Err(global_this.to_invalid_arguments(format_args!( "ptr cannot be zero, that would segfault Bun :(" @@ -521,9 +549,10 @@ fn get_ptr_slice( if byte_off.is_number() { let off = byte_off.to_int64(); if off < 0 { - addr = addr.saturating_sub(usize::try_from(-off).expect("int cast")); + addr = + addr.saturating_sub(usize::try_from(off.unsigned_abs()).unwrap_or(usize::MAX)); } else { - addr = addr.saturating_add(usize::try_from(off).expect("int cast")); + addr = addr.saturating_add(usize::try_from(off).unwrap_or(usize::MAX)); } if addr == 0 { @@ -600,9 +629,8 @@ fn get_cptr(value: JSValue) -> Option { return Some(addr); } } else if value.is_big_int() { - let addr: u64 = value.to_uint64_no_truncate(); - if addr > 0 { - return Some(addr as usize); + if value.is_big_int_in_uint64_range(1, usize::MAX as u64) { + return Some(value.to_uint64_no_truncate() as usize); } } @@ -860,23 +888,20 @@ mod fields { super::to_array_buffer(global, value, byte_offset, length, final_ctx, final_cb) } - // closeCallback → FFI::close_callback(global, JSValue) -> JSValue - pub(super) fn close_callback( + pub(super) fn close_jsc_callback( global: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { let mut iter = callframe.arguments().iter(); - let ctx = eat_required(global, &mut iter)?; - Ok(FfiImpl::close_callback(global, ctx)) + let callback = eat_required(global, &mut iter)?; + Ok(FfiImpl::close_jsc_callback(global, callback)) } - // CString → new_cstring(global, JSValue, ?JSValue, ?JSValue) -> JsResult - pub(super) fn cstring(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + pub(super) fn cfunction(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let mut iter = callframe.arguments().iter(); - let value = eat_required(global, &mut iter)?; - let byte_offset = next_eat(&mut iter); - let length = next_eat(&mut iter); - new_cstring(global, value, byte_offset, length) + let options = eat_required(global, &mut iter)?; + let name = next_eat(&mut iter); + FfiImpl::create_cfunction(global, options, name) } } @@ -893,8 +918,8 @@ fn FIELDS() -> [(&'static str, jsc::JSHostFn); 8] { ("linkSymbols", wrap_host_fn!(fields::link_symbols)), ("toBuffer", wrap_host_fn!(fields::to_buffer)), ("toArrayBuffer", wrap_host_fn!(fields::to_array_buffer)), - ("closeCallback", wrap_host_fn!(fields::close_callback)), - ("CString", wrap_host_fn!(fields::cstring)), + ("closeCallback", wrap_host_fn!(fields::close_jsc_callback)), + ("cfunction", wrap_host_fn!(fields::cfunction)), ] } diff --git a/src/runtime/ffi/abi_type.rs b/src/runtime/ffi/abi_type.rs index c984235a031a..33bb20e208ef 100644 --- a/src/runtime/ffi/abi_type.rs +++ b/src/runtime/ffi/abi_type.rs @@ -45,6 +45,7 @@ pub enum ABIType { NapiEnv = 18, NapiValue = 19, Buffer = 20, + BufferLength = 21, } bun_core::comptime_string_map! { @@ -83,6 +84,8 @@ bun_core::comptime_string_map! { b"usize" => ABIType::Uint64T, b"size_t" => ABIType::Uint64T, b"buffer" => ABIType::Buffer, + b"buffer_length" => ABIType::BufferLength, + b"buffer_bytelength" => ABIType::BufferLength, b"void*" => ABIType::Ptr, b"ptr" => ABIType::Ptr, b"pointer" => ABIType::Ptr, @@ -101,56 +104,47 @@ bun_core::comptime_string_map! { // ───────────────────────────────────────────────────────────────────────────── // Per-variant string table — single source of truth for the four exhaustive // matches that previously lived in typename_label / param_typename_label / -// ToCFormatter / ToJSFormatter. Indexed by `self as usize` (discriminants are -// contiguous 0..=20). +// ToCFormatter / ToJSFormatter. Indexed by `self as usize`. // ───────────────────────────────────────────────────────────────────────────── struct AbiRow { - /// C type name for return/decl position (`typename_label`). c_type: &'static [u8], - /// `(T)` cast prefix emitted by `ToCFormatter` when `exact` is set. Empty - /// when no cast is wanted (Buffer) or the row is unreachable (Void/Napi*). - to_c_cast: &'static str, - /// `JSVALUE_TO_*( ` macro head. `None` for the three early-return arms - /// (Void / NapiEnv / NapiValue) handled inline by `ToCFormatter`. to_c_macro: Option<&'static str>, - /// `(prefix, suffix)` wrapping the symbol in `ToJSFormatter`. `None` for - /// the three special arms (Void / NapiEnv / Buffer) handled inline. to_js: Option<(&'static str, &'static str)>, } #[rustfmt::skip] -static ABI_TABLE: [AbiRow; 21] = { +static ABI_TABLE: [AbiRow; 22] = { const fn r( c_type: &'static [u8], - to_c_cast: &'static str, to_c_macro: Option<&'static str>, to_js: Option<(&'static str, &'static str)>, ) -> AbiRow { - AbiRow { c_type, to_c_cast, to_c_macro, to_js } + AbiRow { c_type, to_c_macro, to_js } } [ - /* Char */ r(b"char", "(char)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Int8T */ r(b"int8_t", "(int8_t)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Uint8T */ r(b"uint8_t", "(uint8_t)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Int16T */ r(b"int16_t", "(int16_t)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Uint16T */ r(b"uint16_t", "(uint16_t)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Int32T */ r(b"int32_t", "(int32_t)", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), - /* Uint32T */ r(b"uint32_t", "(uint32_t)", Some("JSVALUE_TO_INT32("), Some(("UINT32_TO_JSVALUE(", ")"))), - /* Int64T */ r(b"int64_t", "(int64_t)", Some("JSVALUE_TO_INT64("), Some(("INT64_TO_JSVALUE_SLOW(JS_GLOBAL_OBJECT, ", ")"))), - /* Uint64T */ r(b"uint64_t", "(uint64_t)", Some("JSVALUE_TO_UINT64("), Some(("UINT64_TO_JSVALUE_SLOW(JS_GLOBAL_OBJECT, ", ")"))), - /* Double */ r(b"double", "(double)", Some("JSVALUE_TO_DOUBLE("), Some(("DOUBLE_TO_JSVALUE(", ")"))), - /* Float */ r(b"float", "(float)", Some("JSVALUE_TO_FLOAT("), Some(("FLOAT_TO_JSVALUE(", ")"))), - /* Bool */ r(b"bool", "(bool)", Some("JSVALUE_TO_BOOL("), Some(("BOOLEAN_TO_JSVALUE(", ")"))), - /* Ptr */ r(b"void*", "(void*)", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), - /* Void */ r(b"void", "", None, None), - /* CString */ r(b"void*", "(void*)", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), - /* I64Fast */ r(b"int64_t", "(int64_t)", Some("JSVALUE_TO_INT64("), Some(("INT64_TO_JSVALUE(JS_GLOBAL_OBJECT, (int64_t)", ")"))), - /* U64Fast */ r(b"uint64_t", "(uint64_t)", Some("JSVALUE_TO_UINT64("), Some(("UINT64_TO_JSVALUE(JS_GLOBAL_OBJECT, ", ")"))), - /* Function */ r(b"void*", "(void*)", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), - /* NapiEnv */ r(b"napi_env", "", None, None), - /* NapiValue */ r(b"napi_value", "", None, Some(("((EncodedJSValue) {.asNapiValue = ", " } )"))), - /* Buffer */ r(b"void*", "", Some("JSVALUE_TO_TYPED_ARRAY_VECTOR("), None), + /* Char */ r(b"char", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Int8T */ r(b"int8_t", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Uint8T */ r(b"uint8_t", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Int16T */ r(b"int16_t", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Uint16T */ r(b"uint16_t", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Int32T */ r(b"int32_t", Some("JSVALUE_TO_INT32("), Some(("INT32_TO_JSVALUE((int32_t)", ")"))), + /* Uint32T */ r(b"uint32_t", Some("JSVALUE_TO_INT32("), Some(("UINT32_TO_JSVALUE(", ")"))), + /* Int64T */ r(b"int64_t", Some("JSVALUE_TO_INT64("), Some(("INT64_TO_JSVALUE_SLOW(JS_GLOBAL_OBJECT, ", ")"))), + /* Uint64T */ r(b"uint64_t", Some("JSVALUE_TO_UINT64("), Some(("UINT64_TO_JSVALUE_SLOW(JS_GLOBAL_OBJECT, ", ")"))), + /* Double */ r(b"double", Some("JSVALUE_TO_DOUBLE("), Some(("DOUBLE_TO_JSVALUE(", ")"))), + /* Float */ r(b"float", Some("JSVALUE_TO_FLOAT("), Some(("FLOAT_TO_JSVALUE(", ")"))), + /* Bool */ r(b"bool", Some("JSVALUE_TO_BOOL("), Some(("BOOLEAN_TO_JSVALUE(", ")"))), + /* Ptr */ r(b"void*", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), + /* Void */ r(b"void", None, None), + /* CString */ r(b"void*", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), + /* I64Fast */ r(b"int64_t", Some("JSVALUE_TO_INT64("), Some(("INT64_TO_JSVALUE(JS_GLOBAL_OBJECT, (int64_t)", ")"))), + /* U64Fast */ r(b"uint64_t", Some("JSVALUE_TO_UINT64("), Some(("UINT64_TO_JSVALUE(JS_GLOBAL_OBJECT, ", ")"))), + /* Function */ r(b"void*", Some("JSVALUE_TO_PTR("), Some(("PTR_TO_JSVALUE(", ")"))), + /* NapiEnv */ r(b"napi_env", None, None), + /* NapiValue */ r(b"napi_value", None, Some(("((EncodedJSValue) {.asNapiValue = ", " } )"))), + /* Buffer */ r(b"void*", Some("JSVALUE_TO_TYPED_ARRAY_VECTOR("), None), + /* BufferLen */ r(b"uint64_t", None, None), ] }; @@ -167,9 +161,7 @@ impl ABIType { /// See [`ABI_TYPE_LABEL`]. pub const LABEL: &'static __ComptimeStringMap_ABI_TYPE_LABEL = &ABI_TYPE_LABEL; - /// Returns `None` for - /// out-of-range discriminants. The enum is `#[repr(i32)]` with contiguous - /// values `0..=MAX` plus `Buffer = 20`, so range-check then match. + /// Returns `None` for out-of-range discriminants. #[inline] pub const fn from_int(n: i32) -> Option { Some(match n { @@ -194,6 +186,7 @@ impl ABIType { 18 => Self::NapiEnv, 19 => Self::NapiValue, 20 => Self::Buffer, + 21 => Self::BufferLength, _ => return None, }) } @@ -217,19 +210,7 @@ impl ABIType { } pub fn to_c(self, symbol: &[u8]) -> ToCFormatter<'_> { - ToCFormatter { - tag: self, - symbol, - exact: false, - } - } - - pub fn to_c_exact(self, symbol: &[u8]) -> ToCFormatter<'_> { - ToCFormatter { - tag: self, - symbol, - exact: true, - } + ToCFormatter { tag: self, symbol } } pub fn to_js(self, symbol: &[u8]) -> ToJSFormatter<'_> { @@ -254,7 +235,6 @@ impl ABIType { pub struct ToCFormatter<'a> { pub symbol: &'a [u8], pub tag: ABIType, - pub exact: bool, } impl fmt::Display for ToCFormatter<'_> { @@ -268,9 +248,6 @@ impl fmt::Display for ToCFormatter<'_> { _ => unreachable!(), }; }; - if self.exact && !row.to_c_cast.is_empty() { - writer.write_str(row.to_c_cast)?; - } writer.write_str(macro_)?; fmt::Display::fmt(BStr::new(self.symbol), writer)?; writer.write_str(")") diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 72dc9e03d333..ad9a1f5afe3d 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -34,29 +34,6 @@ fn dir_exists(path: &'static [u8]) -> bool { bun_sys::directory_exists_at(bun_sys::Fd::cwd(), &z).unwrap_or(false) } -/// `JSValue.createObject2` — local extern thunk; upstream `bun_jsc` hasn't -/// re-exported it yet. -#[inline] -fn create_object_2( - global: &JSGlobalObject, - key1: &ZigString, - key2: &ZigString, - value1: JSValue, - value2: JSValue, -) -> JSValue { - unsafe extern "C" { - fn JSC__JSValue__createObject2( - global: *const JSGlobalObject, - key1: *const ZigString, - key2: *const ZigString, - value1: JSValue, - value2: JSValue, - ) -> JSValue; - } - // SAFETY: all pointers borrowed for the call; C++ clones key strings. - unsafe { JSC__JSValue__createObject2(global, key1, key2, value1, value2) } -} - /// `bun.String.toJSArray` — local shim over `JSValue::create_array_from_iter`. fn strings_to_js_array(global: &JSGlobalObject, strs: &[bun_core::String]) -> JsResult { JSValue::create_array_from_iter(global, strs.iter(), |s| { @@ -125,6 +102,51 @@ unsafe extern "C" { add_ptr_property: bool, input_function_ptr: *mut c_void, ) -> JSValue; + + fn Bun__CreateJSCFFIFunction( + global: *const JSGlobalObject, + symbol_name: *const ZigString, + arg_types: *const u8, + arg_count: u32, + return_type: u8, + target: *mut c_void, + owner: JSValue, + ) -> JSValue; + + fn Bun__CreateJSCFFICallback( + global: *const JSGlobalObject, + callable: JSValue, + arg_types: *const u8, + arg_count: u32, + return_type: u8, + threadsafe: bool, + ) -> JSValue; +} + +fn create_jsc_ffi_function( + global: &JSGlobalObject, + symbol_name: &ZigString, + function: &Function, + target: *mut c_void, + owner: JSValue, +) -> JSValue { + let arg_types: Vec = function.arg_types.iter().map(|t| *t as u8).collect(); + // SAFETY: `global` is a live JSC handle and `arg_types` outlives the call. + unsafe { + Bun__CreateJSCFFIFunction( + global, + symbol_name, + if arg_types.is_empty() { + core::ptr::null() + } else { + arg_types.as_ptr() + }, + u32::try_from(arg_types.len()).expect("int cast"), + function.return_type as u8, + target, + owner, + ) + } } /// Raw extern fn pointers fed to the TCC-JIT'd C trampolines via `add_symbol`. @@ -210,17 +232,8 @@ impl Default for FFI { } impl FFI { + // Intentional leak when not close()d: dlclose on GC is unsound because .ptr addresses escape the collector's view. pub fn finalize(self: Box) { - // INTENTIONAL no-op when not closed. Compiled trampolines / dlopen'd - // symbols may still be reachable from JS after the wrapper is GC'd - // (e.g. `const { fn } = dlopen(...).symbols`); teardown is owned by - // `close()`. Dropping the Box would run `Function::drop` → - // `tcc_delete()`, freeing the executable pages those JSFunctions still - // jump into. - // - // When `close()` HAS run, the functions map is empty and the dylib / - // shared TCC state are already gone, so the Box only owns the (empty) - // hashmap's retained-capacity buffer. Drop it instead of leaking. if self.closed.get() { drop(self); } else { @@ -643,6 +656,12 @@ impl CompileC { } } + if let Some(node_dir) = CompilerRT::node_dir() { + if state.add_sys_include_path(node_dir).is_err() { + bun_output::scoped_log!(TCC, "TinyCC failed to add sysinclude path"); + } + } + #[cfg(target_os = "macos")] { let mut pathbuf = PathBuffer::uninit(); @@ -1014,6 +1033,12 @@ impl FFI { return Err(JsError::Thrown); } + for func in compile_c.symbols.map.values() { + if let Some(err) = func.reject_cc_unsupported_types_error(global_this) { + return Err(global_this.throw_value(err)); + } + } + if compile_c.symbols.map.len() == 0 { return Err(global_this.throw(format_args!("Expected at least one exported symbol"))); } @@ -1246,9 +1271,12 @@ impl FFI { Ok(js_object) } - pub fn close_callback(_global_this: &JSGlobalObject, ctx: JSValue) -> JSValue { - // SAFETY: ctx encodes a heap::alloc(*mut Function) created by `callback` - drop(unsafe { bun_core::heap::take(ctx.as_ptr_address() as *mut Function) }); + pub fn close_jsc_callback(_global_this: &JSGlobalObject, callback: JSValue) -> JSValue { + unsafe extern "C" { + fn Bun__JSCFFICallbackClose(callback: JSValue); + } + // SAFETY: thin FFI wrapper; the C++ side type-checks the cell (jsDynamicCast) before use. + unsafe { Bun__JSCFFICallbackClose(callback) }; JSValue::UNDEFINED } @@ -1257,11 +1285,6 @@ impl FFI { interface: JSValue, js_callback: JSValue, ) -> JsResult { - if !bun_core::Environment::ENABLE_TINYCC { - return Err(global_this.throw(format_args!( - "bun:ffi callback() is not available in this build (TinyCC is disabled)" - ))); - } jsc::mark_binding(); if !interface.is_object() { return Ok(global_this.to_invalid_arguments(format_args!("Expected object"))); @@ -1282,59 +1305,63 @@ impl FFI { return Ok(val); } + if let Some(err) = func.reject_napi_types_error(global_this) { + return Ok(err); + } + if let Some(err) = func.reject_cc_unsupported_types_error(global_this) { + return Ok(err); + } + // TODO: WeakRefHandle that automatically frees it? func.base_name = Some(ZBox::from_bytes(b"")); js_callback.ensure_still_alive(); - if func - .compile_callback(global_this, js_callback, func.threadsafe) - .is_err() - { - return Ok(ZigString::init(b"Out of memory").to_error_instance(global_this)); - } - match &func.step { - Step::Failed { msg, .. } => { - let message = ZigString::init(msg).to_error_instance(global_this); - Ok(message) - } - Step::Pending => Ok(ZigString::init( - b"Failed to compile, but not sure why. Please report this bug", + let arg_types: Vec = func.arg_types.iter().map(|t| *t as u8).collect(); + // SAFETY: `global_this` is a live JSC handle and `js_callback` is a live callable. + let cb = unsafe { + Bun__CreateJSCFFICallback( + global_this, + js_callback, + if arg_types.is_empty() { + core::ptr::null() + } else { + arg_types.as_ptr() + }, + u32::try_from(arg_types.len()).expect("int cast"), + func.return_type as u8, + func.threadsafe, ) - .to_error_instance(global_this)), - Step::Compiled(_) => { - let function_ = bun_core::heap::into_raw(Box::new(core::mem::take(func))); - // SAFETY: function_ is a valid heap::alloc pointer - let compiled_ptr = unsafe { (*function_).step.compiled_ptr() }; - Ok(create_object_2( - global_this, - &ZigString::static_(b"ptr"), - &ZigString::static_(b"ctx"), - JSValue::from_ptr_address(compiled_ptr as usize), - JSValue::from_ptr_address(function_ as usize), - )) - } + }; + if cb.is_empty() { + return Ok(if global_this.has_exception() { + global_this.take_error(JsError::Thrown) + } else { + ZigString::init(b"Failed to create FFI callback").to_error_instance(global_this) + }); } + Ok(cb) } #[bun_jsc::host_fn(method)] pub fn close(&self, _global_this: &JSGlobalObject, _: &CallFrame) -> JsResult { jsc::mark_binding(); + self.do_close(); + Ok(JSValue::UNDEFINED) + } + + fn do_close(&self) { if self.closed.get() { - return Ok(JSValue::UNDEFINED); + return; } self.closed.set(true); if let Some(dylib) = self.dylib.replace(None) { dylib.close(); } - if let Some(state) = self.shared_state.take() { // SAFETY: state is a valid TCC::State pointer; we have exclusive ownership unsafe { TCC::State::destroy(state.as_ptr()) }; } - self.functions.with_mut(|f| f.clear_retaining_capacity()); - - Ok(JSValue::UNDEFINED) } pub fn print_callback(global: &JSGlobalObject, object: JSValue) -> JSValue { @@ -1351,17 +1378,10 @@ impl FFI { return val; } - let mut arraylist: Vec = Vec::new(); - - function.base_name = Some(ZBox::from_bytes(b"my_callback_function")); - - if function - .print_callback_source_code(None, None, &mut arraylist) - .is_err() - { - return ZigString::init(b"Error while printing code").to_error_instance(global); - } - jsc::bun_string_jsc::create_utf8_for_js(global, &arraylist).unwrap_or(JSValue::ZERO) + let _ = function; + let text: &[u8] = + b"// bun:ffi callbacks are compiled by JavaScriptCore (no C source is generated)\n"; + jsc::bun_string_jsc::create_utf8_for_js(global, text).unwrap_or(JSValue::ZERO) } pub fn print( @@ -1393,6 +1413,11 @@ impl FFI { return Ok(val); } jsc::mark_binding(); + for function in symbols.values() { + if let Some(err) = function.reject_cc_unsupported_types_error(global) { + return Ok(err); + } + } let mut strs: Vec = Vec::with_capacity(symbols.len()); for function in symbols.values_mut() { let mut arraylist: Vec = Vec::new(); @@ -1422,12 +1447,6 @@ fn invalid_options_arg(global: &JSGlobalObject) -> JSValue { impl FFI { pub fn open(global: &JSGlobalObject, name_str: ZigString, object_value: JSValue) -> JSValue { - if !bun_core::Environment::ENABLE_TINYCC { - let _ = global.throw(format_args!( - "bun:ffi dlopen() is not available in this build (TinyCC is disabled)" - )); - return JSValue::ZERO; - } jsc::mark_binding(); let vm = jsc::VirtualMachineRef::get(); let name_slice = name_str.to_slice(); @@ -1532,7 +1551,10 @@ impl FFI { let obj = JSValue::create_empty_object(global, size); let _obj_guard = obj.protected(); - let napi_env = make_napi_env_if_needed(symbols.values(), global); + let lib_ptr: *mut FFI = bun_core::heap::into_raw(Box::new(FFI::default())); + // SAFETY: `lib_ptr` is the fresh allocation from into_raw above. + let js_object = unsafe { FFI::to_js_ptr(lib_ptr, global) }; + let _js_object_guard = js_object.protected(); for function in symbols.values_mut() { let function_name = ZBox::from_bytes(function.base_name.as_ref().unwrap().as_bytes()); @@ -1546,58 +1568,48 @@ impl FFI { BStr::new(function_name.as_bytes()), BStr::new(name) )); - // symbols freed by Drop dylib.close(); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); return ret; }; function.symbol_from_dynamic_library = Some(resolved_symbol); } - if let Err(err) = function.compile(napi_env) { - let ret = global.to_invalid_arguments(format_args!( - "{} when compiling symbol \"{}\" in \"{}\"", - err.name(), - BStr::new(function_name.as_bytes()), - BStr::new(name) - )); + if let Some(err) = function.reject_napi_types_error(global) { dylib.close(); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); + return err; + } + let target = function + .symbol_from_dynamic_library + .expect("symbol was resolved above"); + let str = ZigString::init(function_name.as_bytes()); + let cb = create_jsc_ffi_function(global, &str, function, target, js_object); + if cb.is_empty() { + let ret = if global.has_exception() { + global.take_error(JsError::Thrown) + } else { + global.to_invalid_arguments(format_args!( + "Failed to create FFI function for symbol \"{}\" in \"{}\"", + BStr::new(function_name.as_bytes()), + BStr::new(name) + )) + }; + dylib.close(); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); return ret; } - match &function.step { - Step::Failed { msg, .. } => { - let res = ZigString::init(msg).to_error_instance(global); - dylib.close(); - return res; - } - Step::Pending => { - dylib.close(); - return ZigString::init(b"Failed to compile (nothing happend!)") - .to_error_instance(global); - } - Step::Compiled(compiled) => { - let str = ZigString::init(function_name.as_bytes()); - let cb = new_runtime_function( - global, - &str, - u32::try_from(function.arg_types.len()).expect("int cast"), - compiled.ptr.cast_const(), - true, - function.symbol_from_dynamic_library, - ); - // `cb` is rooted by the `symbolsValue` cached own-property set below. - obj.put(global, str.slice(), cb); - } - } + obj.put(global, str.slice(), cb); } - let lib = Box::new(FFI { - dylib: JsCell::new(Some(dylib)), - functions: JsCell::new(symbols), - ..Default::default() - }); - - let js_object = lib.to_js(global); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + let lib_ref = unsafe { &*lib_ptr }; + lib_ref.functions.set(symbols); + lib_ref.dylib.set(Some(dylib)); symbols_value_set_cached(js_object, global, obj); js_object } @@ -1609,12 +1621,6 @@ impl FFI { } pub fn link_symbols(global: &JSGlobalObject, object_value: JSValue) -> JSValue { - if !bun_core::Environment::ENABLE_TINYCC { - let _ = global.throw(format_args!( - "bun:ffi linkSymbols() is not available in this build (TinyCC is disabled)" - )); - return JSValue::ZERO; - } jsc::mark_binding(); if object_value.is_empty_or_undefined_or_null() { @@ -1640,7 +1646,10 @@ impl FFI { obj.ensure_still_alive(); let _keep = jsc::EnsureStillAlive(obj); - let napi_env = make_napi_env_if_needed(symbols.values(), global); + let lib_ptr: *mut FFI = bun_core::heap::into_raw(Box::new(FFI::default())); + // SAFETY: `lib_ptr` is the fresh allocation from into_raw above. + let js_object = unsafe { FFI::to_js_ptr(lib_ptr, global) }; + let _js_object_guard = js_object.protected(); for function in symbols.values_mut() { let function_name = ZBox::from_bytes(function.base_name.as_ref().unwrap().as_bytes()); @@ -1650,53 +1659,80 @@ impl FFI { "Symbol \"{}\" is missing a \"ptr\" field. When using linkSymbols() or CFunction(), you must provide a \"ptr\" field with the memory address of the native function.", BStr::new(function_name.as_bytes()) )); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); return ret; } - if let Err(err) = function.compile(napi_env) { - let ret = global.to_invalid_arguments(format_args!( - "{} when compiling symbol \"{}\"", - err.name(), - BStr::new(function_name.as_bytes()) - )); - return ret; + if let Some(err) = function.reject_napi_types_error(global) { + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); + return err; } - match &function.step { - Step::Failed { msg, .. } => { - let res = ZigString::init(msg).to_error_instance(global); - return res; - } - Step::Pending => { - return ZigString::static_(b"Failed to compile (nothing happend!)") - .to_error_instance(global); - } - Step::Compiled(compiled) => { - let name = ZigString::init(function_name.as_bytes()); - - let cb = new_runtime_function( - global, - &name, - u32::try_from(function.arg_types.len()).expect("int cast"), - compiled.ptr.cast_const(), - true, - function.symbol_from_dynamic_library, - ); - // `cb` is rooted by the `symbolsValue` cached own-property set below. - obj.put(global, name.slice(), cb); - } + let target = function.symbol_from_dynamic_library.expect("checked above"); + let name = ZigString::init(function_name.as_bytes()); + let cb = create_jsc_ffi_function(global, &name, function, target, js_object); + if cb.is_empty() { + let err = if global.has_exception() { + global.take_error(JsError::Thrown) + } else { + global.to_invalid_arguments(format_args!( + "Failed to create FFI function for symbol \"{}\"", + BStr::new(function_name.as_bytes()) + )) + }; + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.do_close(); + return err; } + obj.put(global, name.slice(), cb); } - let lib = Box::new(FFI { - dylib: JsCell::new(None), - functions: JsCell::new(symbols), - ..Default::default() - }); - - let js_object = lib.to_js(global); + // SAFETY: lib_ptr is the live, JS-owned FFI allocation (from into_raw). + unsafe { &*lib_ptr }.functions.set(symbols); symbols_value_set_cached(js_object, global, obj); js_object } + + pub fn create_cfunction( + global: &JSGlobalObject, + options: JSValue, + name_value: Option, + ) -> JsResult { + jsc::mark_binding(); + + if options.is_empty_or_undefined_or_null() || !options.is_object() { + return Ok(global + .to_invalid_arguments(format_args!("Expected an options object with a \"ptr\""))); + } + + let mut function = Function::default(); + if let Some(err) = generate_symbol_for_function(global, options, &mut function)? { + return Ok(err); + } + let Some(target) = function.symbol_from_dynamic_library else { + return Ok(global.to_invalid_arguments(format_args!( + "Symbol \"CFunction\" is missing a \"ptr\" field. When using linkSymbols() or CFunction(), you must provide a \"ptr\" field with the memory address of the native function." + ))); + }; + + let name = match name_value { + Some(value) if value.is_string() => value.get_zig_string(global)?, + _ => ZigString::static_(b"CFunction"), + }; + if let Some(err) = function.reject_napi_types_error(global) { + return Ok(err); + } + let cb = create_jsc_ffi_function(global, &name, &function, target, JSValue::UNDEFINED); + if cb.is_empty() { + return Ok(if global.has_exception() { + global.take_error(JsError::Thrown) + } else { + global.to_invalid_arguments(format_args!("Failed to create FFI function")) + }); + } + Ok(cb) + } } pub(super) fn generate_symbol_for_function( @@ -1729,7 +1765,6 @@ pub(super) fn generate_symbol_for_function( if val.is_any_int() { let int = val.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { abi_types.push(t); continue; @@ -1770,7 +1805,6 @@ pub(super) fn generate_symbol_for_function( if let Some(ret_value) = value.get_truthy(global, "returns")? { if ret_value.is_any_int() { let int = ret_value.to_int32(); - // Reject Buffer (20); only the string-label path accepts it. if let Some(t) = ABIType::from_int(int).filter(|_| int <= ABIType::MAX) { return_type = t; break 'brk; @@ -1796,7 +1830,7 @@ pub(super) fn generate_symbol_for_function( if return_type == ABIType::NapiEnv { return Ok(Some( - ZigString::static_(b"Cannot return napi_env to JavaScript").to_error_instance(global), + ZigString::static_(b"Cannot return napi_env to JavaScript: a napi_env is an in-parameter for cc()-compiled C, never a return value").to_error_instance(global), )); } @@ -1809,9 +1843,12 @@ pub(super) fn generate_symbol_for_function( )); } - if function.threadsafe && return_type != ABIType::Void { + if return_type == ABIType::BufferLength { return Ok(Some( - ZigString::static_(b"Threadsafe functions must return void").to_error_instance(global), + ZigString::static_( + b"buffer_length is an argument-only type; it cannot be a return type", + ) + .to_error_instance(global), )); } @@ -1828,10 +1865,13 @@ pub(super) fn generate_symbol_for_function( function.symbol_from_dynamic_library = Some(num as *mut c_void); } } else if ptr.is_heap_big_int() { - let num = ptr.to_uint64_no_truncate(); - if num > 0 { - function.symbol_from_dynamic_library = Some(num as *mut c_void); + if !ptr.is_big_int_in_uint64_range(1, usize::MAX as u64) { + return Ok(Some( + global.to_invalid_arguments(format_args!("ptr is out of range.")), + )); } + function.symbol_from_dynamic_library = + Some(ptr.to_uint64_no_truncate() as usize as *mut c_void); } } @@ -1908,10 +1948,6 @@ impl Default for Function { } } -unsafe extern "C" { - fn FFICallbackFunctionWrapper_destroy(_: *mut c_void); -} - impl Drop for Function { fn drop(&mut self) { // base_name, arg_types, Step::Failed.msg are owned and freed by drop glue. @@ -1919,12 +1955,6 @@ impl Drop for Function { // SAFETY: state is a valid TCC::State pointer; we own it unsafe { TCC::State::destroy(state.as_ptr()) }; } - if let Step::Compiled(compiled) = &mut self.step { - if let Some(wrapper) = compiled.ffi_callback_function_wrapper.take() { - // SAFETY: wrapper was created by Bun__createFFICallbackFunction - unsafe { FFICallbackFunctionWrapper_destroy(wrapper.as_ptr()) }; - } - } } } @@ -2072,144 +2102,6 @@ impl Function { Ok(()) } - pub(crate) fn compile_callback( - &mut self, - js_context: &JSGlobalObject, - js_function: JSValue, - is_threadsafe: bool, - ) -> crate::Result<()> { - jsc::mark_binding(); - let mut source_code: Vec = Vec::new(); - // SAFETY: js_context/js_function are live for the call - let ffi_wrapper = unsafe { Bun__createFFICallbackFunction(js_context, js_function) }; - self.print_callback_source_code(Some(js_context), Some(ffi_wrapper), &mut source_code)?; - - #[cfg(all(debug_assertions, unix))] - 'debug_write: { - // SAFETY: best-effort debug write; failures are swallowed - unsafe { - let fd = libc::open( - c"/tmp/bun-ffi-callback-source.c".as_ptr(), - libc::O_CREAT | libc::O_WRONLY, - 0o644, - ); - if fd < 0 { - break 'debug_write; - } - let _ = libc::write(fd, source_code.as_ptr().cast::(), source_code.len()); - let _ = libc::ftruncate(fd, source_code.len() as libc::off_t); - libc::close(fd); - } - } - - source_code.push(0); - // defer source_code.deinit(); - - let tcc_options: &'static ZStr = if cfg!(debug_assertions) { - zstr!("-std=c11 -nostdlib -Wl,--export-all-symbols -g") - } else { - zstr!("-std=c11 -nostdlib -Wl,--export-all-symbols") - }; - let state = match TCC::State::init::(&TCC::Config { - options: Some(NonNull::from(tcc_options)), - output_type: TCC::OutputFormat::Memory, - err: TCC::ConfigErr { - ctx: Some(std::ptr::from_mut::(self)), - handler: Self::handle_tcc_error, - }, - }) { - Ok(s) => s, - Err(TCC::Error::Alloc(bun_alloc::AllocError)) => { - return Err(crate::Error::TCCMissing); - } - // 1. .Memory is always a valid option, so InvalidOptions is - // impossible - // 2. other throwable functions arent called, so their errors - // aren't possible - Err(_) => unreachable!(), - }; - self.state = Some(state); - let _guard = scopeguard::guard(std::ptr::from_mut::(self), |this_ptr| { - // SAFETY: this_ptr is &mut self for the duration of compile_callback() - let this = unsafe { &mut *this_ptr }; - if matches!(this.step, Step::Failed { .. }) { - if let Some(s) = this.state.take() { - // SAFETY: we own the state - unsafe { TCC::State::destroy(s.as_ptr()) }; - } - } - }); - // SAFETY: just stored above - let state = unsafe { self.state.unwrap().as_mut() }; - - if self.needs_napi_env() { - if state - .add_symbol( - zstr!("Bun__thisFFIModuleNapiEnv"), - js_context.make_napi_env_for_ffi().cast_const(), - ) - .is_err() - { - self.fail(b"Failed to add NAPI env symbol"); - return Ok(()); - } - } - - CompilerRT::define(state); - - // SAFETY: source_code was NUL-terminated above - if state - .compile_string(ZStr::from_slice_with_nul(&source_code[..])) - .is_err() - { - self.fail(b"Failed to compile source code"); - return Ok(()); - } - - CompilerRT::inject(state); - let callback_sym: *const c_void = if is_threadsafe { - FFI_Callback_threadsafe_call as *const c_void - } else { - // TODO: stage2 - make these ptrs - match self.arg_types.len() { - 0 => FFI_Callback_call_0 as *const c_void, - 1 => FFI_Callback_call_1 as *const c_void, - 2 => FFI_Callback_call_2 as *const c_void, - 3 => FFI_Callback_call_3 as *const c_void, - 4 => FFI_Callback_call_4 as *const c_void, - 5 => FFI_Callback_call_5 as *const c_void, - 6 => FFI_Callback_call_6 as *const c_void, - 7 => FFI_Callback_call_7 as *const c_void, - _ => FFI_Callback_call as *const c_void, - } - }; - // `callback_sym` is one of the process-lifetime `FFI_Callback_call*` - // extern fns. - if state - .add_symbol(zstr!("FFI_Callback_call"), callback_sym) - .is_err() - { - self.fail(b"Failed to add FFI callback symbol"); - return Ok(()); - } - // TinyCC now manages relocation memory internally - if dangerously_run_without_jit_protections(|| state.relocate()).is_err() { - self.fail(b"tcc_relocate returned a negative value"); - return Ok(()); - } - - let Some(symbol) = state.get_symbol(zstr!("my_callback_function")) else { - self.fail(b"missing generated symbol in source code"); - return Ok(()); - }; - - self.step = Step::Compiled(Compiled { - ptr: symbol.as_ptr().cast::(), - ffi_callback_function_wrapper: NonNull::new(ffi_wrapper), - }); - Ok(()) - } - pub(crate) fn print_source_code(&self, writer: &mut impl std::io::Write) -> crate::Result<()> { if !self.arg_types.is_empty() { writer.write_all(b"#define HAS_ARGUMENTS\n")?; @@ -2362,126 +2254,27 @@ impl Function { Ok(()) } - pub(crate) fn print_callback_source_code( + pub(crate) fn reject_cc_unsupported_types_error( &self, - global_object: Option<&JSGlobalObject>, - context_ptr: Option<*mut c_void>, - writer: &mut impl std::io::Write, - ) -> crate::Result<()> { - { - let ptr = global_object - .map(|g| std::ptr::from_ref(g) as usize) - .unwrap_or(0); - let fmt = bun_fmt::hex_int_upper::<16>(ptr as u64); - writeln!(writer, "#define JS_GLOBAL_OBJECT (void*)0x{}ULL", fmt)?; - } - - writer.write_all(b"#define IS_CALLBACK 1\n")?; - - 'brk: { - if self.return_type.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break 'brk; - } - - for arg in self.arg_types.iter() { - // conditionally include math.h - if arg.is_floating_point() { - writer.write_all(b"#define USES_FLOAT 1\n")?; - break; - } - } - } - - writer.write_all(Self::ffi_header())?; - - // -- Generate the FFI function symbol - writer.write_all(b"\n \n/* --- The Callback Function */\n")?; - let mut first = true; - self.return_type.typename(writer)?; - - writer.write_all(b" my_callback_function")?; - writer.write_all(b"(")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if !first { - writer.write_all(b", ")?; - } - first = false; - arg.typename(writer)?; - write!(writer, " arg{}", i)?; - } - writer.write_all(b") {\n")?; - - if cfg!(debug_assertions) { - writer.write_all(b"#ifdef INJECT_BEFORE\n")?; - writer.write_all(b"INJECT_BEFORE;\n")?; - writer.write_all(b"#endif\n")?; - } - - first = true; - let _ = first; - - if !self.arg_types.is_empty() { - let mut arg_buf = [0u8; 512]; - writeln!( - writer, - " ZIG_REPR_TYPE arguments[{}];", - self.arg_types.len() - )?; - - arg_buf[0..3].copy_from_slice(b"arg"); - for (i, arg) in self.arg_types.iter().enumerate() { - let printed = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + printed]; - writeln!( - writer, - "arguments[{}] = {}.asZigRepr;", - i, - arg.to_js(arg_name) - )?; - } - } - - writer.write_all(b" ")?; - let mut inner_buf_ = [0u8; 372]; - let inner_buf: &[u8]; - + global: &JSGlobalObject, + ) -> Option { + if self.arg_types.contains(&ABIType::BufferLength) + || self.return_type == ABIType::BufferLength { - let ptr = context_ptr.map(|p| p as usize).unwrap_or(0); - let fmt = bun_fmt::hex_int_upper::<16>(ptr as u64); - - let written = if !self.arg_types.is_empty() { - let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]); - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{}ULL, {}, arguments)", - fmt, - self.arg_types.len() - )?; - cursor.position() as usize - } else { - let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]); - write!( - &mut cursor, - "FFI_Callback_call((void*)0x{}ULL, 0, (ZIG_REPR_TYPE*)0)", - fmt - )?; - cursor.position() as usize - }; - inner_buf = &inner_buf_[1..1 + written]; + return Some(global.to_invalid_arguments(format_args!( + "buffer_length is only supported for bun:ffi dlopen/linkSymbols/CFunction arguments (the engine reads the view's byteLength at call time), not in cc(), viewSource, or JSCallback" + ))); } + None + } - if self.return_type == ABIType::Void { - writer.write_all(inner_buf)?; - } else { - let len = inner_buf.len() + 1; - let inner_buf = &mut inner_buf_[0..len]; - inner_buf[0] = b'_'; - write!(writer, "return {}", self.return_type.to_c_exact(inner_buf))?; + pub(crate) fn reject_napi_types_error(&self, global: &JSGlobalObject) -> Option { + if self.needs_napi_env() || self.return_type == ABIType::NapiValue { + return Some(global.to_invalid_arguments(format_args!( + "napi_env / napi_value are only supported in bun:ffi cc() (compiled C source), not in dlopen/linkSymbols/CFunction/JSCallback" + ))); } - - writer.write_all(b";\n}\n\n")?; - Ok(()) + None } fn needs_napi_env(&self) -> bool { @@ -2494,20 +2287,6 @@ impl Function { } } -unsafe extern "C" { - fn FFI_Callback_call(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_0(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_1(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_2(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_3(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_4(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_5(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_threadsafe_call(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_6(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn FFI_Callback_call_7(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue; - fn Bun__createFFICallbackFunction(_: &JSGlobalObject, _: JSValue) -> *mut c_void; -} - // ─── Step ─────────────────────────────────────────────────────────────────── pub enum Step { @@ -2516,28 +2295,14 @@ pub enum Step { Failed { msg: Box<[u8]> }, } -/// Stores no JS function value: symbol functions are rooted by the -/// `symbolsValue` cached own-property on the FFI wrapper, callbacks by the -/// `JSC::Strong` inside `FFICallbackFunctionWrapper`. pub struct Compiled { pub ptr: *mut c_void, - pub ffi_callback_function_wrapper: Option>, } impl Default for Compiled { fn default() -> Self { Self { ptr: core::ptr::null_mut(), - ffi_callback_function_wrapper: None, - } - } -} - -impl Step { - fn compiled_ptr(&self) -> *mut c_void { - match self { - Step::Compiled(c) => c.ptr, - _ => core::ptr::null_mut(), } } } @@ -2552,6 +2317,7 @@ struct CompilerRT; // Process-lifetime singleton — PORTING.md §Forbidden: use OnceLock, never // `static mut` + leak. static COMPILER_RT_DIR: OnceLock = OnceLock::new(); +static COMPILER_RT_NODE_DIR: OnceLock = OnceLock::new(); struct CompilerRtSources; impl CompilerRtSources { @@ -2564,6 +2330,19 @@ impl CompilerRtSources { ("stddef.h", include_bytes!("./ffi-stddef.h")), ("varargs.h", b"// empty"), ]; + + const NODE_HEADERS: &'static [(&'static str, &'static [u8])] = &[ + ("node_api.h", include_bytes!("../napi/node_api.h")), + ( + "node_api_types.h", + include_bytes!("../napi/node_api_types.h"), + ), + ("js_native_api.h", include_bytes!("../napi/js_native_api.h")), + ( + "js_native_api_types.h", + include_bytes!("../napi/js_native_api_types.h"), + ), + ]; } static CREATE_COMPILER_RT_DIR_ONCE: Once = Once::new(); @@ -2594,6 +2373,18 @@ impl CompilerRT { }; // `ZBox::from_bytes` panics on OOM. let _ = COMPILER_RT_DIR.set(ZBox::from_bytes(&*path)); + + let Ok(node_dir) = bun_cc.make_open_path(b"node", bun_sys::OpenDirOptions::default()) + else { + return; + }; + for (name, source) in CompilerRtSources::NODE_HEADERS { + let name_z = ZBox::from_bytes(name.as_bytes()); + let _ = bun_sys::File::write_file(node_dir.fd(), name_z.as_zstr(), source); + } + if let Ok(node_path) = bun_sys::get_fd_path(node_dir.fd(), &mut path_buf) { + let _ = COMPILER_RT_NODE_DIR.set(ZBox::from_bytes(&*node_path)); + } } pub(crate) fn dir() -> Option<&'static ZStr> { @@ -2604,6 +2395,14 @@ impl CompilerRT { .filter(|d| !d.is_empty()) } + pub(crate) fn node_dir() -> Option<&'static ZStr> { + CREATE_COMPILER_RT_DIR_ONCE.call_once(Self::create_compiler_rt_dir); + COMPILER_RT_NODE_DIR + .get() + .map(|b| b.as_zstr()) + .filter(|d| !d.is_empty()) + } + #[inline(never)] extern "C" fn memset(dest: *mut u8, c: u8, byte_count: usize) { // SAFETY: caller (TCC-compiled code) guarantees dest[0..byte_count] is writable diff --git a/test/harness.ts b/test/harness.ts index 634f34e2e22e..7dc068db6eea 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -2189,3 +2189,31 @@ export function getPuppeteerInstallEnv(): Record { // env to whatever later launches puppeteer so it finds the browser. return { PUPPETEER_CACHE_DIR: tmpdirSync("puppeteer-cache") }; } + +const compiledFixtures = new Map(); +export function compileFixture(sourcePath: string, options: { flags?: string[] } = {}): string { + const cacheKey = sourcePath + "\0" + (options.flags ?? []).join("\0"); + const cached = compiledFixtures.get(cacheKey); + if (cached) return cached; + + const outDir = tmpdirSync("ffi-fixture-"); + const base = basename(sourcePath).replace(/\.c$/, ""); + const libExt = isWindows ? "dll" : isMacOS ? "dylib" : "so"; + const flagsTag = options.flags?.length ? "-" + Bun.hash((options.flags ?? []).join(" ")).toString(36) : ""; + const outPath = join(outDir, `${base}${flagsTag}.${libExt}`); + + const cc = which("cc") || which("clang") || which("gcc"); + if (!cc) throw new Error("compileFixture: no C compiler (cc/clang/gcc) found in $PATH"); + + const cmd = isWindows + ? [cc, sourcePath, "-shared", "-o", outPath, ...(options.flags ?? [])] + : [cc, sourcePath, "-shared", "-fPIC", "-O2", "-o", outPath, ...(options.flags ?? [])]; + const { exitCode, stderr } = spawnSync({ cmd, cwd: outDir, stdout: "inherit", stderr: "pipe", env: bunEnv }); + if (exitCode !== 0) { + throw new Error( + `compileFixture: \`${cmd.join(" ")}\` failed (exit ${exitCode}):\n${stderr?.toString?.() ?? stderr}`, + ); + } + compiledFixtures.set(cacheKey, outPath); + return outPath; +} diff --git a/test/integration/bun-types/fixture/ffi.ts b/test/integration/bun-types/fixture/ffi.ts index 9f18e37c70f2..9c05748ef604 100644 --- a/test/integration/bun-types/fixture/ffi.ts +++ b/test/integration/bun-types/fixture/ffi.ts @@ -64,12 +64,12 @@ const lib = dlopen( declare const ptr: Pointer; -tsd.expectType(lib.symbols.sqlite3_libversion()); +tsd.expectType(lib.symbols.sqlite3_libversion()); tsd.expectType(lib.symbols.add(1, 2)); -tsd.expectType(lib.symbols.ptr_type(ptr)); +tsd.expectType(lib.symbols.ptr_type(ptr)); -tsd.expectType(lib.symbols.fn_type(new JSCallback(() => {}, {}))); +tsd.expectType(lib.symbols.fn_type(new JSCallback(() => {}, {}))); function _arg( ...params: [ @@ -161,7 +161,7 @@ const as_const_test = { const lib2 = dlopen(path, as_const_test); -tsd.expectType(lib2.symbols.sqlite3_libversion()); +tsd.expectType(lib2.symbols.sqlite3_libversion()); // tslint:disable-next-line:no-void-expression // eslint-disable-next-line @typescript-eslint/no-invalid-void-type tsd.expectType(lib2.symbols.multi_args(1, 2)); diff --git a/test/js/bun/ffi/cc.test.ts b/test/js/bun/ffi/cc.test.ts index a69589f90e21..803a89c936fe 100644 --- a/test/js/bun/ffi/cc.test.ts +++ b/test/js/bun/ffi/cc.test.ts @@ -917,7 +917,7 @@ describe("double <-> JSValue conversions", () => { huge_bigint: ["number", "Infinity"], negative_huge_bigint: ["number", "-Infinity"], fractional: ["number", "-2.5"], - string: ["number", "2.5"], + string: ["threw", "TypeError"], null_arg: ["number", "0"], undefined_arg: ["number", "NaN"], }, diff --git a/test/js/bun/ffi/ffi-abi-fixture.c b/test/js/bun/ffi/ffi-abi-fixture.c new file mode 100644 index 000000000000..a7ff1383ce5c --- /dev/null +++ b/test/js/bun/ffi/ffi-abi-fixture.c @@ -0,0 +1,64 @@ +#include +#include +#ifdef _WIN32 +#define FFI_EXPORT __declspec(dllexport) +#else +#define FFI_EXPORT __attribute__((visibility("default"))) +#endif + +FFI_EXPORT int8_t abi_i8(int8_t x) { return x; } +FFI_EXPORT uint8_t abi_u8(uint8_t x) { return x; } +FFI_EXPORT int16_t abi_i16(int16_t x) { return x; } +FFI_EXPORT uint16_t abi_u16(uint16_t x){ return x; } +FFI_EXPORT int32_t abi_i32(int32_t x) { return x; } +FFI_EXPORT uint32_t abi_u32(uint32_t x){ return x; } +FFI_EXPORT int64_t abi_i64(int64_t x) { return x; } +FFI_EXPORT uint64_t abi_u64(uint64_t x){ return x; } +FFI_EXPORT float abi_f32(float x) { return x; } +FFI_EXPORT double abi_f64(double x) { return x; } +FFI_EXPORT bool abi_bool(bool x) { return !x; } +FFI_EXPORT char abi_char(char x) { return x; } + +FFI_EXPORT int64_t abi_sum_i32_x10(int32_t a0,int32_t a1,int32_t a2,int32_t a3,int32_t a4,int32_t a5,int32_t a6,int32_t a7,int32_t a8,int32_t a9) { + return (int64_t)a0*1+(int64_t)a1*2+(int64_t)a2*3+(int64_t)a3*4+(int64_t)a4*5+(int64_t)a5*6+(int64_t)a6*7+(int64_t)a7*8+(int64_t)a8*9+(int64_t)a9*10; +} +FFI_EXPORT int64_t abi_sum_i64_x10(int64_t a0,int64_t a1,int64_t a2,int64_t a3,int64_t a4,int64_t a5,int64_t a6,int64_t a7,int64_t a8,int64_t a9) { + return a0*1+a1*2+a2*3+a3*4+a4*5+a5*6+a6*7+a7*8+a8*9+a9*10; +} +FFI_EXPORT double abi_sum_f64_x10(double a0,double a1,double a2,double a3,double a4,double a5,double a6,double a7,double a8,double a9) { + return a0*1+a1*2+a2*3+a3*4+a4*5+a5*6+a6*7+a7*8+a8*9+a9*10; +} +FFI_EXPORT double abi_sum_f32_x10(float a0,float a1,float a2,float a3,float a4,float a5,float a6,float a7,float a8,float a9) { + return (double)a0*1+(double)a1*2+(double)a2*3+(double)a3*4+(double)a4*5+(double)a5*6+(double)a6*7+(double)a7*8+(double)a8*9+(double)a9*10; +} +FFI_EXPORT double abi_mix12(int32_t a0,double a1,int32_t a2,double a3,int32_t a4,double a5,int32_t a6,double a7,int32_t a8,double a9,int32_t a10,double a11) { + return (double)a0*1+a1*2+(double)a2*3+a3*4+(double)a4*5+a5*6+(double)a6*7+a7*8+(double)a8*9+a9*10+(double)a10*11+a11*12; +} +FFI_EXPORT int64_t abi_mix_i64f64(int64_t a0,double a1,int64_t a2,double a3,int64_t a4,double a5,int64_t a6,double a7,int64_t a8,double a9) { + return a0*1+(int64_t)(a1*2)+a2*3+(int64_t)(a3*4)+a4*5+(int64_t)(a5*6)+a6*7+(int64_t)(a7*8)+a8*9+(int64_t)(a9*10); +} +FFI_EXPORT int64_t abi_sum_u8_x12(uint8_t a0,uint8_t a1,uint8_t a2,uint8_t a3,uint8_t a4,uint8_t a5,uint8_t a6,uint8_t a7,uint8_t a8,uint8_t a9,uint8_t a10,uint8_t a11) { + return (int64_t)a0*1+(int64_t)a1*2+(int64_t)a2*3+(int64_t)a3*4+(int64_t)a4*5+(int64_t)a5*6+(int64_t)a6*7+(int64_t)a7*8+(int64_t)a8*9+(int64_t)a9*10+(int64_t)a10*11+(int64_t)a11*12; +} +FFI_EXPORT int64_t abi_sum_i8_x12(int8_t a0,int8_t a1,int8_t a2,int8_t a3,int8_t a4,int8_t a5,int8_t a6,int8_t a7,int8_t a8,int8_t a9,int8_t a10,int8_t a11) { + return (int64_t)a0*1+(int64_t)a1*2+(int64_t)a2*3+(int64_t)a3*4+(int64_t)a4*5+(int64_t)a5*6+(int64_t)a6*7+(int64_t)a7*8+(int64_t)a8*9+(int64_t)a9*10+(int64_t)a10*11+(int64_t)a11*12; +} +FFI_EXPORT int64_t abi_sum_i16_x12(int16_t a0,int16_t a1,int16_t a2,int16_t a3,int16_t a4,int16_t a5,int16_t a6,int16_t a7,int16_t a8,int16_t a9,int16_t a10,int16_t a11) { + return (int64_t)a0*1+(int64_t)a1*2+(int64_t)a2*3+(int64_t)a3*4+(int64_t)a4*5+(int64_t)a5*6+(int64_t)a6*7+(int64_t)a7*8+(int64_t)a8*9+(int64_t)a9*10+(int64_t)a10*11+(int64_t)a11*12; +} +FFI_EXPORT int32_t abi_bools_x10(bool a0,bool a1,bool a2,bool a3,bool a4,bool a5,bool a6,bool a7,bool a8,bool a9) { + return (a0?1:0)*1+(a1?1:0)*2+(a2?1:0)*4+(a3?1:0)*8+(a4?1:0)*16+(a5?1:0)*32+(a6?1:0)*64+(a7?1:0)*128+(a8?1:0)*256+(a9?1:0)*512; +} + +FFI_EXPORT int64_t abi_cb_i32_x10(int64_t (*cb)(int32_t,int32_t,int32_t,int32_t,int32_t,int32_t,int32_t,int32_t,int32_t,int32_t), int32_t k) { + return cb(k, k+1, k+2, k+3, k+4, k+5, k+6, k+7, k+8, k+9); +} +FFI_EXPORT double abi_cb_f64_x10(double (*cb)(double,double,double,double,double,double,double,double,double,double), double k) { + return cb(k, k+0.5, k+1, k+1.5, k+2, k+2.5, k+3, k+3.5, k+4, k+4.5); +} +FFI_EXPORT double abi_cb_mix12(double (*cb)(int32_t,double,int32_t,double,int32_t,double,int32_t,double,int32_t,double,int32_t,double), int32_t i, double d) { + return cb(i, d, i+1, d+1, i+2, d+2, i+3, d+3, i+4, d+4, i+5, d+5); +} +FFI_EXPORT int64_t abi_cb_i64_x10(int64_t (*cb)(int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t), int64_t k) { + return cb(k, k+1, k+2, k+3, k+4, k+5, k+6, k+7, k+8, k+9); +} diff --git a/test/js/bun/ffi/ffi-test.c b/test/js/bun/ffi/ffi-test.c index a71021423d7c..18f9d88a51ee 100644 --- a/test/js/bun/ffi/ffi-test.c +++ b/test/js/bun/ffi/ffi-test.c @@ -85,6 +85,18 @@ double identity_double(double a) { return a; } int8_t identity_int8_t(int8_t a) { return a; } int16_t identity_int16_t(int16_t a) { return a; } int32_t identity_int32_t(int32_t a) { return a; } + +FFI_EXPORT const char *returns_cstring(void) { return "engine cstring"; } + +FFI_EXPORT const char *returns_null_cstring(void) { return NULL; } + +FFI_EXPORT const char *echoes_cstring(const char *s) { return s; } + +FFI_EXPORT uint64_t strlen_cstring(const char *s) { + uint64_t n = 0; + while (s && s[n]) n++; + return n; +} int64_t identity_int64_t(int64_t a) { return a; } uint8_t identity_uint8_t(uint8_t a) { return a; } uint16_t identity_uint16_t(uint16_t a) { return a; } @@ -106,12 +118,13 @@ uint32_t add_uint32_t(uint32_t a, uint32_t b) { return a + b; } uint64_t add_uint64_t(uint64_t a, uint64_t b) { return a + b; } FFI_EXPORT void *ptr_should_point_to_42_as_int32_t(); +FFI_EXPORT void *getNoopDeallocatorCallback(); -void *ptr_should_point_to_42_as_int32_t() { - int32_t *ptr = malloc(sizeof(int32_t)); - *ptr = 42; - return ptr; -} +static int32_t ffi_static_42 = 42; +void *ptr_should_point_to_42_as_int32_t() { return &ffi_static_42; } + +static void noop_deallocator(void *ptr, void *ctx) { (void)ptr; (void)ctx; } +void *getNoopDeallocatorCallback() { return &noop_deallocator; } static uint8_t buffer_with_deallocator[128]; static int deallocatorCalled; @@ -148,4 +161,7 @@ FFI_EXPORT uint32_t cb_identity_42_uint32_t(uint32_t (*cb)()) { return cb(); } FFI_EXPORT uint64_t cb_identity_42_uint64_t(uint64_t (*cb)()) { return cb(); } FFI_EXPORT int16_t cb_identity_neg_42_int16_t(int16_t (*cb)()) { return cb(); } FFI_EXPORT int32_t cb_identity_neg_42_int32_t(int32_t (*cb)()) { return cb(); } -FFI_EXPORT int64_t cb_identity_neg_42_int64_t(int64_t (*cb)()) { return cb(); } \ No newline at end of file +FFI_EXPORT int64_t cb_identity_neg_42_int64_t(int64_t (*cb)()) { return cb(); } + +FFI_EXPORT uint64_t bl_echo_len(const void *buf, uint64_t len) { (void)buf; return len; } +FFI_EXPORT uint32_t bl_last_byte(const uint8_t *buf, uint64_t len) { return len ? buf[len - 1] : 999u; } diff --git a/test/js/bun/ffi/ffi-viewSource-non-object.test.ts b/test/js/bun/ffi/ffi-viewSource-non-object.test.ts index 57697b1195dc..2921f9d343e2 100644 --- a/test/js/bun/ffi/ffi-viewSource-non-object.test.ts +++ b/test/js/bun/ffi/ffi-viewSource-non-object.test.ts @@ -47,7 +47,7 @@ describe("FFI viewSource", () => { const cbSrc = viewSource({ args: ["i32"], returns: "i32" }, true); expect(typeof cbSrc).toBe("string"); - expect(cbSrc).toContain("my_callback_function"); + expect(cbSrc).toContain("compiled by JavaScriptCore"); }); }); diff --git a/test/js/bun/ffi/ffi.test.fixture.callback.c b/test/js/bun/ffi/ffi.test.fixture.callback.c index 1a83b7dce62a..71620d55eb78 100644 --- a/test/js/bun/ffi/ffi.test.fixture.callback.c +++ b/test/js/bun/ffi/ffi.test.fixture.callback.c @@ -1,403 +1 @@ -#define JS_GLOBAL_OBJECT (void*)0x0000000000000000ULL -#define IS_CALLBACK 1 -// This file is part of Bun! -// You can find the original source: -// https://github.com/oven-sh/bun/blob/main/src/runtime/api/FFI.h -// -// clang-format off -// This file is only compatible with 64 bit CPUs -// It must be kept in sync with JSCJSValue.h -// https://github.com/oven-sh/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCJSValue.h -#ifdef IS_CALLBACK -#define INJECT_BEFORE int c = 500; // This is a callback, so we need to inject code before the call -#endif -#define IS_BIG_ENDIAN 0 -#define USE_JSVALUE64 1 -#define USE_JSVALUE32_64 0 - -#define ZIG_REPR_TYPE int64_t - -#ifdef _WIN32 -#define BUN_FFI_IMPORT __declspec(dllimport) -#else -#define BUN_FFI_IMPORT -#endif - -// /* 7.18.1.1 Exact-width integer types */ -typedef unsigned char uint8_t; -typedef signed char int8_t; -typedef short int16_t; -typedef unsigned short uint16_t; -typedef int int32_t; -typedef unsigned int uint32_t; -typedef long long int64_t; -typedef unsigned long long uint64_t; -typedef unsigned long long size_t; -typedef long intptr_t; -typedef uint64_t uintptr_t; -typedef _Bool bool; - -#define true 1 -#define false 0 - -#ifndef SRC_JS_NATIVE_API_TYPES_H_ -typedef struct NapiEnv *napi_env; -typedef int64_t napi_value; -typedef enum { - napi_ok, - napi_invalid_arg, - napi_object_expected, - napi_string_expected, - napi_name_expected, - napi_function_expected, - napi_number_expected, - napi_boolean_expected, - napi_array_expected, - napi_generic_failure, - napi_pending_exception, - napi_cancelled, - napi_escape_called_twice, - napi_handle_scope_mismatch, - napi_callback_scope_mismatch, - napi_queue_full, - napi_closing, - napi_bigint_expected, - napi_date_expected, - napi_arraybuffer_expected, - napi_detachable_arraybuffer_expected, - napi_would_deadlock // unused -} napi_status; -BUN_FFI_IMPORT void* NapiHandleScope__open(void* napi_env, bool detached); -BUN_FFI_IMPORT void NapiHandleScope__close(void* napi_env, void* handleScope); -BUN_FFI_IMPORT extern struct NapiEnv Bun__thisFFIModuleNapiEnv; -#endif - - -#ifdef INJECT_BEFORE -// #include -#endif -// #include - -// This value is 2^49, used to encode doubles such that the encoded value will -// begin with a 15-bit pattern within the range 0x0002..0xFFFC. -#define DoubleEncodeOffsetBit 49 -#define DoubleEncodeOffset (1ll << DoubleEncodeOffsetBit) -#define OtherTag 0x2ll -#define BoolTag 0x4ll -#define UndefinedTag 0x8ll -#define TagValueFalse (OtherTag | BoolTag | false) -#define TagValueTrue (OtherTag | BoolTag | true) -#define TagValueUndefined (OtherTag | UndefinedTag) -#define TagValueNull (OtherTag) -#define NotCellMask (int64_t)(NumberTag | OtherTag) - -#define MAX_INT32 2147483648 -#define MAX_INT52 9007199254740991 - -// If all bits in the mask are set, this indicates an integer number, -// if any but not all are set this value is a double precision number. -#define NumberTag 0xfffe000000000000ll - -// The canonical quiet NaN (PureNaN.h). This is the only NaN that is safe to -// NaN-box: any other payload can collide with the tag ranges above and decode -// as a cell pointer, an immediate, or an Int32 instead of a double. -#define PureNaN 0x7ff8000000000000ll - -typedef void* JSCell; - -typedef union EncodedJSValue { - int64_t asInt64; - -#if USE_JSVALUE64 - JSCell *ptr; -#endif - -napi_value asNapiValue; - -#if IS_BIG_ENDIAN - struct { - int32_t tag; - int32_t payload; - } asBits; -#else - struct { - int32_t payload; - int32_t tag; - } asBits; -#endif - - void* asPtr; - double asDouble; - - ZIG_REPR_TYPE asZigRepr; -} EncodedJSValue; - -EncodedJSValue ValueUndefined = { TagValueUndefined }; -EncodedJSValue ValueTrue = { TagValueTrue }; - -typedef void* JSContext; - -// Bun_FFI_PointerOffsetToArgumentsList is injected into the build -// The value is generated in `make sizegen` -// The value is 6. -// On ARM64_32, the value is something else but it really doesn't matter for our case -// However, I don't want this to subtly break amidst future upgrades to JavaScriptCore -#define LOAD_ARGUMENTS_FROM_CALL_FRAME \ - int64_t *argsPtr = (int64_t*)((size_t*)callFrame + Bun_FFI_PointerOffsetToArgumentsList) - - -#ifdef IS_CALLBACK -void* callback_ctx; -BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args); -// We wrap -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__)); -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) { - EncodedJSValue return_value; - return_value.asZigRepr = FFI_Callback_call(ctx, argCount, args); - return return_value; -} -#endif - -static bool JSVALUE_IS_CELL(EncodedJSValue val) __attribute__((__always_inline__)); -static bool JSVALUE_IS_INT32(EncodedJSValue val) __attribute__((__always_inline__)); -static bool JSVALUE_IS_NUMBER(EncodedJSValue val) __attribute__((__always_inline__)); - -static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) __attribute__((__always_inline__)); -static int64_t JSVALUE_TO_INT64(EncodedJSValue value) __attribute__((__always_inline__)); -uint64_t JSVALUE_TO_UINT64_SLOW(EncodedJSValue value); -int64_t JSVALUE_TO_INT64_SLOW(EncodedJSValue value); - -EncodedJSValue UINT64_TO_JSVALUE_SLOW(void* jsGlobalObject, uint64_t val); -EncodedJSValue INT64_TO_JSVALUE_SLOW(void* jsGlobalObject, int64_t val); -static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) __attribute__((__always_inline__)); -static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) __attribute__((__always_inline__)); - - -static EncodedJSValue INT32_TO_JSVALUE(int32_t val) __attribute__((__always_inline__)); -static EncodedJSValue DOUBLE_TO_JSVALUE(double val) __attribute__((__always_inline__)); -static EncodedJSValue FLOAT_TO_JSVALUE(float val) __attribute__((__always_inline__)); -static EncodedJSValue BOOLEAN_TO_JSVALUE(bool val) __attribute__((__always_inline__)); -static EncodedJSValue PTR_TO_JSVALUE(void* ptr) __attribute__((__always_inline__)); - -static void* JSVALUE_TO_PTR(EncodedJSValue val) __attribute__((__always_inline__)); -static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inline__)); -static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__)); -static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__)); -static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__)); -static uint8_t GET_JSTYPE(EncodedJSValue val) __attribute__((__always_inline__)); -static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) __attribute__((__always_inline__)); -static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) __attribute__((__always_inline__)); -static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) __attribute__((__always_inline__)); -static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) __attribute__((__always_inline__)); - -static bool JSVALUE_IS_CELL(EncodedJSValue val) { - return !(val.asInt64 & NotCellMask); -} - -static bool JSVALUE_IS_INT32(EncodedJSValue val) { - return (val.asInt64 & NumberTag) == NumberTag; -} - -static bool JSVALUE_IS_NUMBER(EncodedJSValue val) { - return val.asInt64 & NumberTag; -} - -static uint8_t GET_JSTYPE(EncodedJSValue val) { - return *(uint8_t*)((uint8_t*)val.asPtr + JSCell__offsetOfType); -} - -static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) { - return type >= JSTypeArrayBufferViewMin && type <= JSTypeArrayBufferViewMax; -} - -static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) { - return JSVALUE_IS_CELL(val) && JSTYPE_IS_TYPED_ARRAY(GET_JSTYPE(val)); -} - -static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) { - return *(void**)((char*)val.asPtr + JSArrayBufferView__offsetOfVector); -} - -static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) { - return *(uint64_t*)((char*)val.asPtr + JSArrayBufferView__offsetOfLength); -} - -// JSValue numbers-as-pointers are represented as a 52-bit integer -// Previously, the pointer was stored at the end of the 64-bit value -// Now, they're stored at the beginning of the 64-bit value -// This behavior change enables the JIT to handle it better -// It also is better readability when console.log(myPtr) -static void* JSVALUE_TO_PTR(EncodedJSValue val) { - if (val.asInt64 == TagValueNull) - return 0; - - if (JSCELL_IS_TYPED_ARRAY(val)) { - return JSVALUE_TO_TYPED_ARRAY_VECTOR(val); - } - - if (JSVALUE_IS_INT32(val)) { - return (void*)(uintptr_t)JSVALUE_TO_INT32(val); - } - - // Assume the JSValue is a double - val.asInt64 -= DoubleEncodeOffset; - return (void*)(uintptr_t)val.asDouble; -} - -static EncodedJSValue PTR_TO_JSVALUE(void* ptr) { - EncodedJSValue val; - if (ptr == 0) { - val.asInt64 = TagValueNull; - return val; - } - - val.asDouble = (double)(uintptr_t)ptr; - val.asInt64 += DoubleEncodeOffset; - return val; -} - -static EncodedJSValue DOUBLE_TO_JSVALUE(double val) { - EncodedJSValue res; - res.asDouble = val; - // Mirrors JSC's purifyNaN(): a NaN payload taken from native memory would - // otherwise be NaN-boxed as-is and decode as a forged JSValue. - if (val != val) { - res.asInt64 = PureNaN; - } - res.asInt64 += DoubleEncodeOffset; - return res; -} - -static int32_t JSVALUE_TO_INT32(EncodedJSValue val) { - if (JSVALUE_IS_INT32(val)) { - return (int32_t)val.asInt64; - } - // Decode a double-encoded integer (JIT tier-up, Math.* provenance, etc.); - // int64_t intermediate keeps u32 callers (uint32_t)JSVALUE_TO_INT32(...) defined. - val.asInt64 -= DoubleEncodeOffset; - // NaN check also catches undefined/null/bool, whose decoded bits are all NaNs. - if (val.asDouble != val.asDouble) return 0; - return (int32_t)(int64_t)val.asDouble; -} - -static EncodedJSValue INT32_TO_JSVALUE(int32_t val) { - EncodedJSValue res; - res.asInt64 = NumberTag | (uint32_t)val; - return res; -} - -static EncodedJSValue UINT32_TO_JSVALUE(uint32_t val) { - EncodedJSValue res; - if(val <= MAX_INT32) { - res.asInt64 = NumberTag | val; - return res; - } else { - EncodedJSValue res; - res.asDouble = val; - res.asInt64 += DoubleEncodeOffset; - return res; - } -} - -static EncodedJSValue FLOAT_TO_JSVALUE(float val) { - return DOUBLE_TO_JSVALUE((double)val); -} - -static EncodedJSValue BOOLEAN_TO_JSVALUE(bool val) { - EncodedJSValue res; - res.asInt64 = val ? TagValueTrue : TagValueFalse; - return res; -} - - -static double JSVALUE_TO_DOUBLE(EncodedJSValue val) { - // Numbers that fit in an int32 are int32-tagged, not double-encoded - // (see JSVALUE_TO_INT64). Subtracting DoubleEncodeOffset from an - // int32-tagged value yields an impure NaN, not the number. - if (JSVALUE_IS_INT32(val)) { - return (double)JSVALUE_TO_INT32(val); - } - - val.asInt64 -= DoubleEncodeOffset; - return val.asDouble; -} - -static float JSVALUE_TO_FLOAT(EncodedJSValue val) { - return (float)JSVALUE_TO_DOUBLE(val); -} - -static bool JSVALUE_TO_BOOL(EncodedJSValue val) { - return val.asInt64 == TagValueTrue; -} - - -static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) { - if (JSVALUE_IS_INT32(value)) { - return (uint64_t)JSVALUE_TO_INT32(value); - } - - if (JSVALUE_IS_NUMBER(value)) { - return (uint64_t)JSVALUE_TO_DOUBLE(value); - } - - if (JSCELL_IS_TYPED_ARRAY(value)) { - return (uint64_t)JSVALUE_TO_TYPED_ARRAY_LENGTH(value); - } - - return JSVALUE_TO_UINT64_SLOW(value); -} -static int64_t JSVALUE_TO_INT64(EncodedJSValue value) { - if (JSVALUE_IS_INT32(value)) { - return (int64_t)JSVALUE_TO_INT32(value); - } - - if (JSVALUE_IS_NUMBER(value)) { - return (int64_t)JSVALUE_TO_DOUBLE(value); - } - - return JSVALUE_TO_INT64_SLOW(value); -} - -static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) { - if (val < MAX_INT32) { - return INT32_TO_JSVALUE((int32_t)val); - } - - if (val < MAX_INT52) { - return DOUBLE_TO_JSVALUE((double)val); - } - - return UINT64_TO_JSVALUE_SLOW(jsGlobalObject, val); -} - -static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) { - if (val >= -MAX_INT32 && val <= MAX_INT32) { - return INT32_TO_JSVALUE((int32_t)val); - } - - if (val >= -MAX_INT52 && val <= MAX_INT52) { - return DOUBLE_TO_JSVALUE((double)val); - } - - return INT64_TO_JSVALUE_SLOW(jsGlobalObject, val); -} - -#ifndef IS_CALLBACK -BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); - -#endif - - -// --- Generated Code --- - - -/* --- The Callback Function */ -bool my_callback_function(void* arg0) { -#ifdef INJECT_BEFORE -INJECT_BEFORE; -#endif - ZIG_REPR_TYPE arguments[1]; -arguments[0] = PTR_TO_JSVALUE(arg0).asZigRepr; - return (bool)JSVALUE_TO_BOOL(_FFI_Callback_call((void*)0x0000000000000000ULL, 1, arguments)); -} - +// bun:ffi callbacks are compiled by JavaScriptCore (no C source is generated) diff --git a/test/js/bun/ffi/ffi.test.fixture.receiver.c b/test/js/bun/ffi/ffi.test.fixture.receiver.c index 5ba908cb953e..03c89180e442 100644 --- a/test/js/bun/ffi/ffi.test.fixture.receiver.c +++ b/test/js/bun/ffi/ffi.test.fixture.receiver.c @@ -8,9 +8,6 @@ // This file is only compatible with 64 bit CPUs // It must be kept in sync with JSCJSValue.h // https://github.com/oven-sh/WebKit/blob/main/Source/JavaScriptCore/runtime/JSCJSValue.h -#ifdef IS_CALLBACK -#define INJECT_BEFORE int c = 500; // This is a callback, so we need to inject code before the call -#endif #define IS_BIG_ENDIAN 0 #define USE_JSVALUE64 1 #define USE_JSVALUE32_64 0 @@ -73,10 +70,6 @@ BUN_FFI_IMPORT extern struct NapiEnv Bun__thisFFIModuleNapiEnv; #endif -#ifdef INJECT_BEFORE -// #include -#endif -// #include // This value is 2^49, used to encode doubles such that the encoded value will // begin with a 15-bit pattern within the range 0x0002..0xFFFC. @@ -146,17 +139,6 @@ typedef void* JSContext; int64_t *argsPtr = (int64_t*)((size_t*)callFrame + Bun_FFI_PointerOffsetToArgumentsList) -#ifdef IS_CALLBACK -void* callback_ctx; -BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args); -// We wrap -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__)); -static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) { - EncodedJSValue return_value; - return_value.asZigRepr = FFI_Callback_call(ctx, argCount, args); - return return_value; -} -#endif static bool JSVALUE_IS_CELL(EncodedJSValue val) __attribute__((__always_inline__)); static bool JSVALUE_IS_INT32(EncodedJSValue val) __attribute__((__always_inline__)); @@ -382,11 +364,8 @@ static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) { return INT64_TO_JSVALUE_SLOW(jsGlobalObject, val); } -#ifndef IS_CALLBACK BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); -#endif - // --- Generated Code --- /* --- The Function To Call */ diff --git a/test/js/bun/ffi/ffi.test.js b/test/js/bun/ffi/ffi.test.js index f2548bfd9543..45d0f6783aa4 100644 --- a/test/js/bun/ffi/ffi.test.js +++ b/test/js/bun/ffi/ffi.test.js @@ -1,13 +1,15 @@ import { afterAll, describe, expect, it } from "bun:test"; import { existsSync } from "fs"; -import { bunEnv, bunExe, isGlibcVersionAtLeast, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, compileFixture, isGlibcVersionAtLeast, isWindows, tempDir } from "harness"; import { platform } from "os"; import { - dlopen as _dlopen, + cc, CFunction, CString, + dlopen, JSCallback, + linkSymbols, ptr, read, suffix, @@ -16,15 +18,15 @@ import { viewSource, } from "bun:ffi"; -const dlopen = (...args) => { - try { - return _dlopen(...args); - } catch (err) { - console.error("To enable this test, run `make compile-ffi-test`."); - throw err; - } -}; -const ok = existsSync("/tmp/bun-ffi-test." + suffix); +let FFI_FIXTURE_PATH = null; +let ABI_FIXTURE_PATH = null; +try { + FFI_FIXTURE_PATH = compileFixture(import.meta.dir + "/ffi-test.c"); + ABI_FIXTURE_PATH = compileFixture(import.meta.dir + "/ffi-abi-fixture.c"); +} catch (e) { + if (!String(e?.message ?? e).includes("no C compiler")) throw e; + console.warn(`[ffi.test] fixture-dependent tests skipped: ${e?.message ?? e}`); +} it("ffi print", async () => { await Bun.write( @@ -156,6 +158,22 @@ function getTypes(fast) { returns: "int32_t", args: ["int32_t"], }, + returns_cstring: { + returns: "cstring", + args: [], + }, + returns_null_cstring: { + returns: "cstring", + args: [], + }, + echoes_cstring: { + returns: "cstring", + args: ["cstring"], + }, + strlen_cstring: { + returns: "uint64_t", + args: ["cstring"], + }, identity_int64_t: { returns: int64_t, args: [int64_t], @@ -307,6 +325,10 @@ function getTypes(fast) { returns: "ptr", args: [], }, + getNoopDeallocatorCallback: { + returns: "ptr", + args: [], + }, getDeallocatorBuffer: { returns: "ptr", args: [], @@ -360,6 +382,7 @@ function ffiRunner(fast) { is_null, does_pointer_equal_42_as_int32_t, ptr_should_point_to_42_as_int32_t, + getNoopDeallocatorCallback, cb_identity_true, cb_identity_false, cb_identity_42_char, @@ -378,7 +401,7 @@ function ffiRunner(fast) { getDeallocatorBuffer, }, close, - } = dlopen("/tmp/bun-ffi-test.dylib", types); + } = dlopen(FFI_FIXTURE_PATH, types); it("primitives", () => { Bun.gc(true); expect(returns_true()).toBe(true); @@ -470,10 +493,14 @@ function ffiRunner(fast) { expect(cptr != 0).toBe(true); expect(typeof cptr === "number").toBe(true); expect(does_pointer_equal_42_as_int32_t(cptr)).toBe(true); - const buffer = toBuffer(cptr, 0, 4); - expect(buffer.readInt32(0)).toBe(42); - expect(new DataView(toArrayBuffer(cptr, 0, 4), 0, 4).getInt32(0, true)).toBe(42); - expect(ptr(buffer)).toBe(cptr); + const noopDeallocator = getNoopDeallocatorCallback(); + { + const buffer = toBuffer(cptr, 0, 4, noopDeallocator); + expect(buffer.readInt32(0)).toBe(42); + expect(new DataView(toArrayBuffer(cptr, 0, 4, noopDeallocator), 0, 4).getInt32(0, true)).toBe(42); + expect(ptr(buffer)).toBe(cptr); + } + Bun.gc(true); expect(new CString(cptr, 0, 1).toString()).toBe("*"); expect(identity_ptr(cptr)).toBe(cptr); const second_ptr = ptr(new Buffer(8)); @@ -647,14 +674,11 @@ it("read", () => { delete globalThis.buffer; }); -if (ok) { - describe("run ffi", () => { - ffiRunner(false); - ffiRunner(true); - }); -} else { - it.skip("run ffi", () => {}); -} +describe.skipIf(!FFI_FIXTURE_PATH)("run ffi", () => { + if (!FFI_FIXTURE_PATH) return; + ffiRunner(false); + ffiRunner(true); +}); it("dlopen throws an error instead of returning it", () => { let err; @@ -715,6 +739,163 @@ it(".ptr is not leaked", () => { } }); +describe.skipIf(!FFI_FIXTURE_PATH)("engine-native cstring", () => { + it("dlopen returns:'cstring' yields a string primitive; NULL yields null", () => { + const { + symbols: { returns_cstring, returns_null_cstring }, + } = dlopen(FFI_FIXTURE_PATH, { + returns_cstring: { returns: "cstring", args: [] }, + returns_null_cstring: { returns: "cstring", args: [] }, + }); + const value = returns_cstring(); + expect(typeof value).toBe("string"); + expect(value).toBe("engine cstring"); + expect(returns_null_cstring()).toBe(null); + }); + + it("args:['cstring'] accepts a JS string, a TypedArray, and a pointer", () => { + const { + symbols: { echoes_cstring, strlen_cstring }, + } = dlopen(FFI_FIXTURE_PATH, { + echoes_cstring: { returns: "cstring", args: ["cstring"] }, + strlen_cstring: { returns: "uint64_t", args: ["cstring"] }, + }); + expect(echoes_cstring("round trip")).toBe("round trip"); + expect(strlen_cstring("héllo")).toBe(6n); + const bytes = Buffer.from("bytes\0", "utf8"); + expect(strlen_cstring(bytes)).toBe(5n); + expect(strlen_cstring(ptr(bytes))).toBe(5n); + }); + + it("a JSCallback receiving a cstring parameter gets a string", () => { + const { + symbols: { echoes_cstring }, + } = dlopen(FFI_FIXTURE_PATH, { + echoes_cstring: { returns: "cstring", args: ["cstring"] }, + }); + let received; + const cb = new JSCallback(s => (received = s), { args: ["cstring"], returns: "void" }); + try { + const call = CFunction({ ptr: cb.ptr, args: ["cstring"], returns: "void" }); + call("hello from js"); + } finally { + cb.close(); + } + expect(received).toBe("hello from js"); + expect(typeof received).toBe("string"); + expect(echoes_cstring(received)).toBe("hello from js"); + }); +}); + +describe("read edge cases", () => { + it("a negative byteOffset does not abort the process", () => { + const buf = new Uint8Array([9, 8, 7, 6]); + const base = ptr(buf) + 2; + expect(read.u8(base, -1)).toBe(8); + expect(read.u8(base, -2)).toBe(9); + expect(() => read.u8(1, -5)).toThrow("ptr cannot be zero"); + expect(() => read.u8(0)).toThrow("ptr cannot be zero"); + }); + + it("read.* and toArrayBuffer accept a BigInt pointer", () => { + const buf = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const address = BigInt(ptr(buf)); + expect(read.u8(address, 0)).toBe(1); + expect(read.u8(address, 3)).toBe(4); + expect(new Uint8Array(toArrayBuffer(address, 0, 8))).toEqual(buf); + expect(() => read.u8(-1n, 0)).toThrow("Expected a pointer"); + expect(() => CFunction({ ptr: -1n, args: [], returns: "void" })).toThrow(/out of range/); + }); +}); + +describe("CString", () => { + it("call and construct forms are identical for falsy pointers", () => { + for (const falsy of [null, undefined, 0]) { + expect(CString(falsy)).toBe(""); + expect(new CString(falsy)).toBe(""); + } + }); + it("accepts a BigInt pointer", () => { + const buf = Buffer.from("bigint ok\0", "utf8"); + const address = BigInt(ptr(buf)); + expect(new CString(address)).toBe("bigint ok"); + expect(CString(address)).toBe("bigint ok"); + }); + it("throws (not stringifies) on an invalid pointer", () => { + expect(() => new CString(-1)).toThrow("ptr must be a number"); + expect(() => CString(-1)).toThrow("ptr must be a number"); + expect(() => new CString(-1n)).toThrow(/out of range/); + expect(() => CString(-1n)).toThrow(/out of range/); + }); + const hello = Buffer.from("Hello, world!\0", "utf8"); + (globalThis.__ffiTestPinnedBuffers ??= []).push(hello); + const helloPtr = ptr(hello); + + it("yields a string primitive", () => { + const cs = new CString(helloPtr); + expect(typeof cs).toBe("string"); + expect(cs).toBe("Hello, world!"); + expect(cs === "Hello, world!").toBe(true); + expect(cs.length).toBe(13); + expect(cs.slice(7)).toBe("world!"); + expect(JSON.stringify(cs)).toBe('"Hello, world!"'); + }); + + it("takes byteOffset and byteLength", () => { + expect(new CString(helloPtr, 7, 5)).toBe("world"); + expect(new CString(helloPtr, 0, 5)).toBe("Hello"); + }); + + it("a falsy pointer yields an empty string", () => { + for (const value of [0, null, undefined]) { + const cs = new CString(value); + expect(typeof cs).toBe("string"); + expect(cs).toBe(""); + } + }); + + it("Bun.FFI.CString is the same constructor, callable with and without new", () => { + expect(Bun.FFI.CString).toBe(CString); + expect(new Bun.FFI.CString(helloPtr, 0, 5)).toBe("Hello"); + expect(Bun.FFI.CString(helloPtr, 0, 5)).toBe("Hello"); + expect(CString(helloPtr, 0, 5)).toBe("Hello"); + expect(CString.name).toBe("CString"); + }); +}); + +describe("CFunction", () => { + it("returns the engine-native callable with a working .close()", () => { + const callback = new JSCallback(() => 42, { returns: "int32_t", args: [] }); + try { + const fn = new CFunction({ ptr: callback.ptr, returns: "int32_t", args: [] }); + expect(typeof fn).toBe("function"); + expect(fn()).toBe(42); + expect(fn()).toBe(42); + expect(fn.close).toBeFunction(); + expect(fn.close()).toBeUndefined(); + expect(fn.close()).toBeUndefined(); + } finally { + callback.close(); + } + }); + + it("passes arguments and marshals the return value", () => { + const add = new JSCallback((a, b) => a + b, { returns: "int32_t", args: ["int32_t", "int32_t"] }); + try { + const fn = new CFunction({ ptr: add.ptr, returns: "int32_t", args: ["int32_t", "int32_t"] }); + expect(fn(40, 2)).toBe(42); + expect(fn(-1, 1)).toBe(0); + fn.close(); + } finally { + add.close(); + } + }); + + it("reports a missing ptr the same way linkSymbols() does", () => { + expect(() => new CFunction({ returns: "int32_t", args: [] })).toThrow(/CFunction.*ptr.*(linkSymbols|CFunction)/); + }); +}); + // Runs in a subprocess: `bun test`'s exit path does not finalize the CFunction's native handle, // which the ASan lane's leak checker then reports against this file. it("JSCallback exceptions propagate out of the native call", async () => { @@ -751,6 +932,62 @@ it("JSCallback exceptions propagate out of the native call", async () => { }); }); +it("worker teardown drops queued threadsafe JSCallback invocations without crashing", async () => { + using dir = tempDir("ffi-jscallback-terminate-queued", { + "main.js": ` + import { join } from "node:path"; + import { Worker } from "node:worker_threads"; + + const sab = new SharedArrayBuffer(4); + const queued = new Int32Array(sab); + + const worker = new Worker(join(import.meta.dir, "worker.js"), { workerData: sab }); + worker.on("error", err => { + console.error("worker error:", err); + process.exit(1); + }); + + // Wait until the worker has queued a batch of threadsafe invocations that its blocked + // event loop cannot drain, then tear it down with those tasks still pending. + await Atomics.waitAsync(queued, 0, 0).value; + await worker.terminate(); + console.log("done"); + `, + "worker.js": ` + import { CFunction, JSCallback } from "bun:ffi"; + import { workerData } from "node:worker_threads"; + + const queued = new Int32Array(workerData); + let ran = 0; + const callback = new JSCallback(() => { ran++; }, { returns: "void", args: [], threadsafe: true }); + const fire = new CFunction({ ptr: callback.ptr, returns: "void", args: [] }); + + // Each call enqueues an invocation onto this worker's event loop; none can run while + // this module keeps the loop occupied, so they are all still queued at terminate(). + for (let i = 0; i < 200; i++) fire(); + + Atomics.store(queued, 0, 1); + Atomics.notify(queued, 0); + while (true) {} + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "main.js"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + stdout: "done\n", + stderr: "", + exitCode: 0, + signalCode: null, + }); +}); + // worker.terminate() delivered inside a threadsafe JSCallback used to trip // "ASSERTION FAILED: !isTerminationException(exception) || hasTerminationRequest()" // in JSC::VM::setException on the worker thread and re-enter the terminated VM. @@ -1120,3 +1357,342 @@ describe.if(!!libPath)("can open more than 63 symbols via", () => { }); } }); + +describe.skipIf(!FFI_FIXTURE_PATH)("engine-native FFI (single implementation)", () => { + const lib = FFI_FIXTURE_PATH; + it("linkSymbols() binds and calls symbols from raw pointers", () => { + const { + symbols: { returns_true, add_int32_t, identity_ptr }, + } = dlopen(lib, { + returns_true: { args: [], returns: "bool" }, + add_int32_t: { args: ["i32", "i32"], returns: "i32" }, + identity_ptr: { args: ["ptr"], returns: "ptr" }, + }); + const linked = linkSymbols({ + isTrue: { ptr: returns_true.ptr, args: [], returns: "bool" }, + sum: { ptr: add_int32_t.ptr, args: ["i32", "i32"], returns: "i32" }, + echoPtr: { ptr: identity_ptr.ptr, args: ["ptr"], returns: "ptr" }, + }); + expect(linked.symbols.isTrue()).toBe(true); + expect(linked.symbols.sum(40, 2)).toBe(42); + expect(linked.symbols.sum(-1, -2)).toBe(-3); + expect(linked.symbols.echoPtr(1234)).toBe(1234); + expect(typeof linked.symbols.sum.ptr).toBe("number"); + linked.close(); + }); + + it("buffer_length passes the view's byteLength, atomically paired with the pointer", () => { + const { + symbols: { bl_echo_len, bl_last_byte }, + } = dlopen(lib, { + bl_echo_len: { args: ["buffer", "buffer_length"], returns: "u64" }, + bl_last_byte: { args: ["buffer", "buffer_length"], returns: "u32" }, + }); + const u8 = new Uint8Array([10, 20, 30, 40, 50, 60, 70, 80]); + expect(bl_echo_len(u8, u8)).toBe(8n); + const sub = u8.subarray(2, 5); + expect(bl_echo_len(sub, sub)).toBe(3n); + expect(bl_last_byte(sub, sub)).toBe(50); + const dv = new DataView(u8.buffer, 1, 4); + expect(bl_echo_len(dv, dv)).toBe(4n); + expect(bl_last_byte(dv, dv)).toBe(50); + expect(bl_echo_len(new Float64Array(3), new Float64Array(3))).toBe(24n); + expect(bl_echo_len(new Uint8Array(0), new Uint8Array(0))).toBe(0n); + expect(() => bl_echo_len(u8, 8)).toThrow(TypeError); + expect(() => bl_echo_len(u8, "8")).toThrow(TypeError); + expect(() => + cc({ + source: import.meta.dir + "/ffi-test.c", + symbols: { bl_echo_len: { args: ["ptr", "buffer_length"], returns: "u64" } }, + }), + ).toThrow(/buffer_length/); + expect(() => viewSource({ f: { args: ["buffer_length"], returns: "void" } })).toThrow(/buffer_length/); + expect(() => viewSource({ f: { args: [], returns: "buffer_length" } })).toThrow(/buffer_length/); + expect(() => dlopen(lib, { f: { args: [], returns: "buffer_length" } })).toThrow(/buffer_length/); + expect(() => new JSCallback(() => {}, { args: ["buffer_length"], returns: "void" })).toThrow(/buffer_length/); + }); + + it("u32 arguments >= 2^31 are not sign-flipped (#7007)", () => { + const { + symbols: { identity_uint32_t }, + } = dlopen(lib, { identity_uint32_t: { args: ["u32"], returns: "u32" } }); + expect(identity_uint32_t(2 ** 31)).toBe(2 ** 31); + expect(identity_uint32_t(2 ** 32 - 1)).toBe(2 ** 32 - 1); + expect(identity_uint32_t(0)).toBe(0); + }); + + it("integer parameters WRAP to width instead of clamping", () => { + const { + symbols: { identity_uint8_t }, + } = dlopen(lib, { identity_uint8_t: { args: ["u8"], returns: "u8" } }); + expect(identity_uint8_t(256)).toBe(0); + expect(identity_uint8_t(257)).toBe(1); + expect(identity_uint8_t(-1)).toBe(255); + }); + + it("pointers above 2^53 round-trip as exact BigInt (#28068) and BigInt addresses are accepted (#22751)", () => { + const { + symbols: { identity_ptr }, + } = dlopen(lib, { identity_ptr: { args: ["ptr"], returns: "ptr" } }); + const big = (1n << 60n) + 7n; + const round = identity_ptr(big); + expect(typeof round).toBe("bigint"); + expect(round).toBe(big); + expect(identity_ptr(1024)).toBe(1024); + expect(identity_ptr(null)).toBe(null); + }); + + it("numeric strings for numeric parameters throw (intentional behavior change)", () => { + const { + symbols: { identity_int32_t }, + } = dlopen(lib, { identity_int32_t: { args: ["i32"], returns: "i32" } }); + expect(() => identity_int32_t("42")).toThrow(TypeError); + expect(identity_int32_t(42)).toBe(42); + }); + + it("dlopen symbols expose intrinsic .ptr (a real address) and .native", () => { + const { + symbols: { returns_true }, + } = dlopen(lib, { returns_true: { args: [], returns: "bool" } }); + expect(typeof returns_true.ptr).toBe("number"); + expect(returns_true.ptr).toBeGreaterThan(0); + expect(returns_true.native).toBe(returns_true); + expect(returns_true()).toBe(true); + }); + + it("CFunction returns the engine cell itself with a callable close()", () => { + const { + symbols: { returns_42_char }, + } = dlopen(lib, { returns_42_char: { args: [], returns: "char" } }); + const fn = new CFunction({ ptr: returns_42_char.ptr, args: [], returns: "char" }); + expect(fn()).toBe(42); + expect(typeof fn.close).toBe("function"); + fn.close(); + expect(fn()).toBe(42); + }); + + it("passing a JSCallback OBJECT (not .ptr) as a function-typed argument works", () => { + const { + symbols: { cb_identity_42_double }, + } = dlopen(lib, { cb_identity_42_double: { args: ["callback"], returns: "double" } }); + const cb = new JSCallback(() => 42.42, { returns: "double", args: [] }); + try { + expect(cb_identity_42_double(cb.ptr)).toBe(42.42); + expect(cb_identity_42_double(cb)).toBe(42.42); + } finally { + cb.close(); + } + }); + + it("a JSCallback instance is the engine cell (instanceof + own ptr) and close() is idempotent", () => { + const cb = new JSCallback(a => a * 2, { args: ["i32"], returns: "i32" }); + expect(cb instanceof JSCallback).toBe(true); + expect(typeof cb.ptr).toBe("number"); + expect(cb.threadsafe).toBe(false); + cb.close(); + cb.close(); + }); + + it("an omitted callback argument throws instead of calling through NULL", () => { + const { + symbols: { cb_identity_true }, + } = dlopen(lib, { cb_identity_true: { args: ["callback"], returns: "bool" } }); + expect(() => cb_identity_true(undefined)).toThrow(TypeError); + }); + + it("napi_env / napi_value are rejected outside cc()", () => { + expect(() => dlopen(lib, { returns_true: { args: ["napi_env"], returns: "napi_value" } })).toThrow( + /napi_env \/ napi_value are only supported in bun:ffi cc\(\)/, + ); + expect(() => new CFunction({ ptr: 1, args: ["napi_env"], returns: "void" })).toThrow( + /napi_env \/ napi_value are only supported in bun:ffi cc\(\)/, + ); + expect(() => new JSCallback(() => {}, { args: ["napi_env"], returns: "void" })).toThrow( + /napi_env \/ napi_value are only supported in bun:ffi cc\(\)/, + ); + expect(() => linkSymbols({ f: { ptr: 1, args: ["napi_env"], returns: "void" } })).toThrow( + /napi_env \/ napi_value are only supported in bun:ffi cc\(\)/, + ); + }); + + it("a hot polymorphic call site stays correct across tiers (CallFFI)", () => { + const { + symbols: { identity_int32_t }, + } = dlopen(lib, { identity_int32_t: { args: ["i32"], returns: "i32" } }); + const wrappers = [() => identity_int32_t(7), () => identity_int32_t(9)]; + let sum = 0; + for (let i = 0; i < 400000; ++i) sum += wrappers[i & 1](); + expect(sum).toBe(200000 * 7 + 200000 * 9); + }); +}); + +describe.skipIf(!ABI_FIXTURE_PATH)("ABI conformance", () => { + if (!ABI_FIXTURE_PATH) return; + const w = (vals, big = false) => + big ? vals.reduce((s, v, i) => s + BigInt(v) * BigInt(i + 1), 0n) : vals.reduce((s, v, i) => s + v * (i + 1), 0); + + it("integer widths and signedness at their boundaries", () => { + const { symbols: s } = dlopen(ABI_FIXTURE_PATH, { + abi_i8: { args: ["i8"], returns: "i8" }, + abi_u8: { args: ["u8"], returns: "u8" }, + abi_i16: { args: ["i16"], returns: "i16" }, + abi_u16: { args: ["u16"], returns: "u16" }, + abi_i32: { args: ["i32"], returns: "i32" }, + abi_u32: { args: ["u32"], returns: "u32" }, + abi_i64: { args: ["i64"], returns: "i64" }, + abi_u64: { args: ["u64"], returns: "u64" }, + abi_bool: { args: ["bool"], returns: "bool" }, + abi_char: { args: ["char"], returns: "char" }, + abi_f32: { args: ["f32"], returns: "f32" }, + abi_f64: { args: ["f64"], returns: "f64" }, + }); + for (const v of [-128, -1, 0, 1, 127]) expect(s.abi_i8(v)).toBe(v); + for (const v of [0, 1, 127, 128, 255]) expect(s.abi_u8(v)).toBe(v); + for (const v of [-32768, -1, 0, 32767]) expect(s.abi_i16(v)).toBe(v); + for (const v of [0, 32767, 32768, 65535]) expect(s.abi_u16(v)).toBe(v); + for (const v of [-2147483648, -1, 0, 2147483647]) expect(s.abi_i32(v)).toBe(v); + for (const v of [0, 2147483647, 2147483648, 4294967295]) expect(s.abi_u32(v)).toBe(v); + for (const v of [-(2n ** 63n), -1n, 0n, 2n ** 63n - 1n]) expect(s.abi_i64(v)).toBe(v); + for (const v of [0n, 2n ** 63n, 2n ** 64n - 1n]) expect(s.abi_u64(v)).toBe(v); + expect(s.abi_bool(true)).toBe(false); + expect(s.abi_bool(false)).toBe(true); + for (const v of [0, 65, 127]) expect(s.abi_char(v)).toBe(v); + for (const v of [0, 1.5, -2.25, 3.4028234663852886e38]) expect(s.abi_f32(v)).toBeCloseTo(v, 6); + for (const v of [0, 1e-300, 1.7976931348623157e308, -Number.EPSILON, Math.PI]) expect(s.abi_f64(v)).toBe(v); + }); + + it("i32 args past the register count (stack spill, all ABIs)", () => { + const { + symbols: { abi_sum_i32_x10 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_i32_x10: { args: Array(10).fill("i32"), returns: "i64" } }); + const cases = [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [-1, -2, -3, -4, -5, -6, -7, -8, -9, -10], + [2147483647, -2147483648, 0, 1, -1, 7, 7, 7, 7, 7], + [100000, 4, 5, -1, 6, 8, 1, 2, 2, 3], + ]; + for (const a of cases) expect(abi_sum_i32_x10(...a)).toBe(w(a, true)); + }); + + it("i64 args past the register count", () => { + const { + symbols: { abi_sum_i64_x10 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_i64_x10: { args: Array(10).fill("i64"), returns: "i64" } }); + const a = [1n, -2n, 3n, 2n ** 40n, -(2n ** 40n), 5n, 6n, -7n, 8n, 9n]; + expect(abi_sum_i64_x10(...a)).toBe(w(a, true)); + }); + + it("f64 args past the FP register count", () => { + const { + symbols: { abi_sum_f64_x10 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_f64_x10: { args: Array(10).fill("f64"), returns: "f64" } }); + const a = [0.5, 1.25, -2.5, 3.125, 4, -5.5, 6.75, 7, 8.5, -9.25]; + expect(abi_sum_f64_x10(...a)).toBeCloseTo(w(a), 9); + }); + + it("f32 args past the FP register count (single-precision handling)", () => { + const { + symbols: { abi_sum_f32_x10 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_f32_x10: { args: Array(10).fill("f32"), returns: "f64" } }); + const a = [0.5, 1.25, -2.5, 3.125, 4, -5.5, 6.75, 7, 8.5, -9.25]; + expect(abi_sum_f32_x10(...a)).toBeCloseTo(w(a), 5); + }); + + it("mixed alternating int/float, 12 args (Win64 positional vs SysV/AAPCS64 separate)", () => { + const args = ["i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64"]; + const { + symbols: { abi_mix12 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_mix12: { args, returns: "f64" } }); + const a = [1, 0.5, -3, 1.5, 5, -2.5, 7, 3.5, -9, 4.5, 11, -5.5]; + expect(abi_mix12(...a)).toBeCloseTo(w(a), 9); + const b = [2147483647, 1e-3, -2147483648, 1e6, 3, 4.25, -6, 7.75, 8, -9.5, 10, 0.125]; + expect(abi_mix12(...b)).toBeCloseTo(w(b), 6); + }); + + it("mixed i64/f64 past the register count", () => { + const args = ["i64", "f64", "i64", "f64", "i64", "f64", "i64", "f64", "i64", "f64"]; + const { + symbols: { abi_mix_i64f64 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_mix_i64f64: { args, returns: "i64" } }); + const a = [10n, 2, 30n, 4, 50n, 6, 70n, 8, 90n, 10]; + const expected = a.reduce((s, v, i) => s + (typeof v === "bigint" ? v * BigInt(i + 1) : BigInt(v * (i + 1))), 0n); + expect(abi_mix_i64f64(...a)).toBe(expected); + }); + + it("u8 args past the register count (sub-word stack packing / Darwin natural alignment)", () => { + const { + symbols: { abi_sum_u8_x12 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_u8_x12: { args: Array(12).fill("u8"), returns: "i64" } }); + const a = [255, 1, 128, 0, 200, 3, 17, 254, 99, 42, 7, 250]; + expect(abi_sum_u8_x12(...a)).toBe(w(a, true)); + }); + + it("i8 args past the register count (stacked byte sign-extension)", () => { + const { + symbols: { abi_sum_i8_x12 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_i8_x12: { args: Array(12).fill("i8"), returns: "i64" } }); + const a = [-128, 127, -1, 0, -100, 3, 17, -2, 99, -42, 7, -50]; + expect(abi_sum_i8_x12(...a)).toBe(w(a, true)); + }); + + it("i16 args past the register count", () => { + const { + symbols: { abi_sum_i16_x12 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_sum_i16_x12: { args: Array(12).fill("i16"), returns: "i64" } }); + const a = [-32768, 32767, -1, 0, -1000, 3, 1717, -2, 9999, -4242, 7, -50]; + expect(abi_sum_i16_x12(...a)).toBe(w(a, true)); + }); + + it("bool args past the register count (each exactly 0/1)", () => { + const { + symbols: { abi_bools_x10 }, + } = dlopen(ABI_FIXTURE_PATH, { abi_bools_x10: { args: Array(10).fill("bool"), returns: "i32" } }); + const bits = [true, false, true, true, false, false, true, false, true, true]; + expect(abi_bools_x10(...bits)).toBe(bits.reduce((s, b, i) => s + (b ? 1 << i : 0), 0)); + }); + + it("callback direction: C invokes JS callbacks with many-arg shapes", () => { + const { symbols: s } = dlopen(ABI_FIXTURE_PATH, { + abi_cb_i32_x10: { args: ["callback", "i32"], returns: "i64" }, + abi_cb_f64_x10: { args: ["callback", "f64"], returns: "f64" }, + abi_cb_mix12: { args: ["callback", "i32", "f64"], returns: "f64" }, + abi_cb_i64_x10: { args: ["callback", "i64"], returns: "i64" }, + }); + const cbI = new JSCallback((...a) => a.reduce((t, v, i) => t + BigInt(v) * BigInt(i + 1), 0n), { + args: Array(10).fill("i32"), + returns: "i64", + }); + const cbF = new JSCallback((...a) => a.reduce((t, v, i) => t + v * (i + 1), 0), { + args: Array(10).fill("f64"), + returns: "f64", + }); + const cbM = new JSCallback((...a) => a.reduce((t, v, i) => t + v * (i + 1), 0), { + args: ["i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64", "i32", "f64"], + returns: "f64", + }); + const cbL = new JSCallback((...a) => a.reduce((t, v, i) => t + BigInt(v) * BigInt(i + 1), 0n), { + args: Array(10).fill("i64"), + returns: "i64", + }); + try { + const ki = 5; + const ai = Array.from({ length: 10 }, (_, i) => ki + i); + expect(s.abi_cb_i32_x10(cbI, ki)).toBe(w(ai, true)); + const kf = 1.5; + const af = Array.from({ length: 10 }, (_, i) => kf + i * 0.5); + expect(s.abi_cb_f64_x10(cbF, kf)).toBeCloseTo(w(af), 9); + const i0 = 7, + d0 = 2.5; + const am = [i0, d0, i0 + 1, d0 + 1, i0 + 2, d0 + 2, i0 + 3, d0 + 3, i0 + 4, d0 + 4, i0 + 5, d0 + 5]; + expect(s.abi_cb_mix12(cbM, i0, d0)).toBeCloseTo(w(am), 9); + const kl = 2n ** 40n; + const al = Array.from({ length: 10 }, (_, i) => kl + BigInt(i)); + expect(s.abi_cb_i64_x10(cbL, kl)).toBe(w(al, true)); + } finally { + cbI.close(); + cbF.close(); + cbM.close(); + cbL.close(); + } + }); +}); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-align.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-align.js new file mode 100644 index 000000000000..643c105c4d80 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-align.js @@ -0,0 +1,69 @@ +//@ requireOptions("--useDollarVM=1") + +// Stack-alignment probes (SPEC section 11.1): each fixture performs an +// aligned 16-byte vector access on a 16-byte-aligned local, which faults if +// the FFI caller (host path, IC stub, DFG/FTL CallFFI, or the callback thunk +// on the way back into native code) mis-aligned the stack. Both probes must +// return exactly 1.0 in every tier. + +function main() { + const fixture = name => $vm.ffiFixture(name); + const probe0 = $vm.ffiFunction({ args: [], returns: "f64" }, fixture("ffi_align_probe_0"), "ffi_align_probe_0"); + const probe9 = $vm.ffiFunction({ args: new Array(9).fill("i32"), returns: "f64" }, fixture("ffi_align_probe_9"), "ffi_align_probe_9"); + const callCbVoid = $vm.ffiFunction({ args: ["function"], returns: "void" }, fixture("ffi_call_cb_void"), "ffi_call_cb_void"); + const callCbI32 = $vm.ffiFunction({ args: ["function", "i32"], returns: "i32" }, fixture("ffi_call_cb_i32"), "ffi_call_cb_i32"); + + if (probe0() !== 1) + throw new Error("ffi_align_probe_0 cold: " + probe0()); + if (probe9(1, 2, 3, 4, 5, 6, 7, 8, 9) !== 1) + throw new Error("ffi_align_probe_9 cold"); + // Missing / extra JS arguments must not change the call frame layout. + if (probe9(1, 2, 3) !== 1) + throw new Error("ffi_align_probe_9 with missing arguments"); + if (probe9(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) !== 1) + throw new Error("ffi_align_probe_9 with extra arguments"); + if (probe0(1) !== 1) + throw new Error("ffi_align_probe_0 with an extra argument"); + + // Hot: every tier's call path must keep 16-byte alignment. + for (let i = 0; i < 3e4; ++i) { + if (probe0() !== 1) + throw new Error("ffi_align_probe_0 hot iteration " + i); + } + for (let i = 0; i < 3e4; ++i) { + if (probe9(i, -i, i, -i, i, -i, i, -i, i) !== 1) + throw new Error("ffi_align_probe_9 hot iteration " + i); + } + // Mixed argument shapes (int32 and double) at the same call site. + for (let i = 0; i < 1e4; ++i) { + if (probe9(i + 0.5, 1, 2, 3, 4, 5, 6, 7, 8) !== 1) + throw new Error("ffi_align_probe_9 double first argument iteration " + i); + } + + // Alignment on the way back out: a callback that runs the probes from + // inside the native -> JS -> native sandwich. + const cbProbe = $vm.ffiCallback({ args: [], returns: "void" }, () => { + if (probe0() !== 1) + throw new Error("probe0 inside callback"); + if (probe9(9, 8, 7, 6, 5, 4, 3, 2, 1) !== 1) + throw new Error("probe9 inside callback"); + }); + for (let i = 0; i < 3000; ++i) + callCbVoid(cbProbe); + + // Nested: FFI -> callback -> FFI -> callback -> probe, to depth 20. + const nestCb = $vm.ffiCallback({ args: ["i32"], returns: "i32" }, depth => { + if (probe0() !== 1) + throw new Error("probe0 at depth " + depth); + if (depth <= 0) + return 0; + return callCbI32(nestCb, depth - 1) + 1; + }); + for (let i = 0; i < 200; ++i) { + if (callCbI32(nestCb, 20) !== 20) + throw new Error("nested alignment ladder iteration " + i); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-arena-depth.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arena-depth.js new file mode 100644 index 000000000000..07e695f9cdf4 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arena-depth.js @@ -0,0 +1,41 @@ +//@ requireOptions("--useDollarVM=1") + +function main() { + const fixture = name => $vm.ffiFixture(name); + const depth = () => $vm.ffiArenaDepth(); + + if (depth() !== 0) + throw new Error("arena depth should start at 0, got " + depth()); + + const strlen = $vm.ffiFunction({ args: ["cstring"], returns: "u64" }, fixture("ffi_strlen"), "ffi_strlen"); + for (let i = 0; i < 2e4; ++i) { + strlen("call " + (i & 7)); + if (depth() !== 0) + throw new Error("arena depth leaked after a normal call at iteration " + i + ": " + depth()); + } + + let calls = 0; + const callback = $vm.ffiCallback({ args: [], returns: "cstring" }, () => { + ++calls; + throw new Error("thrown from callback"); + }); + const callThrough = $vm.ffiFunction({ args: ["ptr"], returns: "cstring" }, fixture("ffi_call_cb_ret_cstring"), "ffi_call_cb_ret_cstring"); + for (let i = 0; i < 2e4; ++i) { + let threw = false; + try { + callThrough(callback.ptr); + } catch (e) { + threw = e instanceof Error && e.message === "thrown from callback"; + } + if (!threw) + throw new Error("expected the callback exception to propagate at iteration " + i); + if (depth() !== 0) + throw new Error("arena depth leaked after a throwing cstring call at iteration " + i + ": " + depth()); + } + callback.close(); + if (calls !== 2e4) + throw new Error("callback ran " + calls + " times, expected 20000"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity-ladders.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity-ladders.js new file mode 100644 index 000000000000..7c00f349aa60 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity-ladders.js @@ -0,0 +1,208 @@ +//@ requireOptions("--useDollarVM=1") + +// Arity ladders that straddle every register->stack boundary of the +// supported ABIs, plus the interleaved ffi_mix_* fixtures. Each mix returns +// the position-weighted checksum sum((k + 1) * arg_k), so any argument that +// lands in the wrong register or stack slot changes the result. + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name); + + // Deterministic PRNG (mulberry32). + let seed = 0x1abe11ed; + function random() { + seed = (seed + 0x6D2B79F5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + } + const randomInt32 = () => (Math.floor(random() * 4294967296) | 0); + const randomSmall = () => Math.floor(random() * 2001) - 1000; + + // ---- ffi_sum_i32_: int64 sums returned as BigInt. + const sumI32Arities = [0, 1, 2, 4, 6, 7, 8, 9, 12, 16]; + const sumI32 = new Map(); + for (const n of sumI32Arities) + sumI32.set(n, bind("ffi_sum_i32_" + n, new Array(n).fill("i32"), "i64")); + + function checkSumI32(n, values) { + let expected = 0n; + for (const v of values) + expected += BigInt(v | 0); + const actual = sumI32.get(n)(...values); + check(actual, expected, "ffi_sum_i32_" + n + "(" + values.join(",") + ")"); + } + for (const n of sumI32Arities) { + checkSumI32(n, new Array(n).fill(0)); + checkSumI32(n, new Array(n).fill(-1)); + checkSumI32(n, new Array(n).fill(2147483647)); + checkSumI32(n, new Array(n).fill(-2147483648)); + // Distinct powers of two: catches any duplicated or swapped argument. + checkSumI32(n, new Array(n).fill(0).map((_, i) => (i % 2 ? -1 : 1) * (1 << (i + 5)))); + for (let iteration = 0; iteration < 30; ++iteration) + checkSumI32(n, new Array(n).fill(0).map(() => randomInt32())); + } + + // ---- ffi_sum_f64_: double sums. + const sumF64Arities = [1, 2, 7, 8, 9, 12]; + const sumF64 = new Map(); + for (const n of sumF64Arities) + sumF64.set(n, bind("ffi_sum_f64_" + n, new Array(n).fill("f64"), "f64")); + function checkSumF64(n, values) { + let expected = 0; + for (const v of values) + expected += v; // same left-to-right order as the fixture + const actual = sumF64.get(n)(...values); + check(actual, expected, "ffi_sum_f64_" + n + "(" + values.join(",") + ")"); + } + for (const n of sumF64Arities) { + checkSumF64(n, new Array(n).fill(0)); + checkSumF64(n, new Array(n).fill(-0.5)); + checkSumF64(n, new Array(n).fill(0).map((_, i) => 1 / (1 << i))); // exact binary fractions + checkSumF64(n, new Array(n).fill(0).map((_, i) => (i % 2 ? -1 : 1) * 2 ** (i * 4))); + for (let iteration = 0; iteration < 30; ++iteration) + checkSumF64(n, new Array(n).fill(0).map(() => randomSmall() * 2 ** (Math.floor(random() * 60) - 30))); + } + + // ---- Sub-8-byte stack ladders (Apple arm64 packing). + const sumU8_10 = bind("ffi_sum_u8_10", new Array(10).fill("u8"), "i64"); + const sumU8_12 = bind("ffi_sum_u8_12", new Array(12).fill("u8"), "i64"); + const sumI16_10 = bind("ffi_sum_i16_10", new Array(10).fill("i16"), "i64"); + const sumI16_12 = bind("ffi_sum_i16_12", new Array(12).fill("i16"), "i64"); + function checkSubword(fn, name, values, widthMask, signed) { + let expected = 0n; + for (const v of values) { + let w = (v | 0) & widthMask; + if (signed && (w & ((widthMask + 1) >>> 1))) + w -= widthMask + 1; + expected += BigInt(w); + } + check(fn(...values), expected, name + "(" + values.join(",") + ")"); + } + for (const [fn, name, n, mask, signed] of [ + [sumU8_10, "ffi_sum_u8_10", 10, 0xff, false], + [sumU8_12, "ffi_sum_u8_12", 12, 0xff, false], + [sumI16_10, "ffi_sum_i16_10", 10, 0xffff, true], + [sumI16_12, "ffi_sum_i16_12", 12, 0xffff, true], + ]) { + checkSubword(fn, name, new Array(n).fill(0), mask, signed); + checkSubword(fn, name, new Array(n).fill(-1), mask, signed); // 255 / -1 + checkSubword(fn, name, new Array(n).fill(0).map((_, i) => 1 << i), mask, signed); // distinct powers of two + checkSubword(fn, name, new Array(n).fill(0).map((_, i) => i + 1), mask, signed); + checkSubword(fn, name, new Array(n).fill(mask), mask, signed); + checkSubword(fn, name, new Array(n).fill((mask + 1) >>> 1), mask, signed); // sign bit + for (let iteration = 0; iteration < 30; ++iteration) + checkSubword(fn, name, new Array(n).fill(0).map(() => randomInt32()), mask, signed); + } + + // ---- Mixes. checksum = sum (k + 1) * cast(arg_k) + const mix1 = bind("ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"], "f64"); + const mix2 = bind("ffi_mix_2", ["f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32"], "f64"); + const mix3 = bind("ffi_mix_3", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "i32"], "f64"); + const mix4 = bind("ffi_mix_4", ["i64", "i64", "i64", "i64", "i64", "i64", "f64", "i64", "f64"], "f64"); + const mix5 = bind("ffi_mix_5", ["u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64"], "f64"); + const mix6 = bind("ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"], "f64"); + const mix7 = bind("ffi_mix_7", ["ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char"], "f64"); + const mix8 = bind("ffi_mix_8", ["f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64"], "f64"); + + // JS reference of the C casts used by the fixtures. + const castByType = { + "i32": v => v | 0, + "f64": v => +v, + "i64": v => Number(BigInt.asIntN(64, BigInt(Math.trunc(v)))), + "f32": v => Math.fround(v), + "ptr": v => Math.trunc(v), // small non-negative pointers only + "u8": v => (v | 0) & 0xff, + "i16": v => ((v | 0) << 16) >> 16, + "i8": v => ((v | 0) << 24) >> 24, + "char": v => ((v | 0) << 24) >> 24, + "u16": v => (v | 0) & 0xffff, + "u32": v => (v | 0) >>> 0, + "u64": v => Number(BigInt.asUintN(64, BigInt(Math.trunc(v)))), + "bool": v => (v ? 1 : 0), + }; + function checksum(types, values) { + let sum = 0; + for (let k = 0; k < types.length; ++k) + sum += (k + 1) * castByType[types[k]](values[k]); + return sum; + } + const mixes = [ + [mix1, "ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"]], + [mix2, "ffi_mix_2", ["f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32", "f32", "i32"]], + [mix3, "ffi_mix_3", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "i32"]], + [mix4, "ffi_mix_4", ["i64", "i64", "i64", "i64", "i64", "i64", "f64", "i64", "f64"]], + [mix5, "ffi_mix_5", ["u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64"]], + [mix6, "ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"]], + [mix7, "ffi_mix_7", ["ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char", "ptr", "char"]], + [mix8, "ffi_mix_8", ["f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64", "f32", "f64"]], + ]; + // Value generators per type. All chosen so that the checksum arithmetic + // is exact in double (weights <= 13, magnitudes <= 2^40). + const generatorByType = { + "i32": () => randomSmall() * 65536 + Math.floor(random() * 65536), + "f64": () => randomSmall() / 8, + "i64": () => randomSmall() * 1048576, + "f32": () => Math.fround(randomSmall() / 16), + "ptr": () => Math.floor(random() * 65536) * 8, + "u8": () => Math.floor(random() * 512) - 128, + "i16": () => Math.floor(random() * 200000) - 100000, + "i8": () => Math.floor(random() * 512) - 256, + "char": () => Math.floor(random() * 512) - 256, + "u16": () => Math.floor(random() * 200000) - 100000, + "u32": () => Math.floor(random() * 4294967296) - 2147483648, + "u64": () => Math.floor(random() * 65536), + "bool": () => [0, 1, 2, -1, 0.5, 0, 1][Math.floor(random() * 7)], + }; + for (const [fn, name, types] of mixes) { + // Distinct-position probe: 1 at each position in turn. + for (let k = 0; k < types.length; ++k) { + const values = types.map((_, i) => (i === k ? 1 : 0)); + check(fn(...values), checksum(types, values), name + " unit vector at " + k); + } + // All-ones and per-type extremes. + check(fn(...types.map(() => 1)), checksum(types, types.map(() => 1)), name + " all ones"); + for (let iteration = 0; iteration < 200; ++iteration) { + const values = types.map(t => generatorByType[t]()); + check(fn(...values), checksum(types, values), name + " random iteration " + iteration + " (" + values.join(",") + ")"); + } + } + + // ---- Hot loops so the ladders are also driven through the JIT tiers. + // Every hot call site below is exact-arity, monomorphic and non-spread so + // it can become a typed CallFFI node (SPEC section 10.2); a spread call + // (CallVarargs) is never converted. + const nine = sumI32.get(9); + for (let i = 0; i < 4e4; ++i) { + const r = nine(1, -2, 3, -4, 5, -6, 7, -8, 100000); + if (r !== 99996n) + throw new Error("ffi_sum_i32_9 hot iteration " + i + " got " + r); + } + // 2^52 keeps every weighted product exactly representable, so FMA + // contraction inside the C fixture cannot change the result. + const mixValues = [7, 1.5, 4503599627370496, 2.5, 4096, 250, -3.25, -1234, 8.75, -99]; + const mixTypes = mixes[0][2]; + const mixExpected = checksum(mixTypes, mixValues); + for (let i = 0; i < 4e4; ++i) { + const r = mix1(7, 1.5, 4503599627370496, 2.5, 4096, 250, -3.25, -1234, 8.75, -99); + if (r !== mixExpected) + throw new Error("ffi_mix_1 hot iteration " + i + " got " + r + " expected " + mixExpected); + } + const mix8Values = mixes[7][2].map((t, i) => (t === "f32" ? Math.fround(i + 0.5) : -(i + 0.25))); + const mix8Expected = checksum(mixes[7][2], mix8Values); + const [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11] = mix8Values; + for (let i = 0; i < 4e4; ++i) { + const r = mix8(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11); + if (r !== mix8Expected) + throw new Error("ffi_mix_8 hot iteration " + i + " got " + r + " expected " + mix8Expected); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity.js new file mode 100644 index 000000000000..18849196c231 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-arity.js @@ -0,0 +1,144 @@ +//@ requireOptions("--useDollarVM=1") + +// Arity handling (SPEC sections 3, 8.1, 8.2, 10.2): missing JS arguments are +// undefined (per-type undefined rules), extra arguments are ignored, and +// non-exact-arity call sites are simply not converted to CallFFI. Also the +// JSFunction surface: length, name, callability protocols, non-constructor. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, fixture("ffi_add_i32"), "ffi_add_i32"); + const addF64 = $vm.ffiFunction({ args: ["f64", "f64"], returns: "f64" }, fixture("ffi_add_f64"), "ffi_add_f64"); + const addF32 = $vm.ffiFunction({ args: ["f32", "f32"], returns: "f32" }, fixture("ffi_add_f32"), "ffi_add_f32"); + const echoBool = $vm.ffiFunction({ args: ["bool"], returns: "bool" }, fixture("ffi_echo_bool"), "ffi_echo_bool"); + const echoPtr = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, fixture("ffi_echo_ptr"), "ffi_echo_ptr"); + const echoI64 = $vm.ffiFunction({ args: ["i64"], returns: "i64" }, fixture("ffi_echo_i64"), "ffi_echo_i64"); + const sum4 = $vm.ffiFunction({ args: ["i32", "i32", "i32", "i32"], returns: "i64" }, fixture("ffi_sum_i32_4"), "ffi_sum_i32_4"); + const sum0 = $vm.ffiFunction({ args: [], returns: "i64" }, fixture("ffi_sum_i32_0"), "ffi_sum_i32_0"); + + // ---- JSFunction surface. + check(addI32.length, 2, "length"); + check(sum4.length, 4, "length of ffi_sum_i32_4"); + check(sum0.length, 0, "length of ffi_sum_i32_0"); + check(addI32.name, "ffi_add_i32", "name"); + check(typeof addI32, "function", "typeof"); + check(addI32 instanceof Function, true, "instanceof Function"); + check(Object.getPrototypeOf(addI32), Function.prototype, "prototype is Function.prototype"); + let constructThrew = false; + try { + new addI32(1, 2); + } catch (e) { + constructThrew = e instanceof TypeError; + } + check(constructThrew, true, "new on an FFI function throws TypeError"); + let reflectConstructThrew = false; + try { + Reflect.construct(addI32, [1, 2]); + } catch (e) { + reflectConstructThrew = e instanceof TypeError; + } + check(reflectConstructThrew, true, "Reflect.construct on an FFI function throws TypeError"); + + // ---- Missing arguments: undefined semantics per type. + check(addI32(), 0, "add_i32()"); + check(addI32(5), 5, "add_i32(5)"); + check(addI32(undefined, undefined), 0, "add_i32(undefined, undefined)"); + check(addF64(), NaN, "add_f64() -> NaN + NaN (missing f64 args are undefined -> NaN)"); + check(addF64(1), NaN, "add_f64(1) -> 1 + NaN"); + check(echoBool(), false, "echo_bool()"); + check(echoPtr(), null, "echo_ptr() -> null pointer"); + check(sum4(1, 2), 3n, "sum_i32_4 with two arguments"); + check(sum4(), 0n, "sum_i32_4 with no arguments"); + // i64 does NOT accept undefined (SPEC section 5): missing i64 arguments throw. + let i64Threw = false; + try { + echoI64(); + } catch (e) { + i64Threw = e instanceof TypeError; + } + check(i64Threw, true, "echo_i64() with a missing i64 argument throws TypeError"); + // f32 follows the same loose rule as f64: a missing argument is undefined -> NaN. + check(addF32(1.5), NaN, "add_f32(1.5) with a missing f32 argument is 1.5 + NaN"); + + // ---- Extra arguments are ignored. + check(addI32(1, 2, 3), 3, "add_i32(1,2,3)"); + check(addI32(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 3, "add_i32 with 12 arguments"); + check(sum0(1, 2, 3), 0n, "sum_i32_0 with extra arguments"); + check(echoBool(true, Symbol("ignored"), {}), true, "extra arguments of unconvertible types are ignored"); + check(addI32(1, 2, Symbol("ignored")), 3, "extra symbol argument is ignored"); + + // ---- Call protocols. + check(addI32.call(undefined, 40, 2), 42, "call"); + check(addI32.call(null, 40, 2, 99), 42, "call with extra"); + check(addI32.call({}, 40), 40, "call with this and a missing argument"); + check(addI32.apply(undefined, [40, 2]), 42, "apply"); + check(addI32.apply(undefined, [40]), 40, "apply short"); + check(addI32.apply(undefined, [40, 2, 3, 4]), 42, "apply long"); + check(addI32.apply(undefined), 0, "apply without a list"); + check(addI32(...[40, 2]), 42, "spread"); + check(addI32(...[40]), 40, "spread short"); + check(addI32(...new Array(30).fill(1)), 2, "spread of 30 ones"); + const bound = addI32.bind(null, 40); + check(bound(2), 42, "bound one argument"); + check(bound(), 40, "bound with a missing argument"); + check(bound(2, 3), 42, "bound with an extra argument"); + check(Reflect.apply(addI32, undefined, [40, 2]), 42, "Reflect.apply"); + check([[1, 2], [3, 4], [5, 6]].map(pair => addI32(...pair)).join(","), "3,7,11", "used in map"); + check(Array.from([[1, 2], [3, 4]], ([a, b]) => addI32(a, b)).join(","), "3,7", "used in Array.from"); + // Passing the FFI function itself as a callback to a builtin. + check([1, 2, 3].reduce(addI32), 6, "reduce with the FFI function directly (extra index/array arguments ignored)"); + + // ---- Hot exact-arity vs hot non-exact-arity call sites. + function exact(a, b) { return addI32(a, b); } + function missingOne(a) { return addI32(a); } + function extraOne(a, b, c) { return addI32(a, b, c); } + function viaCall(a, b) { return addI32.call(undefined, a, b); } + function viaApply(a, b) { return addI32.apply(undefined, [a, b]); } + function viaSpread(pair) { return addI32(...pair); } + noInline(exact); noInline(missingOne); noInline(extraOne); noInline(viaCall); noInline(viaApply); noInline(viaSpread); + for (let i = 0; i < 3e4; ++i) { + check(exact(i, 1), (i + 1) | 0, "hot exact"); + check(missingOne(i), i | 0, "hot missing one"); + check(extraOne(i, 2, 999), (i + 2) | 0, "hot extra one"); + check(viaCall(i, 3), (i + 3) | 0, "hot via call"); + check(viaApply(i, 4), (i + 4) | 0, "hot via apply"); + check(viaSpread([i, 5]), (i + 5) | 0, "hot via spread"); + } + // After tier-up, the same sites with the "wrong" number of arguments. + check(exact(1), 1, "exact site called with one argument after tier-up"); + check(exact(1, 2, 3), 3, "exact site called with three arguments after tier-up"); + // missingOne(a) forwards only `a`; its extra argument (2) never reaches the + // inner one-argument FFI call site, so this is still addI32(1, undefined) = 1 + 0. + check(missingOne(1, 2), 1, "missingOne site called with two arguments"); + check(extraOne(1), 1, "extraOne site called with one argument"); + + // A varargs wrapper (arity unknown at the site). + function varargs(...args) { return addI32(...args); } + noInline(varargs); + for (let i = 0; i < 2e4; ++i) + check(varargs(i, i), (i + i) | 0, "hot varargs"); + check(varargs(), 0, "varargs()"); + check(varargs(7), 7, "varargs(7)"); + check(varargs(1, 2, 3), 3, "varargs(1,2,3)"); + + // arguments object interplay. + function withArguments() { return addI32.apply(null, arguments); } + check(withArguments(9, 10), 19, "arguments object apply"); + check(withArguments(9), 9, "arguments object apply short"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-buffer-length.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-buffer-length.js new file mode 100644 index 000000000000..b2587629aadd --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-buffer-length.js @@ -0,0 +1,138 @@ +//@ requireOptions("--useDollarVM=1") +// FFI Type::BufferLength ("buffer_length"): the length twin of "buffer". Given a +// TypedArray / DataView argument it marshals the view's byteLength() as an unsigned 64-bit +// integer. Bound as args: ["ptr", "buffer_length"] with the SAME view passed for both, the +// engine reads pointer and length off one cell at call time. Argument-only; accepts exactly +// what "buffer" accepts (a view) and throws a TypeError for anything else. Every tier converts +// buffer_length through the C++ path, so a hot function and its noDFG-pinned reference twin +// must agree on every iteration. +if (!$vm.useJIT()) quit(); + +const fixture = name => $vm.ffiFixture(name); +const byteLength = $vm.ffiFunction({ args: ["ptr", "buffer_length"], returns: "u64" }, fixture("ffi_view_byte_length"), "ffi_view_byte_length"); +const byteLengthAlias = $vm.ffiFunction({ args: ["ptr", "buffer_bytelength"], returns: "u64" }, fixture("ffi_view_byte_length"), "ffi_view_byte_length(alias)"); +const lastByte = $vm.ffiFunction({ args: ["ptr", "buffer_length"], returns: "i32" }, fixture("ffi_view_last_byte"), "ffi_view_last_byte"); + +// Oracle twins pinned below the DFG: their (out-of-line C++ conversion) answer is what the +// tiered-up caller must reproduce exactly. +function refByteLength(v) { return byteLength(v, v); } +function refLastByte(v) { return lastByte(v, v); } +noDFG(refByteLength); noDFG(refLastByte); +noInline(refByteLength); noInline(refLastByte); + +function hotByteLength(v) { return byteLength(v, v); } +function hotLastByte(v) { return lastByte(v, v); } +noInline(hotByteLength); noInline(hotLastByte); + +let failures = 0; +function check(actual, expected, label) { + if (actual !== expected) { + print(`FAIL [${label}]: got ${String(actual)} (${typeof actual}), expected ${String(expected)} (${typeof expected})`); + if (++failures > 8) throw new Error("too many failures"); + } +} +function agree(label, hot, ref) { + if (hot !== ref) { + print(`TIER MISMATCH [${label}]: hot=${String(hot)} ref=${String(ref)}`); + if (++failures > 8) throw new Error("too many tier mismatches"); + } +} + +// --------------------------------------------------------------------------------------------- +// 1. The marshalled length is the view's byteLength: several sizes, a DataView, a subarray +// with a byteOffset, and the buffer_bytelength alias spelling. +// --------------------------------------------------------------------------------------------- +for (const size of [0, 1, 4, 4096]) { + const view = new Uint8Array(size); + check(refByteLength(view), BigInt(view.byteLength), `Uint8Array(${size}) byteLength`); + check(byteLengthAlias(view, view), BigInt(view.byteLength), `Uint8Array(${size}) via buffer_bytelength alias`); +} +{ + const backing = new ArrayBuffer(256); + const dataView = new DataView(backing, 32, 96); + check(refByteLength(dataView), 96n, "DataView(32, 96) byteLength"); + const wide = new Float64Array(backing, 64, 10); // byteLength is in BYTES, not elements + check(refByteLength(wide), 80n, "Float64Array(64, 10) byteLength"); + const sub = new Uint8Array(backing).subarray(100, 150); + check(refByteLength(sub), 50n, "subarray(100, 150) byteLength"); + // Pointer + length come off the same cell: the last byte through (ptr, byteLength) is the + // subarray's own last byte, not the backing store's. + sub[sub.length - 1] = 0x5a; + check(refLastByte(sub), 0x5a, "subarray pointer+length agree"); + check(refLastByte(new Uint8Array(0)), -1, "empty view last byte"); +} + +// --------------------------------------------------------------------------------------------- +// 2. Anything that is not a view throws a TypeError -- numbers included (unlike ptr, which +// accepts them). Identical message from a cold and a warmed caller. +// --------------------------------------------------------------------------------------------- +function expectTypeError(thunk, label) { + try { + thunk(); + } catch (error) { + if (!(error instanceof TypeError)) { + print(`FAIL [${label}]: threw ${describeError(error)}, expected a TypeError`); + ++failures; + } + return; + } + print(`FAIL [${label}]: did not throw`); + ++failures; +} +function describeError(error) { + try { return String(error); } catch { return Object.prototype.toString.call(error); } +} +const badValues = [ + [42, "number"], + [4096n, "bigint"], + ["not a view", "string"], + [{}, "plain object"], + [undefined, "undefined"], + [null, "null"], + [new ArrayBuffer(8), "ArrayBuffer (not a view)"], +]; +const validView = new Uint8Array(16); +for (const [bad, label] of badValues) + expectTypeError(() => byteLength(validView, bad), `cold buffer_length=${label}`); + +// --------------------------------------------------------------------------------------------- +// 3. Tier differential: hammer the hot twins alongside the noDFG oracles, then re-check that +// the bad-value TypeErrors still fire from the (now tiered-up) callers. +// --------------------------------------------------------------------------------------------- +const backing = new ArrayBuffer(4096); +const views = [ + new Uint8Array(0), + new Uint8Array(1), + new Uint8Array(4), + new Uint8Array(4096), + new DataView(backing, 8, 24), + new Uint8Array(backing).subarray(17, 900), + new Uint32Array(backing, 64, 7), + new Float64Array(3), +]; +views[1][0] = 0x7f; +views[3][4095] = 0x11; +const iterations = 50000; +for (let i = 0; i < iterations; ++i) { + const view = views[i % views.length]; + agree(`byteLength#${i}`, hotByteLength(view), refByteLength(view)); + agree(`lastByte#${i}`, hotLastByte(view), refLastByte(view)); + if (hotByteLength(view) !== BigInt(view.byteLength)) { + print(`FAIL [hot byteLength#${i}]: ${hotByteLength(view)} != ${view.byteLength}`); + if (++failures > 8) throw new Error("too many failures"); + } +} + +// The bad-value paths must still throw the same TypeError once the callers are hot. +function hotThrows(view, bad) { return byteLength(view, bad); } +noInline(hotThrows); +for (let i = 0; i < 20000; ++i) + hotThrows(validView, validView); +for (const [bad, label] of badValues) + expectTypeError(() => hotThrows(validView, bad), `hot buffer_length=${label}`); + +// buffer_length is argument-only: a "length" return type is rejected at signature creation. +expectTypeError(() => $vm.ffiFunction({ args: ["ptr"], returns: "buffer_length" }, fixture("ffi_view_byte_length"), "bad"), "buffer_length as return type"); + +if (failures) + throw new Error(`ffi-buffer-length: ${failures} failure(s)`); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-callback-throw-unwind.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callback-throw-unwind.js new file mode 100644 index 000000000000..1502eb1e1f0f --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callback-throw-unwind.js @@ -0,0 +1,34 @@ +//@ requireOptions("--useDollarVM=1") +if (!$vm.useJIT()) quit(); +// #2: FTL CallFFI stores topCallFrame but no CallSiteIndex. A callback invoked from inside the +// native call that THROWS then unwinds using the frame's STALE callSiteIndex (left by the last +// operation call in this FTL function). If an earlier try{} region installed a handler at that +// stale index, the exception is delivered to the WRONG catch -- one that does not enclose the call. +const callCbVoid = $vm.ffiFunction({ args: ["ptr"], returns: "void" }, $vm.ffiFixture("ffi_call_cb_void"), "call_cb_void"); +const boom = $vm.ffiCallback({ args: [], returns: "void" }, () => { throw new RangeError("from-callback"); }); +noInline(f); +function f(mode) { + // An earlier try/catch that becomes an FTL exception-handler region + call site. + try { + if (mode === "early") throw new TypeError("early"); // exercises this handler + JSON.parse('{"ok":true}'); // an operation call inside the try (sets a callSiteIndex) + } catch (e) { + return "EARLY_HANDLER:" + e.constructor.name; // must NEVER see the callback's RangeError + } + // The FFI call is OUTSIDE the try. Its callback throws. Correct behavior: it propagates OUT of f. + callCbVoid(boom.ptr); + return "no-exception"; +} +let out; +for (let i = 0; i < 100000; ++i) { + try { + out = f("normal"); + } catch (e) { + out = "PROPAGATED:" + e.constructor.name; // <-- the ONLY correct outcome + } + if (out !== "PROPAGATED:RangeError") { + throw new Error("WRONG at iteration " + i + ": " + out + + " (EARLY_HANDLER means the exception was routed to the try's stale handler)"); + } +} +if (out === "PROPAGATED:RangeError") print("OK: callback exception propagated correctly in all tiers"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-callbacks.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callbacks.js new file mode 100644 index 000000000000..bf68b9285f9c --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callbacks.js @@ -0,0 +1,441 @@ +//@ requireOptions("--useDollarVM=1") + +// JSFFICallback: native -> JS calls through every ffi_call_cb_* fixture. +// Covers argument marshaling into JS (register and stack ladders, sub-8-byte +// arguments, mixed classes), return-value coercion, exceptions surfacing at +// the FFI call site, GC inside a callback while a pointer argument is +// outstanding, re-entrancy (loop and nested to depth 100), and the +// JS -> native -> JS round trip of a callback wrapped back into an FFI +// function. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + if (typeof value === "symbol") + return value.toString(); + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- the only call-site shape the DFG +// Call -> CallFFI conversion accepts (SPEC section 10.2). The hot round-trip +// loop uses it so the JS -> native -> JS sandwich runs under a compiled +// CallFFI rather than a shared spread call site. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function checkThrows(fn, validate, message) { + let thrown = false; + try { + fn(); + } catch (e) { + thrown = true; + if (validate) + validate(e); + } + if (!thrown) + throw new Error(message + ": expected an exception"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name); + const callback = (args, ret, fn) => $vm.ffiCallback({ args, returns: ret }, fn); + + const callCbI32 = bind("ffi_call_cb_i32", ["function", "i32"], "i32"); + const callCbF64x8 = bind("ffi_call_cb_f64_x8", ["function", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64"); + const callCbF64x9 = bind("ffi_call_cb_f64_x9", ["function", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64"); + const callCbI32x9 = bind("ffi_call_cb_i32_x9", ["function", "i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32"], "i64"); + const callCbU8x10 = bind("ffi_call_cb_u8_x10", ["function", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], "i64"); + const callCbMix = bind("ffi_call_cb_mix", ["function", "i32", "f64", "i64", "f32", "ptr"], "f64"); + const callCbVoid = bind("ffi_call_cb_void", ["function"], "void"); + const callCbReentrant = bind("ffi_call_cb_reentrant", ["function", "i32"], "i32"); + const callCbRetI8 = bind("ffi_call_cb_ret_i8", ["function"], "i64_fast"); + const callCbRetU8 = bind("ffi_call_cb_ret_u8", ["function"], "i64_fast"); + const callCbRetI64 = bind("ffi_call_cb_ret_i64", ["function"], "i64"); + const callCbRetU64 = bind("ffi_call_cb_ret_u64", ["function"], "u64"); + const callCbRetBool = bind("ffi_call_cb_ret_bool", ["function"], "i32"); + const callCbRetF32 = bind("ffi_call_cb_ret_f32", ["function"], "f32"); + const callCbRetF64 = bind("ffi_call_cb_ret_f64", ["function"], "f64"); + const callCbRetPtr = bind("ffi_call_cb_ret_ptr", ["function"], "ptr"); + const callCbThenReadU32 = bind("ffi_call_cb_then_read_u32", ["function", "ptr"], "u32"); + const addI32 = bind("ffi_add_i32", ["i32", "i32"], "i32"); + + // ---- Basics: arguments in, results out, .ptr and object forms. + { + const cb = callback(["i32"], "i32", x => x * 2 + 1); + check(typeof cb.ptr, "number", "callback .ptr typeof"); + if (!(cb.ptr > 0)) + throw new Error("callback .ptr should be a positive address"); + check(cb.threadsafe, false, "callback .threadsafe"); + for (const x of [0, 1, -1, 21, 1073741823, -1073741824, 2147483647, -2147483648]) { + const expected = (x * 2 + 1) | 0; + check(callCbI32(cb, x), expected, "ffi_call_cb_i32(cb, " + x + ")"); + check(callCbI32(cb.ptr, x), expected, "ffi_call_cb_i32(cb.ptr, " + x + ")"); + } + for (let i = 0; i < 3e4; ++i) { + const r = callCbI32(cb, i & 1023); + if (r !== ((i & 1023) * 2 + 1)) + throw new Error("hot ffi_call_cb_i32 iteration " + i + " got " + r); + } + } + + // ---- Argument ladders into JS: 8 and 9 doubles, 9 int32s (stack args), 10 uint8s (packed stack args). + { + const received = []; + const recorder = (...args) => { + received.length = 0; + for (const a of args) + received.push(a); + let sum = 0; + for (let i = 0; i < args.length; ++i) + sum += (i + 1) * args[i]; + return sum; + }; + const cbF64x8 = callback(["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64", recorder); + const values8 = [0.5, -1.25, 3.75, -4.5, 5.0625, -6.5, 7.75, -8.875]; + check(callCbF64x8(cbF64x8, ...values8), values8.reduce((s, v, i) => s + (i + 1) * v, 0), "ffi_call_cb_f64_x8 result"); + for (let i = 0; i < 8; ++i) + check(received[i], values8[i], "ffi_call_cb_f64_x8 argument " + i); + + const cbF64x9 = callback(["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64", recorder); + const values9 = [1e10, -0.03125, 2 ** 40, -(2 ** 39), 0.5, 1.5, 2.5, 3.5, -0]; + check(callCbF64x9(cbF64x9, ...values9), values9.reduce((s, v, i) => s + (i + 1) * v, 0), "ffi_call_cb_f64_x9 result"); + for (let i = 0; i < 9; ++i) + check(received[i], values9[i], "ffi_call_cb_f64_x9 argument " + i); + + const recorderInt = (...args) => { + received.length = 0; + for (const a of args) + received.push(a); + let sum = 0; + for (let i = 0; i < args.length; ++i) + sum += (i + 1) * args[i]; + return sum; + }; + const cbI32x9 = callback(["i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32"], "i64", recorderInt); + const ints = [1, -2, 3, -4, 5, -6, 7, -8, 2147483647]; + check(callCbI32x9(cbI32x9, ...ints), 1n - 4n + 9n - 16n + 25n - 36n + 49n - 64n + 9n * 2147483647n, "ffi_call_cb_i32_x9 result"); + for (let i = 0; i < 9; ++i) + check(received[i], ints[i], "ffi_call_cb_i32_x9 argument " + i); + const negatives = [-2147483648, -1, -2147483648, -1, -2147483648, -1, -2147483648, -1, -2147483648]; + callCbI32x9(cbI32x9, ...negatives); + for (let i = 0; i < 9; ++i) + check(received[i], negatives[i], "ffi_call_cb_i32_x9 negative argument " + i); + + const cbU8x10 = callback(["u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], "i64", recorderInt); + const bytes = [255, 0, 128, 1, 200, 17, 254, 3, 99, 250]; + check(callCbU8x10(cbU8x10, ...bytes), BigInt(bytes.reduce((s, v, i) => s + (i + 1) * v, 0)), "ffi_call_cb_u8_x10 result"); + for (let i = 0; i < 10; ++i) + check(received[i], bytes[i], "ffi_call_cb_u8_x10 argument " + i); + // Distinct powers of two catch any swapped or dropped stack byte. + const powers = [1, 2, 4, 8, 16, 32, 64, 128, 3, 5]; + check(callCbU8x10(cbU8x10, ...powers), BigInt(powers.reduce((s, v, i) => s + (i + 1) * v, 0)), "ffi_call_cb_u8_x10 powers"); + for (let i = 0; i < 10; ++i) + check(received[i], powers[i], "ffi_call_cb_u8_x10 powers argument " + i); + } + + // ---- Mixed argument classes; pointer arguments arrive as numbers, null pointers as null. + { + let last = null; + const cbMixFast = callback(["i32", "f64", "i64_fast", "f32", "ptr"], "f64", (a, b, c, d, e) => { + last = [a, b, c, d, e]; + return a + 2 * b + 3 * Number(c) + 4 * d + 5 * (e === null ? -1 : e); + }); + check(callCbMix(cbMixFast, -5, 2.5, 4503599627370496, 1.25, 8192), -5 + 5 + 3 * 4503599627370496 + 5 + 40960, "ffi_call_cb_mix result"); + check(last[0], -5, "mix arg i32"); + check(last[1], 2.5, "mix arg f64"); + check(last[2], 4503599627370496, "mix arg i64_fast (Number range)"); + check(last[3], 1.25, "mix arg f32"); + check(last[4], 8192, "mix arg ptr"); + callCbMix(cbMixFast, 0, -0, 0, Math.fround(1.1), 0); + check(last[1], -0, "mix arg f64 keeps the sign of zero"); + check(last[3], Math.fround(1.1), "mix arg f32 is the exact float value"); + check(last[4], null, "mix arg ptr null becomes JS null"); + const cbMixBig = callback(["i32", "f64", "i64", "f32", "ptr"], "f64", (a, b, c, d, e) => { + last = [a, b, c, d, e]; + return 0; + }); + callCbMix(cbMixBig, 1, 2, 2n ** 62n, 3, 4); + check(last[2], 2n ** 62n, "mix arg i64 as BigInt"); + check(typeof last[2], "bigint", "mix arg i64 typeof"); + } + + // ---- Void callback and side effects. + { + let count = 0; + const cbVoid = callback([], "void", () => { count++; }); + check(callCbVoid(cbVoid), undefined, "ffi_call_cb_void returns undefined"); + check(count, 1, "void callback invoked once"); + for (let i = 0; i < 2e4; ++i) + callCbVoid(cbVoid); + check(count, 2e4 + 1, "void callback hot count"); + } + + // ---- Return-value coercion (what native code sees after conversion). + { + check(callCbRetU8(callback([], "u8", () => 511)), 255, "u8 callback return wraps mod 256"); + check(callCbRetU8(callback([], "u8", () => -1)), 255, "u8 callback return of -1"); + check(callCbRetI8(callback([], "i8", () => 128)), -128, "i8 callback return wraps"); + check(callCbRetI8(callback([], "i8", () => undefined)), 0, "i8 callback returning undefined -> 0"); + check(callCbRetI8(callback([], "i8", () => null)), 0, "i8 callback returning null -> 0"); + check(callCbRetI8(callback([], "i8", () => true)), 1, "i8 callback returning true -> 1"); + check(callCbRetI64(callback([], "i64", () => 2n ** 63n - 1n)), 9223372036854775807n, "i64 callback returning INT64_MAX BigInt"); + check(callCbRetI64(callback([], "i64", () => -1)), -1n, "i64 callback returning -1 number"); + check(callCbRetI64(callback([], "i64", () => 2 ** 53)), 9007199254740992n, "i64 callback returning 2^53 number"); + check(callCbRetI64(callback([], "i64", () => -1.75)), -1n, "i64 callback returning -1.75 truncates"); + check(callCbRetU64(callback([], "u64", () => -1)), 18446744073709551615n, "u64 callback returning -1"); + check(callCbRetU64(callback([], "u64", () => 2n ** 64n + 5n)), 5n, "u64 callback BigInt mod 2^64"); + check(callCbRetBool(callback([], "bool", () => 2)), 10, "bool callback returning 2 -> true"); + check(callCbRetBool(callback([], "bool", () => 0)), 20, "bool callback returning 0 -> false"); + check(callCbRetBool(callback([], "bool", () => null)), 20, "bool callback returning null -> false"); + check(callCbRetBool(callback([], "bool", () => -0.5)), 10, "bool callback returning -0.5 -> true"); + check(callCbRetBool(callback([], "bool", () => NaN)), 20, "bool callback returning NaN -> false"); + check(Number.isNaN(callCbRetF32(callback([], "f32", () => NaN))), true, "f32 callback NaN return"); + check(callCbRetF32(callback([], "f32", () => 1.1)), Math.fround(1.1), "f32 callback return is rounded to float"); + check(callCbRetF64(callback([], "f64", () => -0)), -0, "f64 callback -0 return"); + check(callCbRetF64(callback([], "f64", () => undefined)), NaN, "f64 callback returning undefined -> NaN"); + check(callCbRetPtr(callback([], "ptr", () => 0)), null, "ptr callback returning 0 -> null"); + check(callCbRetPtr(callback([], "ptr", () => 65536)), 65536, "ptr callback returning 65536"); + check(callCbRetPtr(callback([], "ptr", () => null)), null, "ptr callback returning null"); + const array = new Uint8Array(8); + const address = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, fixture("ffi_ptr_identity"), "identity")(array); + check(callCbRetPtr(callback([], "ptr", () => array)), address, "ptr callback returning a TypedArray"); + } + + // ---- Callback returning a value that cannot convert: TypeError at the call site. + { + const badReturns = [ + [callback([], "i8", () => "not a number"), callCbRetI8, "string for i8"], + [callback([], "i8", () => Symbol("s")), callCbRetI8, "symbol for i8"], + [callback([], "ptr", () => "string"), callCbRetPtr, "string for ptr"], + [callback([], "ptr", () => ({})), callCbRetPtr, "plain object for ptr"], + [callback([], "u64", () => "abc"), callCbRetU64, "string for u64"], + ]; + for (const [cb, caller, label] of badReturns) { + checkThrows(() => caller(cb), e => { + if (!(e instanceof TypeError)) + throw new Error(label + ": expected a TypeError, got " + e); + }, label); + } + } + + // ---- A throwing callback: the exception (with its JS stack) surfaces at the FFI call site. + { + function throwingCallback(x) { + if (x === 13) + throw new RangeError("thirteen from callback"); + return x + 1; + } + const cbThrow = callback(["i32"], "i32", throwingCallback); + check(callCbI32(cbThrow, 12), 13, "throwing callback fine path"); + let caught = null; + try { + callCbI32(cbThrow, 13); + } catch (e) { + caught = e; + } + if (!(caught instanceof RangeError)) + throw new Error("expected the callback's RangeError to propagate, got " + caught); + check(caught.message, "thirteen from callback", "callback exception message"); + if (typeof caught.stack !== "string") + throw new Error("expected the callback exception to carry a JS stack, got: " + caught.stack); + // The VM must be fully usable afterwards. + check(callCbI32(cbThrow, 41), 42, "callback usable after an exception"); + // Exceptions from within the last iteration of a native loop over the callback. + const cbThrowOnLast = callback(["i32"], "i32", i => { + if (i === 2) + throw new EvalError("last iteration"); + return i; + }); + checkThrows(() => callCbReentrant(cbThrowOnLast, 3), e => check(e instanceof EvalError, true, "EvalError from loop callback"), "loop callback throw"); + // In a hot loop with try/catch: every throw is caught, none escapes. + let count = 0; + for (let i = 0; i < 5000; ++i) { + try { + callCbI32(cbThrow, 13); + throw new Error("should not reach"); + } catch (e) { + if (e instanceof RangeError) + count++; + else + throw e; + } + } + check(count, 5000, "throwing callback in a hot try/catch loop"); + } + + // ---- gc() / fullGC() inside a callback while a TypedArray pointer argument is outstanding. + { + let churn = null; + const cbGC = callback([], "u32", () => { + for (let i = 0; i < 100; ++i) + churn = { i, payload: new Array(16).fill(i) }; + gc(); + fullGC(); + return 42; + }); + for (let i = 0; i < 20; ++i) { + // The Uint32Array is a temporary: only the outstanding native call + // references its storage while the callback collects. + const result = callCbThenReadU32(cbGC, new Uint32Array([123456789 + i])); + check(result, 123456789 + i, "read after GC-ing callback iteration " + i); + } + // Same, but the view is also written before the call and read after. + const persistent = new Uint32Array(4); + persistent[0] = 0xfeedface; + check(callCbThenReadU32(cbGC, persistent), 0xfeedface >>> 0, "persistent view read after GC-ing callback"); + check(persistent[0], 0xfeedface >>> 0, "persistent view intact after GC-ing callback"); + } + + // ---- Re-entrancy. + { + // (a) Loop of 100 callback invocations, each of which re-enters the engine + // through another FFI call. + let seen = 0; + const cbLoop = callback(["i32"], "i32", i => { + seen++; + return addI32(i, 1); + }); + check(callCbReentrant(cbLoop, 100), 5050, "ffi_call_cb_reentrant(cb, 100)"); + check(seen, 100, "loop callback invocation count"); + check(callCbReentrant(cbLoop, 0), 0, "ffi_call_cb_reentrant depth 0"); + + // (b) True nesting to depth 100: JS -> native -> JS -> native -> ... . + const nestCb = callback(["i32"], "i32", d => 1 + nest(d - 1)); + function nest(depth) { + if (depth <= 0) + return 0; + return callCbI32(nestCb, depth); + } + check(nest(100), 100, "nested FFI/callback depth 100"); + check(nest(1), 1, "nested depth 1"); + + // (c) A callback that calls the very FFI function that invoked it (with a base case). + const selfCb = callback(["i32"], "i32", x => x <= 0 ? 0 : callCbI32(selfCb, x - 1) + 2); + check(callCbI32(selfCb, 40), 80, "self-recursive callback"); + + // (d) An exception thrown at depth 50 unwinds through 50 native frames. + const deepThrowCb = callback(["i32"], "i32", d => { + if (d === 50) + throw new URIError("depth 50"); + return 1 + nestThrow(d - 1); + }); + function nestThrow(depth) { + if (depth <= 0) + return 0; + return callCbI32(deepThrowCb, depth); + } + checkThrows(() => nestThrow(80), e => check(e instanceof URIError, true, "deep unwind error type"), "exception from depth 50"); + // Everything still works afterwards. + check(nest(10), 10, "nested depth 10 after deep unwind"); + } + + // ---- A callback wrapped back into an FFI function: JS -> invoke thunk -> callback thunk -> JS. + { + const roundTrips = [ + [{ args: ["i32", "i32"], returns: "i32" }, (a, b) => (a - b) | 0, [[5, 3, 2], [0x7fffffff, -1, -2147483648], [-2147483648, 1, 2147483647]]], + [{ args: ["f64", "f64"], returns: "f64" }, (a, b) => a / b, [[1, 4, 0.25], [1, 0, Infinity], [-1, 0, -Infinity], [0, 0, NaN]]], + [{ args: ["f32"], returns: "f32" }, x => x * 2, [[1.5, 3], [Math.fround(1.1), Math.fround(1.1) * 2], [NaN, NaN], [1e39, Infinity]]], + [{ args: ["u8", "i16"], returns: "i64" }, (a, b) => BigInt(a * 1000 + b), [[255, -1, 254999n], [0, -32768, -32768n], [-1, 32767, 287767n]]], + [{ args: ["bool", "bool"], returns: "bool" }, (a, b) => a && !b, [[true, false, true], [2, 0, true], [0, 1, false]]], + [{ args: ["i64", "u64"], returns: "i64" }, (a, b) => a - b, [[10n, 3n, 7n], [-1n, 1n, -2n], [0, 0, 0n]]], + [{ args: ["char"], returns: "char" }, c => c, [[-1, -1], [255, -1], [0x80, -128], [127, 127]]], + ]; + for (const [signature, fn, cases] of roundTrips) { + const cb = $vm.ffiCallback(signature, fn); + const wrapped = $vm.ffiFunction(signature, cb, "roundtrip " + $vm.ffiSignatureString(signature)); + for (const c of cases) { + const inputs = c.slice(0, c.length - 1); + const expected = c[c.length - 1]; + check(wrapped(...inputs), expected, "round trip " + $vm.ffiSignatureString(signature) + "(" + inputs.map(describe).join(",") + ")"); + } + // Hot: the JS->native->JS sandwich under the JIT tiers, through a + // dedicated exact-arity monomorphic caller (a spread call site + // could never become a CallFFI). + const c = cases[0]; + const inputs = c.slice(0, c.length - 1); + const expected = c[c.length - 1]; + const caller = makeMonomorphicCaller(inputs.length); + for (let i = 0; i < 1e4; ++i) { + const r = caller(wrapped, inputs); + if (!Object.is(r, expected)) + throw new Error("hot round trip " + $vm.ffiSignatureString(signature) + " iteration " + i + " got " + describe(r)); + } + } + } + + // ---- Property surface; un-close()d callbacks are engine-rooted, so they SURVIVE gc()/fullGC() + // (the destructor runs only after close()); the collection here checks a rooted callback + // stays fully functional across a full GC. + { + const cb = callback(["i32"], "i32", x => x); + check(typeof cb.ptr, "number", "callback .ptr is a number"); + const descriptor = Object.getOwnPropertyDescriptor(cb, "ptr"); + check(descriptor !== undefined, true, "ptr is an own property"); + check(descriptor.writable === true, false, "ptr is read-only"); + check(descriptor.enumerable, false, "ptr is don't-enum"); + check(descriptor.configurable, false, "ptr is don't-delete"); + const threadsafeDescriptor = Object.getOwnPropertyDescriptor(cb, "threadsafe"); + check(threadsafeDescriptor !== undefined, true, "threadsafe is an own property"); + check(cb.threadsafe, false, "threadsafe is false"); + check(Object.keys(cb).length, 0, "own properties are non-enumerable"); + for (let i = 0; i < 200; ++i) + callback(["i32"], "i32", x => x + i); + gc(); + fullGC(); + // The surviving callback still works after a full collection. + check(callCbI32(cb, 41), 41, "callback survives GC"); + } + + // ---- The single close() rule (SPEC section 9.1) as seen from JS: `ptr` + // becomes null, close() is idempotent, the entry code stays alive with + // the cell (a pointer captured before close() keeps working), and the + // $vm glue rejects the closed object wherever it takes a pointer. + { + const closed = callback(["i32"], "i32", x => x + 100); + const entryBefore = closed.ptr; + check(typeof entryBefore, "number", "ptr before close"); + check(callCbI32(entryBefore, 5), 105, "call through the raw entry pointer before close"); + check(typeof closed.close, "function", "close is callable from JS"); + check(closed.close(), undefined, "close() returns undefined"); + check(closed.ptr, null, "ptr is null after close"); + check(closed.close(), undefined, "close() is idempotent"); + check(closed.ptr, null, "ptr stays null after a second close"); + const descriptor = Object.getOwnPropertyDescriptor(closed, "ptr"); + check(descriptor.value, null, "closed ptr descriptor value"); + check(descriptor.enumerable, false, "closed ptr stays don't-enum"); + // Nothing native-side was dropped: the code lives with the cell, so + // the entry pointer obtained earlier is still a valid callback. + check(callCbI32(entryBefore, 6), 106, "entry code alive after close"); + gc(); + check(callCbI32(entryBefore, 7), 107, "entry code alive after close and GC"); + // The $vm glue rejects a closed callback wherever it converts it to + // a pointer (target of ffiFunction, ffiCString, ffiRead). + for (const [label, use] of [ + ["ffiFunction target", () => $vm.ffiFunction({ args: ["i32"], returns: "i32" }, closed, "closed target")], + ["ffiCString", () => $vm.ffiCString(closed)], + ["ffiRead", () => $vm.ffiRead(closed, "u8")], + ]) { + checkThrows(use, e => { + if (!(e instanceof TypeError)) + throw new Error(label + " with a closed callback: expected a TypeError, got " + e); + if (String(e.message).indexOf("closed") === -1) + throw new Error(label + " with a closed callback: unexpected message: " + e.message); + }, label + " must reject a closed callback"); + } + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-callffi-was-compiled.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callffi-was-compiled.js new file mode 100644 index 000000000000..8e421a81cbd3 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-callffi-was-compiled.js @@ -0,0 +1,76 @@ +//@ requireOptions("--useDollarVM=1", "--useConcurrentJIT=0", "--jitPolicyScale=0") + +// Proves the ByteCodeParser feed + strength-reduction conversion are not +// dead code: after a hot exact-arity monomorphic call site, at least one +// CallFFI node must have been compiled by the DFG or FTL, and creating an FFI +// function must have compiled an IC entry stub (SPEC sections 10.2, 11.2). +// The compile counts are process-global atomics read via +// $vm.ffiCompileCounts(). + +function main() { + const before = $vm.ffiCompileCounts(); + if (typeof before !== "object" || typeof before.icStub !== "number" || typeof before.dfgCallFFI !== "number" || typeof before.ftlCallFFI !== "number") + throw new Error("bad $vm.ffiCompileCounts() shape: " + JSON.stringify(before)); + + const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, $vm.ffiFixture("ffi_add_i32"), "ffi_add_i32"); + const echoF64 = $vm.ffiFunction({ args: ["f64"], returns: "f64" }, $vm.ffiFixture("ffi_echo_f64"), "ffi_echo_f64"); + const echoBool = $vm.ffiFunction({ args: ["bool"], returns: "bool" }, $vm.ffiFixture("ffi_echo_bool"), "ffi_echo_bool"); + const echoPtr = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, $vm.ffiFixture("ffi_echo_ptr"), "ffi_echo_ptr"); + const echoI64 = $vm.ffiFunction({ args: ["i64"], returns: "i64" }, $vm.ffiFixture("ffi_echo_i64"), "ffi_echo_i64"); + + const afterCreation = $vm.ffiCompileCounts(); + if (afterCreation.icStub <= before.icStub) { + // The IC stub is generated eagerly in JSFFIFunction::create() when + // Options::useFFIICStub() (default true). + throw new Error("no IC stub was compiled by JSFFIFunction creation: before " + before.icStub + ", after " + afterCreation.icStub); + } + if (afterCreation.icStub < before.icStub + 5) + throw new Error("expected one IC stub per JSFFIFunction: before " + before.icStub + ", after " + afterCreation.icStub); + + // Exact-arity, monomorphic, hot: everything the conversion requires. + function hot(a, b) { + return addI32(a, b); + } + noInline(hot); + function hotTyped(d, flag, view, big) { + // Several typed CallFFI conversions in one code block. + const x = echoF64(d) + (echoBool(flag) ? 1 : 0); + const p = echoPtr(view); + const b = echoI64(big); + return x + (p === null ? 0 : 1) + Number(b & 0xffn); + } + noInline(hotTyped); + + const view = new Uint8Array(4); + let sink = 0; + for (let i = 0; i < 1e5; ++i) + sink += hot(i, 1); + if (sink !== 5000050000) + throw new Error("hot arithmetic wrong: " + sink); + for (let i = 0; i < 1e5; ++i) + sink += hotTyped(i + 0.5, i & 1, view, BigInt(i) & 0x7fn); + if (!Number.isFinite(sink)) + throw new Error("hotTyped produced a non-finite sum"); + + const counts = $vm.ffiCompileCounts(); + // Only demand CallFFI compilation when the DFG actually compiled the hot + // callers in this configuration (some harness configs disable the DFG). + const dfgRan = numberOfDFGCompiles(hot) > 0 || numberOfDFGCompiles(hotTyped) > 0; + if (dfgRan && counts.dfgCallFFI + counts.ftlCallFFI === 0) + throw new Error("DFG compiled the hot callers but no CallFFI node was compiled: " + JSON.stringify(counts)); + if (counts.dfgCallFFI + counts.ftlCallFFI < before.dfgCallFFI + before.ftlCallFFI) + throw new Error("compile counters went backwards"); + + // Results are still exactly right after tier-up. + if (hot(2147483647, 1) !== -2147483648) + throw new Error("hot(overflow) wrong after compilation"); + if (hot(-5, 10) !== 5) + throw new Error("hot(-5, 10) wrong after compilation"); + if (hotTyped(1.5, true, view, 255n) !== 1.5 + 1 + 1 + 255) + throw new Error("hotTyped exact value wrong after compilation"); + if (hotTyped(-0.25, false, null, -1n) !== -0.25 + 0 + 0 + Number(BigInt.asIntN(64, -1n) & 0xffn)) + throw new Error("hotTyped with null pointer wrong after compilation"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-canary.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-canary.js new file mode 100644 index 000000000000..15728b8856d8 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-canary.js @@ -0,0 +1,100 @@ +//@ requireOptions("--useDollarVM=1") + +// Callee-saved register canary (SPEC section 11.1): ffi_canary_call loads +// sentinels into every ABI-callee-saved GPR/FPR, calls the callback, and +// returns a bitmask of the registers that were clobbered. The callback thunk +// (native entry -> callbackDispatch -> JS) plus everything the JS side does +// must preserve all of them, in every JIT tier and with callbacks of every +// arity and behavior. + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const canary = $vm.ffiFunction({ args: ["function"], returns: "i32" }, fixture("ffi_canary_call"), "ffi_canary_call"); + const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, fixture("ffi_add_i32"), "ffi_add_i32"); + const sumF64_12 = $vm.ffiFunction({ args: new Array(12).fill("f64"), returns: "f64" }, fixture("ffi_sum_f64_12"), "ffi_sum_f64_12"); + const callCbI32 = $vm.ffiFunction({ args: ["function", "i32"], returns: "i32" }, fixture("ffi_call_cb_i32"), "ffi_call_cb_i32"); + const alignProbe0 = $vm.ffiFunction({ args: [], returns: "f64" }, fixture("ffi_align_probe_0"), "ffi_align_probe_0"); + const makeCanaryCallback = fn => $vm.ffiCallback({ args: [], returns: "void" }, fn); + + let sink = 0; + // Callbacks of every "arity"/shape wrapped as void(void) native callbacks. + const behaviours = [ + () => { }, + () => { sink++; }, + (a) => { sink += a === undefined ? 1 : 0; }, + (a, b, c, d, e, f, g, h, i, j, k, l) => { sink += (a === undefined) + (l === undefined); }, + (...rest) => { sink += rest.length; }, + function usesArguments() { sink += arguments.length; }, + () => { let x = 0; for (let i = 0; i < 200; ++i) x = Math.imul(x + i, 31) ^ (x >>> 7); sink += x & 1; }, + () => { const o = []; for (let i = 0; i < 500; ++i) o.push({ i, s: "s" + i }); sink += o.length; }, + () => { sink += addI32(20, 22); }, // re-enter an FFI function from inside the callback + () => { sink += sumF64_12(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) | 0; }, // fp-heavy re-entry + () => { const inner = makeCanaryCallback(() => { sink++; }); sink += canary(inner); }, // nested canary + () => { const cb = $vm.ffiCallback({ args: ["i32"], returns: "i32" }, x => x * 3); sink += callCbI32(cb, 14); }, + () => { let d = 0.5; for (let i = 0; i < 100; ++i) d = Math.sqrt(d + i) * 1.0001; sink += d | 0; }, // touches many FP registers + () => { gc(); }, + () => { fullGC(); }, + () => { try { throw new Error("caught inside"); } catch (e) { sink += e.message.length; } }, + () => { sink += alignProbe0() === 1 ? 1 : 100; }, // stack alignment inside the callback frame + () => { const big = 2n ** 200n + 1n; sink += Number(big % 3n); }, + () => { sink += "abc".repeat(64).length; }, + async () => { sink++; }, // returns a promise (ignored by the void return conversion) + () => 12345, // returns a value for a void callback: ignored + ]; + + for (let i = 0; i < behaviours.length; ++i) { + const cb = makeCanaryCallback(behaviours[i]); + const mask = canary(cb); + check(mask, 0, "canary with behaviour #" + i); + } + + // The same callbacks, but hot: the FFI call to ffi_canary_call itself goes + // through the IC stub / DFG / FTL paths, whose register state differs. The + // full-heap gc()/fullGC() behaviours (#13/#14) stay in the cold pass only: + // hundreds of synchronous full collections would blow the per-test time + // budget in debug builds without adding register coverage. + const hotBehaviours = behaviours.filter((_, index) => index !== 13 && index !== 14); + const hotCallbacks = hotBehaviours.map(makeCanaryCallback); + for (let iteration = 0; iteration < 4000; ++iteration) { + const cb = hotCallbacks[iteration % hotCallbacks.length]; + const mask = canary(cb); + if (mask !== 0) + throw new Error("canary clobber mask 0x" + mask.toString(16) + " at hot iteration " + iteration + " (behaviour #" + (iteration % hotCallbacks.length) + ")"); + } + + // A monomorphic hot loop so the caller reliably tiers up with one callback. + const trivial = makeCanaryCallback(() => { sink++; }); + for (let iteration = 0; iteration < 2e4; ++iteration) { + if (canary(trivial) !== 0) + throw new Error("canary trivial hot iteration " + iteration); + } + + // A canary callback that throws: the exception propagates from the + // canary's FFI call site (the canary's own return value is then not + // observable, but the throw path must not corrupt the frame either). + const throwing = makeCanaryCallback(() => { throw new TypeError("boom"); }); + let caught = 0; + for (let i = 0; i < 200; ++i) { + try { + canary(throwing); + } catch (e) { + if (!(e instanceof TypeError)) + throw new Error("wrong exception type from throwing canary callback: " + e); + caught++; + } + } + check(caught, 200, "throwing canary callbacks"); + // ... and the canary is unharmed afterwards. + check(canary(trivial), 0, "canary after exceptions"); + + if (sink < 0) + throw new Error("unreachable"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors-host.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors-host.js new file mode 100644 index 000000000000..384d9a580368 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors-host.js @@ -0,0 +1,261 @@ +//@ requireOptions("--useDollarVM=1", "--useFFIICStub=0", "--useFFICallInDFG=0") + +// Same battery as ffi-conversion-errors.js, but with the IC stub and the +// DFG/FTL CallFFI conversion disabled so every call takes the C++ host path +// (SPEC section 8.2). Argument-conversion failures must be TypeErrors with a +// message identical between the cold call and the hot (tiered-up caller) +// call. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (typeof value === "symbol") + return value.toString(); + if (typeof value === "function") + return "function"; + if (Object.is(value, -0)) + return "-0"; + try { + return String(value); + } catch { + return Object.prototype.toString.call(value); + } +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- kept identical to +// ffi-conversion-errors.js so the two files differ only in the option pair +// (here every call still takes the C++ host path). +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (fixtureName, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(fixtureName), fixtureName + "(" + args.join(",") + ")"); + + const detachedView = (() => { + const buffer = new ArrayBuffer(8); + const view = new Uint8Array(buffer); + if (typeof transferArrayBuffer === "function") + transferArrayBuffer(buffer); + else + buffer.transfer(); + return view; + })(); + const symbol = Symbol("bad"); + const plainObject = { valueOf() { return 42; }, toString() { return "42"; } }; + const array = [1, 2, 3]; + const jsFunction = function () { return 7; }; + const proxy = new Proxy({}, {}); + + // [callable, valid argument for warm-up, bad argument, label] + const echoI32 = bind("ffi_echo_i32", ["i32"], "i32"); + const echoU8 = bind("ffi_echo_u8", ["u8"], "u8"); + const echoI16 = bind("ffi_echo_i16", ["i16"], "i16"); + const echoBool = bind("ffi_echo_bool", ["bool"], "bool"); + const echoF64 = bind("ffi_echo_f64", ["f64"], "f64"); + const echoF32 = bind("ffi_echo_f32", ["f32"], "f32"); + const echoI64 = bind("ffi_echo_i64", ["i64"], "i64"); + const echoU64 = bind("ffi_echo_u64", ["u64"], "u64"); + const echoI64Fast = bind("ffi_echo_i64", ["i64_fast"], "i64_fast"); + const echoPtr = bind("ffi_echo_ptr", ["ptr"], "ptr"); + const echoCString = bind("ffi_echo_cstring", ["cstring"], "cstring"); + const bufferArg = bind("ffi_ptr_identity", ["buffer"], "ptr"); + const functionArg = bind("ffi_ptr_identity", ["function"], "ptr"); + const validCallback = $vm.ffiCallback({ args: [], returns: "void" }, () => { }); + const validView = new Uint8Array(16); + + const cases = [ + [echoI32, 1, symbol, "Symbol -> i32"], + [echoI32, 1, "42", "string -> i32 (strings never coerce into numeric params)"], + [echoU8, 1, "255", "string -> u8"], + [echoF64, 1.5, "1.5", "string -> f64"], + [echoF32, 1.5, "1.5", "string -> f32"], + [echoU8, 1, symbol, "Symbol -> u8"], + [echoF64, 1.5, symbol, "Symbol -> f64"], + [echoF32, 1.5, symbol, "Symbol -> f32"], + [echoI64, 1, symbol, "Symbol -> i64"], + [echoI64, 1, plainObject, "object -> i64"], + [echoI64, 1, "5", "string -> i64"], + [echoI64, 1, undefined, "undefined -> i64"], + [echoI64, 1, null, "null -> i64"], + [echoI64, 1, true, "boolean -> i64"], + [echoU64, 1, "5", "string -> u64"], + [echoU64, 1, undefined, "undefined -> u64"], + [echoU64, 1, plainObject, "object -> u64"], + [echoI64Fast, 1, symbol, "Symbol -> i64_fast"], + [echoI64Fast, 1, "5", "string -> i64_fast"], + [echoPtr, validView, symbol, "Symbol -> ptr"], + [echoPtr, validView, plainObject, "object -> ptr"], + [echoPtr, validView, "hello", "JS string -> ptr (only cstring transcodes)"], + [echoPtr, validView, array, "array -> ptr"], + [echoPtr, validView, jsFunction, "JS function -> ptr"], + [echoPtr, validView, proxy, "proxy -> ptr"], + // (BigInt -> ptr / cstring is ACCEPTED as an exact 64-bit address -- + // oven-sh/bun#22751, #28068 -- and is covered by ffi-pointers-and-buffers.js.) + [echoPtr, validView, true, "boolean -> ptr"], + [echoCString, validView, symbol, "Symbol -> cstring"], + [echoCString, validView, plainObject, "object -> cstring"], + [echoCString, validView, true, "boolean -> cstring"], + [bufferArg, validView, 5, "number -> buffer (buffer requires a view)"], + [bufferArg, validView, null, "null -> buffer"], + [bufferArg, validView, undefined, "undefined -> buffer"], + [bufferArg, validView, plainObject, "object -> buffer"], + [bufferArg, validView, "abc", "string -> buffer"], + [bufferArg, validView, new ArrayBuffer(8), "ArrayBuffer -> buffer (not a view)"], + [bufferArg, validView, symbol, "Symbol -> buffer"], + [functionArg, validCallback, "cb", "JS string -> function"], + [functionArg, validCallback, plainObject, "object -> function"], + [functionArg, validCallback, jsFunction, "raw JS function -> function (must be a JSFFICallback)"], + [functionArg, validCallback, symbol, "Symbol -> function"], + [functionArg, validCallback, true, "boolean -> function"], + ]; + + // ---- The loose-coercion contract (bun parity): [callable, input, expected, label]. + // These MUST NOT throw; they pin the exact coerced value the callee receives. + function checkCoercion(actual, expected, label) { + if (Number.isNaN(expected) ? !Number.isNaN(actual) : !Object.is(actual, expected)) + throw new Error("coercion " + label + ": expected " + describe(expected) + " but got " + describe(actual)); + } + const coercions = [ + [echoI32, plainObject, 42, "object.valueOf -> i32"], + [echoI32, array, 0, "array -> i32 (Number([1,2,3]) = NaN -> 0)"], + [echoI32, 10n, 10, "BigInt -> i32"], + [echoI32, true, 1, "true -> i32"], + [echoI32, null, 0, "null -> i32"], + [echoI32, undefined, 0, "undefined -> i32"], + [echoI32, 4294902015, -65281, "u32 pattern into i32 wraps (bun#7007 class)"], + [echoU8, 300, 44, "u8 wraps mod 256 (300 -> 44), never clamps"], + [echoU8, -1, 255, "u8 wraps negative (-1 -> 255)"], + [echoI16, jsFunction, 0, "function -> i16 (Number(fn) = NaN -> 0)"], + [echoBool, plainObject, true, "object -> bool"], + [echoBool, 1n, true, "BigInt 1n -> bool"], + [echoF64, plainObject, 42, "object.valueOf -> f64"], + [echoF64, true, 1, "true -> f64"], + [echoF64, null, 0, "null -> f64"], + [echoF64, undefined, NaN, "undefined -> f64 (Number(undefined) = NaN)"], + [echoF64, 2n, 2, "BigInt -> f64 (Number(5n)-style)"], + [echoF32, undefined, NaN, "undefined -> f32"], + [echoF32, null, 0, "null -> f32"], + [echoI64, 5, 5n, "number -> i64"], + [echoI64, 5n, 5n, "BigInt -> i64"], + ]; + for (const [callable, input, expected, label] of coercions) { + let actual; + try { + actual = callable(input); + } catch (e) { + throw new Error("coercion " + label + ": threw " + e); + } + checkCoercion(actual, expected, "cold " + label); + } + // ...and after tier-up the SAME coercions produce the SAME values. + for (const [callable, input, expected, label] of coercions) { + const caller = makeMonomorphicCaller(1); + for (let i = 0; i < 5000; ++i) + caller(callable, [input]); + checkCoercion(caller(callable, [input]), expected, "hot " + label); + } + + const coldMessages = new Map(); + for (const [callable, good, bad, label] of cases) { + // Sanity: the valid argument works. + callable(good); + let error = null; + try { + callable(bad); + } catch (e) { + error = e; + } + if (error === null) + throw new Error("cold: " + label + ": " + describe(bad) + " did not throw"); + if (!(error instanceof TypeError)) + throw new Error("cold: " + label + ": expected a TypeError, got " + error); + if (typeof error.message !== "string" || !error.message.length) + throw new Error("cold: " + label + ": TypeError has no message"); + coldMessages.set(label, error.message); + // The function must remain usable. + callable(good); + } + + // Warm every callable with valid arguments through its own monomorphic + // caller so that caller tiers up, then re-trigger the same error through + // the SAME (tiered-up) call site and demand the identical message. + for (const [callable, good, bad, label] of cases) { + const caller = makeMonomorphicCaller(1); + const goodArgs = [good]; + const badArgs = [bad]; + for (let i = 0; i < 3000; ++i) + caller(callable, goodArgs); + for (let i = 0; i < 150; ++i) { + let error = null; + try { + caller(callable, badArgs); + } catch (e) { + error = e; + } + if (error === null) + throw new Error("hot: " + label + " iteration " + i + " did not throw"); + if (!(error instanceof TypeError)) + throw new Error("hot: " + label + " iteration " + i + ": expected a TypeError, got " + error); + if (error.message !== coldMessages.get(label)) + throw new Error("hot: " + label + " iteration " + i + ": message \"" + error.message + "\" != cold \"" + coldMessages.get(label) + "\""); + // Interleave valid calls so the site stays optimized. + caller(callable, goodArgs); + } + } + + // A single hot function that alternates good and bad values (the same + // compiled CallFFI site takes both the fast and the throwing slow path). + function guarded(value) { + try { + return { ok: true, value: echoI32(value) }; + } catch (e) { + return { ok: false, error: e }; + } + } + noInline(guarded); + for (let i = 0; i < 8000; ++i) { + const result = guarded(i); + if (!result.ok || result.value !== (i | 0)) + throw new Error("guarded warm iteration " + i); + } + for (let i = 0; i < 3000; ++i) { + const bad = (i % 5) === 4; + const result = guarded(bad ? symbol : i); + if (bad) { + if (result.ok) + throw new Error("guarded(Symbol) did not throw at iteration " + i); + if (!(result.error instanceof TypeError)) + throw new Error("guarded(Symbol) wrong error at iteration " + i + ": " + result.error); + if (result.error.message !== coldMessages.get("Symbol -> i32")) + throw new Error("guarded(Symbol) message differs from the cold message at iteration " + i); + } else if (!result.ok || result.value !== (i | 0)) + throw new Error("guarded good iteration " + i); + } + + // FFI-SPEC-GAP: SPEC section 11.4 lists "detached buffer as ptr" among + // the TypeError cases, but the normative conversion table (section 5) + // says "vector() (0 if detached)". The normative rule wins here: detached + // views convert to a null pointer in every tier, without throwing. + for (let i = 0; i < 3; ++i) { + if (echoPtr(detachedView) !== null) + throw new Error("detached view as ptr should yield null (iteration " + i + ")"); + if (bufferArg(detachedView) !== null) + throw new Error("detached view as buffer should yield null (iteration " + i + ")"); + if (echoCString(detachedView) !== null) + throw new Error("detached view as cstring should yield null (iteration " + i + ")"); + } + for (let i = 0; i < 5000; ++i) { + if (echoPtr(i & 1 ? detachedView : validView) === undefined) + throw new Error("unreachable"); + } + if (echoPtr(detachedView) !== null) + throw new Error("detached view as ptr should yield null when hot"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors.js new file mode 100644 index 000000000000..f5f60991931a --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-conversion-errors.js @@ -0,0 +1,266 @@ +//@ requireOptions("--useDollarVM=1") + +// Argument-conversion failures must be TypeErrors, identical whether they +// come from the C++ host path, the IC stub's slow path or the DFG/FTL +// operationFFIWriteSlot slow path (SPEC section 11.4). This file captures +// each error message cold, then re-triggers the same failure from a hot +// (tiered-up) call site and requires the identical constructor and message. +// ffi-conversion-errors-host.js runs the same battery with the IC stub and +// CallFFI disabled. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (typeof value === "symbol") + return value.toString(); + if (typeof value === "function") + return "function"; + if (Object.is(value, -0)) + return "-0"; + try { + return String(value); + } catch { + return Object.prototype.toString.call(value); + } +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- the only call-site shape the DFG +// Call -> CallFFI conversion accepts (SPEC section 10.2). Each case below +// warms through its own caller so the bad value reaches the SAME optimized +// call site (typed check or operationFFIWriteSlot slow path) instead of a +// polymorphic shared site the DFG never converts. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (fixtureName, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(fixtureName), fixtureName + "(" + args.join(",") + ")"); + + const detachedView = (() => { + const buffer = new ArrayBuffer(8); + const view = new Uint8Array(buffer); + if (typeof transferArrayBuffer === "function") + transferArrayBuffer(buffer); + else + buffer.transfer(); + return view; + })(); + const symbol = Symbol("bad"); + const plainObject = { valueOf() { return 42; }, toString() { return "42"; } }; + const array = [1, 2, 3]; + const jsFunction = function () { return 7; }; + const proxy = new Proxy({}, {}); + + // [callable, valid argument for warm-up, bad argument, label] + const echoI32 = bind("ffi_echo_i32", ["i32"], "i32"); + const echoU8 = bind("ffi_echo_u8", ["u8"], "u8"); + const echoI16 = bind("ffi_echo_i16", ["i16"], "i16"); + const echoBool = bind("ffi_echo_bool", ["bool"], "bool"); + const echoF64 = bind("ffi_echo_f64", ["f64"], "f64"); + const echoF32 = bind("ffi_echo_f32", ["f32"], "f32"); + const echoI64 = bind("ffi_echo_i64", ["i64"], "i64"); + const echoU64 = bind("ffi_echo_u64", ["u64"], "u64"); + const echoI64Fast = bind("ffi_echo_i64", ["i64_fast"], "i64_fast"); + const echoPtr = bind("ffi_echo_ptr", ["ptr"], "ptr"); + const echoCString = bind("ffi_echo_cstring", ["cstring"], "cstring"); + const bufferArg = bind("ffi_ptr_identity", ["buffer"], "ptr"); + const functionArg = bind("ffi_ptr_identity", ["function"], "ptr"); + const validCallback = $vm.ffiCallback({ args: [], returns: "void" }, () => { }); + const validView = new Uint8Array(16); + + const cases = [ + [echoI32, 1, symbol, "Symbol -> i32"], + [echoI32, 1, "42", "string -> i32 (strings never coerce into numeric params)"], + [echoU8, 1, "255", "string -> u8"], + [echoF64, 1.5, "1.5", "string -> f64"], + [echoF32, 1.5, "1.5", "string -> f32"], + [echoU8, 1, symbol, "Symbol -> u8"], + [echoF64, 1.5, symbol, "Symbol -> f64"], + [echoF32, 1.5, symbol, "Symbol -> f32"], + [echoI64, 1, symbol, "Symbol -> i64"], + [echoI64, 1, plainObject, "object -> i64"], + [echoI64, 1, "5", "string -> i64"], + [echoI64, 1, undefined, "undefined -> i64"], + [echoI64, 1, null, "null -> i64"], + [echoI64, 1, true, "boolean -> i64"], + [echoU64, 1, "5", "string -> u64"], + [echoU64, 1, undefined, "undefined -> u64"], + [echoU64, 1, plainObject, "object -> u64"], + [echoI64Fast, 1, symbol, "Symbol -> i64_fast"], + [echoI64Fast, 1, "5", "string -> i64_fast"], + [echoPtr, validView, symbol, "Symbol -> ptr"], + [echoPtr, validView, plainObject, "object -> ptr"], + [echoPtr, validView, "hello", "JS string -> ptr (only cstring transcodes)"], + [echoPtr, validView, array, "array -> ptr"], + [echoPtr, validView, jsFunction, "JS function -> ptr"], + [echoPtr, validView, proxy, "proxy -> ptr"], + // (BigInt -> ptr / cstring is ACCEPTED as an exact 64-bit address -- + // oven-sh/bun#22751, #28068 -- and is covered by ffi-pointers-and-buffers.js.) + [echoPtr, validView, true, "boolean -> ptr"], + [echoCString, validView, symbol, "Symbol -> cstring"], + [echoCString, validView, plainObject, "object -> cstring"], + [echoCString, validView, true, "boolean -> cstring"], + [bufferArg, validView, 5, "number -> buffer (buffer requires a view)"], + [bufferArg, validView, null, "null -> buffer"], + [bufferArg, validView, undefined, "undefined -> buffer"], + [bufferArg, validView, plainObject, "object -> buffer"], + [bufferArg, validView, "abc", "string -> buffer"], + [bufferArg, validView, new ArrayBuffer(8), "ArrayBuffer -> buffer (not a view)"], + [bufferArg, validView, symbol, "Symbol -> buffer"], + [functionArg, validCallback, "cb", "JS string -> function"], + [functionArg, validCallback, plainObject, "object -> function"], + [functionArg, validCallback, jsFunction, "raw JS function -> function (must be a JSFFICallback)"], + [functionArg, validCallback, symbol, "Symbol -> function"], + [functionArg, validCallback, true, "boolean -> function"], + ]; + + // ---- The loose-coercion contract (bun parity): [callable, input, expected, label]. + // These MUST NOT throw; they pin the exact coerced value the callee receives. + function checkCoercion(actual, expected, label) { + if (Number.isNaN(expected) ? !Number.isNaN(actual) : !Object.is(actual, expected)) + throw new Error("coercion " + label + ": expected " + describe(expected) + " but got " + describe(actual)); + } + const coercions = [ + [echoI32, plainObject, 42, "object.valueOf -> i32"], + [echoI32, array, 0, "array -> i32 (Number([1,2,3]) = NaN -> 0)"], + [echoI32, 10n, 10, "BigInt -> i32"], + [echoI32, true, 1, "true -> i32"], + [echoI32, null, 0, "null -> i32"], + [echoI32, undefined, 0, "undefined -> i32"], + [echoI32, 4294902015, -65281, "u32 pattern into i32 wraps (bun#7007 class)"], + [echoU8, 300, 44, "u8 wraps mod 256 (300 -> 44), never clamps"], + [echoU8, -1, 255, "u8 wraps negative (-1 -> 255)"], + [echoI16, jsFunction, 0, "function -> i16 (Number(fn) = NaN -> 0)"], + [echoBool, plainObject, true, "object -> bool"], + [echoBool, 1n, true, "BigInt 1n -> bool"], + [echoF64, plainObject, 42, "object.valueOf -> f64"], + [echoF64, true, 1, "true -> f64"], + [echoF64, null, 0, "null -> f64"], + [echoF64, undefined, NaN, "undefined -> f64 (Number(undefined) = NaN)"], + [echoF64, 2n, 2, "BigInt -> f64 (Number(5n)-style)"], + [echoF32, undefined, NaN, "undefined -> f32"], + [echoF32, null, 0, "null -> f32"], + [echoI64, 5, 5n, "number -> i64"], + [echoI64, 5n, 5n, "BigInt -> i64"], + ]; + for (const [callable, input, expected, label] of coercions) { + let actual; + try { + actual = callable(input); + } catch (e) { + throw new Error("coercion " + label + ": threw " + e); + } + checkCoercion(actual, expected, "cold " + label); + } + // ...and after tier-up the SAME coercions produce the SAME values. + for (const [callable, input, expected, label] of coercions) { + const caller = makeMonomorphicCaller(1); + for (let i = 0; i < 5000; ++i) + caller(callable, [input]); + checkCoercion(caller(callable, [input]), expected, "hot " + label); + } + + const coldMessages = new Map(); + for (const [callable, good, bad, label] of cases) { + // Sanity: the valid argument works. + callable(good); + let error = null; + try { + callable(bad); + } catch (e) { + error = e; + } + if (error === null) + throw new Error("cold: " + label + ": " + describe(bad) + " did not throw"); + if (!(error instanceof TypeError)) + throw new Error("cold: " + label + ": expected a TypeError, got " + error); + if (typeof error.message !== "string" || !error.message.length) + throw new Error("cold: " + label + ": TypeError has no message"); + coldMessages.set(label, error.message); + // The function must remain usable. + callable(good); + } + + // Warm every callable with valid arguments through its own monomorphic + // caller so that caller tiers up with a converted CallFFI site, then + // re-trigger the same error through the SAME (optimized) call site and + // demand the identical message. + for (const [callable, good, bad, label] of cases) { + const caller = makeMonomorphicCaller(1); + const goodArgs = [good]; + const badArgs = [bad]; + for (let i = 0; i < 3000; ++i) + caller(callable, goodArgs); + for (let i = 0; i < 150; ++i) { + let error = null; + try { + caller(callable, badArgs); + } catch (e) { + error = e; + } + if (error === null) + throw new Error("hot: " + label + " iteration " + i + " did not throw"); + if (!(error instanceof TypeError)) + throw new Error("hot: " + label + " iteration " + i + ": expected a TypeError, got " + error); + if (error.message !== coldMessages.get(label)) + throw new Error("hot: " + label + " iteration " + i + ": message \"" + error.message + "\" != cold \"" + coldMessages.get(label) + "\""); + // Interleave valid calls so the site stays optimized. + caller(callable, goodArgs); + } + } + + // A single hot function that alternates good and bad values (the same + // compiled CallFFI site takes both the fast and the throwing slow path). + function guarded(value) { + try { + return { ok: true, value: echoI32(value) }; + } catch (e) { + return { ok: false, error: e }; + } + } + noInline(guarded); + for (let i = 0; i < 8000; ++i) { + const result = guarded(i); + if (!result.ok || result.value !== (i | 0)) + throw new Error("guarded warm iteration " + i); + } + for (let i = 0; i < 3000; ++i) { + const bad = (i % 5) === 4; + const result = guarded(bad ? symbol : i); + if (bad) { + if (result.ok) + throw new Error("guarded(Symbol) did not throw at iteration " + i); + if (!(result.error instanceof TypeError)) + throw new Error("guarded(Symbol) wrong error at iteration " + i + ": " + result.error); + if (result.error.message !== coldMessages.get("Symbol -> i32")) + throw new Error("guarded(Symbol) message differs from the cold message at iteration " + i); + } else if (!result.ok || result.value !== (i | 0)) + throw new Error("guarded good iteration " + i); + } + + // FFI-SPEC-GAP: SPEC section 11.4 lists "detached buffer as ptr" among + // the TypeError cases, but the normative conversion table (section 5) + // says "vector() (0 if detached)". The normative rule wins here: detached + // views convert to a null pointer in every tier, without throwing. + for (let i = 0; i < 3; ++i) { + if (echoPtr(detachedView) !== null) + throw new Error("detached view as ptr should yield null (iteration " + i + ")"); + if (bufferArg(detachedView) !== null) + throw new Error("detached view as buffer should yield null (iteration " + i + ")"); + if (echoCString(detachedView) !== null) + throw new Error("detached view as cstring should yield null (iteration " + i + ")"); + } + for (let i = 0; i < 5000; ++i) { + if (echoPtr(i & 1 ? detachedView : validView) === undefined) + throw new Error("unreachable"); + } + if (echoPtr(detachedView) !== null) + throw new Error("detached view as ptr should yield null when hot"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-fuzz-signatures.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-fuzz-signatures.js new file mode 100644 index 000000000000..b4d8e87e31b7 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-fuzz-signatures.js @@ -0,0 +1,320 @@ +//@ requireOptions("--useDollarVM=1") + +// Seeded fuzz (500+ cases) over the echo/add/sum fixtures with random edge +// values, verified against a JS reference implementation of the SPEC +// section 5 conversion rules in both directions. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (typeof value === "symbol") + return value.toString(); + if (Object.is(value, -0)) + return "-0"; + return String(value); +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- the only call-site shape the DFG +// Call -> CallFFI conversion accepts (SPEC section 10.2). The hot phase below +// gives each callee its own caller so the optimized typed path is what the +// random edges keep flowing through. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (fixtureName, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(fixtureName), fixtureName + "->" + ret); + + // ---- Deterministic PRNG (xorshift128+ over two 64-bit BigInt states, simplified via mulberry32). + let state = 0x0badc0de | 0; + function random() { + state = (state + 0x6D2B79F5) | 0; + let t = Math.imul(state ^ (state >>> 15), 1 | state); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + } + const randomIntBits = () => Math.floor(random() * 4294967296) - 2147483648; // uniform int32 + const pick = list => list[Math.floor(random() * list.length)]; + + // ---- JS reference of SPEC section 5. + const twoTo32 = 4294967296; + const twoTo64 = 2n ** 64n; + const twoTo63 = 2n ** 63n; + const MAX_INT52 = 9007199254740991; + // ECMAScript ToInt32 for the value classes we generate (number, boolean, undefined, null). + function toInt32(v) { + if (typeof v === "boolean") + return v ? 1 : 0; + if (v === undefined || v === null) + return 0; + const n = Number(v); + if (!Number.isFinite(n)) + return 0; + const t = Math.trunc(n) % twoTo32; + const u = t < 0 ? t + twoTo32 : t; + const r = u >= 2147483648 ? u - twoTo32 : u; + // Math.trunc(-0.999999) is -0 and survives the arithmetic above; ECMAScript + // ToInt32(-0.999999) is +0, and Object.is distinguishes 0 from -0, so + // normalize (r is always an integer here, so `+ 0` only flips -0 to +0). + return r + 0; + } + // Both hardware truncations agree exactly when |d| < 2^63; the fuzzer only + // generates such doubles (the arch-specific saturation edges live in + // testFFI's doubleToInt64 corpus). + function doubleToInt64(d) { + return BigInt(Math.trunc(d)); + } + const reference = { + "char": v => (toInt32(v) << 24) >> 24, + "i8": v => (toInt32(v) << 24) >> 24, + "u8": v => toInt32(v) & 0xff, + "i16": v => (toInt32(v) << 16) >> 16, + "u16": v => toInt32(v) & 0xffff, + "i32": v => toInt32(v), + "u32": v => toInt32(v) >>> 0, + "bool": v => Boolean(v), + "i64": v => { + if (typeof v === "bigint") + return BigInt.asIntN(64, v); + if (Number.isInteger(v) && Math.abs(v) <= 2147483647) + return BigInt(v); // int32 -> sign-extend + return BigInt.asIntN(64, doubleToInt64(v)); + }, + "u64": v => { + if (typeof v === "bigint") + return BigInt.asUintN(64, v); + if (Number.isInteger(v) && Math.abs(v) <= 2147483647) + return BigInt.asUintN(64, BigInt(v)); // int32 -> sign-extend then reinterpret + return BigInt.asUintN(64, doubleToInt64(v)); + }, + "i64_fast": v => { + const r = reference["i64"](v); + return (r >= BigInt(-MAX_INT52) && r <= BigInt(MAX_INT52)) ? Number(r) : r; + }, + "u64_fast": v => { + const r = reference["u64"](v); + return r < BigInt(MAX_INT52) ? Number(r) : r; + }, + // Bun parity: plain Number(); f32 is Math.fround of the same. + "f64": v => Number(v), + "f32": v => Math.fround(Number(v)), + "ptr": v => { + let bits; + if (v === null || v === undefined) + bits = 0n; + else if (Number.isInteger(v) && Math.abs(v) <= 2147483647) + bits = BigInt.asUintN(64, BigInt(v)); + else + bits = BigInt.asUintN(64, doubleToInt64(v)); + if (bits === 0n) + return null; + // Addresses above 2^53 are surfaced as an exact BigInt (bun#28068). + return bits <= 9007199254740991n ? Number(bits) : bits; + }, + }; + reference["cstring"] = value => (value === null || value === undefined ? null : String(value)); + + // ---- Value generators (per FFI type). + const int32Edges = [0, 1, -1, 2147483647, -2147483648, 2147483646, -2147483647, 65535, 65536, -65536, 255, 256, 127, 128, -128, -129, 32767, 32768, -32768]; + const doubleEdges = [0, -0, 0.5, -0.5, 1.5, -1.5, 2.5, 0.999999, -0.999999, 2 ** 31, -(2 ** 31), 2 ** 32 + 5, -(2 ** 32) - 5, 2 ** 52, 2 ** 53, 2 ** 53 - 1, -(2 ** 53), 2 ** 62, -(2 ** 62), 1e15 + 0.75, -1e15 - 0.75, NaN, Infinity, -Infinity, Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON]; + const bigIntEdges = [0n, 1n, -1n, twoTo63 - 1n, -twoTo63, twoTo63, twoTo64 - 1n, twoTo64, twoTo64 + 12345n, -twoTo64, 2n ** 100n + 7n, -(2n ** 90n), 9007199254740993n, 4611686018427387904n]; + const oddballs = [true, false, undefined, null]; + function genFor(type) { + switch (type) { + case "char": case "i8": case "u8": case "i16": case "u16": case "i32": case "u32": case "bool": + switch (Math.floor(random() * 4)) { + case 0: return pick(int32Edges); + case 1: return pick(doubleEdges); + case 2: return pick(oddballs); + default: return randomIntBits() * (random() < 0.5 ? 1 : 2.3); + } + case "i64": case "u64": case "i64_fast": case "u64_fast": { + switch (Math.floor(random() * 4)) { + case 0: return pick(int32Edges); + case 1: return pick(bigIntEdges); + case 2: return BigInt.asIntN(64, BigInt(randomIntBits()) * BigInt(randomIntBits()) * 4294967311n); + default: { + // doubles strictly inside (-2^63, 2^63) so both hardware truncations agree + const d = pick(doubleEdges.filter(x => Number.isFinite(x) && Math.abs(x) < 9007199254740992 * 512)); + return d; + } + } + } + case "f64": + return random() < 0.8 ? pick(doubleEdges) : randomIntBits() / (1 + Math.floor(random() * 7)); + case "f32": + return random() < 0.7 ? pick(doubleEdges) : randomIntBits() / 8; + case "cstring": + switch (Math.floor(random() * 5)) { + case 0: return pick([null, undefined]); + case 1: return ""; + case 2: return pick(["a", "hello", "with space", "0123456789".repeat(20)]); + case 3: return pick(["h\u00e9!", "\u2603 snowman", "\u{1F600} astral", "mix\u00e9d\u2603up"]); + default: return String(randomIntBits()); + } + case "ptr": + switch (Math.floor(random() * 4)) { + case 0: return pick([0, null, undefined, 4096, 65535, 0x7fffffff, -1, -4096]); + case 1: return pick([2 ** 40, 2 ** 47 - 1, 140737488355327, 0x00007fffdeadbee0]); + case 2: return Math.floor(random() * 2 ** 46); + default: return -Math.floor(random() * 2 ** 30); + } + } + throw new Error("no generator for " + type); + } + + // ---- Fixture bindings by declared FFI type (echo family). + const echoBindings = { + "char": bind("ffi_echo_char", ["char"], "char"), + "i8": bind("ffi_echo_i8", ["i8"], "i8"), + "u8": bind("ffi_echo_u8", ["u8"], "u8"), + "i16": bind("ffi_echo_i16", ["i16"], "i16"), + "u16": bind("ffi_echo_u16", ["u16"], "u16"), + "i32": bind("ffi_echo_i32", ["i32"], "i32"), + "u32": bind("ffi_echo_u32", ["u32"], "u32"), + "bool": bind("ffi_echo_bool", ["bool"], "bool"), + "i64": bind("ffi_echo_i64", ["i64"], "i64"), + "u64": bind("ffi_echo_u64", ["u64"], "u64"), + "i64_fast": bind("ffi_echo_i64", ["i64_fast"], "i64_fast"), + "u64_fast": bind("ffi_echo_u64", ["u64_fast"], "u64_fast"), + "f64": bind("ffi_echo_f64", ["f64"], "f64"), + "f32": bind("ffi_echo_f32", ["f32"], "f32"), + "ptr": bind("ffi_echo_ptr", ["ptr"], "ptr"), + "cstring": bind("ffi_echo_cstring", ["cstring"], "cstring"), + }; + // Echo semantics: the native fixture returns its argument unchanged, so the + // result is the JS->native argument conversion followed by the native->JS + // return boxing of the same type. + function echoReference(type, value) { + const asArgument = reference[type](value); + switch (type) { + case "bool": + return asArgument; // already a boolean + case "ptr": + return asArgument; // null or number + case "cstring": + return asArgument === null || asArgument === undefined ? null : String(asArgument); + default: + return asArgument; + } + } + const echoTypes = Object.keys(echoBindings); + + // ---- Two-argument adders. + const addI32 = bind("ffi_add_i32", ["i32", "i32"], "i32"); + const addF64 = bind("ffi_add_f64", ["f64", "f64"], "f64"); + const addI64 = bind("ffi_add_i64", ["i64", "i64"], "i64"); + const addU64 = bind("ffi_add_u64", ["u64", "u64"], "u64"); + const addF32 = bind("ffi_add_f32", ["f32", "f32"], "f32"); + const adders = [ + ["i32", addI32, (a, b) => (reference["i32"](a) + reference["i32"](b)) | 0], + ["f64", addF64, (a, b) => reference["f64"](a) + reference["f64"](b)], + ["i64", addI64, (a, b) => BigInt.asIntN(64, reference["i64"](a) + reference["i64"](b))], + ["u64", addU64, (a, b) => BigInt.asUintN(64, reference["u64"](a) + reference["u64"](b))], + ["f32", addF32, (a, b) => Math.fround(Math.fround(a) + Math.fround(b))], + ]; + + // ---- Sum ladders. + const sumI32_16 = bind("ffi_sum_i32_16", new Array(16).fill("i32"), "i64"); + const sumF64_12 = bind("ffi_sum_f64_12", new Array(12).fill("f64"), "f64"); + const sumU8_12 = bind("ffi_sum_u8_12", new Array(12).fill("u8"), "i64"); + const sumI16_12 = bind("ffi_sum_i16_12", new Array(12).fill("i16"), "i64"); + + let executed = 0; + let mismatches = 0; + function fail(message) { + mismatches++; + throw new Error(message); + } + function verify(label, actual, expected) { + executed++; + if (!Object.is(actual, expected)) + fail(label + ": expected " + describe(expected) + " but got " + describe(actual)); + } + + const totalCases = 600; + for (let caseIndex = 0; caseIndex < totalCases; ++caseIndex) { + const kind = random(); + if (kind < 0.55) { + // Echo case. + const type = pick(echoTypes); + const value = genFor(type); + const expected = echoReference(type, value); + const actual = echoBindings[type](value); + verify("echo " + type + "(" + describe(value) + ")", actual, expected); + } else if (kind < 0.8) { + // Adder case. + const [type, fn, ref] = pick(adders); + const a = genFor(type === "f32" ? "f32" : type); + const b = genFor(type === "f32" ? "f32" : type); + verify(type + " add(" + describe(a) + ", " + describe(b) + ")", fn(a, b), ref(a, b)); + } else if (kind < 0.9) { + // 16-way int32 ladder. + const values = new Array(16).fill(0).map(() => genFor("i32")); + let expected = 0n; + for (const v of values) + expected += BigInt(reference["i32"](v)); + verify("sum_i32_16(" + values.map(describe).join(",") + ")", sumI32_16(...values), expected); + } else if (kind < 0.95) { + // 12-way double ladder (finite dyadic values). Fold left-to-right + // starting from the first operand exactly like the fixture's + // `a0 + a1 + ... + a11`, so even the sign of a zero sum matches. + const values = new Array(12).fill(0).map(() => Math.round(random() * 1024 - 512) / 8); + let expected = values[0]; + for (let i = 1; i < values.length; ++i) + expected += values[i]; + verify("sum_f64_12(" + values.join(",") + ")", sumF64_12(...values), expected); + } else { + // Sub-8-byte stack ladders. + const useSigned = random() < 0.5; + const type = useSigned ? "i16" : "u8"; + const fn = useSigned ? sumI16_12 : sumU8_12; + const values = new Array(12).fill(0).map(() => genFor(type)); + let expected = 0n; + for (const v of values) + expected += BigInt(reference[type](v)); + verify("sum_" + type + "_12(" + values.map(describe).join(",") + ")", fn(...values), expected); + } + } + if (executed !== totalCases || mismatches !== 0) + throw new Error("fuzz bookkeeping: executed " + executed + ", mismatches " + mismatches); + + // A second, tighter phase: monomorphic random calls per callee, each + // through its own dedicated exact-arity caller, so the callers tier up + // (typed CallFFI sites) while the fuzzer keeps feeding random edges + // through the optimized code. + for (const [type, fn, ref] of adders) { + const caller = makeMonomorphicCaller(2); + const args = [0, 0]; + for (let i = 0; i < 3000; ++i) { + const a = genFor(type === "f32" ? "f32" : type); + const b = genFor(type === "f32" ? "f32" : type); + args[0] = a; + args[1] = b; + const actual = caller(fn, args); + const expected = ref(a, b); + if (!Object.is(actual, expected)) + throw new Error("hot fuzz " + type + " add(" + describe(a) + ", " + describe(b) + "): expected " + describe(expected) + " but got " + describe(actual) + " at iteration " + i); + } + } + for (const type of echoTypes) { + const fn = echoBindings[type]; + const caller = makeMonomorphicCaller(1); + const args = [0]; + for (let i = 0; i < 2000; ++i) { + const value = genFor(type); + args[0] = value; + const actual = caller(fn, args); + const expected = echoReference(type, value); + if (!Object.is(actual, expected)) + throw new Error("hot fuzz echo " + type + "(" + describe(value) + "): expected " + describe(expected) + " but got " + describe(actual) + " at iteration " + i); + } + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-hooks-and-owner.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-hooks-and-owner.js new file mode 100644 index 000000000000..b01c6484d44d --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-hooks-and-owner.js @@ -0,0 +1,53 @@ +//@ requireOptions("--useDollarVM=1") +// JSFFIFunction owner cell + CallHooks: +// 1) hooks bracket EVERY call as before:N / after:N with the token round-tripping; +// 2) after fires even when the call throws (a callback raised an exception mid-call); +// 3) a hooked function is host-path-only: it must NOT be lifted into a CallFFI node -- observable +// as the hooks still firing on every call after the caller is FTL-hot (a CallFFI node would +// bypass ffiHostCall, the only place hooks run, so the log would go quiet); +// 4) the owner is kept alive by the function (WeakRef stays populated while the fn is reachable). +if (!$vm.useJIT()) quit(); + +const owner = { hookLog: [] }; +const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, $vm.ffiFixture("ffi_add_i32"), "add_i32", { owner, hooks: "test" }); + +// (1) bracketing + token round trip +addI32(1, 2); +if (owner.hookLog.length !== 2) throw new Error("expected before+after, got " + JSON.stringify(owner.hookLog)); +const t = owner.hookLog[0].split(":")[1]; +if (owner.hookLog[0] !== "before:" + t || owner.hookLog[1] !== "after:" + t) + throw new Error("bad bracket order/token: " + JSON.stringify(owner.hookLog)); + +// (2) after runs even when the native call throws (callback throws inside the call) +const callCbVoid = $vm.ffiFunction({ args: ["ptr"], returns: "void" }, $vm.ffiFixture("ffi_call_cb_void"), "call_cb_void", { owner, hooks: "test" }); +const boom = $vm.ffiCallback({ args: [], returns: "void" }, () => { throw new RangeError("cb"); }); +owner.hookLog.length = 0; +let threw = false; +try { callCbVoid(boom.ptr); } catch (e) { threw = e instanceof RangeError; } +if (!threw) throw new Error("callback exception did not propagate"); +if (owner.hookLog.length !== 2 || !owner.hookLog[1].startsWith("after:")) + throw new Error("after hook did not run on the throwing call: " + JSON.stringify(owner.hookLog)); + +// (3) host-path-only: hooks keep firing on every call even when the caller is FTL-hot. +function hot(a, b) { return addI32(a, b); } +noInline(hot); +owner.hookLog.length = 0; +let sum = 0; +for (let i = 0; i < 20000; ++i) sum += hot(i, 1); +const expected = (19999 * 20000) / 2 + 20000; // sum over i in [0,20000) of (i+1) = 200010000 +if (sum !== expected) throw new Error("wrong sum " + sum + " != " + expected); +if (owner.hookLog.length !== 40000) + throw new Error("hooks stopped firing when hot (CallFFI took over?): " + owner.hookLog.length + " entries for 20000 calls"); + +// (4) owner liveness: the function alone must keep its owner reachable. +let ref; +(function () { + const localOwner = { hookLog: [] }; + ref = new WeakRef(localOwner); + globalThis.keptFn = $vm.ffiFunction({ args: ["i32"], returns: "i32" }, $vm.ffiFixture("ffi_echo_i32"), "echo42", { owner: localOwner, hooks: "test" }); +})(); +for (let i = 0; i < 5; ++i) { fullGC(); edenGC(); } +if (ref.deref() === undefined) throw new Error("owner was collected while its function is still reachable"); +if (keptFn(42) !== 42) throw new Error("kept function no longer callable"); +globalThis.keptFn = null; +print("ffi hooks + owner: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-host-path.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-host-path.js new file mode 100644 index 000000000000..3f1ed87aba67 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-host-path.js @@ -0,0 +1,222 @@ +//@ requireOptions("--useDollarVM=1", "--useFFIICStub=0", "--useFFICallInDFG=0") + +// FFI-SPEC-GAP: SPEC section 11.4 describes the host-path-vs-tiers +// differential as "a single file"; this companion file (row T also owns +// JSTests/stress/ffi-*.js) forces the host path with the option pair the +// spec names, so that path is exercised by the harness on every run. +// The C++ host-call path (SPEC section 8.2) only: no IC stub, no CallFFI. +// This is the same battery and the same HARDCODED expected table as +// ffi-tier-differential.js, so the host path is pinned to exactly the same +// answers the JIT tiers must produce (SPEC section 11.4). + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + if (typeof value === "symbol") + return value.toString(); + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread. Under this file's options the +// callee is always the C++ host path, but the caller shape is kept identical +// to ffi-tier-differential.js so the two differ only in the option pair. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name); + + const echoChar = bind("ffi_echo_char", ["char"], "char"); + const echoI8 = bind("ffi_echo_i8", ["i8"], "i8"); + const echoU8 = bind("ffi_echo_u8", ["u8"], "u8"); + const echoI16 = bind("ffi_echo_i16", ["i16"], "i16"); + const echoU16 = bind("ffi_echo_u16", ["u16"], "u16"); + const echoI32 = bind("ffi_echo_i32", ["i32"], "i32"); + const echoU32 = bind("ffi_echo_u32", ["u32"], "u32"); + const echoI64 = bind("ffi_echo_i64", ["i64"], "i64"); + const echoU64 = bind("ffi_echo_u64", ["u64"], "u64"); + const echoI64Fast = $vm.ffiFunction({ args: ["i64_fast"], returns: "i64_fast" }, fixture("ffi_echo_i64"), "ffi_echo_i64:fast"); + const echoU64Fast = $vm.ffiFunction({ args: ["u64_fast"], returns: "u64_fast" }, fixture("ffi_echo_u64"), "ffi_echo_u64:fast"); + const echoF32 = bind("ffi_echo_f32", ["f32"], "f32"); + const echoF64 = bind("ffi_echo_f64", ["f64"], "f64"); + const echoBool = bind("ffi_echo_bool", ["bool"], "bool"); + const echoPtr = bind("ffi_echo_ptr", ["ptr"], "ptr"); + const echoNapiValue = bind("ffi_echo_jsvalue", ["napi_value"], "napi_value"); + const addI32 = bind("ffi_add_i32", ["i32", "i32"], "i32"); + const addF64 = bind("ffi_add_f64", ["f64", "f64"], "f64"); + const addI64 = bind("ffi_add_i64", ["i64", "i64"], "i64"); + const addU64 = bind("ffi_add_u64", ["u64", "u64"], "u64"); + const addF32 = bind("ffi_add_f32", ["f32", "f32"], "f32"); + const sumI32_9 = bind("ffi_sum_i32_9", ["i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32"], "i64"); + const sumF64_9 = bind("ffi_sum_f64_9", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64"); + const sumU8_12 = bind("ffi_sum_u8_12", ["u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], "i64"); + const sumI16_10 = bind("ffi_sum_i16_10", ["i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16"], "i64"); + const mix1 = bind("ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"], "f64"); + const mix6 = bind("ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"], "f64"); + const widenChar = bind("ffi_widen_char", ["char"], "i64_fast"); + const widenU16 = bind("ffi_widen_u16", ["u16"], "i64_fast"); + const twoAsBool = bind("ffi_ret_two_as_bool", [], "bool"); + const retNullPtr = bind("ffi_ret_null_ptr", [], "ptr"); + const highPtr = bind("ffi_high_ptr", [], "ptr"); + const retNegOneI8 = bind("ffi_ret_neg_one_i8", [], "i8"); + const retNegOneU32 = bind("ffi_ret_neg_one_u32", [], "u32"); + const retNegOneU64 = bind("ffi_ret_neg_one_u64", [], "u64"); + const retDenormalF32 = bind("ffi_ret_denormal_f32", [], "f32"); + const retNegZeroF64 = bind("ffi_ret_neg_zero_f64", [], "f64"); + const retInfF64 = bind("ffi_ret_inf_f64", [], "f64"); + + const sharedObject = { shared: true }; + + // Each row: [callable, [arguments...], expectedLiteral, label] + // Every expected value below is a literal, not a computed reference. + const battery = [ + [echoChar, [-1], -1, "char(-1)"], + [echoChar, [255], -1, "char(255)"], + [echoChar, [0x80], -128, "char(0x80)"], + [echoI8, [127], 127, "i8(127)"], + [echoI8, [128], -128, "i8(128)"], + [echoI8, [0x1ff], -1, "i8(0x1ff)"], + [echoU8, [-1], 255, "u8(-1)"], + [echoU8, [511], 255, "u8(511)"], + [echoU8, [256], 0, "u8(256)"], + [echoI16, [32768], -32768, "i16(32768)"], + [echoI16, [-32769], 32767, "i16(-32769)"], + [echoU16, [-1], 65535, "u16(-1)"], + [echoU16, [70000], 4464, "u16(70000)"], + [echoI32, [2147483648], -2147483648, "i32(2^31)"], + [echoI32, [-2147483649], 2147483647, "i32(-2^31-1)"], + [echoI32, [4294967301], 5, "i32(2^32+5)"], + [echoI32, [-1.9], -1, "i32(-1.9)"], + [echoI32, [NaN], 0, "i32(NaN)"], + [echoI32, [Infinity], 0, "i32(Infinity)"], + [echoI32, [undefined], 0, "i32(undefined)"], + [echoI32, [true], 1, "i32(true)"], + [echoU32, [-1], 4294967295, "u32(-1)"], + [echoU32, [2147483648], 2147483648, "u32(2^31)"], + [echoU32, [4294967296], 0, "u32(2^32)"], + [echoI64, [0], 0n, "i64(0)"], + [echoI64, [-1], -1n, "i64(-1)"], + [echoI64, [4294967296], 4294967296n, "i64(2^32)"], + [echoI64, [2n ** 63n - 1n], 9223372036854775807n, "i64(2^63-1)"], + [echoI64, [2n ** 63n], -9223372036854775808n, "i64(2^63)"], + [echoI64, [-1.5], -1n, "i64(-1.5)"], + [echoI64, [9007199254740992], 9007199254740992n, "i64(2^53 as number)"], + [echoU64, [-1], 18446744073709551615n, "u64(-1)"], + [echoU64, [-2147483648], 18446744071562067968n, "u64(-2^31)"], + [echoU64, [2n ** 64n + 3n], 3n, "u64(2^64+3)"], + [echoI64Fast, [9007199254740991], 9007199254740991, "i64_fast(2^53-1)"], + [echoI64Fast, [-9007199254740991], -9007199254740991, "i64_fast(-(2^53-1))"], + [echoI64Fast, [2n ** 53n], 9007199254740992n, "i64_fast(2^53)"], + [echoI64Fast, [-(2n ** 53n)], -9007199254740992n, "i64_fast(-2^53)"], + [echoI64Fast, [-1], -1, "i64_fast(-1)"], + [echoU64Fast, [9007199254740990], 9007199254740990, "u64_fast(2^53-2)"], + [echoU64Fast, [2n ** 53n - 1n], 9007199254740991n, "u64_fast(2^53-1)"], + [echoU64Fast, [-1], 18446744073709551615n, "u64_fast(-1)"], + [echoF32, [1.1], 1.100000023841858, "f32(1.1)"], + [echoF32, [-0], -0, "f32(-0)"], + [echoF32, [NaN], NaN, "f32(NaN)"], + [echoF32, [1e39], Infinity, "f32(1e39)"], + [echoF32, [16777217], 16777216, "f32(2^24+1)"], + [echoF64, [-0], -0, "f64(-0)"], + [echoF64, [NaN], NaN, "f64(NaN)"], + [echoF64, [Number.MIN_VALUE], 5e-324, "f64(min denormal)"], + [echoF64, [undefined], NaN, "f64(undefined) -> NaN"], + [echoBool, [2], true, "bool(2)"], + [echoBool, [-1], true, "bool(-1)"], + [echoBool, [0], false, "bool(0)"], + [echoBool, [0.5], true, "bool(0.5)"], + [echoBool, [-0], false, "bool(-0)"], + [echoBool, [NaN], false, "bool(NaN)"], + [echoBool, [256], true, "bool(256)"], + [echoBool, [null], false, "bool(null)"], + [echoPtr, [0], null, "ptr(0)"], + [echoPtr, [null], null, "ptr(null)"], + [echoPtr, [undefined], null, "ptr(undefined)"], + [echoPtr, [-1], 18446744073709551615n, "ptr(-1) (exact BigInt, > 2^53)"], + [echoPtr, [1099511627776], 1099511627776, "ptr(2^40)"], + [echoNapiValue, [sharedObject], sharedObject, "napi_value(object)"], + [echoNapiValue, ["x"], "x", "napi_value(string)"], + [echoNapiValue, [-0], -0, "napi_value(-0)"], + [addI32, [2147483647, 1], -2147483648, "add_i32 overflow"], + [addI32, [-2147483648, -1], 2147483647, "add_i32 underflow"], + [addI32, [7], 7, "add_i32 missing argument"], + [addI32, [7, 8, 9], 15, "add_i32 extra argument"], + [addF64, [0.1, 0.2], 0.30000000000000004, "add_f64(0.1, 0.2)"], + [addF64, [-0, -0], -0, "add_f64(-0, -0)"], + [addF64, [Infinity, -Infinity], NaN, "add_f64(inf, -inf)"], + [addI64, [2n ** 63n - 1n, 1n], -9223372036854775808n, "add_i64 wrap"], + [addI64, [-1, -1], -2n, "add_i64(-1,-1)"], + [addU64, [-1, 2], 1n, "add_u64 wrap"], + [addU64, [2n ** 32n, 2n ** 32n], 8589934592n, "add_u64(2^32,2^32)"], + [addF32, [16777216, 1], 16777216, "add_f32 precision loss"], + [addF32, [0.5, 0.25], 0.75, "add_f32 dyadics"], + [addF32, [3.4e38, 3.4e38], Infinity, "add_f32 overflow to +inf"], + [sumI32_9, [1, -2, 3, -4, 5, -6, 7, -8, 100000], 99996n, "sum_i32_9"], + [sumI32_9, [2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647], 19327352823n, "sum_i32_9 max"], + [sumF64_9, [1, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625], 1.99609375, "sum_f64_9 dyadics"], + [sumU8_12, [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], 3060n, "sum_u8_12 max"], + [sumU8_12, [1, 2, 4, 8, 16, 32, 64, 128, -1, 256, 257, 511], 766n, "sum_u8_12 wrapped powers"], + [sumI16_10, [-32768, -32768, 32767, 32767, -1, 1, 40000, -40000, 65535, 65536], -3n, "sum_i16_10 edges"], + [mix1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 385, "mix_1 identity ramp"], + [mix1, [-2147483648, -0.5, -1000000, -1.5, 4096, 255, 0, -32768, 2, 2147483647], 19324112699, "mix_1 edges"], + [mix6, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 91, "mix_6 all ones"], + [mix6, [2, 0, -3, -1, -0.25, NaN, 0.5, 256, true, false, null, undefined, true], 28.25, "mix_6 truthiness edges"], + [widenChar, [-1], -1, "widen_char(-1)"], + [widenChar, [200], -56, "widen_char(200)"], + [widenU16, [-1], 65535, "widen_u16(-1)"], + [twoAsBool, [], true, "ret_two_as_bool"], + [retNullPtr, [], null, "ret_null_ptr"], + [highPtr, [], 0x00007fffdeadbee0, "high_ptr"], + [retNegOneI8, [], -1, "ret_neg_one_i8"], + [retNegOneU32, [], 4294967295, "ret_neg_one_u32"], + [retNegOneU64, [], 18446744073709551615n, "ret_neg_one_u64"], + [retDenormalF32, [], 2 ** -149, "ret_denormal_f32"], + [retNegZeroF64, [], -0, "ret_neg_zero_f64"], + [retInfF64, [], Infinity, "ret_inf_f64"], + ]; + + function runBattery(phase) { + for (const [callable, args, expected, label] of battery) { + const actual = callable(...args); + if (!Object.is(actual, expected)) + throw new Error(phase + " " + label + ": expected " + describe(expected) + " but got " + describe(actual)); + } + } + + // Cold pass (whatever tier the harness starts in). + runBattery("cold"); + // Warm each row through its OWN exact-arity, non-spread, single-callee + // caller (same shape as ffi-tier-differential.js), then re-run the whole + // battery. + for (const [callable, args, expected, label] of battery) { + const caller = makeMonomorphicCaller(args.length); + for (let i = 0; i < 4000; ++i) { + const actual = caller(callable, args); + if (!Object.is(actual, expected)) + throw new Error("warm " + label + " iteration " + i + ": expected " + describe(expected) + " but got " + describe(actual)); + } + } + runBattery("hot"); + // A few thousand mixed iterations across every row (megamorphic-ish). + for (let i = 0; i < 6000; ++i) { + const [callable, args, expected, label] = battery[i % battery.length]; + const actual = callable(...args); + if (!Object.is(actual, expected)) + throw new Error("mixed " + label + " iteration " + i + ": expected " + describe(expected) + " but got " + describe(actual)); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-jsvalue.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-jsvalue.js new file mode 100644 index 000000000000..a81abd692564 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-jsvalue.js @@ -0,0 +1,87 @@ +//@ requireOptions("--useDollarVM=1") + +// The "jsvalue" type is a raw EncodedJSValue pass-through in both directions: every JS value +// kind (objects, functions, symbols, -0, BigInt) must round-trip with identity intact. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (typeof value === "symbol") + return value.toString(); + if (Object.is(value, -0)) + return "-0"; + if (value !== null && (typeof value === "object" || typeof value === "function")) { + // Object.create(null), Proxy, etc. may have no toString/valueOf, so + // String(value) would throw "No default value" while merely + // formatting a message; describe by structure instead. + const tag = Object.prototype.toString.call(value); + return typeof value === "function" ? "[function " + (value.name || "anonymous") + "]" : tag; + } + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const echoNapiValue = $vm.ffiFunction({ args: ["jsvalue"], returns: "jsvalue" }, fixture("ffi_echo_jsvalue"), "ffi_echo_jsvalue"); + + // ---- napi_value: identity of arbitrary JSValues in both directions. + const object = { deep: { array: [1, 2, 3] } }; + const array = [1, "two", 3n]; + const fn = function named() { return 1; }; + const symbol = Symbol("napi"); + const registrySymbol = Symbol.for("napi.registry"); + const bigint = 123456789012345678901234567890n; + const values = [ + object, array, fn, symbol, registrySymbol, bigint, 0, -0, 1, -1, 0.5, NaN, Infinity, -Infinity, + 2147483647, -2147483648, 2147483648, 4294967295, Number.MAX_SAFE_INTEGER, Number.MIN_VALUE, + true, false, null, undefined, "", "string", "\u{1F600}", 0n, -1n, + new Uint8Array(3), new ArrayBuffer(2), Object.freeze({}), Object.create(null), + echoNapiValue, // a JSFFIFunction itself + $vm.ffiCallback({ args: [], returns: "void" }, () => { }), // a JSFFICallback + new Proxy({}, {}), new Error("as a value"), Promise.resolve(1), new Map(), new WeakRef(object), + ]; + for (const value of values) { + const result = echoNapiValue(value); + check(result, value, "napi_value identity for " + describe(value)); + if ((typeof value === "object" && value !== null) || typeof value === "function" || typeof value === "symbol") { + if (result !== value) + throw new Error("napi_value must preserve object identity (===), got a different object for " + describe(value)); + } + } + // Missing napi_value argument: undefined bits pass through. + check(echoNapiValue(), undefined, "missing napi_value argument is undefined"); + // Hot identity through the tiers with a few classes of values. + for (let i = 0; i < 3e4; ++i) { + const value = values[i % values.length]; + const result = echoNapiValue(value); + if (!Object.is(result, value)) + throw new Error("hot napi_value identity iteration " + i + " for " + describe(value) + " got " + describe(result)); + } + // Values created inside the loop (young objects): identity, and no GC crash. + for (let i = 0; i < 5000; ++i) { + const young = { i, payload: new Array(8).fill(i) }; + if (echoNapiValue(young) !== young) + throw new Error("young object identity iteration " + i); + if ((i & 1023) === 0) + gc(); + } + // napi_value inside a callback: JS -> native -> JS receives the very same values. + const seen = []; + const cb = $vm.ffiCallback({ args: ["jsvalue"], returns: "jsvalue" }, v => { seen.push(v); return v; }); + const throughCallback = $vm.ffiFunction({ args: ["jsvalue"], returns: "jsvalue" }, cb, "napi_value round trip"); + for (const value of values) { + seen.length = 0; + const result = throughCallback(value); + check(result, value, "callback napi_value round trip for " + describe(value)); + check(seen.length, 1, "callback invoked once"); + check(seen[0], value, "callback saw the identical value"); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-no-jit.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-no-jit.js new file mode 100644 index 000000000000..ceb7d891070c --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-no-jit.js @@ -0,0 +1,49 @@ +//@ runNoJIT +//@ requireOptions("--useDollarVM=1") + +// bun:ffi requires the JIT (SPEC section 0.1): with --useJIT=false the +// creation of JSFFIFunction / JSFFICallback must throw a TypeError with the +// message "bun:ffi requires the JIT", and nothing must crash. (When the +// harness runs this file with the JIT enabled anyway, creation must succeed.) + +function expectRequiresJIT(fn, label) { + let error = null; + try { + fn(); + } catch (e) { + error = e; + } + if (error === null) + throw new Error(label + ": expected a TypeError, nothing thrown"); + if (!(error instanceof TypeError)) + throw new Error(label + ": expected a TypeError, got " + error); + if (String(error.message).indexOf("bun:ffi requires the JIT") === -1) + throw new Error(label + ": unexpected message: " + error.message); +} + +const signature = { args: ["i32", "i32"], returns: "i32" }; +const callbackSignature = { args: ["i32"], returns: "i32" }; +const target = $vm.ffiFixture("ffi_add_i32"); + +if ($vm.useJIT()) { + // The harness may also run this file in JIT configurations: then creation + // works and the function is callable. + const add = $vm.ffiFunction(signature, target, "ffi_add_i32"); + if (add(40, 2) !== 42) + throw new Error("JIT configuration: ffi_add_i32(40, 2) !== 42"); + const cb = $vm.ffiCallback(callbackSignature, x => x + 1); + if (typeof cb.ptr !== "number") + throw new Error("JIT configuration: callback .ptr should be a number"); +} else { + for (let i = 0; i < 3; ++i) { + expectRequiresJIT(() => $vm.ffiFunction(signature, target, "ffi_add_i32"), "$vm.ffiFunction without JIT"); + expectRequiresJIT(() => $vm.ffiCallback(callbackSignature, x => x + 1), "$vm.ffiCallback without JIT"); + } + // Signature-only APIs and fixtures still work without the JIT. + if ($vm.ffiSignatureString(signature) !== "i32(i32,i32)") + throw new Error("ffiSignatureString should work without the JIT: " + $vm.ffiSignatureString(signature)); + if (typeof target !== "number" || !(target > 0)) + throw new Error("ffiFixture should return a pointer without the JIT: " + target); + if (!Array.isArray($vm.ffiFixtures()) || $vm.ffiFixtures().length < 90) + throw new Error("ffiFixtures should work without the JIT"); +} diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-non-int32-int-args.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-non-int32-int-args.js new file mode 100644 index 000000000000..98c29a531e8d --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-non-int32-int-args.js @@ -0,0 +1,23 @@ +//@ requireOptions("--useDollarVM=1") +// An integer FFI parameter validly accepts doubles / booleans / null (the conversion table), +// so a call site that passes such values must NOT get an unconditional Int32Use check (which +// would OSR-exit every call -> a deopt storm). The site must stay compiled and agree with the +// interpreter. We assert (a) tier agreement and (b) that the function is not endlessly recompiled. +if (!$vm.useJIT()) quit(); +const echoI32 = $vm.ffiFunction({ args: ["i32"], returns: "i32" }, $vm.ffiFixture("ffi_echo_i32"), "echo_i32"); +function ref(v) { return echoI32(v); } +function hot(v) { return echoI32(v); } +noDFG(ref); noInline(ref); noInline(hot); +let failures = 0; +const args = [true, false, null, undefined, 0.5, -1.5, 3.9, 2147483647.0, 1, 0]; +for (let i = 0; i < 200000; ++i) { + const a = args[i % args.length]; + const h = hot(a), r = ref(a); + if (!Object.is(h, r)) { print(`MISMATCH ${String(a)}: hot=${h} ref=${r}`); if (++failures > 4) throw new Error("tier mismatch"); } +} +// After 200k calls hot() must be optimized and STAY optimized (not exit-storming). +const compiles = numberOfDFGCompiles(hot); +print("DFG compiles of hot():", compiles); +if (compiles > 6) throw new Error(`hot() recompiled ${compiles} times -- deopt storm on valid non-int32 args`); +if (failures) throw new Error("failures"); +print("ffi non-int32 int args: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-osr-and-exceptions.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-osr-and-exceptions.js new file mode 100644 index 000000000000..2a2a6aea1d3b --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-osr-and-exceptions.js @@ -0,0 +1,244 @@ +//@ requireOptions("--useDollarVM=1") + +// FFI calls in optimized code: OSR-exit-inducing argument type changes midway +// through a hot loop, and exceptions (from callbacks and from argument +// conversion) thrown inside DFG/FTL-compiled code with and without a +// surrounding try/catch. Results must be identical before/after any exit. + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, fixture("ffi_add_i32"), "ffi_add_i32"); + const addF64 = $vm.ffiFunction({ args: ["f64", "f64"], returns: "f64" }, fixture("ffi_add_f64"), "ffi_add_f64"); + const echoBool = $vm.ffiFunction({ args: ["bool"], returns: "bool" }, fixture("ffi_echo_bool"), "ffi_echo_bool"); + const echoU8 = $vm.ffiFunction({ args: ["u8"], returns: "u8" }, fixture("ffi_echo_u8"), "ffi_echo_u8"); + const echoPtr = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, fixture("ffi_echo_ptr"), "ffi_echo_ptr"); + const callCbVoid = $vm.ffiFunction({ args: ["function"], returns: "void" }, fixture("ffi_call_cb_void"), "ffi_call_cb_void"); + const callCbI32 = $vm.ffiFunction({ args: ["function", "i32"], returns: "i32" }, fixture("ffi_call_cb_i32"), "ffi_call_cb_i32"); + + // ---- 1. Type change after the loop is hot: int32 -> double -> boolean -> undefined. + function hotAdd(a, b) { + return addI32(a, b); + } + noInline(hotAdd); + for (let i = 0; i < 3e4; ++i) { + const r = hotAdd(i, 1); + if (r !== ((i + 1) | 0)) + throw new Error("hotAdd int32 phase iteration " + i + " got " + r); + } + // Now feed non-int32 values through the same (compiled) call site. + check(hotAdd(0.5, 5), 5, "hotAdd(0.5, 5) after tier-up (toInt32(0.5) == 0)"); + check(hotAdd(2.9, -3.9), -1, "hotAdd(2.9, -3.9)"); + check(hotAdd(4294967296 + 7, 1), 8, "hotAdd(2^32 + 7, 1)"); + check(hotAdd(true, false), 1, "hotAdd(true, false)"); + check(hotAdd(undefined, 41), 41, "hotAdd(undefined, 41)"); + check(hotAdd(null, -2), -2, "hotAdd(null, -2)"); + check(hotAdd(NaN, 3), 3, "hotAdd(NaN, 3)"); + check(hotAdd(Infinity, 3), 3, "hotAdd(Infinity, 3)"); + check(hotAdd(-0, 3), 3, "hotAdd(-0, 3)"); + // Alternating types every iteration (the site cannot stay speculated). + for (let i = 0; i < 1e4; ++i) { + const a = (i & 1) ? i + 0.5 : i; + const r = hotAdd(a, 2); + if (r !== ((i + 2) | 0)) + throw new Error("hotAdd alternating phase iteration " + i + " got " + r); + } + // And back to int32 only: still correct after re-optimization. + for (let i = 0; i < 2e4; ++i) { + const r = hotAdd(i, -i); + if (r !== 0) + throw new Error("hotAdd re-warm iteration " + i + " got " + r); + } + + // ---- 2. Double edges: NaN / -0 / infinities through a hot double call site. + function hotAddF64(a, b) { + return addF64(a, b); + } + noInline(hotAddF64); + for (let i = 0; i < 3e4; ++i) { + const r = hotAddF64(i * 0.5, 0.25); + if (r !== i * 0.5 + 0.25) + throw new Error("hotAddF64 iteration " + i + " got " + r); + } + check(hotAddF64(NaN, 1), NaN, "hotAddF64(NaN, 1)"); + check(hotAddF64(-0, -0), -0, "hotAddF64(-0, -0)"); + check(hotAddF64(-0, 0), 0, "hotAddF64(-0, 0)"); + check(hotAddF64(Infinity, -Infinity), NaN, "hotAddF64(Inf, -Inf)"); + check(hotAddF64(1, undefined), NaN, "hotAddF64(1, undefined)"); // undefined -> NaN + check(hotAddF64(2, 3), 5, "hotAddF64 int32 arguments (Int32 -> Double)"); + check(hotAddF64(1e308, 1e308), Infinity, "hotAddF64 overflow"); + + // ---- 3. bool / u8 / ptr sites that see every input class after warm-up. + function hotBool(x) { + return echoBool(x); + } + noInline(hotBool); + for (let i = 0; i < 2e4; ++i) { + if (hotBool(true) !== true) + throw new Error("hotBool warm iteration " + i); + } + check(hotBool(0), false, "hotBool(0)"); + check(hotBool(2), true, "hotBool(2)"); + check(hotBool(-0), false, "hotBool(-0)"); + check(hotBool(NaN), false, "hotBool(NaN)"); + check(hotBool(0.5), true, "hotBool(0.5)"); + check(hotBool(null), false, "hotBool(null)"); + check(hotBool(undefined), false, "hotBool(undefined)"); + check(hotBool(256), true, "hotBool(256): any non-zero int32 is true (never and32(1))"); + for (let i = 0; i < 2e4; ++i) { + if (hotBool(i & 3) !== ((i & 3) !== 0)) + throw new Error("hotBool int32 phase iteration " + i); + } + + function hotU8(x) { + return echoU8(x); + } + noInline(hotU8); + for (let i = 0; i < 2e4; ++i) { + if (hotU8(i) !== (i & 0xff)) + throw new Error("hotU8 warm iteration " + i); + } + check(hotU8(-1), 255, "hotU8(-1)"); + check(hotU8(3.99), 3, "hotU8(3.99)"); + check(hotU8(300), 44, "hotU8(300) wraps mod 256"); + check(hotU8(true), 1, "hotU8(true)"); + check(hotU8(null), 0, "hotU8(null)"); + let symbolThrew = false; + try { + hotU8(Symbol("bad")); + } catch (e) { + symbolThrew = e instanceof TypeError; + } + check(symbolThrew, true, "hotU8(Symbol) throws a TypeError (Symbols do not coerce)"); + + function hotPtr(x) { + return echoPtr(x); + } + noInline(hotPtr); + const view = new Uint8Array(8); + const viewAddress = hotPtr(view); + for (let i = 0; i < 2e4; ++i) { + if (hotPtr(view) !== viewAddress) + throw new Error("hotPtr view warm iteration " + i); + } + check(hotPtr(0), null, "hotPtr(0)"); + check(hotPtr(null), null, "hotPtr(null)"); + check(hotPtr(4096), 4096, "hotPtr(number)"); + check(hotPtr(new ArrayBuffer(4)) > 0, true, "hotPtr(ArrayBuffer)"); + check(hotPtr(view), viewAddress, "hotPtr(view) after other classes"); + + // ---- 4. Exceptions thrown inside optimized code. + // (a) Conversion errors from an FFI argument, caught inside the hot loop. + function guarded(value) { + try { + return { ok: true, value: hotPtr(value) }; + } catch (e) { + if (!(e instanceof TypeError)) + throw new Error("expected TypeError from pointer conversion, got " + e); + return { ok: false, message: e.message }; + } + } + noInline(guarded); + for (let i = 0; i < 1e4; ++i) { + const good = guarded(view); + if (!good.ok || good.value !== viewAddress) + throw new Error("guarded warm iteration " + i); + } + const symbolResult = guarded(Symbol("nope")); + check(symbolResult.ok, false, "guarded(Symbol) throws TypeError in optimized code"); + const stringResult = guarded("not a pointer"); + check(stringResult.ok, false, "guarded(string) throws TypeError in optimized code"); + const objectResult = guarded({ length: 4 }); + check(objectResult.ok, false, "guarded(plain object) throws TypeError in optimized code"); + check(guarded(view).value, viewAddress, "guarded still fine after exceptions"); + for (let i = 0; i < 5000; ++i) { + const bad = (i % 100) === 99; + const result = guarded(bad ? Symbol.iterator : view); + if (bad !== !result.ok) + throw new Error("guarded mixed iteration " + i); + } + + // (b) A throwing callback inside a hot loop, try/catch inside the loop. + const throwingCb = $vm.ffiCallback({ args: [], returns: "void" }, () => { + throw new RangeError("callback says no"); + }); + function loopWithCatch(iterations) { + let caught = 0; + for (let i = 0; i < iterations; ++i) { + try { + callCbVoid(throwingCb); + } catch (e) { + if (e instanceof RangeError) + caught++; + else + throw e; + } + } + return caught; + } + noInline(loopWithCatch); + check(loopWithCatch(10), 10, "loopWithCatch cold"); + check(loopWithCatch(15000), 15000, "loopWithCatch hot"); + + // (c) A throwing callback with the try/catch OUTSIDE the hot function: + // the exception unwinds out of optimized code exactly once. + let armed = -1; + const armedCb = $vm.ffiCallback({ args: ["i32"], returns: "i32" }, x => { + if (x === armed) + throw new EvalError("armed at " + x); + return x * 2; + }); + function unguardedLoop(count) { + let sum = 0; + for (let i = 0; i < count; ++i) + sum += callCbI32(armedCb, i); + return sum; + } + noInline(unguardedLoop); + check(unguardedLoop(1000), 999000, "unguardedLoop warm 1"); + for (let i = 0; i < 30; ++i) + check(unguardedLoop(1000), 999000, "unguardedLoop warm loop " + i); + armed = 500; + let seen = null; + try { + unguardedLoop(1000); + } catch (e) { + seen = e; + } + if (!(seen instanceof EvalError) || seen.message !== "armed at 500") + throw new Error("expected the armed EvalError from optimized code, got " + seen); + armed = -1; + check(unguardedLoop(1000), 999000, "unguardedLoop after the exception"); + + // (d) An exception thrown by a JS callee INSIDE a callback that itself was + // invoked from an FFI call inside a try: nested unwinding. + const outerCb = $vm.ffiCallback({ args: ["i32"], returns: "i32" }, x => { + if (x < 3) + return unguardedLoopThrow(x); + return x; + }); + function unguardedLoopThrow(x) { + armed = 0; + try { + return unguardedLoop(10); + } finally { + armed = -1; + } + } + let nested = null; + try { + callCbI32(outerCb, 1); + } catch (e) { + nested = e; + } + if (!(nested instanceof EvalError)) + throw new Error("expected the nested EvalError, got " + nested); + check(callCbI32(outerCb, 7), 7, "outer callback usable after nested throw"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-pointers-and-buffers.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-pointers-and-buffers.js new file mode 100644 index 000000000000..d56249ade0b3 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-pointers-and-buffers.js @@ -0,0 +1,193 @@ +//@ requireOptions("--useDollarVM=1") + +// Pointer-family conversions: TypedArray / DataView / ArrayBuffer / number +// arguments, detached views, cstring transcoding of JS strings (a new +// capability, SPEC section 5), pointer round trips and raw memory pokes via +// $vm.ffiRead / $vm.ffiWrite. + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name); + + const ptrIdentity = bind("ffi_ptr_identity", ["ptr"], "ptr"); + const ptrWriteU32 = bind("ffi_ptr_write_u32", ["ptr", "u32"], "void"); + const ptrReadU32 = bind("ffi_ptr_read_u32", ["ptr"], "u32"); + const strlen = bind("ffi_strlen", ["cstring"], "u64"); + const highPtr = bind("ffi_high_ptr", [], "ptr"); + const retNullPtr = bind("ffi_ret_null_ptr", [], "ptr"); + const echoPtr = bind("ffi_echo_ptr", ["ptr"], "ptr"); + const echoCString = bind("ffi_echo_cstring", ["cstring"], "cstring"); + const bufferArg = $vm.ffiFunction({ args: ["buffer"], returns: "ptr" }, fixture("ffi_ptr_identity"), "ffi_ptr_identity(buffer)"); + + // ---- Null in, null out. + check(retNullPtr(), null, "ffi_ret_null_ptr()"); + check(ptrIdentity(0), null, "ffi_ptr_identity(0)"); + check(ptrIdentity(null), null, "ffi_ptr_identity(null)"); + check(ptrIdentity(undefined), null, "ffi_ptr_identity(undefined)"); + check(echoPtr(0), null, "ffi_echo_ptr(0)"); + check(echoPtr(null), null, "ffi_echo_ptr(null)"); + + // ---- TypedArray / DataView / ArrayBuffer addresses are consistent. + const buffer = new ArrayBuffer(64); + const u8 = new Uint8Array(buffer); + const u32 = new Uint32Array(buffer); + const u32Offset = new Uint32Array(buffer, 8, 4); + const dataView = new DataView(buffer, 12, 8); + const base = ptrIdentity(u8); + check(typeof base, "number", "typed array address typeof"); + if (!(base > 0)) + throw new Error("expected a positive address, got " + base); + check(ptrIdentity(u8), base, "address is stable"); + check(ptrIdentity(u32), base, "views of the same buffer share the base address"); + check(ptrIdentity(buffer), base, "ArrayBuffer -> data()"); + check(ptrIdentity(u32Offset), base + 8, "byteOffset is honored (Uint32Array)"); + check(ptrIdentity(dataView), base + 12, "byteOffset is honored (DataView)"); + check(ptrIdentity(u8.subarray(3)), base + 3, "byteOffset is honored (subarray)"); + check(bufferArg(u32Offset), base + 8, "Type::Buffer view"); + check(ptrIdentity(base), base, "numeric pointer round trip"); + check(echoPtr(base + 5), base + 5, "numeric pointer arithmetic round trip"); + + // ---- Writes/reads through native pointers. + ptrWriteU32(u32, 0xdeadbeef); + check(u32[0], 0xdeadbeef >>> 0, "ffi_ptr_write_u32 through a Uint32Array"); + ptrWriteU32(u32Offset, 7); + check(u32[2], 7, "ffi_ptr_write_u32 through an offset view"); + ptrWriteU32(base + 4, 0x11223344); + check(u32[1], 0x11223344, "ffi_ptr_write_u32 through a numeric pointer"); + u32[3] = 0xffffffff; + check(ptrReadU32(u32Offset.subarray(1)), 4294967295, "ffi_ptr_read_u32 returns unsigned above INT32_MAX"); + u32[3] = 0x80000000; + check(ptrReadU32(base + 12), 2147483648, "ffi_ptr_read_u32 of 0x80000000"); + ptrWriteU32(u32, -1); // u32 argument: toInt32 then reinterpret + check(u32[0], 4294967295, "u32 argument -1 wraps to 0xffffffff"); + ptrWriteU32(u32, 4294967296 + 9); + check(u32[0], 9, "u32 argument wraps mod 2^32"); + + // ---- $vm.ffiRead / $vm.ffiWrite over the same memory. + $vm.ffiWrite(base, "u8", 200); + check(u8[0], 200, "$vm.ffiWrite u8"); + check($vm.ffiRead(base, "u8"), 200, "$vm.ffiRead u8"); + check($vm.ffiRead(base, "i8"), -56, "$vm.ffiRead i8 sign"); + const f64 = new Float64Array(buffer, 32, 2); + $vm.ffiWrite(base + 32, "f64", -0.5); + check(f64[0], -0.5, "$vm.ffiWrite f64"); + f64[1] = Math.PI; + check($vm.ffiRead(base + 40, "f64"), Math.PI, "$vm.ffiRead f64"); + $vm.ffiWrite(base + 32, "f32", 1.5); + check(new Float32Array(buffer, 32, 1)[0], 1.5, "$vm.ffiWrite f32"); + $vm.ffiWrite(base + 8, "i32", -123456789); + check($vm.ffiRead(base + 8, "i32"), -123456789, "$vm.ffiRead i32"); + check(new Int32Array(buffer, 8, 1)[0], -123456789, "$vm.ffiWrite i32 visible to JS"); + + // ---- cstring arguments: JS strings are transcoded to NUL-terminated UTF-8. + check(strlen("hello"), 5n, 'strlen("hello")'); + check(strlen(""), 0n, 'strlen("")'); + check(strlen("héllo"), 6n, "strlen of a Latin-1 string counts UTF-8 bytes"); + check(strlen("\u{1D11E}"), 4n, "strlen of an astral character counts 4 UTF-8 bytes"); + check(strlen("→←"), 6n, "strlen of two BMP arrows"); + check(strlen("mixed é \u{1F600} end"), BigInt(6 + 2 + 1 + 4 + 4), "strlen mixed"); + let rope = ""; + for (let i = 0; i < 200; ++i) + rope += "ab"; // built by concatenation -> rope until resolved + check(strlen(rope), 400n, "strlen of a rope"); + let astralRope = ""; + for (let i = 0; i < 50; ++i) + astralRope += "\u{1D11E}x"; + check(strlen(astralRope), 250n, "strlen of an astral rope"); + // A NUL-containing string is truncated at the NUL by strlen (the copy is faithful). + check(strlen("abc\0def"), 3n, "strlen stops at embedded NUL"); + // TypedArrays are also accepted for cstring parameters (pointer semantics). + const cstringBytes = new Uint8Array([0x66, 0x66, 0x69, 0x00, 0x21]); // "ffi\0!" + check(strlen(cstringBytes), 3n, "strlen of a Uint8Array cstring"); + check(strlen(cstringBytes.subarray(1)), 2n, "strlen of a Uint8Array subarray cstring"); + + const utf8Bytes = new Uint8Array([0x68, 0xc3, 0xa9, 0x21, 0x00]); // "hé!" + check(echoCString(utf8Bytes), "hé!", "ffi_echo_cstring decodes the returned UTF-8 to a string"); + check($vm.ffiCString(ptrIdentity(utf8Bytes)), "hé!", "$vm.ffiCString decodes UTF-8"); + check($vm.ffiCString(ptrIdentity(cstringBytes)), "ffi", "$vm.ffiCString stops at NUL"); + check(echoCString(0), null, "ffi_echo_cstring(0) is null"); + check(echoCString("round trip"), "round trip", "a JS string round-trips through cstring"); + check(echoCString(null), null, "ffi_echo_cstring(null)"); + check(echoCString("transient"), "transient", "arena-copied cstring argument round-trips"); + + // ---- ffi_high_ptr: full 47-bit user-space pointer round trip. + check(highPtr(), 0x00007fffdeadbee0, "ffi_high_ptr()"); + check(ptrIdentity(highPtr()), 0x00007fffdeadbee0, "ffi_high_ptr round trip through ffi_ptr_identity"); + check(ptrIdentity(0x00007fffdeadbee0), 0x00007fffdeadbee0, "high pointer literal round trip"); + // Sign-extension of int32 pointer arguments: -1 becomes all-ones. That + // address exceeds 2^53, so it comes back as an EXACT BigInt rather than a + // lossy double (SPEC section 5 pointer rule, oven-sh/bun#28068). + check(ptrIdentity(-1), 18446744073709551615n, "ffi_ptr_identity(-1) reads back as exactly 0xFFFFFFFFFFFFFFFF"); + check(ptrIdentity(-4096), 18446744073709547520n, "ffi_ptr_identity(-4096) sign-extends (exact)"); + // ...and a BigInt address round-trips back into a pointer argument unchanged. + check(ptrIdentity(18446744073709551615n), 18446744073709551615n, "BigInt pointer argument round trip"); + check(ptrIdentity(0x123456789abn), 0x123456789ab, "small BigInt pointer comes back as a plain number"); + + // ---- Detached buffers: vector() is null, so the pointer is 0 -> null (SPEC section 5). + { + const detachable = new ArrayBuffer(16); + const detachedView = new Uint8Array(detachable); + if (typeof transferArrayBuffer === "function") + transferArrayBuffer(detachable); + else + detachable.transfer(); + if (detachedView.length !== 0) + throw new Error("expected a detached view"); + for (let i = 0; i < 3; ++i) + check(ptrIdentity(detachedView), null, "detached TypedArray converts to a null pointer (iteration " + i + ")"); + } + + // ---- Hot loops: cell arguments through the JIT tiers with GC pressure. + const hot = new Uint32Array(4); + for (let i = 0; i < 3e4; ++i) { + ptrWriteU32(hot, i); + if (hot[0] !== (i >>> 0)) + throw new Error("hot ffi_ptr_write_u32 iteration " + i); + if (ptrReadU32(hot) !== (i >>> 0)) + throw new Error("hot ffi_ptr_read_u32 iteration " + i); + } + for (let i = 0; i < 5000; ++i) { + // Temporary view: must stay alive for the duration of the call even + // though nothing but the call references it (conservative scan / + // DFG keep-alive, SPEC section 15.1). + ptrWriteU32(new Uint32Array(2), i); + if ((i & 511) === 0) + gc(); + } + for (let i = 0; i < 2e4; ++i) { + if (strlen("tier " + (i & 7)) !== 6n) + throw new Error("hot strlen iteration " + i); + } + let stableString = "stable string"; + for (let i = 0; i < 3e4; ++i) { + if (strlen(stableString) !== 13n) + throw new Error("hot strlen (stable) iteration " + i); + } + const hotAddress = ptrIdentity(hot); + for (let i = 0; i < 2e4; ++i) { + if (highPtr() !== 0x00007fffdeadbee0) + throw new Error("hot ffi_high_ptr iteration " + i); + if (retNullPtr() !== null) + throw new Error("hot ffi_ret_null_ptr iteration " + i); + if (ptrIdentity(hot) !== hotAddress) + throw new Error("hot typed array address changed at iteration " + i); + if (ptrIdentity(hotAddress) !== hotAddress) + throw new Error("hot numeric pointer round trip iteration " + i); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-ptr-object-arg.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-ptr-object-arg.js new file mode 100644 index 000000000000..4295f968fb36 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-ptr-object-arg.js @@ -0,0 +1,38 @@ +//@ requireOptions("--useDollarVM=1") +// Pointer-family arguments accept an object carrying a numeric/BigInt `ptr` property (Bun's +// documented FFIType.function / pointer forms accept a JSCallback / Pointer / CString object). +// The property get may run a getter, so its exceptions must propagate; a non-numeric `ptr` +// falls through to the normal type error. Tier-differential against a noDFG oracle. +if (!$vm.useJIT()) quit(); + +const identity = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, $vm.ffiFixture("ffi_ptr_identity"), "ffi_ptr_identity"); +function ref(v) { try { return identity(v); } catch (e) { return "threw:" + e.constructor.name; } } +function hot(v) { try { return identity(v); } catch (e) { return "threw:" + e.constructor.name; } } +noDFG(ref); noInline(ref); noInline(hot); + +let failures = 0; +const check = (l, got, want) => { if (!Object.is(got, want)) { print(`FAIL ${l}: got ${String(got)} want ${String(want)}`); if (++failures > 8) throw new Error("too many"); } }; + +const buf = new Uint8Array(8); +const addr = ref(buf); // a real address (number) +const withPtr = { ptr: addr }; // JSCallback / Pointer-style wrapper +const withBigPtr = { ptr: 4294967297n }; // BigInt ptr (> 2^32) +class Wrapper { get ptr() { return addr; } } // getter on the prototype (like CString) +const viaGetter = new Wrapper(); +const throwing = { get ptr() { throw new RangeError("ptr getter"); } }; +const badPtr = { ptr: "not a number" }; // must still be a TypeError +const noPtr = {}; + +for (let i = 0; i < 30000; ++i) { + check(`obj#${i}`, hot(withPtr), addr); + check(`bigint#${i}`, hot(withBigPtr), 4294967297); + check(`getter#${i}`, hot(viaGetter), addr); + check(`getter-throws#${i}`, hot(throwing), "threw:RangeError"); + check(`badptr#${i}`, hot(badPtr), "threw:TypeError"); + check(`noptr#${i}`, hot(noPtr), "threw:TypeError"); + // and the reference tier agrees on every one + check(`agree-obj#${i}`, hot(withPtr), ref(withPtr)); + check(`agree-throw#${i}`, hot(throwing), ref(throwing)); +} +if (failures) throw new Error(`${failures} failure(s)`); +print("ffi ptr-object arg: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-signature-errors.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-signature-errors.js new file mode 100644 index 000000000000..7753f8b3e3b8 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-signature-errors.js @@ -0,0 +1,134 @@ +//@ requireOptions("--useDollarVM=1") + +// Signature validation (SPEC sections 2, 3, 11.2): every invalid descriptor +// is a TypeError; valid ones intern to canonical strings. + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function expectTypeError(fn, label) { + let error = null; + try { + fn(); + } catch (e) { + error = e; + } + if (error === null) + throw new Error(label + ": expected a TypeError, nothing thrown"); + if (!(error instanceof TypeError)) + throw new Error(label + ": expected a TypeError, got " + error); +} + +function main() { + const target = $vm.ffiFixture("ffi_echo_i32"); + const dummy = () => 1; + + // ---- Invalid descriptors: TypeError from ffiFunction, ffiCallback and ffiSignatureString alike. + const invalidDescriptors = [ + [{ args: ["void"], returns: "i32" }, "void as an argument"], + [{ args: ["i32", "void", "i32"], returns: "i32" }, "void as a middle argument"], + [{ args: ["int128"], returns: "i32" }, "unknown type string"], + [{ args: ["i33"], returns: "i32" }, "unknown type string i33"], + [{ args: ["I32"], returns: "i32" }, "type strings are case sensitive"], + [{ args: [""], returns: "i32" }, "empty type string"], + [{ args: ["i32 "], returns: "i32" }, "type string with trailing space"], + [{ args: ["i32"], returns: "napi_env" }, "napi_env (removed type) as return type"], + [{ args: ["napi_env", "i32"], returns: "i32" }, "napi_env (removed type) as an argument"], + [{ args: [18], returns: "i32" }, "reserved tag 18 (was napi_env) as an argument"], + [{ args: ["i32"], returns: 18 }, "reserved tag 18 (was napi_env) as return type"], + [{ args: ["i32"], returns: "buffer" }, "buffer as return type"], + [{ args: ["i32"], returns: "buffer_length" }, "buffer_length as return type"], + [{ args: ["i32"], returns: 21 }, "buffer_length tag (21) as return type"], + [{ args: ["i32"], returns: "unknown" }, "unknown return type string"], + [{ args: ["i32"], returns: 22 }, "return tag out of range"], + [{ args: [22], returns: "i32" }, "argument tag out of range"], + [{ args: [-1], returns: "i32" }, "negative argument tag"], + [{ args: [1.5], returns: "i32" }, "fractional argument tag"], + [{ args: [13], returns: "i32" }, "void tag (13) as an argument"], + [{ args: [{}], returns: "i32" }, "object as a type"], + [{ args: [null], returns: "i32" }, "null as a type"], + [{ args: [Symbol("i32")], returns: "i32" }, "symbol as a type"], + [{ args: new Array(33).fill("i32"), returns: "i32" }, "33 arguments"], + [{ args: new Array(64).fill("f64"), returns: "f64" }, "64 arguments"], + [{ args: "i32", returns: "i32" }, "args is not an array"], + [{ args: [true], returns: "i32" }, "boolean as a type"], + [{ args: [[]], returns: "i32" }, "array as a type"], + [null, "null descriptor"], + [undefined, "undefined descriptor"], + [42, "number descriptor"], + ["f64(i32)", "string descriptor"], + ]; + for (const [descriptor, label] of invalidDescriptors) { + expectTypeError(() => $vm.ffiFunction(descriptor, target, "bad"), "ffiFunction: " + label); + expectTypeError(() => $vm.ffiCallback(descriptor, dummy), "ffiCallback: " + label); + expectTypeError(() => $vm.ffiSignatureString(descriptor), "ffiSignatureString: " + label); + } + + // The 32-argument boundary is exact. + const thirtyTwo = { args: new Array(32).fill("i32"), returns: "i64" }; + const fn32 = $vm.ffiFunction(thirtyTwo, $vm.ffiFixture("ffi_sum_i32_16"), "arity32"); + check(fn32.length, 32, "length of a 32-argument FFI function"); + + // ---- Canonical signature strings: interning smoke test + aliases + numeric tags. + check($vm.ffiSignatureString({ args: ["i32", "f64"], returns: "f64" }), "f64(i32,f64)", "canonical string"); + check($vm.ffiSignatureString({ args: ["int32_t", "double"], returns: "double" }), "f64(i32,f64)", "aliases canonicalize"); + check($vm.ffiSignatureString({ args: [5, 9], returns: 9 }), "f64(i32,f64)", "numeric tags canonicalize"); + check($vm.ffiSignatureString({ args: [], returns: "void" }), "void()", "empty signature"); + check($vm.ffiSignatureString({ args: [], returns: 13 }), "void()", "numeric void return tag"); + check($vm.ffiSignatureString({ args: ["char"], returns: "char" }), "char(char)", "char keeps its own name"); + check($vm.ffiSignatureString({ args: ["int8_t"], returns: "int8_t" }), "i8(i8)", "int8_t is i8, not char"); + check($vm.ffiSignatureString({ args: ["napi_value", "jsvalue"], returns: "napi_value" }), "jsvalue(jsvalue,jsvalue)", "napi_value is the legacy spelling of jsvalue"); + check($vm.ffiSignatureString({ args: ["buffer", "cstring", "function"], returns: "ptr" }), "ptr(buffer,cstring,function)", "pointer family names"); + check($vm.ffiSignatureString({ args: ["i64_fast", "u64_fast"], returns: "u64_fast" }), "u64_fast(i64_fast,u64_fast)", "fast 64-bit names"); + const everyAlias = [ + ["int8_t", "i8"], ["uint8_t", "u8"], ["int16_t", "i16"], ["uint16_t", "u16"], ["int32_t", "i32"], ["int", "i32"], + ["c_int", "i32"], ["uint32_t", "u32"], ["c_uint", "u32"], ["int64_t", "i64"], ["isize", "i64"], ["uint64_t", "u64"], + ["usize", "u64"], ["size_t", "u64"], ["double", "f64"], ["float", "f32"], ["void*", "ptr"], ["pointer", "ptr"], + // "char*" is a POINTER alias (tag 12, Bun's FFIType parity), not cstring. + ["char*", "ptr"], ["callback", "function"], ["fn", "function"], ["bool", "bool"], ["char", "char"], + ["ptr", "ptr"], ["cstring", "cstring"], ["jsvalue", "jsvalue"], ["napi_value", "jsvalue"], + ]; + for (const [alias, canonical] of everyAlias) + check($vm.ffiSignatureString({ args: [alias], returns: "i32" }), "i32(" + canonical + ")", "alias " + alias); + // Every numeric tag in order. Tag 13 (void) is not a valid argument and tag 18 is the + // reserved (formerly napi_env) tag, invalid in every position; both are skipped here (18's + // rejection is covered by the invalid-descriptor table above). + const canonicalByTag = ["char", "i8", "u8", "i16", "u16", "i32", "u32", "i64", "u64", "f64", "f32", "bool", "ptr", "void", "cstring", "i64_fast", "u64_fast", "function", null, "jsvalue", "buffer", "buffer_length"]; + for (let tag = 0; tag < canonicalByTag.length; ++tag) { + if (canonicalByTag[tag] === "void" || canonicalByTag[tag] === null) + continue; // void is not a valid argument; 18 is reserved + check($vm.ffiSignatureString({ args: [tag], returns: 5 }), "i32(" + canonicalByTag[tag] + ")", "numeric tag " + tag); + } + + // Structural interning: equal shapes give the same string, order matters. + check($vm.ffiSignatureString({ args: ["i32", "f64"], returns: "f64" }) === $vm.ffiSignatureString({ args: [5, "double"], returns: 9 }), true, "interning agrees across spellings"); + if ($vm.ffiSignatureString({ args: ["f64", "i32"], returns: "f64" }) === $vm.ffiSignatureString({ args: ["i32", "f64"], returns: "f64" })) + throw new Error("argument order must matter"); + + // ---- The `ptr` parameter of $vm.ffiFunction must be a pointer number or a JSFFICallback. + expectTypeError(() => $vm.ffiFunction({ args: ["i32"], returns: "i32" }, "not a pointer", "bad"), "string as ptr"); + expectTypeError(() => $vm.ffiFunction({ args: ["i32"], returns: "i32" }, {}, "bad"), "object as ptr"); + expectTypeError(() => $vm.ffiFunction({ args: ["i32"], returns: "i32" }, Symbol("p"), "bad"), "symbol as ptr"); + expectTypeError(() => $vm.ffiFunction({ args: ["i32"], returns: "i32" }, dummy, "bad"), "raw JS function as ptr"); + expectTypeError(() => $vm.ffiCallback({ args: ["i32"], returns: "i32" }, 42), "non-callable callback target"); + expectTypeError(() => $vm.ffiCallback({ args: ["i32"], returns: "i32" }, {}), "object callback target"); + + // ---- Unknown fixture names throw (but not TypeError necessarily). + let threw = false; + try { + $vm.ffiFixture("ffi_no_such_fixture"); + } catch (e) { + threw = true; + } + check(threw, true, "unknown fixture name throws"); + const names = $vm.ffiFixtures(); + check(Array.isArray(names), true, "$vm.ffiFixtures() returns an array"); + check(names.includes("ffi_echo_i32"), true, "fixture list contains ffi_echo_i32"); + check(names.includes("ffi_canary_call"), true, "fixture list contains ffi_canary_call"); + check(names.length >= 90, true, "fixture list is complete"); +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-subword-and-returns.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-subword-and-returns.js new file mode 100644 index 000000000000..193865ef1d7e --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-subword-and-returns.js @@ -0,0 +1,160 @@ +//@ requireOptions("--useDollarVM=1") + +// Sub-word extension probes (caller side) and return-value normalization +// probes (callee side): widen fixtures, ffi_ret_neg_one_*, float edge +// returners, bool and char rules (SPEC sections 2, 4, 5, 7.2). + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name + "->" + ret); + + // ---- Caller-side extension: the callee widens whatever low bits it got. + const widenChar = bind("ffi_widen_char", ["char"], "i64"); + const widenI8 = bind("ffi_widen_i8", ["i8"], "i64"); + const widenU8 = bind("ffi_widen_u8", ["u8"], "i64"); + const widenI16 = bind("ffi_widen_i16", ["i16"], "i64"); + const widenU16 = bind("ffi_widen_u16", ["u16"], "i64"); + const widenCases = [ + [widenChar, "widen_char", [[0, 0n], [-1, -1n], [255, -1n], [0xff, -1n], [127, 127n], [128, -128n], [-128, -128n], [0x1ff, -1n], [0x180, -128n], [-129, 127n]]], + [widenI8, "widen_i8", [[0, 0n], [-1, -1n], [255, -1n], [127, 127n], [128, -128n], [-128, -128n], [0x17f, 127n], [0x180, -128n], [-129, 127n], [0x7fffffff, -1n]]], + [widenU8, "widen_u8", [[0, 0n], [-1, 255n], [255, 255n], [256, 0n], [511, 255n], [128, 128n], [-128, 128n], [0x101, 1n], [0x7fffffff, 255n]]], + [widenI16, "widen_i16", [[0, 0n], [-1, -1n], [65535, -1n], [32767, 32767n], [32768, -32768n], [-32768, -32768n], [-32769, 32767n], [0x12345, 9029n], [65536, 0n]]], + [widenU16, "widen_u16", [[0, 0n], [-1, 65535n], [65535, 65535n], [65536, 0n], [32768, 32768n], [-32768, 32768n], [0x18000, 32768n], [70000, 4464n]]], + ]; + for (const [fn, name, cases] of widenCases) { + for (const [input, expected] of cases) + check(fn(input), expected, name + "(" + input + ")"); + } + + // ---- Callee-side return normalization: -1 through every integer width. + const negOnes = [ + ["ffi_ret_neg_one_i8", "i8", -1], + ["ffi_ret_neg_one_i16", "i16", -1], + ["ffi_ret_neg_one_i32", "i32", -1], + ["ffi_ret_neg_one_i64", "i64", -1n], + ["ffi_ret_neg_one_u8", "u8", 255], + ["ffi_ret_neg_one_u16", "u16", 65535], + ["ffi_ret_neg_one_u32", "u32", 4294967295], + ["ffi_ret_neg_one_u64", "u64", 18446744073709551615n], + // Reinterpretations of the same all-ones bit pattern: + ["ffi_ret_neg_one_i8", "u8", 255], + ["ffi_ret_neg_one_u8", "i8", -1], + ["ffi_ret_neg_one_i32", "u32", 4294967295], + ["ffi_ret_neg_one_u32", "i32", -1], + ["ffi_ret_neg_one_i64", "u64", 18446744073709551615n], + ["ffi_ret_neg_one_u64", "i64", -1n], + ["ffi_ret_neg_one_i64", "i64_fast", -1], + ["ffi_ret_neg_one_u64", "u64_fast", 18446744073709551615n], + ]; + for (let i = 0; i < negOnes.length; ++i) { + const [name, type, expected] = negOnes[i]; + const fn = bind(name, [], type); + check(fn(), expected, name + " as " + type); + for (let iteration = 0; iteration < 5000; ++iteration) { + const result = fn(); + if (!Object.is(result, expected)) + throw new Error(name + " as " + type + " hot iteration " + iteration + ": got " + describe(result)); + } + } + + // ---- bool normalization: only the low byte of a native bool return is + // defined; a callee returning 2 in an int8 register declared as bool must + // still surface as `true`. + const twoAsBool = bind("ffi_ret_two_as_bool", [], "bool"); + check(twoAsBool(), true, "ffi_ret_two_as_bool"); + for (let i = 0; i < 2e4; ++i) { + if (twoAsBool() !== true) + throw new Error("ffi_ret_two_as_bool hot iteration " + i); + } + const twoAsI8 = bind("ffi_ret_two_as_bool", [], "i8"); + check(twoAsI8(), 2, "ffi_ret_two_as_bool declared i8"); + + const echoBool = bind("ffi_echo_bool", ["bool"], "bool"); + for (const [input, expected] of [[2, true], [-1, true], [0, false], [0.5, true], [-0, false], [NaN, false], [1e-300, true], [true, true], [false, false], [null, false], [undefined, false], [256, true], [65536, true]]) + check(echoBool(input), expected, "ffi_echo_bool(" + describe(input) + ")"); + for (let i = 0; i < 2e4; ++i) { + // 256 has a zero low byte: `and32(0xff)` or `and32(1)` mis-conversions would return false. + if (echoBool(256) !== true) + throw new Error("ffi_echo_bool(256) hot iteration " + i); + if (echoBool(2) !== true) + throw new Error("ffi_echo_bool(2) hot iteration " + i); + if (echoBool(0) !== false) + throw new Error("ffi_echo_bool(0) hot iteration " + i); + } + + // ---- char is signed on every target (SPEC section 2). + const echoChar = bind("ffi_echo_char", ["char"], "char"); + check(echoChar(-1), -1, "ffi_echo_char(-1)"); + check(echoChar(255), -1, "ffi_echo_char(255)"); + check(echoChar(0x80), -128, "ffi_echo_char(0x80)"); + check(echoChar(0x7f), 127, "ffi_echo_char(0x7f)"); + check(widenChar(-1), -1n, "ffi_widen_char(-1)"); + for (let i = 0; i < 2e4; ++i) { + if (echoChar(255) !== -1) + throw new Error("ffi_echo_char(255) hot iteration " + i); + } + + // ---- Floating-point edge returns (purifyNaN, sign of zero, denormals, infinity). + const retNanF32 = bind("ffi_ret_nan_f32", [], "f32"); + const retImpureNanF64 = bind("ffi_ret_impure_nan_f64", [], "f64"); + const retNegZeroF64 = bind("ffi_ret_neg_zero_f64", [], "f64"); + const retDenormalF32 = bind("ffi_ret_denormal_f32", [], "f32"); + const retInfF64 = bind("ffi_ret_inf_f64", [], "f64"); + const echoF32 = bind("ffi_echo_f32", ["f32"], "f32"); + const echoF64 = bind("ffi_echo_f64", ["f64"], "f64"); + for (let i = 0; i < 1e4; ++i) { + if (!Number.isNaN(retNanF32())) + throw new Error("ffi_ret_nan_f32 iteration " + i); + if (!Number.isNaN(retImpureNanF64())) + throw new Error("ffi_ret_impure_nan_f64 iteration " + i); + if (!Object.is(retNegZeroF64(), -0)) + throw new Error("ffi_ret_neg_zero_f64 iteration " + i); + if (retDenormalF32() !== 2 ** -149) + throw new Error("ffi_ret_denormal_f32 iteration " + i + ": " + retDenormalF32()); + if (retInfF64() !== Infinity) + throw new Error("ffi_ret_inf_f64 iteration " + i); + if (!Number.isNaN(echoF32(NaN))) + throw new Error("ffi_echo_f32(NaN) iteration " + i); + if (!Number.isNaN(echoF64(NaN))) + throw new Error("ffi_echo_f64(NaN) iteration " + i); + if (!Object.is(echoF32(-0), -0)) + throw new Error("ffi_echo_f32(-0) iteration " + i); + if (!Object.is(echoF64(-0), -0)) + throw new Error("ffi_echo_f64(-0) iteration " + i); + } + check(echoF32(2 ** -149), 2 ** -149, "denormal f32 argument round trip"); + check(echoF64(5e-324), 5e-324, "denormal f64 argument round trip"); + check(echoF32(3.4028234663852886e38), 3.4028234663852886e38, "FLT_MAX round trip"); + check(echoF32(1e39), Infinity, "f32 overflow to +inf"); + check(echoF32(-1e39), -Infinity, "f32 overflow to -inf"); + + // u32 returns above INT32_MAX become doubles, not negative int32s. + const echoU32 = bind("ffi_echo_u32", ["u32"], "u32"); + const echoI32 = bind("ffi_echo_i32", ["i32"], "i32"); + for (let i = 0; i < 2e4; ++i) { + if (echoU32(-1) !== 4294967295) + throw new Error("u32 return of 0xffffffff iteration " + i + ": " + echoU32(-1)); + if (echoU32(2147483648) !== 2147483648) + throw new Error("u32 return of 0x80000000 iteration " + i); + if (echoI32(-1) !== -1) + throw new Error("i32 return of -1 iteration " + i); + if (echoI32(2147483648) !== -2147483648) + throw new Error("i32 wrap of 0x80000000 iteration " + i); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-tailcall.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-tailcall.js new file mode 100644 index 000000000000..703f14b99435 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-tailcall.js @@ -0,0 +1,51 @@ +//@ requireOptions("--useDollarVM=1") +"use strict"; +// An FFI call in TAIL position (arrow expression body / `return sym(...)` in strict code) is a +// bytecode TailCall. The parser emits it as a plain Call so it can become CallFFI; that must not +// change semantics: the value returned, exceptions from the FFI conversion, and deep call chains +// (no tail-call frame reuse assumed) all behave identically to the interpreter. +if (!$vm.useJIT()) quit(); +const fixture = name => $vm.ffiFixture(name); +const identity = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, fixture("ffi_ptr_identity"), "ffi_ptr_identity"); +const addI32 = $vm.ffiFunction({ args: ["i32", "i32"], returns: "i32" }, fixture("ffi_add_i32"), "ffi_add_i32"); + +// Tail position via arrow expression bodies (strict => TailCall bytecode). +const tailIdentity = (v) => identity(v); +const tailAdd = (a, b) => addI32(a, b); +// Tail position via explicit `return` in a strict function. +function returnsCall(a, b) { return addI32(a, b); } +noInline(tailIdentity); noInline(tailAdd); noInline(returnsCall); + +// Interpreter/baseline oracles pinned below the DFG. +function refIdentity(v) { return identity(v); } +function refAdd(a, b) { return addI32(a, b); } +noDFG(refIdentity); noDFG(refAdd); noInline(refIdentity); noInline(refAdd); + +let failures = 0; +const check = (label, got, want) => { if (!Object.is(got, want)) { print(`FAIL ${label}: got ${String(got)} want ${String(want)}`); if (++failures > 8) throw new Error("too many failures"); } }; + +for (let i = 0; i < 40000; ++i) { + const a = (i * 7) | 0, b = -(i % 101); + check(`tailAdd#${i}`, tailAdd(a, b), refAdd(a, b)); + check(`returnsCall#${i}`, returnsCall(a, b), refAdd(a, b)); + check(`tailIdentity#${i}`, tailIdentity(i), refIdentity(i)); + // The tail-position result must be USABLE (not lost): compose it. + check(`compose#${i}`, addI32(tailAdd(a, b), 1), (a + b + 1) | 0); +} + +// Exceptions raised by the FFI conversion must propagate out of the tail-call arrow correctly. +const sym = Symbol("no-coerce"); +let threw = 0; +for (let i = 0; i < 40000; ++i) { + try { tailAdd(sym, 1); print("FAIL: symbol arg did not throw at " + i); ++failures; } + catch (e) { if (e instanceof TypeError) ++threw; else { print("FAIL: wrong error " + e); ++failures; } } +} +check("threw-count", threw, 40000); + +// A DEEP chain of tail-position FFI calls: with real tail calls this reuses frames; converted to +// plain Calls it grows the stack per level. It must complete (depth is modest) with the same value. +const step = (n) => n === 0 ? 0 : (addI32(step(n - 1), 1) | 0); +check("deep-chain", step(2000), 2000); + +if (failures) throw new Error(`${failures} failure(s)`); +print("ffi tail-call: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-burst.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-burst.js new file mode 100644 index 000000000000..8383172de44f --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-burst.js @@ -0,0 +1,30 @@ +//@ requireOptions("--useDollarVM=1") +// Burst: many foreign-thread invocations queue up (each on its own OS thread) BEFORE a single +// drain. Records must all survive queuing (refcounted C data), and one drain must deliver every +// one, in some order, with exact values. Also multiple distinct threadsafe callbacks interleaved. +if (!$vm.useJIT()) quit(); +const callFromThread = $vm.ffiFunction({ args: ["ptr", "i32", "i64", "u64", "f64"], returns: "void" }, + $vm.ffiFixture("ffi_call_cb_from_thread"), "call_cb_from_thread"); +const seenA = [], seenB = []; +const cbA = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, (a) => seenA.push(a), { threadsafe: true }); +const cbB = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, (a, b) => seenB.push([a, b]), { threadsafe: true }); +const N = 300; +for (let i = 0; i < N; ++i) { + callFromThread(cbA.ptr, i, 1n, 1n, 0); // queued, not run + callFromThread(cbB.ptr, i * 2, BigInt(i) - 500n, 1n, 0); +} +if (seenA.length !== 0 || seenB.length !== 0) throw new Error("ran inline"); +const delivered = $vm.drainThreadsafeCallbacks(); +if (delivered !== 2 * N) throw new Error("expected " + (2 * N) + " delivered, got " + delivered); +if (seenA.length !== N || seenB.length !== N) throw new Error("counts: " + seenA.length + "/" + seenB.length); +seenA.sort((x, y) => x - y); +for (let i = 0; i < N; ++i) if (seenA[i] !== i) throw new Error("A[" + i + "]=" + seenA[i]); +seenB.sort((x, y) => x[0] - y[0]); +for (let i = 0; i < N; ++i) { + if (seenB[i][0] !== i * 2) throw new Error("B i32 " + seenB[i][0]); + if (seenB[i][1] !== BigInt(i) - 500n) throw new Error("B i64 " + String(seenB[i][1])); +} +// A second drain finds nothing left. +if ($vm.drainThreadsafeCallbacks() !== 0) throw new Error("queue not empty after drain"); +cbA.close(); cbB.close(); +print("ffi threadsafe burst: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-throw.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-throw.js new file mode 100644 index 000000000000..f89cf0c2d8a6 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback-throw.js @@ -0,0 +1,34 @@ +//@ requireOptions("--useDollarVM=1") +// A THROWING threadsafe callback: the drain must stop running further invocations once one +// throws, propagate the exception, and still RETIRE the counts of the un-run records -- so a +// callback close()d while records were queued (the deferred-unroot path) is not leaked/rooted +// forever, and the queue is empty afterwards. (Regression for the drain-loop early-return.) +if (!$vm.useJIT()) quit(); +const callFromThread = $vm.ffiFunction({ args: ["ptr", "i32", "i64", "u64", "f64"], returns: "void" }, + $vm.ffiFixture("ffi_call_cb_from_thread"), "call_cb_from_thread"); +let ran = 0; +const boom = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, + (a) => { ran++; throw new RangeError("cb-throw " + a); }, { threadsafe: true }); +const other = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, + () => { ran++; }, { threadsafe: true }); +// Queue three records: boom (throws first), then two more that must NOT run this drain. +callFromThread(boom.ptr, 1, 2n, 3n, 4.5); +callFromThread(other.ptr, 2, 2n, 3n, 4.5); +callFromThread(boom.ptr, 3, 2n, 3n, 4.5); +// Close one callback WHILE its records are queued (deferred-unroot path). +other.close(); +let threw = false; +try { $vm.drainThreadsafeCallbacks(); } catch (e) { threw = e instanceof RangeError && /cb-throw 1/.test(e.message); } +if (!threw) throw new Error("expected the first callback's RangeError to propagate from drain"); +if (ran !== 1) throw new Error("invocations after the throw must not run this drain: ran=" + ran); +// The queue was fully consumed (the un-run records were retired, not left queued). +if ($vm.drainThreadsafeCallbacks() !== 0) throw new Error("queue not empty after the throwing drain"); +// GC must be fine: no leaked-root cell, no swept-cell record. +for (let i = 0; i < 4; ++i) { fullGC(); edenGC(); } +// And the surviving callback still works after all that. +callFromThread(boom.ptr, 9, 2n, 3n, 4.5); +let threw2 = false; +try { $vm.drainThreadsafeCallbacks(); } catch (e) { threw2 = /cb-throw 9/.test(e.message); } +if (!threw2) throw new Error("post-recovery throwing invocation did not propagate"); +boom.close(); +print("ffi threadsafe throwing callback: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback.js new file mode 100644 index 000000000000..90f3a016ab02 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-threadsafe-callback.js @@ -0,0 +1,76 @@ +//@ requireOptions("--useDollarVM=1") +// Threadsafe callbacks. The C caller invokes the callback from a FOREIGN OS thread; the engine +// must NOT run JS there. It copies the raw argument slots into a record and hands it to the +// registered dispatch function (here $vm's queue); the JS thread later drains the queue, and only +// THEN are the raw slots converted to JS values (so i64/u64 BigInt boxing happens on the JS +// thread -- the shape of oven-sh/bun#35406). Assertions: +// 1) the callback does NOT run inline during the FFI call (queued, count 0 before drain); +// 2) drain delivers it exactly once with exact values: i32, i64->BigInt, u64->BigInt, f64; +// 3) values that don't fit an int32 (u64 = 2^64-1) round-trip exactly as BigInt; +// 4) a callback close()d before the drain still DELIVERS its accepted invocations, and the +// pending count keeps the (JS-unreachable) cell + callable rooted until they drain. +if (!$vm.useJIT()) quit(); + +const callFromThread = $vm.ffiFunction({ args: ["ptr", "i32", "i64", "u64", "f64"], returns: "void" }, + $vm.ffiFixture("ffi_call_cb_from_thread"), "call_cb_from_thread"); + +let seen = []; +const cb = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, + (a, b, c, d) => seen.push([a, b, c, d]), { threadsafe: true }); + +if (cb.threadsafe !== true) throw new Error("expected .threadsafe === true, got " + cb.threadsafe); + +for (let i = 0; i < 200; ++i) { + seen = []; + // (1) invoked from a foreign thread: must be queued, NOT run inline. + callFromThread(cb.ptr, -7 - i, -9007199254740993n, 18446744073709551615n, 2.5); + if (seen.length !== 0) throw new Error("threadsafe callback ran INLINE (should be queued): " + JSON.stringify(seen)); + + // (2) the JS thread drains: exactly one invocation delivered, exact values. + const delivered = $vm.drainThreadsafeCallbacks(); + if (delivered !== 1) throw new Error("expected 1 delivered, got " + delivered); + if (seen.length !== 1) throw new Error("expected 1 seen after drain, got " + seen.length); + const [a, b, c, d] = seen[0]; + if (a !== -7 - i) throw new Error("i32 wrong: " + a); + if (b !== -9007199254740993n) throw new Error("i64 wrong: " + String(b) + " (typeof " + typeof b + ")"); + // (3) 2^64-1 must be an exact BigInt, boxed on the JS thread. + if (c !== 18446744073709551615n) throw new Error("u64 wrong: " + String(c) + " (typeof " + typeof c + ")"); + if (d !== 2.5) throw new Error("f64 wrong: " + d); +} + +// (4) close() before drain -- WITH a full GC in between and NO JS reference to the callback. +// Two guarantees are exercised at once. LIFETIME: the queued records hold a raw pointer to the +// cell, so the pending-invocation count must keep the cell (and, through its barrier, the +// callable) rooted until every record drains -- close() while records are queued must NOT unroot +// (that was a use-after-free), even across full GCs with no JS reference. DELIVERY: an invocation +// accepted while the callback was open is a commitment, so it still RUNS after close(); close() +// only refuses NEW foreign-thread calls. So the drained records execute the callable on a cell no +// JS code can reach any more, and must produce the right values. +seen = []; +{ + let victim = $vm.ffiCallback({ args: ["i32", "i64", "u64", "f64"], returns: "void" }, + (a) => seen.push(a), { threadsafe: true }); + callFromThread(victim.ptr, 111, 2n, 3n, 4.5); // queued, not run + callFromThread(victim.ptr, 222, 2n, 3n, 4.5); // a second record for the same cell + victim.close(); // close while records are queued + victim = null; // drop the only JS reference +} +for (let i = 0; i < 5; ++i) { fullGC(); edenGC(); } // cell must survive: it is rooted until drain +const late = $vm.drainThreadsafeCallbacks(); // must NOT crash / touch a swept cell +if (late !== 2) throw new Error("expected 2 records drained even when closed, got " + late); +// Accepted-while-open invocations are delivered even after close(): the callable ran on the +// unreachable (but rooted) cell and observed the right arguments, in order. +if (seen.length !== 2 || seen[0] !== 111 || seen[1] !== 222) + throw new Error("post-close delivery wrong: " + JSON.stringify(seen)); +// A closed callback that is fully drained is now collectible; more GC must not resurrect issues. +for (let i = 0; i < 3; ++i) { fullGC(); } +if ($vm.drainThreadsafeCallbacks() !== 0) throw new Error("queue not empty"); +seen = []; // reset before section (5); it is the shared sink of the still-open `cb` + +// (5) the other test callback still works after all that. +callFromThread(cb.ptr, 5, 6n, 7n, 8.5); +if ($vm.drainThreadsafeCallbacks() !== 1) throw new Error("post-race delivery failed"); +if (seen.length !== 1 || seen[0][0] !== 5) throw new Error("wrong post-race value: " + JSON.stringify(seen)); +cb.close(); + +print("ffi threadsafe callback: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-tier-differential.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-tier-differential.js new file mode 100644 index 000000000000..fa79abb178ff --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-tier-differential.js @@ -0,0 +1,222 @@ +//@ requireOptions("--useDollarVM=1") + +// One battery, one HARDCODED expected table, no reference implementation: +// the harness runs this file under all of its option matrices (LLInt-only, +// baseline, DFG-eager, FTL-eager, no-cjit, ...) and the sibling +// ffi-host-path.js runs the identical battery with --useFFIICStub=0 +// --useFFICallInDFG=0, so any tier that disagrees with any other tier fails +// against the same literal constants (SPEC section 11.4). + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (Object.is(value, -0)) + return "-0"; + if (typeof value === "symbol") + return value.toString(); + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- the only call-site shape the DFG +// ByteCodeParser constant-callee feed and the strength-reduction +// Call -> CallFFI conversion accept (SPEC section 10.2). A shared +// `callable(...args)` warm site would stay polymorphic (and a spread call is +// never converted), so the typed CallFFI path would never join the differential. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const bind = (name, args, ret) => $vm.ffiFunction({ args, returns: ret }, fixture(name), name); + + const echoChar = bind("ffi_echo_char", ["char"], "char"); + const echoI8 = bind("ffi_echo_i8", ["i8"], "i8"); + const echoU8 = bind("ffi_echo_u8", ["u8"], "u8"); + const echoI16 = bind("ffi_echo_i16", ["i16"], "i16"); + const echoU16 = bind("ffi_echo_u16", ["u16"], "u16"); + const echoI32 = bind("ffi_echo_i32", ["i32"], "i32"); + const echoU32 = bind("ffi_echo_u32", ["u32"], "u32"); + const echoI64 = bind("ffi_echo_i64", ["i64"], "i64"); + const echoU64 = bind("ffi_echo_u64", ["u64"], "u64"); + const echoI64Fast = $vm.ffiFunction({ args: ["i64_fast"], returns: "i64_fast" }, fixture("ffi_echo_i64"), "ffi_echo_i64:fast"); + const echoU64Fast = $vm.ffiFunction({ args: ["u64_fast"], returns: "u64_fast" }, fixture("ffi_echo_u64"), "ffi_echo_u64:fast"); + const echoF32 = bind("ffi_echo_f32", ["f32"], "f32"); + const echoF64 = bind("ffi_echo_f64", ["f64"], "f64"); + const echoBool = bind("ffi_echo_bool", ["bool"], "bool"); + const echoPtr = bind("ffi_echo_ptr", ["ptr"], "ptr"); + const echoNapiValue = bind("ffi_echo_jsvalue", ["napi_value"], "napi_value"); + const addI32 = bind("ffi_add_i32", ["i32", "i32"], "i32"); + const addF64 = bind("ffi_add_f64", ["f64", "f64"], "f64"); + const addI64 = bind("ffi_add_i64", ["i64", "i64"], "i64"); + const addU64 = bind("ffi_add_u64", ["u64", "u64"], "u64"); + const addF32 = bind("ffi_add_f32", ["f32", "f32"], "f32"); + const sumI32_9 = bind("ffi_sum_i32_9", ["i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32", "i32"], "i64"); + const sumF64_9 = bind("ffi_sum_f64_9", ["f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64", "f64"], "f64"); + const sumU8_12 = bind("ffi_sum_u8_12", ["u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8", "u8"], "i64"); + const sumI16_10 = bind("ffi_sum_i16_10", ["i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16", "i16"], "i64"); + const mix1 = bind("ffi_mix_1", ["i32", "f64", "i64", "f32", "ptr", "u8", "f64", "i16", "f64", "i32"], "f64"); + const mix6 = bind("ffi_mix_6", ["bool", "bool", "i32", "bool", "f64", "bool", "f32", "bool", "bool", "bool", "bool", "bool", "bool"], "f64"); + const widenChar = bind("ffi_widen_char", ["char"], "i64_fast"); + const widenU16 = bind("ffi_widen_u16", ["u16"], "i64_fast"); + const twoAsBool = bind("ffi_ret_two_as_bool", [], "bool"); + const retNullPtr = bind("ffi_ret_null_ptr", [], "ptr"); + const highPtr = bind("ffi_high_ptr", [], "ptr"); + const retNegOneI8 = bind("ffi_ret_neg_one_i8", [], "i8"); + const retNegOneU32 = bind("ffi_ret_neg_one_u32", [], "u32"); + const retNegOneU64 = bind("ffi_ret_neg_one_u64", [], "u64"); + const retDenormalF32 = bind("ffi_ret_denormal_f32", [], "f32"); + const retNegZeroF64 = bind("ffi_ret_neg_zero_f64", [], "f64"); + const retInfF64 = bind("ffi_ret_inf_f64", [], "f64"); + + const sharedObject = { shared: true }; + + // Each row: [callable, [arguments...], expectedLiteral, label] + // Every expected value below is a literal, not a computed reference. + const battery = [ + [echoChar, [-1], -1, "char(-1)"], + [echoChar, [255], -1, "char(255)"], + [echoChar, [0x80], -128, "char(0x80)"], + [echoI8, [127], 127, "i8(127)"], + [echoI8, [128], -128, "i8(128)"], + [echoI8, [0x1ff], -1, "i8(0x1ff)"], + [echoU8, [-1], 255, "u8(-1)"], + [echoU8, [511], 255, "u8(511)"], + [echoU8, [256], 0, "u8(256)"], + [echoI16, [32768], -32768, "i16(32768)"], + [echoI16, [-32769], 32767, "i16(-32769)"], + [echoU16, [-1], 65535, "u16(-1)"], + [echoU16, [70000], 4464, "u16(70000)"], + [echoI32, [2147483648], -2147483648, "i32(2^31)"], + [echoI32, [-2147483649], 2147483647, "i32(-2^31-1)"], + [echoI32, [4294967301], 5, "i32(2^32+5)"], + [echoI32, [-1.9], -1, "i32(-1.9)"], + [echoI32, [NaN], 0, "i32(NaN)"], + [echoI32, [Infinity], 0, "i32(Infinity)"], + [echoI32, [undefined], 0, "i32(undefined)"], + [echoI32, [true], 1, "i32(true)"], + [echoU32, [-1], 4294967295, "u32(-1)"], + [echoU32, [2147483648], 2147483648, "u32(2^31)"], + [echoU32, [4294967296], 0, "u32(2^32)"], + [echoI64, [0], 0n, "i64(0)"], + [echoI64, [-1], -1n, "i64(-1)"], + [echoI64, [4294967296], 4294967296n, "i64(2^32)"], + [echoI64, [2n ** 63n - 1n], 9223372036854775807n, "i64(2^63-1)"], + [echoI64, [2n ** 63n], -9223372036854775808n, "i64(2^63)"], + [echoI64, [-1.5], -1n, "i64(-1.5)"], + [echoI64, [9007199254740992], 9007199254740992n, "i64(2^53 as number)"], + [echoU64, [-1], 18446744073709551615n, "u64(-1)"], + [echoU64, [-2147483648], 18446744071562067968n, "u64(-2^31)"], + [echoU64, [2n ** 64n + 3n], 3n, "u64(2^64+3)"], + [echoI64Fast, [9007199254740991], 9007199254740991, "i64_fast(2^53-1)"], + [echoI64Fast, [-9007199254740991], -9007199254740991, "i64_fast(-(2^53-1))"], + [echoI64Fast, [2n ** 53n], 9007199254740992n, "i64_fast(2^53)"], + [echoI64Fast, [-(2n ** 53n)], -9007199254740992n, "i64_fast(-2^53)"], + [echoI64Fast, [-1], -1, "i64_fast(-1)"], + [echoU64Fast, [9007199254740990], 9007199254740990, "u64_fast(2^53-2)"], + [echoU64Fast, [2n ** 53n - 1n], 9007199254740991n, "u64_fast(2^53-1)"], + [echoU64Fast, [-1], 18446744073709551615n, "u64_fast(-1)"], + [echoF32, [1.1], 1.100000023841858, "f32(1.1)"], + [echoF32, [-0], -0, "f32(-0)"], + [echoF32, [NaN], NaN, "f32(NaN)"], + [echoF32, [1e39], Infinity, "f32(1e39)"], + [echoF32, [16777217], 16777216, "f32(2^24+1)"], + [echoF64, [-0], -0, "f64(-0)"], + [echoF64, [NaN], NaN, "f64(NaN)"], + [echoF64, [Number.MIN_VALUE], 5e-324, "f64(min denormal)"], + [echoF64, [undefined], NaN, "f64(undefined) -> NaN"], + [echoBool, [2], true, "bool(2)"], + [echoBool, [-1], true, "bool(-1)"], + [echoBool, [0], false, "bool(0)"], + [echoBool, [0.5], true, "bool(0.5)"], + [echoBool, [-0], false, "bool(-0)"], + [echoBool, [NaN], false, "bool(NaN)"], + [echoBool, [256], true, "bool(256)"], + [echoBool, [null], false, "bool(null)"], + [echoPtr, [0], null, "ptr(0)"], + [echoPtr, [null], null, "ptr(null)"], + [echoPtr, [undefined], null, "ptr(undefined)"], + [echoPtr, [-1], 18446744073709551615n, "ptr(-1) (exact BigInt, > 2^53)"], + [echoPtr, [1099511627776], 1099511627776, "ptr(2^40)"], + [echoNapiValue, [sharedObject], sharedObject, "napi_value(object)"], + [echoNapiValue, ["x"], "x", "napi_value(string)"], + [echoNapiValue, [-0], -0, "napi_value(-0)"], + [addI32, [2147483647, 1], -2147483648, "add_i32 overflow"], + [addI32, [-2147483648, -1], 2147483647, "add_i32 underflow"], + [addI32, [7], 7, "add_i32 missing argument"], + [addI32, [7, 8, 9], 15, "add_i32 extra argument"], + [addF64, [0.1, 0.2], 0.30000000000000004, "add_f64(0.1, 0.2)"], + [addF64, [-0, -0], -0, "add_f64(-0, -0)"], + [addF64, [Infinity, -Infinity], NaN, "add_f64(inf, -inf)"], + [addI64, [2n ** 63n - 1n, 1n], -9223372036854775808n, "add_i64 wrap"], + [addI64, [-1, -1], -2n, "add_i64(-1,-1)"], + [addU64, [-1, 2], 1n, "add_u64 wrap"], + [addU64, [2n ** 32n, 2n ** 32n], 8589934592n, "add_u64(2^32,2^32)"], + [addF32, [16777216, 1], 16777216, "add_f32 precision loss"], + [addF32, [0.5, 0.25], 0.75, "add_f32 dyadics"], + [addF32, [3.4e38, 3.4e38], Infinity, "add_f32 overflow to +inf"], + [sumI32_9, [1, -2, 3, -4, 5, -6, 7, -8, 100000], 99996n, "sum_i32_9"], + [sumI32_9, [2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647], 19327352823n, "sum_i32_9 max"], + [sumF64_9, [1, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625], 1.99609375, "sum_f64_9 dyadics"], + [sumU8_12, [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], 3060n, "sum_u8_12 max"], + [sumU8_12, [1, 2, 4, 8, 16, 32, 64, 128, -1, 256, 257, 511], 766n, "sum_u8_12 wrapped powers"], + [sumI16_10, [-32768, -32768, 32767, 32767, -1, 1, 40000, -40000, 65535, 65536], -3n, "sum_i16_10 edges"], + [mix1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 385, "mix_1 identity ramp"], + [mix1, [-2147483648, -0.5, -1000000, -1.5, 4096, 255, 0, -32768, 2, 2147483647], 19324112699, "mix_1 edges"], + [mix6, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 91, "mix_6 all ones"], + [mix6, [2, 0, -3, -1, -0.25, NaN, 0.5, 256, true, false, null, undefined, true], 28.25, "mix_6 truthiness edges"], + [widenChar, [-1], -1, "widen_char(-1)"], + [widenChar, [200], -56, "widen_char(200)"], + [widenU16, [-1], 65535, "widen_u16(-1)"], + [twoAsBool, [], true, "ret_two_as_bool"], + [retNullPtr, [], null, "ret_null_ptr"], + [highPtr, [], 0x00007fffdeadbee0, "high_ptr"], + [retNegOneI8, [], -1, "ret_neg_one_i8"], + [retNegOneU32, [], 4294967295, "ret_neg_one_u32"], + [retNegOneU64, [], 18446744073709551615n, "ret_neg_one_u64"], + [retDenormalF32, [], 2 ** -149, "ret_denormal_f32"], + [retNegZeroF64, [], -0, "ret_neg_zero_f64"], + [retInfF64, [], Infinity, "ret_inf_f64"], + ]; + + function runBattery(phase) { + for (const [callable, args, expected, label] of battery) { + const actual = callable(...args); + if (!Object.is(actual, expected)) + throw new Error(phase + " " + label + ": expected " + describe(expected) + " but got " + describe(actual)); + } + } + + // Cold pass (whatever tier the harness starts in). + runBattery("cold"); + // Warm each row through its OWN exact-arity, non-spread, single-callee + // caller so that caller tiers up with a monomorphic CallFFI site, then + // re-run the whole battery. + for (const [callable, args, expected, label] of battery) { + const caller = makeMonomorphicCaller(args.length); + for (let i = 0; i < 4000; ++i) { + const actual = caller(callable, args); + if (!Object.is(actual, expected)) + throw new Error("warm " + label + " iteration " + i + ": expected " + describe(expected) + " but got " + describe(actual)); + } + } + runBattery("hot"); + // A few thousand mixed iterations across every row (megamorphic-ish). + for (let i = 0; i < 6000; ++i) { + const [callable, args, expected, label] = battery[i % battery.length]; + const actual = callable(...args); + if (!Object.is(actual, expected)) + throw new Error("mixed " + label + " iteration " + i + ": expected " + describe(expected) + " but got " + describe(actual)); + } +} + +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-typedarray-storage-modes.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-typedarray-storage-modes.js new file mode 100644 index 000000000000..a6fffc26f4dd --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-typedarray-storage-modes.js @@ -0,0 +1,139 @@ +//@ requireOptions("--useDollarVM=1") + +// TypedArray storage modes vs. the ptr/buffer argument fast paths (SPEC +// sections 5 and 8.3): a JSArrayBufferView's vector lives in different +// places depending on its mode -- +// FastTypedArray small array, GC-auxiliary storage, no ArrayBuffer +// OversizeTypedArray large array, malloc'd (Gigacage) storage, no ArrayBuffer +// WastefulTypedArray ArrayBuffer-backed (created up front, or MATERIALIZED +// on demand by touching .buffer, which MOVES the storage: +// JSArrayBufferView::slowDownAndWasteMemory) +// The IC stub / DFG / FTL read the vector with a single offsetOfVector() load, +// so every mode must yield the right pointer, and a mode transition between +// two calls must be picked up (the freshly materialized buffer, not the stale +// pre-transition storage). None of this may crash the engine at any tier. + +// The stress harness also runs every file with the JIT disabled (lockdown / +// no-jit configs), where bun:ffi creation throws by design (SPEC section +// 0.1); like every ffi-*.js file, gate the body on $vm.useJIT() +// (ffi-no-jit.js covers the no-JIT behavior explicitly). +if (!$vm.useJIT()) + quit(); + +const identity = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, $vm.ffiFixture("ffi_ptr_identity"), "ffi_ptr_identity"); +const readU32 = $vm.ffiFunction({ args: ["ptr"], returns: "u32" }, $vm.ffiFixture("ffi_ptr_read_u32"), "ffi_ptr_read_u32"); +const writeU32 = $vm.ffiFunction({ args: ["ptr", "u32"], returns: "void" }, $vm.ffiFixture("ffi_ptr_write_u32"), "ffi_ptr_write_u32"); +const bufferIdentity = $vm.ffiFunction({ args: ["buffer"], returns: "ptr" }, $vm.ffiFixture("ffi_ptr_identity"), "ffi_buffer_identity"); +const callVoid = $vm.ffiFunction({ args: ["ptr"], returns: "void" }, $vm.ffiFixture("ffi_call_cb_void"), "ffi_call_cb_void"); + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + String(expected) + " but got " + String(actual)); +} + +function currentPtr(view) { return identity(view); } + +function main() { + // ---- 1. Fast (small) vs Oversize (large) vs Wasteful (buffer-backed) views: + // pointer identity and read-through must agree across many iterations + // (hot enough for baseline -> DFG -> FTL on the call sites below). + const fast = new Uint32Array(4); // FastTypedArray + fast[0] = 0xF00D; + const oversize = new Uint32Array(1 << 16); // OversizeTypedArray (256KB) + oversize[0] = 0x0517E; + const wasteful = new Uint32Array(new ArrayBuffer(64)); // Wasteful from birth + wasteful[0] = 0xBEEF; + + for (let i = 0; i < 2e4; ++i) { + check(readU32(fast), 0xF00D, "fast read"); + check(readU32(wasteful), 0xBEEF, "wasteful read"); + // The buffer-typed argument path (requires a view; §5) sees the same storage. + check(bufferIdentity(fast) === currentPtr(fast), true, "buffer arg == ptr arg (fast)"); + // Write then read back through the oversize (malloc'd Gigacage) storage. + writeU32(oversize, i & 0xffff); + check(oversize[0], i & 0xffff, "oversize write visible to JS"); + check(readU32(oversize), (i & 0xffff) >>> 0, "oversize read after write"); + } + + // ---- 2. Materialize .buffer AFTER the call site is hot: the storage + // MOVES (slowDownAndWasteMemory). Subsequent calls must see the new + // location, and reads/writes must round-trip through it. + const migrant = new Uint32Array(4); // starts Fast + migrant[0] = 0xAAAA; + for (let i = 0; i < 2e4; ++i) + check(readU32(migrant), 0xAAAA, "pre-materialization read"); + const before = currentPtr(migrant); + const buffer = migrant.buffer; // <-- materialize: mode transition happens here + const after = currentPtr(migrant); + check(migrant[0], 0xAAAA, "contents survive materialization"); + check(readU32(migrant), 0xAAAA, "read after materialization (same hot site)"); + if (buffer.byteLength !== 16) + throw new Error("materialized buffer has wrong length: " + buffer.byteLength); + // The engine reads the vector fresh on every call, so writes through the + // NEW storage are visible; a stale cached pre-transition pointer would not be. + writeU32(migrant, 0xC0FFEE); + check(migrant[0], 0xC0FFEE, "write after materialization lands in the new storage"); + check(new Uint32Array(buffer)[0], 0xC0FFEE, "write visible through the materialized buffer"); + // (before/after may or may not differ numerically depending on the + // allocator; the invariant is behavior, asserted above, not the address.) + void before; void after; + + // Keep the hot site polymorphic across modes so one compiled body sees all three. + const rotation = [fast, oversize, wasteful, migrant]; + for (let i = 0; i < 4e4; ++i) { + const view = rotation[i & 3]; + const expected = view[0]; + check(readU32(view), expected, "rotating-mode read " + (i & 3)); + } + + // ---- 3. A view over a SLICE of a buffer (byteOffset != 0): the pointer + // must include the offset (vector() already does; this pins it). + const slab = new ArrayBuffer(64); + const whole = new Uint32Array(slab); + whole[3] = 0xDEAD10; + const sliced = new Uint32Array(slab, 12, 4); // byteOffset 12 -> element index 3 + for (let i = 0; i < 2e4; ++i) + check(readU32(sliced), 0xDEAD10, "sliced view reads at its byteOffset"); + writeU32(sliced, 0x51CE); + check(whole[3], 0x51CE, "write through sliced view lands at the offset"); + + // ---- 4. Mode transition triggered INSIDE the FFI call by a JS callback, + // while native code (conceptually) still holds the pre-call vector. This + // is the documented user-error case (like detach-during-callback); the + // engine's contract is only that it does not crash and stays consistent + // AFTER the call. The callback materializes .buffer on a Fast view. + const trigger = new Uint32Array(4); + trigger[0] = 0x1EAD; + const materializeInCallback = $vm.ffiCallback({ args: [], returns: "void" }, () => { + trigger.buffer; // slowDownAndWasteMemory mid-FFI-call + }); + for (let i = 0; i < 5e3; ++i) { + // Pass the callback's native entrypoint as a plain pointer; the fixture calls it. + callVoid(materializeInCallback.ptr); + check(readU32(trigger), 0x1EAD, "read of the (already-materialized) view after callback " + i); + } + + // ---- 5. Detach hot: the same site sees a live view, then that view is + // detached; conversion yields a null pointer (0) from then on (§5), at every tier. + const doomed = new ArrayBuffer(32); + const doomedView = new Uint32Array(doomed); + for (let i = 0; i < 2e4; ++i) + currentPtr(doomedView); + check(currentPtr(doomedView) !== null && currentPtr(doomedView) !== 0, true, "live view yields a non-null pointer"); + if (typeof doomed.transfer === "function") + doomed.transfer(); + else if (typeof transferArrayBuffer === "function") + transferArrayBuffer(doomed); + else + return; // No detach primitive in this shell; sections 1-4 still ran. + check(doomedView.length, 0, "view is detached"); + for (let i = 0; i < 100; ++i) + check(currentPtr(doomedView), null, "detached view -> null pointer after tier-up (iteration " + i + ")"); +} +noInline(main); + +main(); +// A full GC and a second run make the storage-lifetime story exercise +// reclamation of the (now-unreferenced) pre-materialization storage. +gc(); +main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-types-echo.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-types-echo.js new file mode 100644 index 000000000000..54204acd8c42 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-types-echo.js @@ -0,0 +1,210 @@ +//@ requireOptions("--useDollarVM=1") + +// Every FFI type through its ffi_echo_* fixture with edge values, cold and +// in a loop hot enough to reach the optimizing tiers; results must be +// identical across tiers (SPEC sections 5 and 11.4). + +function describe(value) { + if (typeof value === "bigint") + return String(value) + "n"; + if (typeof value === "symbol") + return value.toString(); + if (Object.is(value, -0)) + return "-0"; + return String(value); +} + +function check(actual, expected, message) { + if (!Object.is(actual, expected)) + throw new Error(message + ": expected " + describe(expected) + " but got " + describe(actual)); +} + +// Returns a NEW caller function every time (a distinct FunctionExecutable / +// CodeBlock via `new Function`), so the FFI call site inside it is +// monomorphic, exact-arity and non-spread -- the only call-site shape the DFG +// ByteCodeParser constant-callee feed and the strength-reduction +// Call -> CallFFI conversion accept (SPEC section 10.2). Warming the FFI +// functions through one shared `fn(x)` site in a table loop would leave that +// site polymorphic, and the typed CallFFI path would never be compiled. +function makeMonomorphicCaller(arity) { + const argumentList = Array.from({ length: arity }, (_, i) => "args[" + i + "]").join(", "); + return new Function("callable", "args", "return callable(" + argumentList + ");"); +} + +function main() { + const fixture = name => $vm.ffiFixture(name); + const echo = (fixtureName, type, name) => $vm.ffiFunction({ args: [type], returns: type }, fixture(fixtureName), name || (fixtureName + ":" + type)); + + const echoChar = echo("ffi_echo_char", "char"); + const echoI8 = echo("ffi_echo_i8", "i8"); + const echoU8 = echo("ffi_echo_u8", "u8"); + const echoI16 = echo("ffi_echo_i16", "i16"); + const echoU16 = echo("ffi_echo_u16", "u16"); + const echoI32 = echo("ffi_echo_i32", "i32"); + const echoU32 = echo("ffi_echo_u32", "u32"); + const echoI64 = echo("ffi_echo_i64", "i64"); + const echoU64 = echo("ffi_echo_u64", "u64"); + const echoI64Fast = echo("ffi_echo_i64", "i64_fast", "ffi_echo_i64:i64_fast"); + const echoU64Fast = echo("ffi_echo_u64", "u64_fast", "ffi_echo_u64:u64_fast"); + const echoF32 = echo("ffi_echo_f32", "f32"); + const echoF64 = echo("ffi_echo_f64", "f64"); + const echoBool = echo("ffi_echo_bool", "bool"); + const echoPtr = echo("ffi_echo_ptr", "ptr"); + const echoCString = echo("ffi_echo_cstring", "cstring"); + const echoNapiValue = echo("ffi_echo_jsvalue", "napi_value"); + + const object = { tag: "object" }; + const symbol = Symbol("napi"); + + const batteries = [ + [echoChar, "char", [ + [0, 0], [1, 1], [-1, -1], [127, 127], [-128, -128], [128, -128], [255, -1], [0x1ff, -1], + [-129, 127], [200, -56], [true, 1], [false, 0], [undefined, 0], [null, 0], [1.9, 1], [-1.9, -1], + ]], + [echoI8, "i8", [ + [0, 0], [127, 127], [-128, -128], [128, -128], [255, -1], [256, 0], [-129, 127], [0.9, 0], + [true, 1], [undefined, 0], [null, 0], [NaN, 0], [Infinity, 0], [-Infinity, 0], + ]], + [echoU8, "u8", [ + [0, 0], [255, 255], [256, 0], [-1, 255], [511, 255], [-129, 127], [300.7, 44], [128, 128], + [true, 1], [false, 0], [undefined, 0], [null, 0], [NaN, 0], [-0, 0], + ]], + [echoI16, "i16", [ + [0, 0], [32767, 32767], [32768, -32768], [-32769, 32767], [-1, -1], [65535, -1], [0x12345, 0x2345], + [-32768, -32768], [65536, 0], [1.5, 1], [undefined, 0], [true, 1], + ]], + [echoU16, "u16", [ + [0, 0], [65535, 65535], [65536, 0], [-1, 65535], [70000, 4464], [32768, 32768], [-32768, 32768], + [undefined, 0], [null, 0], [NaN, 0], [true, 1], + ]], + [echoI32, "i32", [ + [0, 0], [1, 1], [-1, -1], [2147483647, 2147483647], [-2147483648, -2147483648], + [2147483648, -2147483648], [-2147483649, 2147483647], [4294967301, 5], [4294967296, 0], + [-1.5, -1], [1.9, 1], [-0.9, 0], [0.5, 0], [-0, 0], [NaN, 0], [Infinity, 0], [-Infinity, 0], + [1e10, 1410065408], [undefined, 0], [null, 0], [true, 1], [false, 0], + ]], + [echoU32, "u32", [ + [0, 0], [-1, 4294967295], [4294967295, 4294967295], [4294967296, 0], [2147483648, 2147483648], + [2147483647, 2147483647], [-0.5, 0], [1e10, 1410065408], [-2147483648, 2147483648], [undefined, 0], + [null, 0], [true, 1], [NaN, 0], + ]], + [echoI64, "i64", [ + [0, 0n], [1, 1n], [-1, -1n], [2 ** 53, 9007199254740992n], [-(2 ** 53), -9007199254740992n], + [123n, 123n], [-123n, -123n], [2n ** 63n - 1n, 9223372036854775807n], [-(2n ** 63n), -9223372036854775808n], + [2n ** 64n + 7n, 7n], [2n ** 63n, -9223372036854775808n], [-1.5, -1n], [2.9, 2n], [-0, 0n], + [2147483647, 2147483647n], [-2147483648, -2147483648n], [4294967296, 4294967296n], + ]], + [echoU64, "u64", [ + [0, 0n], [1, 1n], [-1, 18446744073709551615n], [4294967295, 4294967295n], [2n ** 64n - 1n, 18446744073709551615n], + [2n ** 64n + 7n, 7n], [-2n, 18446744073709551614n], [2.5, 2n], [2 ** 53, 9007199254740992n], + [-2147483648, 18446744071562067968n], [9007199254740991, 9007199254740991n], + ]], + [echoI64Fast, "i64_fast", [ + [0, 0], [-1, -1], [42, 42], [2 ** 53 - 1, 9007199254740991], [-(2 ** 53 - 1), -9007199254740991], + [2n ** 53n, 9007199254740992n], [-(2n ** 53n), -9007199254740992n], [1n << 62n, 4611686018427387904n], + [2n ** 63n - 1n, 9223372036854775807n], [-2, -2], [3.7, 3], [-3.7, -3], + ]], + [echoU64Fast, "u64_fast", [ + [0, 0], [123, 123], [2 ** 53 - 2, 9007199254740990], [2n ** 53n - 1n, 9007199254740991n], + [2n ** 53n, 9007199254740992n], [-1, 18446744073709551615n], [2n ** 64n - 1n, 18446744073709551615n], + [4.9, 4], + ]], + [echoF32, "f32", [ + [0, 0], [-0, -0], [1.5, 1.5], [1.1, Math.fround(1.1)], [-1.1, Math.fround(-1.1)], [NaN, NaN], + [Infinity, Infinity], [-Infinity, -Infinity], [3.4e38, Math.fround(3.4e38)], [3.5e38, Infinity], + [1e-45, Math.fround(1e-45)], [16777217, 16777216], [-16777217, -16777216], [2 ** -149, 2 ** -149], + ]], + [echoF64, "f64", [ + [0, 0], [-0, -0], [1.5, 1.5], [NaN, NaN], [Infinity, Infinity], [-Infinity, -Infinity], + [Number.MAX_VALUE, Number.MAX_VALUE], [Number.MIN_VALUE, Number.MIN_VALUE], [Number.EPSILON, Number.EPSILON], + [Math.PI, Math.PI], [-1e308, -1e308], [undefined, NaN], [123456789.123456789, 123456789.123456789], + ]], + [echoBool, "bool", [ + [0, false], [1, true], [2, true], [-1, true], [0.5, true], [-0, false], [NaN, false], [Infinity, true], + [true, true], [false, false], [undefined, false], [null, false], [255, true], [256, true], + ]], + [echoPtr, "ptr", [ + [0, null], [null, null], [undefined, null], [4096, 4096], [1, 1], [-1, 18446744073709551615n], + [2 ** 40, 2 ** 40], [0.9, null], [-0, null], [65536.7, 65536], + ]], + [echoCString, "cstring", [ + [0, null], [null, null], [undefined, null], + ["hello", "hello"], ["", ""], ["\u00e9\u2603 utf8", "\u00e9\u2603 utf8"], + ]], + [echoNapiValue, "napi_value", [ + [0, 0], [42, 42], [-0, -0], [NaN, NaN], [1.5, 1.5], [undefined, undefined], [null, null], [true, true], + [false, false], ["string", "string"], [object, object], [symbol, symbol], [9007199254740993n, 9007199254740993n], + ]], + ]; + + // Cold: every case exactly once, warm-up free. + for (const [fn, label, cases] of batteries) { + for (const [input, expected] of cases) + check(fn(input), expected, label + " cold echo(" + describe(input) + ")"); + } + + // Hot: enough iterations to tier the per-battery caller through baseline + // into DFG/FTL, where the exact-arity monomorphic call site becomes a + // typed CallFFI node (ffi-callffi-was-compiled.js machine-checks that + // conversion via $vm.ffiCompileCounts()). + for (const [fn, label, cases] of batteries) { + // (a) Monomorphic input through a dedicated single-callee caller: the + // typed CallFFI fast path is what the loop settles on. + const monoCaller = makeMonomorphicCaller(1); + const [monoInput, monoExpected] = cases[1]; + const monoArgs = [monoInput]; + for (let i = 0; i < 4e4; ++i) { + const result = monoCaller(fn, monoArgs); + if (!Object.is(result, monoExpected)) + throw new Error(label + " hot mono iteration " + i + ": expected " + describe(monoExpected) + " but got " + describe(result)); + } + // (b) Mixed inputs cycling through every edge case at another dedicated + // single-callee site (forces exits, slow paths and re-optimization). + const mixedCaller = makeMonomorphicCaller(1); + const inputs = cases.map(c => [c[0]]); + for (let i = 0; i < 1.5e4; ++i) { + const k = i % cases.length; + const result = mixedCaller(fn, inputs[k]); + if (!Object.is(result, cases[k][1])) + throw new Error(label + " hot mixed iteration " + i + " echo(" + describe(cases[k][0]) + "): expected " + describe(cases[k][1]) + " but got " + describe(result)); + } + } + + // char signedness lock across a dedicated widening fixture. + const widenChar = $vm.ffiFunction({ args: ["char"], returns: "i64_fast" }, fixture("ffi_widen_char"), "ffi_widen_char"); + check(widenChar(-1), -1, "ffi_widen_char(-1)"); + check(widenChar(255), -1, "ffi_widen_char(255)"); + check(widenChar(128), -128, "ffi_widen_char(128)"); + check(widenChar(127), 127, "ffi_widen_char(127)"); + for (let i = 0; i < 2e4; ++i) { + if (widenChar(-1) !== -1) + throw new Error("ffi_widen_char(-1) !== -1 in hot loop"); + } + + // typeof edges of the fast 64-bit variants. + check(typeof echoI64Fast(2 ** 53 - 2), "number", "i64_fast typeof at 2^53-2"); + check(typeof echoI64Fast(2 ** 53 - 1), "number", "i64_fast typeof at 2^53-1"); + check(typeof echoI64Fast(2n ** 53n), "bigint", "i64_fast typeof at 2^53"); + check(typeof echoI64Fast(-(2 ** 53 - 1)), "number", "i64_fast typeof at -(2^53-1)"); + check(typeof echoI64Fast(-(2n ** 53n)), "bigint", "i64_fast typeof at -2^53"); + check(typeof echoU64Fast(2 ** 53 - 2), "number", "u64_fast typeof at 2^53-2"); + check(typeof echoU64Fast(2n ** 53n - 1n), "bigint", "u64_fast typeof at 2^53-1 (strict < quirk)"); + check(typeof echoU64Fast(2n ** 53n), "bigint", "u64_fast typeof at 2^53"); + check(typeof echoI64(0), "bigint", "i64 is always a BigInt"); + check(typeof echoU64(0), "bigint", "u64 is always a BigInt"); + + // Debug builds must not assert on NaN through f32/f64 (purifyNaN). + for (let i = 0; i < 1e4; ++i) { + if (!Number.isNaN(echoF32(NaN))) + throw new Error("echo f32 NaN not NaN"); + if (!Number.isNaN(echoF64(NaN))) + throw new Error("echo f64 NaN not NaN"); + } +} + +// FFI-SPEC-GAP: the stress harness also runs every file under --useJIT=false +// (lockdown/no-jit configs), where bun:ffi creation throws by design (SPEC +// section 0.1). Every ffi-*.js file therefore gates its body on $vm.useJIT(); +// ffi-no-jit.js covers the no-JIT behavior explicitly. +if ($vm.useJIT()) + main(); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-float-args.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-float-args.js new file mode 100644 index 000000000000..8d7d8b797638 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-float-args.js @@ -0,0 +1,51 @@ +//@ requireOptions("--useDollarVM=1") +// An f32/f64 parameter whose call site does NOT speculate a number (FFIDFG falls back to +// UntypedUse) has no single SSA operand: the value is converted into the canonical slot by +// operationFFIWriteSlot. The FTL direct call must reload that slot AS A FLOAT so the value goes +// out in an FPR -- reloading it as an integer sends the bits in a GPR and the callee reads junk. +if (!$vm.useJIT()) quit(); + +const fixture = name => $vm.ffiFixture(name); +const addF64 = $vm.ffiFunction({ args: ["f64", "f64"], returns: "f64" }, fixture("ffi_add_f64"), "add_f64"); +const addF32 = $vm.ffiFunction({ args: ["f32", "f32"], returns: "f32" }, fixture("ffi_add_f32"), "add_f32"); +const echoF64 = $vm.ffiFunction({ args: ["f64"], returns: "f64" }, fixture("ffi_echo_f64"), "echo_f64"); + +// Oracles pinned below the DFG. +function refAddF64(a, b) { return addF64(a, b); } +function refAddF32(a, b) { return addF32(a, b); } +function refEchoF64(a) { return echoF64(a); } +noDFG(refAddF64); noDFG(refAddF32); noDFG(refEchoF64); +noInline(refAddF64); noInline(refAddF32); noInline(refEchoF64); + +function hotAddF64(a, b) { return addF64(a, b); } +function hotAddF32(a, b) { return addF32(a, b); } +function hotEchoF64(a) { return echoF64(a); } +noInline(hotAddF64); noInline(hotAddF32); noInline(hotEchoF64); + +let failures = 0; +function agree(label, hot, ref) { + const same = Object.is(hot, ref); + if (!same) { print(`MISMATCH [${label}]: hot=${String(hot)} ref=${String(ref)}`); if (++failures > 8) throw new Error("too many mismatches"); } +} + +// Poison the argument prediction so FFIDFG picks UntypedUse: feed values that are numbers most of +// the time but sometimes null/undefined/boolean, so neither shouldSpeculateDoubleReal() nor +// shouldSpeculateNumber() holds at the call site. +const poison = [1.5, 2.25, null, undefined, true, false, -0.5, 1e300, NaN, 0]; +const iterations = 30000; +for (let i = 0; i < iterations; ++i) { + const a = poison[i % poison.length]; + const b = poison[(i + 3) % poison.length]; + agree(`addF64#${i}`, hotAddF64(a, b), refAddF64(a, b)); + agree(`addF32#${i}`, hotAddF32(a, b), refAddF32(a, b)); + agree(`echoF64#${i}`, hotEchoF64(a), refEchoF64(a)); +} + +// And the plain numeric case must still be exact after all that. +for (let i = 0; i < 5000; ++i) { + agree(`exact#${i}`, hotAddF64(1.5, 2.25), 3.75); + agree(`exactEcho#${i}`, hotEchoF64(1e300), 1e300); +} + +if (failures) throw new Error(`${failures} mismatch(es)`); +print("ffi untyped float args: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-int-stack-args.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-int-stack-args.js new file mode 100644 index 000000000000..e4d17a4eb3d6 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-untyped-int-stack-args.js @@ -0,0 +1,25 @@ +//@ requireOptions("--useDollarVM=1") +// Regression: with int32 speculation gated on profiling, an i32/u32 parameter can carry an +// UntypedUse edge. On the FTL DIRECT-call path that operand must be reloaded as a B3 Int32; as an +// Int64 CCallValue lays a STACK argument at 8-byte stride, but Darwin/arm64 packs a stacked +// int32_t at 4-byte natural stride, so the 9th/10th arguments (spilled past the 8 GPRs) would +// shift. Poison the profile so the args stay UntypedUse, pass values whose position matters +// (weighted sum), and compare the FTL-hot twin against a noDFG oracle. +if (!$vm.useJIT()) quit(); +const sum10 = $vm.ffiFunction({ args: ["i32","i32","i32","i32","i32","i32","i32","i32","i32","i32"], returns: "i64" }, + $vm.ffiFixture("ffi_sum_i32_x10"), "sum_i32_x10"); +function ref(a,b,c,d,e,f,g,h,i,j) { return sum10(a,b,c,d,e,f,g,h,i,j); } +function hot(a,b,c,d,e,f,g,h,i,j) { return sum10(a,b,c,d,e,f,g,h,i,j); } +noDFG(ref); noInline(ref); noInline(hot); +let failures = 0; +// Mix ints with booleans / null / doubles so FFIDFG's shouldSpeculateInt32() gate falls to +// UntypedUse for these arguments (the exact edge kind the direct-call reload must handle). +const vals = [7, true, null, 3, false, 2.0, 9, 1, 12345, -6, 100000, 4]; +for (let it = 0; it < 60000; ++it) { + const a = vals[it % vals.length], b = vals[(it + 1) % vals.length], c = 5, d = -1, e = it & 7, f = 8, + g = vals[(it + 3) % vals.length], h = 2, i = it & 3, j = vals[(it + 5) % vals.length]; + const hv = hot(a,b,c,d,e,f,g,h,i,j), rv = ref(a,b,c,d,e,f,g,h,i,j); + if (hv !== rv) { print(`MISMATCH it=${it}: hot=${hv} ref=${rv} args=${[a,b,c,d,e,f,g,h,i,j]}`); if (++failures > 5) throw new Error("stack arg stride mismatch"); } +} +if (failures) throw new Error(failures + " mismatches"); +print("ffi untyped int stack args: all checks passed"); diff --git a/test/js/bun/jsc-stress/fixtures/ffi/ffi-view-args.js b/test/js/bun/jsc-stress/fixtures/ffi/ffi-view-args.js new file mode 100644 index 000000000000..2f63c9e7b486 --- /dev/null +++ b/test/js/bun/jsc-stress/fixtures/ffi/ffi-view-args.js @@ -0,0 +1,191 @@ +//@ requireOptions("--useDollarVM=1") +// Pointer-family FFI arguments accept typed-array / DataView VIEWS directly, resolved inline in the +// DFG and (now) the FTL to the view's data pointer. This is a per-call tier-differential test: every +// hot (FTL-bound) function has a noDFG-pinned twin as the interpreter/baseline oracle, and the two +// must agree on EVERY iteration -- for every view type, storage mode, and the guard cases (detached, +// shared / resizable) that must punt to the C++ conversion in every tier. +if (!$vm.useJIT()) quit(); + +const fixture = name => $vm.ffiFixture(name); +const identity = $vm.ffiFunction({ args: ["ptr"], returns: "ptr" }, fixture("ffi_ptr_identity"), "ffi_ptr_identity"); +const strlen = $vm.ffiFunction({ args: ["cstring"], returns: "u64" }, fixture("ffi_strlen"), "ffi_strlen"); +const bufIdentity = $vm.ffiFunction({ args: ["buffer"], returns: "ptr" }, fixture("ffi_ptr_identity"), "ffi_ptr_identity(buffer)"); +const readU32 = $vm.ffiFunction({ args: ["ptr"], returns: "u32" }, fixture("ffi_ptr_read_u32"), "ffi_ptr_read_u32"); + +// Oracle twins: same call, pinned below the DFG, so their result is the interpreter/baseline +// (out-of-line C++ conversion) answer that the JIT tiers must match exactly. +function refIdentity(v) { return identity(v); } +function refBufIdentity(v) { return bufIdentity(v); } +function refStrlen(v) { return strlen(v); } +noDFG(refIdentity); noDFG(refBufIdentity); noDFG(refStrlen); +noInline(refIdentity); noInline(refBufIdentity); noInline(refStrlen); + +function hotIdentity(v) { return identity(v); } +function hotBufIdentity(v) { return bufIdentity(v); } +function hotStrlen(v) { return strlen(v); } +noInline(hotIdentity); noInline(hotBufIdentity); noInline(hotStrlen); + +let failures = 0; +function agree(label, hot, ref) { + // A pointer is a number, jsNull for 0, or an exact BigInt above 2^53 -- compare exactly. + if (hot !== ref) { + print(`TIER MISMATCH [${label}]: hot=${String(hot)} ref=${String(ref)}`); + if (++failures > 8) throw new Error("too many tier mismatches"); + } +} + +const iterations = 30000; + +// --------------------------------------------------------------------------------------------- +// 1. Every view type resolves to base + byteOffset, and stays tier-stable. +// --------------------------------------------------------------------------------------------- +const backing = new ArrayBuffer(512); +const views = [ + ["Int8Array", new Int8Array(backing, 8)], + ["Uint8Array", new Uint8Array(backing, 16)], + ["Uint8ClampedArray", new Uint8ClampedArray(backing, 24)], + ["Int16Array", new Int16Array(backing, 32)], + ["Uint16Array", new Uint16Array(backing, 40)], + ["Int32Array", new Int32Array(backing, 48)], + ["Uint32Array", new Uint32Array(backing, 56)], + ["Float32Array", new Float32Array(backing, 64)], + ["Float64Array", new Float64Array(backing, 72)], + ["BigInt64Array", new BigInt64Array(backing, 80)], + ["BigUint64Array", new BigUint64Array(backing, 88)], + ["DataView", new DataView(backing, 96)], +]; +const basePtr = refIdentity(new Uint8Array(backing)); +if (typeof basePtr !== "number" || basePtr === 0) + throw new Error("bad base pointer: " + String(basePtr)); +for (const [name, view] of views) { + const expected = basePtr + view.byteOffset; + if (refIdentity(view) !== expected) + throw new Error(`${name}: byteOffset not applied by the reference path: ${refIdentity(view)} vs ${expected}`); +} +for (let i = 0; i < iterations; ++i) { + for (const [name, view] of views) { + agree(`identity(${name})#${i}`, hotIdentity(view), refIdentity(view)); + agree(`buffer(${name})#${i}`, hotBufIdentity(view), refBufIdentity(view)); + } +} + +// --------------------------------------------------------------------------------------------- +// 2. Storage modes: a Fast (GC-heap vector) view whose .buffer is materialized MID-LOOP moves to +// Wasteful storage (the vector may be re-pointed); an Oversize (Gigacage) view; a subarray. +// Whatever the pointer is at each instant, both tiers must agree on it -- and the pointer must +// still address the LIVE bytes (proven by reading through it). +// --------------------------------------------------------------------------------------------- +const fastView = new Uint32Array(64); // Fast: small, GC-heap vector +const oversize = new Uint8Array(4 * 1024 * 1024); // Oversize: Gigacage-allocated +const sub = oversize.subarray(4096, 8192); +fastView[0] = 0xdeadbeef; +oversize[4096] = 0x7f; +let materialized = false; +for (let i = 0; i < iterations; ++i) { + agree(`fast#${i}`, hotIdentity(fastView), refIdentity(fastView)); + agree(`oversize#${i}`, hotIdentity(oversize), refIdentity(oversize)); + agree(`subarray#${i}`, hotIdentity(sub), refIdentity(sub)); + // The pointer must address live memory: read the value we stored, through the returned pointer. + if (readU32(hotIdentity(fastView)) !== 0xdeadbeef) + throw new Error(`fast view pointer does not address live data at ${i}`); + if (hotIdentity(sub) !== hotIdentity(oversize) + 4096) + throw new Error(`subarray offset lost at ${i}`); + if (!materialized && i === (iterations >> 1)) { + void fastView.buffer; // Fast -> Wasteful: storage may move + materialized = true; + } +} +// After materialization the read-through invariant must still hold at the (possibly new) pointer. +if (readU32(hotIdentity(fastView)) !== 0xdeadbeef || readU32(refIdentity(fastView)) !== 0xdeadbeef) + throw new Error("materialized wasteful view lost its data or its pointer"); + +// --------------------------------------------------------------------------------------------- +// 3. cstring parameter with a view: the inline path hands over the vector and C reads real, +// NUL-terminated bytes -- including through a byteOffset'd subarray. +// --------------------------------------------------------------------------------------------- +const strBuf = new Uint8Array(64); // "abc\0" at 0, then "engine-native\0" at 16 +strBuf.set([97, 98, 99, 0], 0); +strBuf.set([101, 110, 103, 105, 110, 101, 45, 110, 97, 116, 105, 118, 101, 0], 16); +const strView = strBuf.subarray(16); +for (let i = 0; i < iterations; ++i) { + agree(`strlen(buf)#${i}`, hotStrlen(strBuf), refStrlen(strBuf)); + agree(`strlen(sub)#${i}`, hotStrlen(strView), refStrlen(strView)); + if (hotStrlen(strBuf) !== 3n || hotStrlen(strView) !== 13n) + throw new Error(`cstring-from-view read wrong bytes at ${i}: ${hotStrlen(strBuf)}, ${hotStrlen(strView)}`); +} + +// --------------------------------------------------------------------------------------------- +// 4. The number paths of the same untyped conversion must be undisturbed by the new view checks +// (regression cover): int32 / double / negative / null / large all agree tier-to-tier. +// --------------------------------------------------------------------------------------------- +const numberArgs = [0, 1, 4096, -1, 2147483647, -2147483648, 4294967296, 1.5e9, null, undefined]; +for (let i = 0; i < iterations; ++i) + for (const n of numberArgs) + agree(`number(${String(n)})#${i}`, hotIdentity(n), refIdentity(n)); + +// --------------------------------------------------------------------------------------------- +// 5. GUARD: a DETACHED view must never leak its stale pointer through the inline path (null +// vector -> slow path); whatever the C++ conversion decides (0 / null / throw), the tiers agree. +// --------------------------------------------------------------------------------------------- +function tryHot(fn, v) { try { return fn(v); } catch (e) { return "threw:" + e.constructor.name; } } +const detached = new Uint8Array(new ArrayBuffer(64)); +const stalePtr = refIdentity(detached); +if (typeof detached.buffer.transfer === "function") detached.buffer.transfer(); +else if (typeof transferArrayBuffer === "function") transferArrayBuffer(detached.buffer); +else throw new Error("no way to detach an ArrayBuffer in this shell"); +for (let i = 0; i < iterations; ++i) { + const hot = tryHot(hotIdentity, detached), ref = tryHot(refIdentity, detached); + agree(`detached#${i}`, hot, ref); + if (hot === stalePtr && stalePtr !== 0) + throw new Error(`detached view leaked its stale pointer at ${i}: ${String(hot)}`); +} + +// --------------------------------------------------------------------------------------------- +// 6. GUARD: RESIZABLE and GROWABLE-SHARED backed views carry the isResizableOrGrowableShared +// mode bits and must take the C++ path in every tier; a plain (fixed) SharedArrayBuffer view +// does not carry them. In all cases the requirement is tier AGREEMENT, whatever the C++ +// conversion's policy for these buffers is. +// --------------------------------------------------------------------------------------------- +const guardedViews = []; +guardedViews.push(["resizable", new Uint8Array(new ArrayBuffer(64, { maxByteLength: 256 }))]); +if (typeof SharedArrayBuffer === "function") { + guardedViews.push(["shared-fixed", new Uint8Array(new SharedArrayBuffer(64))]); + let growable; + try { growable = new SharedArrayBuffer(64, { maxByteLength: 256 }); } catch (e) { growable = null; } + if (growable) + guardedViews.push(["shared-growable", new Uint8Array(growable)]); +} +for (let i = 0; i < iterations; ++i) + for (const [name, view] of guardedViews) + agree(`${name}#${i}`, tryHot(hotIdentity, view), tryHot(refIdentity, view)); + +// --------------------------------------------------------------------------------------------- +// 7. buffer-typed parameter rejects NON-view values consistently in every tier (numbers throw +// in C++; the inline path must not accept them either). +// --------------------------------------------------------------------------------------------- +for (let i = 0; i < iterations; ++i) { + const hot = tryHot(hotBufIdentity, 1234), ref = tryHot(refBufIdentity, 1234); + agree(`buffer(number)#${i}`, hot, ref); + if (!String(hot).startsWith("threw:")) + throw new Error(`buffer param accepted a number in the JIT at ${i}: ${String(hot)}`); +} + +// --------------------------------------------------------------------------------------------- +// 8. A view argument followed by a THROWING argument: the exception propagates cleanly (the +// partially-written slot buffer is never observed) and identically across tiers. +// --------------------------------------------------------------------------------------------- +const add = $vm.ffiFunction({ args: ["ptr", "i32"], returns: "ptr" }, fixture("ffi_ptr_identity"), "identity2"); +function hotAdd(v, x) { return add(v, x); } +function refAdd(v, x) { return add(v, x); } +noDFG(refAdd); noInline(refAdd); noInline(hotAdd); +const poison = { valueOf() { throw new RangeError("poison"); } }; +const sym = Symbol("s"); +function tryCall(fn, a, b) { try { return fn(a, b); } catch (e) { return "threw:" + e.constructor.name; } } +for (let i = 0; i < iterations; ++i) { + agree(`view+poison#${i}`, tryCall(hotAdd, strBuf, poison), tryCall(refAdd, strBuf, poison)); + agree(`view+symbol#${i}`, tryCall(hotAdd, strBuf, sym), tryCall(refAdd, strBuf, sym)); +} + +if (failures) + throw new Error(`${failures} tier mismatch(es) reported above`); +print("ffi view args: all checks passed"); diff --git a/test/js/bun/jsc-stress/jsc-stress.test.ts b/test/js/bun/jsc-stress/jsc-stress.test.ts index 32736352c60b..9ee07adfdf61 100644 --- a/test/js/bun/jsc-stress/jsc-stress.test.ts +++ b/test/js/bun/jsc-stress/jsc-stress.test.ts @@ -23,10 +23,15 @@ function parseJSCFlags(filePath: string): Record { if (line === "// @bun" || line.trim() === "") continue; if (!line.startsWith("//@")) break; - const match = line.match(/^\/\/@ (runDefault|runFTLNoCJIT|runDefaultWasm)\((.*)\)/); - if (!match) continue; + const match = line.match(/^\/\/@ (runDefault|runFTLNoCJIT|runDefaultWasm|requireOptions)\((.*)\)/); + const noJIT = /^\/\/@ runNoJIT\b/.test(line); + if (!match && !noJIT) continue; - const [, mode, argsStr] = match; + if (noJIT) { + env["BUN_JSC_useJIT"] = "false"; + continue; + } + const [, mode, argsStr] = match!; // runFTLNoCJIT implies these flags (from WebKit's run-jsc-stress-tests) if (mode === "runFTLNoCJIT") { @@ -146,6 +151,42 @@ const wasmFixtures = [ "jspi-exceptions-from-js.js", ]; +const ffiFixturesDir = path.join(fixturesDir, "ffi"); +const ffiFixtures = [ + "ffi-align.js", + "ffi-arena-depth.js", + "ffi-arity-ladders.js", + "ffi-arity.js", + "ffi-buffer-length.js", + "ffi-callback-throw-unwind.js", + "ffi-callbacks.js", + "ffi-callffi-was-compiled.js", + "ffi-canary.js", + "ffi-conversion-errors-host.js", + "ffi-conversion-errors.js", + "ffi-fuzz-signatures.js", + "ffi-hooks-and-owner.js", + "ffi-host-path.js", + "ffi-jsvalue.js", + "ffi-no-jit.js", + "ffi-non-int32-int-args.js", + "ffi-osr-and-exceptions.js", + "ffi-pointers-and-buffers.js", + "ffi-ptr-object-arg.js", + "ffi-signature-errors.js", + "ffi-subword-and-returns.js", + "ffi-tailcall.js", + "ffi-threadsafe-callback-burst.js", + "ffi-threadsafe-callback-throw.js", + "ffi-threadsafe-callback.js", + "ffi-tier-differential.js", + "ffi-typedarray-storage-modes.js", + "ffi-types-echo.js", + "ffi-untyped-float-args.js", + "ffi-untyped-int-stack-args.js", + "ffi-view-args.js", +]; + const preloadPath = path.join(import.meta.dir, "preload.js"); // Under ASAN, JSC disables the wasm fault signal handler (and therefore wasm @@ -229,4 +270,48 @@ describe.concurrent("JSC JIT Stress Tests", () => { ); } }); + + describe("FFI (bun:ffi engine)", () => { + const probeEnv = { ...bunEnv, BUN_JSC_useDollarVM: "1" }; + const probe = Bun.spawnSync({ + cmd: [ + bunExe(), + "-e", + 'process.stdout.write(typeof globalThis.$vm === "object" && typeof $vm.ffiFunction === "function" ? "1" : "0")', + ], + env: probeEnv, + stdout: "pipe", + }); + const hasDollarVM = probe.stdout.toString() === "1"; + + for (const fixture of ffiFixtures) { + test.skipIf(!hasDollarVM)( + fixture, + async () => { + const fixturePath = path.join(ffiFixturesDir, fixture); + const jscEnv = parseJSCFlags(fixturePath); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "--preload", preloadPath, fixturePath], + env: { ...fixtureEnv, ...jscEnv, BUN_JSC_useDollarVM: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + if (exitCode !== 0) { + console.log("stdout:", stdout); + console.log("stderr:", stderr); + } + expect(exitCode).toBe(0); + }, + fixtureTimeout, + ); + } + }); }); diff --git a/test/js/bun/jsc-stress/preload.js b/test/js/bun/jsc-stress/preload.js index 215af6a069ea..b0b991f5d067 100644 --- a/test/js/bun/jsc-stress/preload.js +++ b/test/js/bun/jsc-stress/preload.js @@ -24,3 +24,11 @@ globalThis.callerIsBBQOrOMGCompiled = function () { }; if (!globalThis.$) globalThis.$ = {}; if (!$.agent) $.agent = { report: function () {} }; + +// GC + tier-control helpers used by the ffi stress fixtures. +const jsc = require("bun:jsc"); +globalThis.gc = () => Bun.gc(true); +globalThis.fullGC = jsc.fullGC; +globalThis.edenGC = jsc.edenGC; +globalThis.numberOfDFGCompiles = jsc.numberOfDFGCompiles; +globalThis.noDFG = jsc.noFTL; diff --git a/test/js/bun/jsc-stress/testFFI.test.ts b/test/js/bun/jsc-stress/testFFI.test.ts new file mode 100644 index 000000000000..f0e91d9442fd --- /dev/null +++ b/test/js/bun/jsc-stress/testFFI.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test"; +import { existsSync } from "fs"; +import { bunExe, isWindows } from "harness"; +import path from "path"; + +const binaryName = isWindows ? "testFFI.exe" : "testFFI"; + +function findTestFFI(): string | null { + const candidates = [ + process.env.BUN_TESTFFI_PATH, + path.join(path.dirname(bunExe()), binaryName), + path.join(import.meta.dir, "../../../../build/debug-local/deps/WebKit/bin", binaryName), + path.join(import.meta.dir, "../../../../build/release-local/deps/WebKit/bin", binaryName), + path.join(import.meta.dir, "../../../../build/debug/deps/WebKit/bin", binaryName), + path.join(import.meta.dir, "../../../../build/release/deps/WebKit/bin", binaryName), + ].filter(Boolean) as string[]; + return candidates.find(candidate => existsSync(candidate)) ?? null; +} + +const testFFI = findTestFFI(); + +test.skipIf(!testFFI)( + "testFFI (JavaScriptCore FFI C++/ABI checks)", + async () => { + await using proc = Bun.spawn({ + cmd: [testFFI!], + env: { ...process.env, ASAN_OPTIONS: "detect_leaks=0" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + const output = stdout + stderr; + if (exitCode !== 0 || !/OK: \d+ checks passed, 0 failed\./.test(output)) { + console.log(output); + } + expect(exitCode).toBe(0); + expect(output).toMatch(/OK: \d+ checks passed, 0 failed\./); + }, + 300_000, +); diff --git a/test/js/sql/sql-mysql-bigint-out-of-range.test.ts b/test/js/sql/sql-mysql-bigint-out-of-range.test.ts new file mode 100644 index 000000000000..dca8daff763e --- /dev/null +++ b/test/js/sql/sql-mysql-bigint-out-of-range.test.ts @@ -0,0 +1,26 @@ +import { SQL } from "bun"; +import { describe, expect, test } from "bun:test"; +import { describeWithContainer, isDockerEnabled } from "harness"; + +async function assertOutOfRangeBigIntRejected(url: string) { + await using sql = new SQL(url); + await sql`select 1`; + await expect(sql`select ${2n ** 65n} as v`.execute()).rejects.toMatchObject({ code: "ERR_OUT_OF_RANGE" }); + expect(await sql`select ${-1n} as v`).toEqual([{ v: -1 }]); +} + +if (isDockerEnabled()) { + describeWithContainer("mysql", { image: "mysql_plain" }, container => { + test("an out-of-range BigInt bind parameter is rejected, not truncated", async () => { + await container.ready; + await assertOutOfRangeBigIntRejected(`mysql://root@${container.host}:${container.port}/bun_sql_test`); + }); + }); +} else { + const url = process.env.MYSQL_URL; + describe("mysql (local)", () => { + test.skipIf(!url)("an out-of-range BigInt bind parameter is rejected, not truncated", async () => { + await assertOutOfRangeBigIntRejected(url!); + }); + }); +} diff --git a/test/napi/napi-app/bundled_napi_headers.c b/test/napi/napi-app/bundled_napi_headers.c new file mode 100644 index 000000000000..e8a940974b9d --- /dev/null +++ b/test/napi/napi-app/bundled_napi_headers.c @@ -0,0 +1,5 @@ +#include +napi_value passthrough(napi_env env, napi_value v) { + (void)env; + return v; +} diff --git a/test/napi/napi-value-ffi.test.ts b/test/napi/napi-value-ffi.test.ts index 8dfba97bb511..5f56ec590884 100644 --- a/test/napi/napi-value-ffi.test.ts +++ b/test/napi/napi-value-ffi.test.ts @@ -1,7 +1,7 @@ import { spawnSync } from "bun"; -import { cc, dlopen } from "bun:ffi"; +import { cc } from "bun:ffi"; import { beforeAll, describe, expect, it } from "bun:test"; -import { existsSync, statSync } from "fs"; +import { existsSync } from "fs"; import { bunEnv, bunExe, canBuildNodeAddons, isASAN, isWindows } from "harness"; import { join } from "path"; @@ -26,23 +26,12 @@ const symbols = { }, }; -let addon1, addon2, cc1, cc2; +let cc1, cc2; const nodeApiHeadersInclude = join(__dirname, "napi-app/node_modules/node-api-headers/include"); -// The addons here don't link against bun, so existing binaries stay valid across -// bun builds. `bun install` triggers a full `node-gyp rebuild` (clean + build of -// every target in napi-app), so skip it when the two .node files this test needs -// already exist and are newer than their sources (napi.test.ts or a previous run -// usually has built them already). function needsInstall(): boolean { - if (!existsSync(nodeApiHeadersInclude)) return true; - for (const name of ["ffi_addon_1", "ffi_addon_2"]) { - const built = join(__dirname, `napi-app/build/Debug/${name}.node`); - if (!existsSync(built)) return true; - if (statSync(built).mtimeMs < statSync(join(__dirname, `napi-app/${name}.c`)).mtimeMs) return true; - } - return false; + return !existsSync(nodeApiHeadersInclude); } beforeAll(() => { @@ -62,8 +51,6 @@ beforeAll(() => { throw new Error("build failed"); } } - addon1 = dlopen(join(__dirname, `napi-app/build/Debug/ffi_addon_1.node`), symbols).symbols; - addon2 = dlopen(join(__dirname, `napi-app/build/Debug/ffi_addon_2.node`), symbols).symbols; // TinyCC's setjmp/longjmp error handling conflicts with ASan. // Skip cc() calls on ASan, and catch errors on Windows. if (!isASAN) { @@ -85,19 +72,14 @@ beforeAll(() => { } }); -describe.skipIf(isFFIUnavailable)("ffi napi integration", () => { - it("has a different napi_env for each ffi library", () => { - addon1.set_instance_data(undefined, 5); - addon2.set_instance_data(undefined, 6); - expect(addon1.get_instance_data()).toBe(5); - expect(addon2.get_instance_data()).toBe(6); - }); - - // broken - it.todo("passes values correctly", () => { - expect(addon1.get_type(undefined, 123).toString()).toBe("number"); - expect(addon1.get_type(undefined, "hello").toString()).toBe("string"); - expect(addon1.get_type(undefined, 190n).toString()).toBe("bigint"); +describe.skipIf(isFFIUnavailable)("cc() bundled N-API headers", () => { + it.todoIf(isWindows || isASAN)("resolves without any -I flag", () => { + const { symbols } = cc({ + source: join(__dirname, "napi-app/bundled_napi_headers.c"), + symbols: { passthrough: { args: ["napi_env", "napi_value"], returns: "napi_value" } }, + }); + const marker = { marker: 42 }; + expect(symbols.passthrough(undefined, marker)).toBe(marker); }); });