Skip to content
Closed
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
Prev Previous commit
Next Next commit
tests
  • Loading branch information
cjihrig authored and ShogunPanda committed Apr 11, 2026
commit c4e89d0ecb4ccbc8162b389a072e9f7439274770
280 changes: 122 additions & 158 deletions test/ffi/test-ffi-calls.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,112 +2,120 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { gcUntil } = require('../common/gc');
const assert = require('node:assert');

const test = require('node:test');
const { spawnSync } = require('node:child_process');
const ffi = require('node:ffi');
const { cString, fixtureSymbols, libraryPath } = require('./ffi-test-common');

const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);

{
assert.strictEqual(symbols.add_i8(120, 10), -126);
assert.strictEqual(symbols.add_u8(250, 10), 4);
assert.strictEqual(symbols.add_i16(10_000, -58), 9_942);
assert.strictEqual(symbols.add_u16(65_530, 10), 4);
assert.strictEqual(symbols.add_i32(-10, 52), 42);
assert.strictEqual(symbols.add_u32(0xFFFFFFFF, 1), 0);
assert.strictEqual(symbols.add_i64(20n, 22n), 42n);
assert.strictEqual(symbols.add_u64(20n, 22n), 42n);

if (symbols.char_is_signed()) {
assert.strictEqual(symbols.identity_char(-1), -1);
assert.strictEqual(symbols.identity_char(-128), -128);
test('character operations', (t) => {
const signed = symbols.char_is_signed();

t.assert.strictEqual(typeof signed, 'number');
if (signed) {
t.assert.strictEqual(symbols.identity_char(-1), -1);
t.assert.strictEqual(symbols.identity_char(-128), -128);
} else {
assert.strictEqual(symbols.identity_char(255), 255);
assert.strictEqual(symbols.identity_char(128), 128);
t.assert.strictEqual(symbols.identity_char(255), 255);
t.assert.strictEqual(symbols.identity_char(128), 128);
}
}

{
assert.strictEqual(symbols.add_f32(1.25, 2.75), 4);
assert.strictEqual(symbols.multiply_f64(6, 7), 42);
assert.strictEqual(symbols.sum_five_i32(10, 8, 7, 9, 8), 42);
assert.strictEqual(symbols.sum_five_f64(10, 8, 7, 9, 8), 42);
assert.strictEqual(symbols.mixed_operation(10, 2.5, 3.5, 4), 20);
}

{
assert.strictEqual(symbols.logical_and(1, 1), 1);
assert.strictEqual(symbols.logical_and(1, 0), 0);
assert.strictEqual(symbols.logical_or(0, 1), 1);
assert.strictEqual(symbols.logical_not(0), 1);
});

test('unsigned integer operations', (t) => {
t.assert.strictEqual(symbols.add_u8(250, 10), 4);
t.assert.strictEqual(symbols.add_u16(65_530, 10), 4);
t.assert.strictEqual(symbols.add_u32(0xFFFFFFFF, 1), 0);
t.assert.strictEqual(symbols.add_u64(20n, 22n), 42n);
});

test('signed integer operations', (t) => {
t.assert.strictEqual(symbols.add_i8(120, 10), -126);
t.assert.strictEqual(symbols.add_i16(10_000, -58), 9_942);
t.assert.strictEqual(symbols.add_i32(-10, 52), 42);
t.assert.strictEqual(symbols.add_i64(20n, 22n), 42n);
});

test('floating point and mixed operations', (t) => {
t.assert.strictEqual(symbols.add_f32(1.25, 2.75), 4);
t.assert.strictEqual(symbols.multiply_f64(6, 7), 42);
t.assert.strictEqual(symbols.sum_five_i32(10, 8, 7, 9, 8), 42);
t.assert.strictEqual(symbols.sum_five_f64(10, 8, 7, 9, 8), 42);
t.assert.strictEqual(symbols.mixed_operation(10, 2.5, 3.5, 4), 20);
});

test('boolean operations and bool signature validation', (t) => {
t.assert.strictEqual(symbols.logical_and(1, 1), 1);
t.assert.strictEqual(symbols.logical_and(1, 0), 0);
t.assert.strictEqual(symbols.logical_or(0, 1), 1);
t.assert.strictEqual(symbols.logical_not(0), 1);

const boolAdder = lib.getFunction('add_u8', {
parameters: ['bool', 'bool'],
result: 'bool',
});
assert.strictEqual(boolAdder(1, 0), 1);
assert.throws(() => boolAdder(true, false), /Argument 0 must be a uint8/);
}
t.assert.strictEqual(boolAdder(1, 0), 1);
t.assert.throws(() => boolAdder(true, false), /Argument 0 must be a uint8/);
});

{
test('pointer roundtrips and conversions', (t) => {
const address = 0x1234n;

assert.strictEqual(symbols.identity_pointer(address), address);
assert.strictEqual(symbols.pointer_to_usize(address), address);
assert.strictEqual(symbols.usize_to_pointer(address), address);
}
t.assert.strictEqual(symbols.identity_pointer(address), address);
t.assert.strictEqual(symbols.pointer_to_usize(address), address);
t.assert.strictEqual(symbols.usize_to_pointer(address), address);
});

{
assert.strictEqual(symbols.string_length('hello ffi'), 9n);
assert.strictEqual(symbols.string_length(cString('hello ffi')), 9n);
assert.strictEqual(symbols.safe_strlen(null), -1);
test('string and buffer operations', (t) => {
t.assert.strictEqual(symbols.string_length('hello ffi'), 9n);
t.assert.strictEqual(symbols.string_length(cString('hello ffi')), 9n);
t.assert.strictEqual(symbols.safe_strlen(null), -1);

const concatenated = symbols.string_concat('hello ', 'ffi');
assert.strictEqual(typeof concatenated, 'bigint');
assert.strictEqual(ffi.toString(concatenated), 'hello ffi');
t.assert.strictEqual(typeof concatenated, 'bigint');
t.assert.strictEqual(ffi.toString(concatenated), 'hello ffi');
symbols.free_string(concatenated);

const duplicated = symbols.string_duplicate('copied string');
assert.strictEqual(ffi.toString(duplicated), 'copied string');
t.assert.strictEqual(ffi.toString(duplicated), 'copied string');
symbols.free_string(duplicated);

const buffer = Buffer.from([1, 2, 3, 4]);
assert.strictEqual(symbols.sum_buffer(buffer, BigInt(buffer.length)), 10n);
t.assert.strictEqual(symbols.sum_buffer(buffer, BigInt(buffer.length)), 10n);
symbols.reverse_buffer(buffer, BigInt(buffer.length));
assert.deepStrictEqual([...buffer], [4, 3, 2, 1]);
t.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);
t.assert.strictEqual(symbols.sum_buffer(typed, BigInt(typed.byteLength)), 26n);

const arrayBuffer = new Uint8Array([9, 10, 11, 12]).buffer;
assert.strictEqual(symbols.sum_buffer(arrayBuffer, BigInt(arrayBuffer.byteLength)), 42n);
}
t.assert.strictEqual(symbols.sum_buffer(arrayBuffer, BigInt(arrayBuffer.byteLength)), 42n);
});

