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);
});
});