Commit 617e082ff20 for nodejs
commit 617e082ff20a486031df2ca117bb5f1cb226df32
Author: Donghoon Kang <d159123@naver.com>
Date: Sat Oct 3 18:51:13 2026 +0900
ffi: accept safe integer numbers for 64-bit arguments
Allow safe integer numbers as int64 and uint64 arguments alongside
bigint values. Reject negative numbers for uint64 and numbers outside
the safe integer range. Keep 64-bit return values as bigint.
Apply validation and conversion across the Fast API, shared-buffer,
and generic argument conversion paths.
Add coverage for Number and BigInt boundaries, invalid inputs, and
single-argument calls before and after optimization.
Signed-off-by: HoonDongKang <d159123@naver.com>
Assisted-by: Codex:Astra-medium
PR-URL: https://github.com/nodejs/node/pull/66197
Fixes: https://github.com/nodejs/node/issues/66198
Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
diff --git a/doc/api/ffi.md b/doc/api/ffi.md
index 342acfbec50..ee56074a682 100644
--- a/doc/api/ffi.md
+++ b/doc/api/ffi.md
@@ -548,7 +548,16 @@ For 8-, 16-, and 32-bit integer types and for floating-point types, pass
JavaScript `number` values that match the declared type.
For 64-bit integer types (`int64` and `uint64`), pass JavaScript `bigint`
-values.
+values within the declared type's range or safe integer `number` values.
+For `int64`, numbers must be between `Number.MIN_SAFE_INTEGER` and
+`Number.MAX_SAFE_INTEGER`, inclusive. For `uint64`, numbers must be between
+`0` and `Number.MAX_SAFE_INTEGER`, inclusive. This allows buffer lengths such
+as `buffer.byteLength` to be passed without an explicit `BigInt()` conversion.
+Use `bigint` for integers outside JavaScript's safe integer range.
+
+Invalid arguments, including fractional numbers, `NaN`, infinities, and values
+outside these ranges, throw `ERR_INVALID_ARG_VALUE`. Return values for 64-bit
+integer types are always exposed as `bigint` values.
For pointer-like arguments:
diff --git a/lib/internal/ffi-shared-buffer.js b/lib/internal/ffi-shared-buffer.js
index 13002193cd9..68b39ab62f9 100644
--- a/lib/internal/ffi-shared-buffer.js
+++ b/lib/internal/ffi-shared-buffer.js
@@ -1,6 +1,7 @@
'use strict';
const {
+ BigInt,
DataView,
DataViewPrototypeGetBigInt64,
DataViewPrototypeGetBigUint64,
@@ -23,6 +24,7 @@ const {
DataViewPrototypeSetUint32,
DataViewPrototypeSetUint8,
NumberIsInteger,
+ NumberIsSafeInteger,
ObjectDefineProperty,
ReflectApply,
TypeError,
@@ -132,14 +134,25 @@ function writeNumericArg(view, info, offset, arg, index) {
return;
}
if (kind === 'i64') {
- if (typeof arg !== 'bigint' || arg < I64_MIN || arg > I64_MAX) {
+ if (typeof arg === 'number') {
+ if (!NumberIsSafeInteger(arg)) {
+ throwFFIArgError(`Argument ${index} must be ${info.label}`);
+ }
+ arg = BigInt(arg);
+ } else if (typeof arg !== 'bigint' || arg < I64_MIN || arg > I64_MAX) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
sI64(view, offset, arg, true);
return;
}
+
if (kind === 'u64') {
- if (typeof arg !== 'bigint' || arg < 0n || arg > U64_MAX) {
+ if (typeof arg === 'number') {
+ if (!NumberIsSafeInteger(arg) || arg < 0) {
+ throwFFIArgError(`Argument ${index} must be ${info.label}`);
+ }
+ arg = BigInt(arg);
+ } else if (typeof arg !== 'bigint' || arg < 0n || arg > U64_MAX) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
sU64(view, offset, arg, true);
diff --git a/lib/internal/ffi/fast-api.js b/lib/internal/ffi/fast-api.js
index 38f4f4e3437..531d4d41005 100644
--- a/lib/internal/ffi/fast-api.js
+++ b/lib/internal/ffi/fast-api.js
@@ -3,8 +3,10 @@
const {
ArrayBufferPrototypeGetDetached,
ArrayPrototypeIncludes,
+ BigInt,
DataViewPrototypeGetBuffer,
NumberIsInteger,
+ NumberIsSafeInteger,
ObjectDefineProperty,
ReflectApply,
SafeWeakMap,
@@ -83,15 +85,26 @@ function throwFFIArgCountError(expected, actual) {
`Invalid argument count: expected ${expected}, got ${actual}`);
}
-function validateFastIntegerArg(type, value, index) {
+function validateAndConvertFastIntegerArg(type, value, index) {
const info = fastIntegerTypeInfo[type];
- if (info === undefined) return;
+ if (info === undefined) return value;
+
+ // The native Fast API expects BigInt for 64-bit integer arguments.
+ if (info.kind === 'bigint' && typeof value === 'number') {
+ if (!NumberIsSafeInteger(value) || (info.min === 0n && value < 0)) {
+ throwFFIArgError(`Argument ${index} must be ${info.label}`);
+ }
+ return BigInt(value);
+ }
+
const validType = info.kind === 'number' ?
typeof value === 'number' && NumberIsInteger(value) :
typeof value === 'bigint';
if (!validType || value < info.min || value > info.max) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
+
+ return value;
}
function needsRawPointerConversion(type) {
@@ -223,7 +236,8 @@ function getFastArgumentIndexes(argumentsTypes) {
}
function convertFastArg(type, value, stringState, index) {
- validateFastIntegerArg(type, value, index);
+ value = validateAndConvertFastIntegerArg(type, value, index);
+
return needsPointerConversion(type) ?
convertPointerArg(type, value, stringState, index) : value;
}
@@ -295,9 +309,8 @@ function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
if (arguments.length !== 1) {
throwFFIArgCountError(1, arguments.length);
}
- validateFastIntegerArg(t0, a0, 0);
+ let arg = validateAndConvertFastIntegerArg(t0, a0, 0);
validateFastPointerArg(t0, a0, 0);
- let arg = a0;
if (needsNullPointerConversion(t0) &&
(arg === null || arg === undefined)) {
arg = 0n;
diff --git a/src/ffi/types.cc b/src/ffi/types.cc
index bf3e7e675ed..5a910e63a3e 100644
--- a/src/ffi/types.cc
+++ b/src/ffi/types.cc
@@ -648,28 +648,43 @@ Maybe<FFIArgumentCategory> ToFFIArgument(Environment* env,
*static_cast<uint32_t*>(ret) = arg->Uint32Value(context).FromJust();
} else if (type == &ffi_type_sint64) {
- if (!arg->IsBigInt()) {
- THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
- return {};
- }
+ if (arg->IsBigInt()) {
+ bool lossless;
+ int64_t value = arg.As<BigInt>()->Int64Value(&lossless);
+ if (!lossless) {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
+ return {};
+ }
- bool lossless;
- *static_cast<int64_t*>(ret) = arg.As<BigInt>()->Int64Value(&lossless);
- if (!lossless) {
- THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
- return {};
+ *static_cast<int64_t*>(ret) = value;
+ } else {
+ int64_t value;
+ if (!GetStrictSignedInteger(
+ arg, -kMaxSafeJsInteger, kMaxSafeJsInteger, &value)) {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be an int64", index);
+ return {};
+ }
+
+ *static_cast<int64_t*>(ret) = value;
}
} else if (type == &ffi_type_uint64) {
- if (!arg->IsBigInt()) {
- THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
- return {};
- }
+ if (arg->IsBigInt()) {
+ bool lossless;
+ uint64_t value = arg.As<BigInt>()->Uint64Value(&lossless);
+ if (!lossless) {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
+ return {};
+ }
- bool lossless;
- *static_cast<uint64_t*>(ret) = arg.As<BigInt>()->Uint64Value(&lossless);
- if (!lossless) {
- THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
- return {};
+ *static_cast<uint64_t*>(ret) = value;
+ } else {
+ uint64_t value;
+ if (!GetStrictUnsignedInteger(arg, kMaxSafeJsInteger, &value)) {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Argument %u must be a uint64", index);
+ return {};
+ }
+
+ *static_cast<uint64_t*>(ret) = value;
}
} else if (type == &ffi_type_float) {
if (!arg->IsNumber()) {
diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c
index 71c54a49fef..3e24b7cd3d3 100644
--- a/test/ffi/fixture_library/ffi_test_library.c
+++ b/test/ffi/fixture_library/ffi_test_library.c
@@ -49,6 +49,30 @@ FFI_EXPORT int32_t identity_i32(int32_t value) {
return value;
}
+FFI_EXPORT int64_t identity_i64(int64_t value) {
+ return value;
+}
+
+FFI_EXPORT uint64_t identity_u64(uint64_t value) {
+ return value;
+}
+
+FFI_EXPORT int64_t identity_i64_fallback(void* pointer,
+ int64_t value,
+ void (*callback)(void)) {
+ (void)pointer;
+ (void)callback;
+ return value;
+}
+
+FFI_EXPORT uint64_t identity_u64_fallback(void* pointer,
+ uint64_t value,
+ void (*callback)(void)) {
+ (void)pointer;
+ (void)callback;
+ return value;
+}
+
FFI_EXPORT char identity_char(char value) {
return value;
}
diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js
index bc3be0072db..0dbddb244b7 100644
--- a/test/ffi/test-ffi-calls.js
+++ b/test/ffi/test-ffi-calls.js
@@ -22,6 +22,10 @@ test('ffi calls support integer arithmetic and char semantics', () => {
assert.strictEqual(symbols.add_u32(0xFFFFFFFF, 1), 0);
assert.strictEqual(symbols.add_i64(20n, 22n), 42n);
assert.strictEqual(symbols.add_u64(20n, 22n), 42n);
+ assert.strictEqual(symbols.add_i64(20, 22), symbols.add_i64(20n, 22n));
+ assert.strictEqual(symbols.add_u64(20, 22), symbols.add_u64(20n, 22n));
+ assert.strictEqual(symbols.add_i64(-20, 22n), 2n);
+ assert.strictEqual(symbols.add_u64(20n, 22), 42n);
if (symbols.char_is_signed()) {
assert.strictEqual(symbols.identity_char(-1), -1);
@@ -104,15 +108,18 @@ test('ffi strings and buffers cross the boundary correctly', () => {
symbols.free_string(duplicated);
const buffer = Buffer.from([1, 2, 3, 4]);
- assert.strictEqual(symbols.sum_buffer(buffer, BigInt(buffer.length)), 10n);
- symbols.reverse_buffer(buffer, BigInt(buffer.length));
+ assert.strictEqual(
+ symbols.sum_buffer(buffer, buffer.length),
+ symbols.sum_buffer(buffer, BigInt(buffer.length)),
+ );
+ symbols.reverse_buffer(buffer, buffer.length);
assert.deepStrictEqual([...buffer], [4, 3, 2, 1]);
const typed = new Uint8Array([5, 6, 7, 8]);
- assert.strictEqual(symbols.sum_buffer(typed, BigInt(typed.byteLength)), 26n);
+ assert.strictEqual(symbols.sum_buffer(typed, typed.byteLength), 26n);
const arrayBuffer = new Uint8Array([9, 10, 11, 12]).buffer;
- assert.strictEqual(symbols.sum_buffer(arrayBuffer, BigInt(arrayBuffer.byteLength)), 42n);
+ assert.strictEqual(symbols.sum_buffer(arrayBuffer, arrayBuffer.byteLength), 42n);
} finally {
lib.close();
}
@@ -204,13 +211,20 @@ test('ffi validates invalid arguments', () => {
assert.throws(() => symbols.add_i16(40_000, 1), /Argument 0 must be an int16/);
assert.throws(() => symbols.add_u16(Number.NaN, 1), /Argument 0 must be a uint16/);
assert.throws(() => symbols.add_u16(70_000, 1), /Argument 0 must be a uint16/);
- assert.throws(() => symbols.add_i64(1, 2n), /Argument 0 must be an int64/);
- assert.throws(() => symbols.add_i64(1.5, 2n), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(1.5, 2), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(Number.NaN, 2), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(Number.POSITIVE_INFINITY, 2), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(Number.NEGATIVE_INFINITY, 2), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(Number.MAX_SAFE_INTEGER + 1, 2), /Argument 0 must be an int64/);
+ assert.throws(() => symbols.add_i64(Number.MIN_SAFE_INTEGER - 1, 2), /Argument 0 must be an int64/);
assert.throws(() => symbols.add_i64(2n ** 63n, 2n), /Argument 0 must be an int64/);
assert.throws(() => symbols.add_i64(-(2n ** 63n) - 1n, 2n), /Argument 0 must be an int64/);
assert.throws(() => symbols.add_u64('1', 2n), /Argument 0 must be a uint64/);
- assert.throws(() => symbols.add_u64(1, 2n), /Argument 0 must be a uint64/);
- assert.throws(() => symbols.add_u64(Number.NaN, 2n), /Argument 0 must be a uint64/);
+ assert.throws(() => symbols.add_u64(-1, 2), /Argument 0 must be a uint64/);
+ assert.throws(() => symbols.add_u64(1.5, 2), /Argument 0 must be a uint64/);
+ assert.throws(() => symbols.add_u64(Number.NaN, 2), /Argument 0 must be a uint64/);
+ assert.throws(() => symbols.add_u64(Number.POSITIVE_INFINITY, 2), /Argument 0 must be a uint64/);
+ assert.throws(() => symbols.add_u64(Number.MAX_SAFE_INTEGER + 1, 2), /Argument 0 must be a uint64/);
assert.throws(() => symbols.add_u64(-1n, 2n), /Argument 0 must be a uint64/);
assert.throws(() => symbols.add_u64(2n ** 64n, 2n), /Argument 0 must be a uint64/);
assert.throws(() => symbols.identity_pointer(-1n), /Argument 0 must be a non-negative pointer bigint/);
diff --git a/test/ffi/test-ffi-fast-integer-validation.js b/test/ffi/test-ffi-fast-integer-validation.js
index e2994c89523..866f9175221 100644
--- a/test/ffi/test-ffi-fast-integer-validation.js
+++ b/test/ffi/test-ffi-fast-integer-validation.js
@@ -32,9 +32,9 @@ test('fast FFI validates integer argument ranges', () => {
function callU32(value) { return functions.add_u32(value, 0); }
- function callI64(value) { return functions.add_i64(value, 0n); }
+ function callI64(value) { return functions.add_i64(value, 0); }
- function callU64(value) { return functions.add_u64(value, 0n); }
+ function callU64(value) { return functions.add_u64(value, 0); }
for (const [fn, value] of [
[callI8, 0],
@@ -43,8 +43,8 @@ test('fast FFI validates integer argument ranges', () => {
[callU16, 0],
[callI32, 0],
[callU32, 0],
- [callI64, 0n],
- [callU64, 0n],
+ [callI64, 0],
+ [callU64, 0],
]) {
optimize(fn, value);
}
@@ -62,6 +62,24 @@ test('fast FFI validates integer argument ranges', () => {
assert.throws(() => callU32(-1), expect);
assert.throws(() => callU32(1.5), expect);
assert.throws(() => callU32('1'), expect);
+ assert.strictEqual(callI64(Number.MAX_SAFE_INTEGER),
+ BigInt(Number.MAX_SAFE_INTEGER));
+ assert.strictEqual(callI64(Number.MIN_SAFE_INTEGER),
+ BigInt(Number.MIN_SAFE_INTEGER));
+ assert.strictEqual(callU64(Number.MAX_SAFE_INTEGER),
+ BigInt(Number.MAX_SAFE_INTEGER));
+ assert.strictEqual(callI64((2n ** 63n) - 1n), (2n ** 63n) - 1n);
+ assert.strictEqual(callU64((2n ** 64n) - 1n), (2n ** 64n) - 1n);
+ assert.throws(() => callI64(Number.MAX_SAFE_INTEGER + 1), expect);
+ assert.throws(() => callI64(Number.MIN_SAFE_INTEGER - 1), expect);
+ assert.throws(() => callI64(1.5), expect);
+ assert.throws(() => callI64(Number.NaN), expect);
+ assert.throws(() => callI64(Number.POSITIVE_INFINITY), expect);
+ assert.throws(() => callU64(-1), expect);
+ assert.throws(() => callU64(Number.MAX_SAFE_INTEGER + 1), expect);
+ assert.throws(() => callU64(1.5), expect);
+ assert.throws(() => callU64(Number.NaN), expect);
+ assert.throws(() => callU64(Number.POSITIVE_INFINITY), expect);
assert.throws(() => callI64(2n ** 63n), expect);
assert.throws(() => callU64(2n ** 64n), expect);
} finally {
@@ -70,6 +88,60 @@ test('fast FFI validates integer argument ranges', () => {
}
});
+test('fast FFI converts single i64/u64 Number arguments before and after optimization', () => {
+ const { lib, functions } = ffi.dlopen(libraryPath, {
+ identity_i64: { return: 'int64', arguments: ['int64'] },
+ identity_u64: { return: 'uint64', arguments: ['uint64'] },
+ });
+
+ try {
+ // The native signature must have one argument to exercise the single-argument wrapper.
+ function callI64(value) { return functions.identity_i64(value); }
+
+ function callU64(value) { return functions.identity_u64(value); }
+
+ for (const optimized of [false, true]) {
+ if (optimized) {
+ optimize(callI64, -42);
+ optimize(callU64, 42);
+ }
+
+ for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) {
+ assert.strictEqual(callI64(value), BigInt(value));
+ assert.strictEqual(callU64(value), BigInt(value));
+ }
+ for (const value of [-42, Number.MIN_SAFE_INTEGER,
+ -(2n ** 63n), (2n ** 63n) - 1n]) {
+ assert.strictEqual(callI64(value), BigInt(value));
+ }
+ assert.strictEqual(callU64((2n ** 64n) - 1n), (2n ** 64n) - 1n);
+
+ const signedError = {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: 'Argument 0 must be an int64',
+ };
+ const unsignedError = {
+ code: 'ERR_INVALID_ARG_VALUE',
+ message: 'Argument 0 must be a uint64',
+ };
+ for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1,
+ 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) {
+ assert.throws(() => callI64(value), signedError);
+ assert.throws(() => callU64(value), unsignedError);
+ }
+ for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) {
+ assert.throws(() => callI64(value), signedError);
+ }
+ for (const value of [-1, -1n, 2n ** 64n]) {
+ assert.throws(() => callU64(value), unsignedError);
+ }
+ }
+ } finally {
+ eval('%WaitForBackgroundOptimization()');
+ lib.close();
+ }
+});
+
test('fast FFI validates pointer BigInt ranges', () => {
const lib = new ffi.DynamicLibrary(libraryPath);
try {
diff --git a/test/ffi/test-ffi-shared-buffer.js b/test/ffi/test-ffi-shared-buffer.js
index a56df8daeab..81f905bc06e 100644
--- a/test/ffi/test-ffi-shared-buffer.js
+++ b/test/ffi/test-ffi-shared-buffer.js
@@ -74,7 +74,7 @@ test('f32/f64 round-trip', () => {
}
});
-test('i64/u64 BigInt round-trip', () => {
+test('i64/u64 Number and BigInt round-trip', () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
add_i64: { return: 'i64', arguments: ['i64', 'i64'] },
add_u64: { return: 'u64', arguments: ['u64', 'u64'] },
@@ -82,6 +82,10 @@ test('i64/u64 BigInt round-trip', () => {
try {
assert.strictEqual(functions.add_i64(10n, 20n), 30n);
assert.strictEqual(functions.add_u64(10n, 20n), 30n);
+ assert.strictEqual(functions.add_i64(10, 20), 30n);
+ assert.strictEqual(functions.add_u64(10, 20), 30n);
+ assert.strictEqual(functions.add_i64(-10, 20n), 10n);
+ assert.strictEqual(functions.add_u64(10n, 20), 30n);
} finally {
lib.close();
}
@@ -300,7 +304,7 @@ test('integer boundaries for i8/u8/i16/u16/i32/u32', () => {
}
});
-test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => {
+test('i64/u64 Number and BigInt boundaries', () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
add_i64: { return: 'i64', arguments: ['i64', 'i64'] },
add_u64: { return: 'u64', arguments: ['u64', 'u64'] },
@@ -315,6 +319,9 @@ test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => {
assert.strictEqual(functions.add_i64(I64_MIN, 0n), I64_MIN);
assert.strictEqual(functions.add_u64(U64_MAX, 0n), U64_MAX);
assert.strictEqual(functions.add_u64(0n, 0n), 0n);
+ assert.strictEqual(functions.add_i64(Number.MAX_SAFE_INTEGER, 0), BigInt(Number.MAX_SAFE_INTEGER));
+ assert.strictEqual(functions.add_i64(Number.MIN_SAFE_INTEGER, 0), BigInt(Number.MIN_SAFE_INTEGER));
+ assert.strictEqual(functions.add_u64(Number.MAX_SAFE_INTEGER, 0), BigInt(Number.MAX_SAFE_INTEGER));
const expect = { code: 'ERR_INVALID_ARG_VALUE' };
assert.throws(() => functions.add_i64(I64_MAX + 1n, 0n), expect);
@@ -322,13 +329,95 @@ test('i64/u64 BigInt boundaries and Number/BigInt type mismatches', () => {
assert.throws(() => functions.add_u64(U64_MAX + 1n, 0n), expect);
assert.throws(() => functions.add_u64(-1n, 0n), expect);
- assert.throws(() => functions.add_i64(1, 2n), expect);
+ assert.throws(() => functions.add_i64(Number.MAX_SAFE_INTEGER + 1, 0), expect);
+ assert.throws(() => functions.add_i64(Number.MIN_SAFE_INTEGER - 1, 0), expect);
+ assert.throws(() => functions.add_i64(1.5, 0), expect);
+ assert.throws(() => functions.add_i64(Number.NaN, 0), expect);
+ assert.throws(() => functions.add_i64(Number.POSITIVE_INFINITY, 0), expect);
+ assert.throws(() => functions.add_u64(-1, 0), expect);
+ assert.throws(() => functions.add_u64(Number.MAX_SAFE_INTEGER + 1, 0), expect);
+ assert.throws(() => functions.add_u64(1.5, 0), expect);
+ assert.throws(() => functions.add_u64(Number.NaN, 0), expect);
+ assert.throws(() => functions.add_u64(Number.POSITIVE_INFINITY, 0), expect);
assert.throws(() => functions.add_i64(1n, '2'), expect);
} finally {
lib.close();
}
});
+test('SB wrapper accepts safe Number values for i64/u64', () => {
+ const { lib, functions } = ffi.dlopen(libraryPath, {
+ identity_i64_fallback: { return: 'i64', arguments: ['pointer', 'i64', 'function'] },
+ identity_u64_fallback: { return: 'u64', arguments: ['pointer', 'u64', 'function'] },
+ });
+
+ try {
+ // A function argument excludes Fast API; BigInt and null pointers use SB.
+ for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) {
+ assert.strictEqual(functions.identity_i64_fallback(0n, value, null), BigInt(value));
+ assert.strictEqual(functions.identity_u64_fallback(0n, value, null), BigInt(value));
+ }
+ for (const value of [-42, Number.MIN_SAFE_INTEGER, -(2n ** 63n), (2n ** 63n) - 1n]) {
+ assert.strictEqual(functions.identity_i64_fallback(0n, value, null), BigInt(value));
+ }
+ assert.strictEqual(functions.identity_u64_fallback(0n, (2n ** 64n) - 1n, null),
+ (2n ** 64n) - 1n);
+
+ const signedError = { code: 'ERR_INVALID_ARG_VALUE' };
+ const unsignedError = { code: 'ERR_INVALID_ARG_VALUE' };
+ for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1,
+ 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) {
+ assert.throws(() => functions.identity_i64_fallback(0n, value, null), signedError);
+ assert.throws(() => functions.identity_u64_fallback(0n, value, null), unsignedError);
+ }
+ for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) {
+ assert.throws(() => functions.identity_i64_fallback(0n, value, null), signedError);
+ }
+ for (const value of [-1, -1n, 2n ** 64n]) {
+ assert.throws(() => functions.identity_u64_fallback(0n, value, null), unsignedError);
+ }
+ } finally {
+ lib.close();
+ }
+});
+
+test('generic conversion accepts safe Number values for i64/u64', () => {
+ const { lib, functions } = ffi.dlopen(libraryPath, {
+ identity_i64_fallback: { return: 'i64', arguments: ['pointer', 'i64', 'function'] },
+ identity_u64_fallback: { return: 'u64', arguments: ['pointer', 'u64', 'function'] },
+ });
+ const buffer = Buffer.alloc(1);
+
+ try {
+ // A Buffer pointer selects generic conversion before validating i64/u64.
+ for (const value of [0, -0, 42, Number.MAX_SAFE_INTEGER, 0n, 42n]) {
+ assert.strictEqual(functions.identity_i64_fallback(buffer, value, null), BigInt(value));
+ assert.strictEqual(functions.identity_u64_fallback(buffer, value, null), BigInt(value));
+ }
+ for (const value of [-42, Number.MIN_SAFE_INTEGER, -(2n ** 63n), (2n ** 63n) - 1n]) {
+ assert.strictEqual(functions.identity_i64_fallback(buffer, value, null), BigInt(value));
+ }
+ assert.strictEqual(functions.identity_u64_fallback(buffer, (2n ** 64n) - 1n, null),
+ (2n ** 64n) - 1n);
+
+ const signedError = { code: 'ERR_INVALID_ARG_VALUE' };
+ const unsignedError = { code: 'ERR_INVALID_ARG_VALUE' };
+ for (const value of [Number.MAX_SAFE_INTEGER + 1, Number.MIN_SAFE_INTEGER - 1,
+ 1.5, NaN, Infinity, -Infinity, '1', null, undefined, true, {}]) {
+ assert.throws(() => functions.identity_i64_fallback(buffer, value, null), signedError);
+ assert.throws(() => functions.identity_u64_fallback(buffer, value, null), unsignedError);
+ }
+ for (const value of [-(2n ** 63n) - 1n, 2n ** 63n]) {
+ assert.throws(() => functions.identity_i64_fallback(buffer, value, null), signedError);
+ }
+ for (const value of [-1, -1n, 2n ** 64n]) {
+ assert.throws(() => functions.identity_u64_fallback(buffer, value, null), unsignedError);
+ }
+ } finally {
+ lib.close();
+ }
+});
+
test('char type picks signed/unsigned range based on host ABI', () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
char_is_signed: { return: 'i32', arguments: [] },