{
test('typed array get/set operations', (t) => {
const ints = new Int32Array([10, 20, 30, 40]);
assert.strictEqual(symbols.array_get_i32(ints, 2n), 30);
t.assert.strictEqual(symbols.array_get_i32(ints, 2n), 30);
symbols.array_set_i32(ints, 1n, 22);
assert.deepStrictEqual([...ints], [10, 22, 30, 40]);
t.assert.deepStrictEqual([...ints], [10, 22, 30, 40]);

const doubles = new Float64Array([1, 2, 3, 4]);
assert.strictEqual(symbols.array_get_f64(doubles, 1n), 2);
t.assert.strictEqual(symbols.array_get_f64(doubles, 1n), 2);
symbols.array_set_f64(doubles, 2n, 39.5);
assert.deepStrictEqual([...doubles], [1, 2, 39.5, 4]);
}
t.assert.deepStrictEqual([...doubles], [1, 2, 39.5, 4]);
});

{
test('counter state management', (t) => {
symbols.reset_counter();
assert.strictEqual(symbols.get_counter(), 0);
t.assert.strictEqual(symbols.get_counter(), 0);
symbols.increment_counter();
symbols.increment_counter();
assert.strictEqual(symbols.get_counter(), 2);
t.assert.strictEqual(symbols.get_counter(), 2);
symbols.reset_counter();
assert.strictEqual(symbols.get_counter(), 0);
}
t.assert.strictEqual(symbols.get_counter(), 0);
});

