Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/runtime/ffi/FFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,13 @@ static EncodedJSValue DOUBLE_TO_JSVALUE(double val) {
}

static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
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;
return (int32_t)(int64_t)val.asDouble;
Comment thread
robobun marked this conversation as resolved.
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
Expand Down
83 changes: 83 additions & 0 deletions test/js/bun/ffi/cc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,89 @@ describe.skipIf(isFFIUnavailable)("double <-> JSValue conversions", () => {
});
});

// JSVALUE_TO_INT32 must decode double-encoded JSValues: whether a JS number
// is int32-tagged or double-encoded is the engine's choice (JIT tier, double
// speculation, Math.* provenance), so an int-typed JSCallback return that
// truncates the raw encoded bits hands C 0 once the callback tiers up.
it("double-encoded JS numbers returned from an int-typed JSCallback reach C as the integer", async () => {
using dir = tempDir("bun-ffi-int32-cb-return", {
"cb.c": /* c */ `
typedef int (*cb_i32)(int);
int call_i32(cb_i32 f, int x) { return f(x); }
typedef unsigned int (*cb_u32)(int);
unsigned int call_u32(cb_u32 f, int x) { return f(x); }
typedef signed char (*cb_i8)(int);
int call_i8(cb_i8 f, int x) { return (int)f(x); }
typedef unsigned short (*cb_u16)(int);
int call_u16(cb_u16 f, int x) { return (int)f(x); }
`,
"fixture.js": /* js */ `
import { cc, JSCallback } from "bun:ffi";
import path from "path";

const { symbols } = cc({
source: path.join(import.meta.dir, "cb.c"),
symbols: {
call_i32: { args: ["function", "i32"], returns: "i32" },
call_u32: { args: ["function", "i32"], returns: "u32" },
call_i8: { args: ["function", "i32"], returns: "i32" },
call_u16: { args: ["function", "i32"], returns: "i32" },
},
});

// +0.5 then -0.5 on a runtime value: integer-valued, but the intermediate
// pins a double-represented result regardless of JIT tier or const-folding.
const asDouble = x => {
const v = x + 0.5;
return v - 0.5;
};
const echoDouble = new JSCallback(asDouble, { args: ["i32"], returns: "i32" });
// Plain int32-tagged return must keep working.
const echoInt = new JSCallback(x => x, { args: ["i32"], returns: "i32" });
const fractional = new JSCallback(() => 5.7, { args: ["i32"], returns: "i32" });
const negFractional = new JSCallback(() => -5.7, { args: ["i32"], returns: "i32" });
const u32Double = new JSCallback(x => asDouble(x) + 3000000000, { args: ["i32"], returns: "u32" });
const i8Double = new JSCallback(asDouble, { args: ["i32"], returns: "i8" });
const u16Double = new JSCallback(asDouble, { args: ["i32"], returns: "u16" });

const results = {
echo_double: symbols.call_i32(echoDouble.ptr, 938),
echo_int: symbols.call_i32(echoInt.ptr, 938),
fractional: symbols.call_i32(fractional.ptr, 0),
neg_fractional: symbols.call_i32(negFractional.ptr, 0),
u32_double: symbols.call_u32(u32Double.ptr, 0),
i8_double: symbols.call_i8(i8Double.ptr, -7),
u16_double: symbols.call_u16(u16Double.ptr, 40000),
};
for (const cb of [echoDouble, echoInt, fractional, negFractional, u32Double, i8Double, u16Double]) cb.close();
console.log(JSON.stringify(results));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const results = stdout.startsWith("{") ? JSON.parse(stdout) : stdout;
expect({ results, stderr, exitCode }).toMatchObject({
results: {
echo_double: 938,
echo_int: 938,
fractional: 5,
neg_fractional: -5,
u32_double: 3000000000,
i8_double: -7,
u16_double: 40000,
},
exitCode: 0,
});
});

// napi_create_double and napi_create_date take the double from the addon
// verbatim, so they are the same boundary. cc()-compiled C resolves napi_*
// from the host process; that lookup is only exercised on POSIX today (see
Expand Down
8 changes: 7 additions & 1 deletion test/js/bun/ffi/ffi.test.fixture.callback.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ static EncodedJSValue DOUBLE_TO_JSVALUE(double val) {
}

static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
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;
return (int32_t)(int64_t)val.asDouble;
}

static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
Expand Down
8 changes: 7 additions & 1 deletion test/js/bun/ffi/ffi.test.fixture.receiver.c
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ static EncodedJSValue DOUBLE_TO_JSVALUE(double val) {
}

static int32_t JSVALUE_TO_INT32(EncodedJSValue val) {
return val.asInt64;
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;
return (int32_t)(int64_t)val.asDouble;
}

static EncodedJSValue INT32_TO_JSVALUE(int32_t val) {
Expand Down
Loading