diff --git a/bench/ffi/bun.js b/bench/ffi/bun.js index 5800b53b2466..56c5d24036e7 100644 --- a/bench/ffi/bun.js +++ b/bench/ffi/bun.js @@ -1,4 +1,4 @@ -import { CString, dlopen, ptr } from "bun:ffi"; +import { cc, CString, dlopen, ptr } from "bun:ffi"; import { bench, group, run } from "../runner.mjs"; const { napiNoop, napiHash, napiString } = require(import.meta.dir + "/src/ffi_napi_bench.node"); @@ -35,6 +35,35 @@ group("bun:ffi", () => { bench("string arg: TypedArray", () => ffi_strlen(strBuf)); }); +const { + symbols: { cc_noop, cc_sink, cc_identity, cc_add, cc_sum10, cc_sum_of_squares, cc_is_null, cc_same }, +} = cc({ + source: import.meta.dir + "/cc.c", + symbols: { + cc_noop: { args: [], returns: "void" }, + cc_sink: { args: ["i32"], returns: "void" }, + cc_identity: { args: ["i32"], returns: "i32" }, + cc_add: { args: ["i32", "i32"], returns: "i32" }, + cc_sum10: { args: Array(10).fill("i32"), returns: "i32" }, + cc_sum_of_squares: { args: ["f64", "f64"], returns: "f64" }, + cc_is_null: { args: ["ptr"], returns: "bool" }, + cc_same: { args: ["ptr", "ptr"], returns: "bool" }, + }, +}); + +group("bun:ffi cc()", () => { + bench("noop", () => cc_noop()); + bench("sink(i32)", () => cc_sink(1)); + bench("identity(i32)", () => cc_identity(1)); + bench("add(i32, i32)", () => cc_add(1, 2)); + bench("sum10(i32 x 10)", () => cc_sum10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + bench("sum_of_squares(f64, f64)", () => cc_sum_of_squares(1.5, 2.5)); + bench("is_null(ptr): raw pointer", () => cc_is_null(strPtr)); + bench("is_null(ptr): TypedArray", () => cc_is_null(strBuf)); + bench("same(ptr, ptr)", () => cc_same(strPtr, strBuf)); + bench("add(i32, i32), one argument missing", () => cc_add(1)); +}); + if (process.env.SHOW_NAPI) group("bun:napi", () => { bench("noop", () => napiNoop()); diff --git a/bench/ffi/cc.c b/bench/ffi/cc.c new file mode 100644 index 000000000000..49464faa45c6 --- /dev/null +++ b/bench/ffi/cc.c @@ -0,0 +1,10 @@ +void cc_noop(void) {} +void cc_sink(int a) {} +int cc_identity(int a) { return a; } +int cc_add(int a, int b) { return a + b; } +int cc_sum10(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + return a + b + c + d + e + f + g + h + i + j; +} +double cc_sum_of_squares(double a, double b) { return a * a + b * b; } +_Bool cc_is_null(const void* a) { return a == 0; } +_Bool cc_same(const void* a, const void* b) { return a == b; } diff --git a/src/jsc/bindings/JSFFIFunction.h b/src/jsc/bindings/JSFFIFunction.h index 05318ca9ae67..a322ef63264d 100644 --- a/src/jsc/bindings/JSFFIFunction.h +++ b/src/jsc/bindings/JSFFIFunction.h @@ -32,16 +32,15 @@ using CFFIFunction = FFIFunction; * Call a C function with low overhead, modeled after JSC::JSNativeStdFunction * * The C function is expected to know how to get the arguments out of the JSC::CallFrame and - * return a JSC::EncodedJSValue. To do that, the argumentOffset is inlined at compile-time - * into Bun's binary and again inlined into the C function. + * return a JSC::EncodedJSValue. The slot indices it needs are defined into it when TinyCC compiles it. * * This is used by functions compiled with TinyCC * * It was about 20% faster than using the JavaScriptCore C API for functions with 1 argument * - * There is no wrapper function. It does zero bounds checking on the arguments. + * There is no wrapper function. This class does not check the arguments: the C function does. * It does not check for exceptions. It does not check for return value. - * It is the caller's responsibility to not buffer overflow the arguments + * A function cc() compiles pads a call that passed too few arguments (LOAD_ARGUMENTS_FROM_CALL_FRAME in src/runtime/ffi/FFI.h). * For all those reasons, this shouldn't be used directly. */ class JSFFIFunction final : public JSC::JSFunction { diff --git a/src/jsc/bindings/ffi.cpp b/src/jsc/bindings/ffi.cpp index 2f4b6f561cee..e8175e1e8f3b 100644 --- a/src/jsc/bindings/ffi.cpp +++ b/src/jsc/bindings/ffi.cpp @@ -5,13 +5,20 @@ typedef struct FFIFields { uint32_t JSArrayBufferView__offsetOfByteOffset; uint32_t JSArrayBufferView__offsetOfVector; uint32_t JSCell__offsetOfType; + // Register (8-byte slot) indices into a CallFrame, not byte offsets. + uint32_t CallFrame__argumentCountIncludingThisSlot; + uint32_t CallFrame__firstArgumentSlot; } FFIFields; extern "C" FFIFields Bun__FFI__offsets = { 0 }; +static_assert(sizeof(JSC::Register) == sizeof(void*), "the cc() wrapper indexes the CallFrame as an array of pointer-sized slots"); + extern "C" void Bun__FFI__ensureOffsetsAreLoaded() { Bun__FFI__offsets.JSArrayBufferView__offsetOfLength = JSC::JSArrayBufferView::offsetOfLength(); Bun__FFI__offsets.JSArrayBufferView__offsetOfByteOffset = JSC::JSArrayBufferView::offsetOfByteOffset(); Bun__FFI__offsets.JSArrayBufferView__offsetOfVector = JSC::JSArrayBufferView::offsetOfVector(); Bun__FFI__offsets.JSCell__offsetOfType = JSC::JSCell::typeInfoTypeOffset(); + Bun__FFI__offsets.CallFrame__argumentCountIncludingThisSlot = static_cast(JSC::CallFrameSlot::argumentCountIncludingThis); + Bun__FFI__offsets.CallFrame__firstArgumentSlot = static_cast(JSC::CallFrameSlot::firstArgument); } diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index f75943e4b4f7..9498a4634f19 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -74,8 +74,6 @@ pub mod marked_argument_buffer; pub mod regular_expression; #[path = "ScriptExecutionStatus.rs"] pub mod script_execution_status; -#[path = "sizes.rs"] -pub mod sizes; #[path = "SourceProvider.rs"] pub mod source_provider; #[path = "URLSearchParams.rs"] diff --git a/src/jsc/sizes.rs b/src/jsc/sizes.rs deleted file mode 100644 index d5b83de50d55..000000000000 --- a/src/jsc/sizes.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! This namespace contains JSC C++ type sizes/alignments exported from a code -//! generator. Do not rely on any of these values in new code. If possible, -//! rewrite old ones to use another approach. -//! -//! It is not reliable to interpret C++ classes as raw bytes, since the -//! memory layout is not guaranteed by the compiler. - -pub const BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST: usize = 6; diff --git a/src/runtime/ffi/FFI.h b/src/runtime/ffi/FFI.h index 9628e3b0a8b8..fbb3ad2fd7e5 100644 --- a/src/runtime/ffi/FFI.h +++ b/src/runtime/ffi/FFI.h @@ -128,13 +128,23 @@ EncodedJSValue ValueTrue = { TagValueTrue }; typedef void* JSContext; -// Bun_FFI_PointerOffsetToArgumentsList is injected into the build -// The value is BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST in src/jsc/sizes.rs -// 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) +// A call frame holds only the arguments the call passed. A call with fewer than `count` continues on a copy of its argument slots, padded with undefined. +static inline void* Bun_FFI_padArguments(void* callFrame, EncodedJSValue* paddedFrame, int32_t count) { + EncodedJSValue *passed = (EncodedJSValue*)callFrame + Bun_FFI_PointerOffsetToArgumentsList; + EncodedJSValue *padded = paddedFrame + Bun_FFI_PointerOffsetToArgumentsList; + int32_t passedCount = ((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload - 1; + int32_t i = 0; + for (; i < passedCount; i++) padded[i].asInt64 = passed[i].asInt64; + for (; i < count; i++) padded[i].asInt64 = TagValueUndefined; + return paddedFrame; +} + +#define LOAD_ARGUMENTS_FROM_CALL_FRAME(count) \ + EncodedJSValue Bun_FFI_paddedFrame[Bun_FFI_PointerOffsetToArgumentsList + count]; \ + if (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload <= count) \ + callFrame = Bun_FFI_padArguments(callFrame, Bun_FFI_paddedFrame, count) + +#define ARGUMENT_FROM_CALL_FRAME(i) (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentsList + (i)]) @@ -208,7 +218,8 @@ static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) { // 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) + // null or undefined, which is what an argument the call did not pass is + if ((val.asInt64 & ~UndefinedTag) == TagValueNull) return 0; if (JSCELL_IS_TYPED_ARRAY(val)) { diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index f6fbb13d66e2..88dade2bcbe5 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -64,12 +64,15 @@ fn dangerously_run_without_jit_protections(func: impl FnOnce() -> R) -> R { func() } +/// Mirrors `FFIFields` in `src/jsc/bindings/ffi.cpp`. #[repr(C)] struct Offsets { js_array_buffer_view_offset_of_length: u32, js_array_buffer_view_offset_of_byte_offset: u32, js_array_buffer_view_offset_of_vector: u32, js_cell_offset_of_type: u32, + call_frame_argument_count_including_this_slot: u32, + call_frame_first_argument_slot: u32, } unsafe extern "C" { @@ -2101,47 +2104,16 @@ impl Function { )?; } - if !self.arg_types.is_empty() { - writer.write_all(b" LOAD_ARGUMENTS_FROM_CALL_FRAME;\n")?; - for (i, arg) in self.arg_types.iter().enumerate() { - if *arg == ABIType::NapiEnv { - write!( - writer, - " napi_env arg{} = (napi_env)&Bun__thisFFIModuleNapiEnv;\n argsPtr++;\n", - i - )?; - } else if *arg == ABIType::NapiValue { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else if arg.needs_a_cast_in_c() { - if i < self.arg_types.len() - 1 { - writeln!( - writer, - " EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};", - i - )?; - } else { - write!( - writer, - " EncodedJSValue arg{};\n arg{}.asInt64 = *argsPtr;\n", - i, i - )?; - } - } else { - if i < self.arg_types.len() - 1 { - writeln!(writer, " int64_t arg{} = *argsPtr++;", i)?; - } else { - writeln!(writer, " int64_t arg{} = *argsPtr;", i)?; - } - } - } + // The wrapper fills in a napi_env parameter itself: its slot in the frame is a placeholder that is never read. + let slots_read = self + .arg_types + .iter() + .rposition(|arg| *arg != ABIType::NapiEnv) + .map_or(0, |last| last + 1); + if slots_read > 0 { + writeln!(writer, " LOAD_ARGUMENTS_FROM_CALL_FRAME({slots_read});")?; } - let mut arg_buf = [0u8; 512]; - writer.write_all(b" ")?; if self.return_type != ABIType::Void { self.return_type.typename(writer)?; @@ -2149,7 +2121,10 @@ impl Function { } write!(writer, "{}(", BStr::new(self.base_name.as_bytes()))?; first = true; - arg_buf[0..3].copy_from_slice(b"arg"); + // Each argument is read from the frame where the call uses it: a local per argument costs a store and a load. + const SLOT: &[u8] = b"ARGUMENT_FROM_CALL_FRAME("; + let mut slot_buf = [0u8; 64]; + slot_buf[..SLOT.len()].copy_from_slice(SLOT); for (i, arg) in self.arg_types.iter().enumerate() { if !first { writer.write_all(b", ")?; @@ -2157,12 +2132,14 @@ impl Function { first = false; writer.write_all(b" ")?; - let length_buf = bun_core::fmt::print_int(&mut arg_buf[3..], i); - let arg_name = &arg_buf[0..3 + length_buf]; + let digits = bun_core::fmt::print_int(&mut slot_buf[SLOT.len()..], i); + slot_buf[SLOT.len() + digits] = b')'; + let slot = &slot_buf[..SLOT.len() + digits + 1]; if arg.needs_a_cast_in_c() { - write!(writer, "{}", arg.to_c(arg_name))?; + write!(writer, "{}", arg.to_c(slot))?; } else { - writer.write_all(arg_name)?; + writer.write_all(slot)?; + writer.write_all(b".asInt64")?; } } writer.write_all(b");\n")?; @@ -2488,7 +2465,11 @@ impl CompilerRT { state.define_symbols(&[ ( "Bun_FFI_PointerOffsetToArgumentsList", - bun_jsc::sizes::BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST as i64, + offsets.call_frame_first_argument_slot as i64, + ), + ( + "Bun_FFI_PointerOffsetToArgumentCountIncludingThis", + offsets.call_frame_argument_count_including_this_slot as i64, ), ( "JSArrayBufferView__offsetOfLength", diff --git a/test/js/bun/ffi/cc.test.ts b/test/js/bun/ffi/cc.test.ts index 69a5d0e3ba64..2c40d234af17 100644 --- a/test/js/bun/ffi/cc.test.ts +++ b/test/js/bun/ffi/cc.test.ts @@ -75,10 +75,9 @@ describe.skipIf(isASAN)("given an add(a, b) function", () => { expect(() => res.symbols.add("1", "2")).toThrow(); }); - // looks like `b` defaults to `0`, is this U.B. or expected? - it.skip("when passed too few arguments, throws an error", () => { + it("when passed too few arguments, the missing ones are undefined", () => { // @ts-expect-error - expect(() => res.symbols.add(1)).toThrow(); + expect(res.symbols.add(1)).toBe(res.symbols.add(1, undefined)); }); it("when passed too many arguments, still works", () => { @@ -101,6 +100,165 @@ describe.skipIf(isASAN)("given an add(a, b) function", () => { }); }); // +// A call frame holds only the arguments the call passed, and every cc() symbol reads its arguments from it. +// Spawned: a slot past the passed arguments, read as a pointer or a napi_value, can crash the process. +describe("calling a symbol with fewer arguments than it declares", () => { + it("gives C undefined for each missing argument", async () => { + using dir = tempDir("bun-ffi-cc-short-call", { + "short-call.c": /* c */ ` + typedef long long bits; + union caster { bits i; double d; float f; }; + bits i32_bits(int x) { return x; } + bits u8_bits(unsigned char x) { return x; } + bits bool_bits(_Bool x) { return x; } + bits f64_bits(double x) { union caster c; c.d = x; return c.i; } + bits f32_bits(float x) { union caster c; c.i = 0; c.f = x; return c.i; } + bits ptr_bits(void* x) { return (bits)x; } + bits cstring_bits(const char* x) { return (bits)x; } + bits function_bits(void (*x)(void)) { return (bits)x; } + int first_byte(const unsigned char* p) { return p ? p[0] : -1; } + + /* Eight hex digits per int: the text is what C received, whatever the return conversion does. */ + static char text[96]; + static const char* hex(const int* values, int count) { + char* out = text; + for (int i = 0; i < count; i++) { + for (int shift = 28; shift >= 0; shift -= 4) *out++ = "0123456789abcdef"[((unsigned)values[i] >> shift) & 15]; + *out++ = ' '; + } + out[-1] = 0; + return text; + } + static int seen[10]; + void record10(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + seen[0] = a; seen[1] = b; seen[2] = c; seen[3] = d; seen[4] = e; + seen[5] = f; seen[6] = g; seen[7] = h; seen[8] = i; seen[9] = j; + } + const char* recorded(void) { return hex(seen, 10); } + const char* hex2(int a, int b) { seen[0] = a; seen[1] = b; return hex(seen, 2); } + const char* hex10(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) { + record10(a, b, c, d, e, f, g, h, i, j); + return hex(seen, 10); + } + + typedef struct test_env* env_t; + typedef struct test_value* value_t; + value_t echo_napi_value(env_t env, value_t value) { return value; } + `, + "short-call-fixture.js": /* js */ ` + import { cc, CString } from "bun:ffi"; + import path from "path"; + + // No buffer, i64 or u64: an explicit undefined is not a value they take (a buffer must be a view, a 64-bit integer a number). + const types = ["i32", "u8", "bool", "f64", "f32", "ptr", "cstring", "function"]; + const ten = Array(10).fill("i32"); + const { symbols } = cc({ + source: path.join(import.meta.dir, "short-call.c"), + symbols: { + ...Object.fromEntries(types.map(type => [type + "_bits", { args: [type], returns: "i64" }])), + first_byte: { args: ["ptr"], returns: "i32" }, + // No "cstring" return, so this one is the compiled function itself, with no JS wrapper around it. + record10: { args: ten, returns: "void" }, + recorded: { args: [], returns: "cstring" }, + hex2: { args: ["i32", "i32"], returns: "cstring" }, + hex10: { args: ten, returns: "cstring" }, + echo_napi_value: { args: ["napi_env", "napi_value"], returns: "napi_value" }, + }, + }); + const { record10, recorded, hex2, hex10, echo_napi_value } = symbols; + const U = undefined; + const rest = Array(8).fill(U); + const seen = call => { + record10(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + call(); + return String(recorded()); + }; + + // What C received from a short call, and from the same call with undefined passed for each missing argument. + const short = {}; + const padded = {}; + for (const type of types) { + const bits = symbols[type + "_bits"]; + short[type] = String(bits()); + padded[type] = String(bits(U)); + } + + const fromTimer = Promise.withResolvers(); + setTimeout(() => fromTimer.resolve(String(symbols.ptr_bits())), 0); + short["ptr, from a timer callback"] = await fromTimer.promise; + padded["ptr, from a timer callback"] = String(symbols.ptr_bits(U)); + + const protocols = { + "direct": [() => record10(1, 2), () => record10(1, 2, ...rest)], + "call": [() => record10.call(null, 1, 2), () => record10.call(null, 1, 2, ...rest)], + "apply": [() => record10.apply(null, [1, 2]), () => record10.apply(null, [1, 2, ...rest])], + "Reflect.apply": [() => Reflect.apply(record10, null, [1, 2]), () => Reflect.apply(record10, null, [1, 2, ...rest])], + "bind": [() => record10.bind(null, 1, 2)(), () => record10.bind(null, 1, 2, ...rest)()], + "spread": [() => record10(...[1, 2]), () => record10(...[1, 2, ...rest])], + "new": [() => new record10(1, 2), () => new record10(1, 2, ...rest)], + // Array.from calls it as record10(1, 0). + "a built-in's callback": [() => Array.from([1], record10), () => record10(1, 0, ...rest)], + }; + for (const [name, [shortCall, paddedCall]] of Object.entries(protocols)) { + short["no wrapper, " + name] = seen(shortCall); + padded["no wrapper, " + name] = seen(paddedCall); + } + + short["cstring, 2 parameters"] = String(hex2(1)); + padded["cstring, 2 parameters"] = String(hex2(1, U)); + short["cstring, 2 parameters, .native"] = String(new CString(hex2.native(1))); + padded["cstring, 2 parameters, .native"] = String(new CString(hex2.native(1, U))); + short["cstring, 10 parameters"] = String(hex10(1, 2)); + padded["cstring, 10 parameters"] = String(hex10(1, 2, ...rest)); + short["cstring, 10 parameters, .native"] = String(new CString(hex10.native(1, 2))); + padded["cstring, 10 parameters, .native"] = String(new CString(hex10.native(1, 2, ...rest))); + + // The wrapper fills in napi_env itself: its position in the argument list is a placeholder. + short["napi_value"] = typeof echo_napi_value(); + padded["napi_value"] = typeof echo_napi_value(U, U); + short["napi_value, after the placeholder"] = typeof echo_napi_value(null); + padded["napi_value, after the placeholder"] = typeof echo_napi_value(null, U); + + const controls = { + "every argument": seen(() => record10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)), + "one argument too many": seen(() => record10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)), + "napi_value": echo_napi_value(U, 42), + // A missing pointer is NULL, so C that checks for NULL does not read through it. + "missing pointers": ["ptr", "cstring", "function"].map(type => String(symbols[type + "_bits"]())), + "first_byte": [symbols.first_byte(), symbols.first_byte(U), symbols.first_byte(new Uint8Array([7]))], + }; + console.log(JSON.stringify({ short, padded, controls })); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "short-call-fixture.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + // stderr is in the received object so that a failure shows it. It is not asserted: debug builds print warnings there. + const results = stdout.startsWith("{") ? JSON.parse(stdout) : stdout; + const oneToTen = "00000001 00000002 00000003 00000004 00000005 00000006 00000007 00000008 00000009 0000000a"; + expect({ results, stderr, exitCode }).toMatchObject({ + results: { + short: results.padded, + controls: { + "every argument": oneToTen, + "one argument too many": oneToTen, + "napi_value": 42, + "missing pointers": ["0", "0", "0"], + "first_byte": [-1, -1, 7], + }, + }, + exitCode: 0, + }); + }); +}); + describe("given a source file with syntax errors", () => { const source = /* c */ ` int add(int a, int b) { diff --git a/test/js/bun/ffi/ffi.test.fixture.receiver.c b/test/js/bun/ffi/ffi.test.fixture.receiver.c index 330b97431203..02159ae87162 100644 --- a/test/js/bun/ffi/ffi.test.fixture.receiver.c +++ b/test/js/bun/ffi/ffi.test.fixture.receiver.c @@ -130,13 +130,23 @@ EncodedJSValue ValueTrue = { TagValueTrue }; typedef void* JSContext; -// Bun_FFI_PointerOffsetToArgumentsList is injected into the build -// The value is BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST in src/jsc/sizes.rs -// 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) +// A call frame holds only the arguments the call passed. A call with fewer than `count` continues on a copy of its argument slots, padded with undefined. +static inline void* Bun_FFI_padArguments(void* callFrame, EncodedJSValue* paddedFrame, int32_t count) { + EncodedJSValue *passed = (EncodedJSValue*)callFrame + Bun_FFI_PointerOffsetToArgumentsList; + EncodedJSValue *padded = paddedFrame + Bun_FFI_PointerOffsetToArgumentsList; + int32_t passedCount = ((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload - 1; + int32_t i = 0; + for (; i < passedCount; i++) padded[i].asInt64 = passed[i].asInt64; + for (; i < count; i++) padded[i].asInt64 = TagValueUndefined; + return paddedFrame; +} + +#define LOAD_ARGUMENTS_FROM_CALL_FRAME(count) \ + EncodedJSValue Bun_FFI_paddedFrame[Bun_FFI_PointerOffsetToArgumentsList + count]; \ + if (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload <= count) \ + callFrame = Bun_FFI_padArguments(callFrame, Bun_FFI_paddedFrame, count) + +#define ARGUMENT_FROM_CALL_FRAME(i) (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentsList + (i)]) @@ -210,7 +220,8 @@ static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) { // 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) + // null or undefined, which is what an argument the call did not pass is + if ((val.asInt64 & ~UndefinedTag) == TagValueNull) return 0; if (JSCELL_IS_TYPED_ARRAY(val)) { @@ -373,10 +384,8 @@ float not_a_callback(float arg0); /* ---- Your Wrapper Function ---- */ ZIG_REPR_TYPE JSFunctionCall(void* JS_GLOBAL_OBJECT, void* callFrame) { - LOAD_ARGUMENTS_FROM_CALL_FRAME; - EncodedJSValue arg0; - arg0.asInt64 = *argsPtr; - float return_value = not_a_callback( JSVALUE_TO_FLOAT(arg0)); + LOAD_ARGUMENTS_FROM_CALL_FRAME(1); + float return_value = not_a_callback( JSVALUE_TO_FLOAT(ARGUMENT_FROM_CALL_FRAME(0))); return FLOAT_TO_JSVALUE(return_value).asZigRepr; }