{
test('register and invoke callbacks', (t) => {
const seen = [];
const intCallback = lib.registerCallback(
{ parameters: ['i32'], result: 'i32' },
Expand All @@ -123,18 +131,18 @@ const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);
);

try {
assert.strictEqual(symbols.call_int_callback(intCallback, 21), 42);
t.assert.strictEqual(symbols.call_int_callback(intCallback, 21), 42);
symbols.call_string_callback(stringCallback, cString('hello callback'));
assert.deepStrictEqual(seen, ['hello callback']);
assert.strictEqual(symbols.call_binary_int_callback(binaryCallback, 19, 23), 42);
t.assert.deepStrictEqual(seen, ['hello callback']);
t.assert.strictEqual(symbols.call_binary_int_callback(binaryCallback, 19, 23), 42);
} finally {
lib.unregisterCallback(intCallback);
lib.unregisterCallback(stringCallback);
lib.unregisterCallback(binaryCallback);
}
}
});

{
test('callback ref/unref and callback validation errors', (t) => {
let called = false;
const values = [];
const voidCallback = lib.registerCallback(() => {
Expand All @@ -154,52 +162,52 @@ const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);
symbols.call_void_callback(voidCallback);
symbols.call_callback_multiple_times(countingCallback, 5);

assert.strictEqual(called, true);
assert.deepStrictEqual(values, [0, 1, 2, 3, 4]);
t.assert.strictEqual(called, true);
t.assert.deepStrictEqual(values, [0, 1, 2, 3, 4]);
} finally {
lib.unregisterCallback(voidCallback);
lib.unregisterCallback(countingCallback);
}

assert.throws(() => lib.refCallback(voidCallback), /Callback not found/);
assert.throws(() => lib.unregisterCallback(-1n), /The first argument must be a non-negative bigint/);
}

{
assert.throws(() => symbols.add_i32(1), /Invalid argument count: expected 2, got 1/);
assert.throws(() => symbols.add_i32('1', 2), /Argument 0 must be an int32/);
assert.throws(() => symbols.add_i8(1.5, 1), /Argument 0 must be an int8/);
assert.throws(() => symbols.add_i8(200, 1), /Argument 0 must be an int8/);
assert.throws(() => symbols.add_u8(Number.NaN, 1), /Argument 0 must be a uint8/);
assert.throws(() => symbols.add_u8(300, 1), /Argument 0 must be a uint8/);
assert.throws(() => symbols.add_i16(1.5, 1), /Argument 0 must be an int16/);
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(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(-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/);
assert.throws(() => symbols.string_length(Symbol('x')), /must be a buffer, an ArrayBuffer, a string, or a bigint/);
t.assert.throws(() => lib.refCallback(voidCallback), /Callback not found/);
t.assert.throws(() => lib.unregisterCallback(-1n), /The first argument must be a non-negative bigint/);
});

test('argument validation and range errors', (t) => {
t.assert.throws(() => symbols.add_i32(1), /Invalid argument count: expected 2, got 1/);
t.assert.throws(() => symbols.add_i32('1', 2), /Argument 0 must be an int32/);
t.assert.throws(() => symbols.add_i8(1.5, 1), /Argument 0 must be an int8/);
t.assert.throws(() => symbols.add_i8(200, 1), /Argument 0 must be an int8/);
t.assert.throws(() => symbols.add_u8(Number.NaN, 1), /Argument 0 must be a uint8/);
t.assert.throws(() => symbols.add_u8(300, 1), /Argument 0 must be a uint8/);
t.assert.throws(() => symbols.add_i16(1.5, 1), /Argument 0 must be an int16/);
t.assert.throws(() => symbols.add_i16(40_000, 1), /Argument 0 must be an int16/);
t.assert.throws(() => symbols.add_u16(Number.NaN, 1), /Argument 0 must be a uint16/);
t.assert.throws(() => symbols.add_u16(70_000, 1), /Argument 0 must be a uint16/);
t.assert.throws(() => symbols.add_i64(1, 2n), /Argument 0 must be an int64/);
t.assert.throws(() => symbols.add_i64(1.5, 2n), /Argument 0 must be an int64/);
t.assert.throws(() => symbols.add_i64(2n ** 63n, 2n), /Argument 0 must be an int64/);
t.assert.throws(() => symbols.add_i64(-(2n ** 63n) - 1n, 2n), /Argument 0 must be an int64/);
t.assert.throws(() => symbols.add_u64('1', 2n), /Argument 0 must be a uint64/);
t.assert.throws(() => symbols.add_u64(1, 2n), /Argument 0 must be a uint64/);
t.assert.throws(() => symbols.add_u64(Number.NaN, 2n), /Argument 0 must be a uint64/);
t.assert.throws(() => symbols.add_u64(-1n, 2n), /Argument 0 must be a uint64/);
t.assert.throws(() => symbols.add_u64(2n ** 64n, 2n), /Argument 0 must be a uint64/);
t.assert.throws(() => symbols.identity_pointer(-1n), /Argument 0 must be a non-negative pointer bigint/);
t.assert.throws(() => symbols.string_length(Symbol('x')), /must be a buffer, an ArrayBuffer, a string, or a bigint/);

if (process.arch === 'ia32' || process.arch === 'arm') {
assert.throws(() => symbols.identity_pointer(2n ** 32n), /platform pointer range|non-negative pointer bigint/);
t.assert.throws(() => symbols.identity_pointer(2n ** 32n), /platform pointer range|non-negative pointer bigint/);
}
}
});

{
assert.strictEqual(symbols.divide_i32(84, 2), 42);
assert.strictEqual(symbols.divide_i32(84, 0), 0);
assert.strictEqual(symbols.safe_strlen(null), -1);
}
test('safe operation behavior', (t) => {
t.assert.strictEqual(symbols.divide_i32(84, 2), 42);
t.assert.strictEqual(symbols.divide_i32(84, 0), 0);
t.assert.strictEqual(symbols.safe_strlen(null), -1);
});

function assertInvalidCallbackReturnAborts(returnExpression) {
function assertInvalidCallbackReturnAborts(t, returnExpression) {
const { stderr, status, signal } = spawnSync(process.execPath, [
'--experimental-ffi',
'-e',
Expand All @@ -216,59 +224,15 @@ functions.call_int_callback(callback, 21);`,
encoding: 'utf8',
});

assert.ok(common.nodeProcessAborted(status, signal),
`status: ${status}, signal: ${signal}\nstderr: ${stderr}`);
assert.match(stderr, /Callback returned invalid value for declared FFI type/);
t.assert.ok(common.nodeProcessAborted(status, signal),
`status: ${status}, signal: ${signal}\nstderr: ${stderr}`);
t.assert.match(stderr, /Callback returned invalid value for declared FFI type/);
}

assertInvalidCallbackReturnAborts('1.5');
assertInvalidCallbackReturnAborts('2 ** 40');

(async () => {
{
let callback = () => 1;
const ref = new WeakRef(callback);
const pointer = lib.registerCallback(
{ parameters: ['i32'], result: 'i32' },
callback,
);

lib.unrefCallback(pointer);
callback = null;

await gcUntil('ffi unrefCallback releases callback function', () => {
return ref.deref() === undefined;
});

assert.strictEqual(symbols.call_int_callback(pointer, 21), 0);

lib.unregisterCallback(pointer);
}

{
let callback = () => 1;
const ref = new WeakRef(callback);
const pointer = lib.registerCallback({ result: 'i32' }, callback);
test('invalid callback return aborts for non-integer', (t) => {
assertInvalidCallbackReturnAborts(t, '1.5');
});

lib.unrefCallback(pointer);
lib.refCallback(pointer);
callback = null;

for (let i = 0; i < 5; i++) {
await gcUntil('ffi refCallback retains callback function', () => true, 1);
assert.strictEqual(typeof ref.deref(), 'function');
}

lib.unregisterCallback(pointer);
}

{
const callback = lib.registerCallback(() => {});

lib.close();

assert.throws(() => lib.unregisterCallback(callback), /Library is closed/);
assert.throws(() => lib.refCallback(callback), /Library is closed/);
assert.throws(() => lib.unrefCallback(callback), /Library is closed/);
}
})().then(common.mustCall());
test('invalid callback return aborts for out-of-range integer', (t) => {
assertInvalidCallbackReturnAborts(t, '2 ** 40');
});
Loading