Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
45 changes: 45 additions & 0 deletions test/ffi/ffi-callback-test-common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');

function spawnAbortingChild(source) {
const args = ['-e', source];
if (common.isWindows) {
return spawnSync(process.execPath, args, { encoding: 'utf8' });
}

// Avoid writing core files for these intentional aborts.
return spawnSync('/bin/sh', [
'-c', 'ulimit -c 0 && exec "$@"',
'sh', process.execPath, ...args,
], { encoding: 'utf8' });
}

function assertAborts(source, message) {
const { stderr, status, signal } = spawnAbortingChild(source);
assert.ok(common.nodeProcessAborted(status, signal),
`status: ${status}, signal: ${signal}
stderr: ${stderr}`);
assert.match(stderr, message);
}

function assertCallbackAborts(callbackBody, message) {
assertAborts(
`'use strict';
const ffi = require('node:ffi');
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
const { lib, functions } = ffi.dlopen(libraryPath, fixtureSymbols);
const callback = lib.registerCallback(
{ arguments: ['i32'], return: 'i32' },
() => { ${callbackBody} },
);
functions.call_int_callback(callback, 21);`,
message,
);
}

module.exports = {
assertAborts,
assertCallbackAborts,
};
2 changes: 1 addition & 1 deletion test/ffi/ffi.status
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ prefix ffi

[$system==solaris] # Also applies to SmartOS
# Bundled libffi callbacks crash on SmartOS.
test-ffi-calls: SKIP
test-ffi-callback*: SKIP
test-ffi-shared-buffer: SKIP
test-ffi-weakref-calls: SKIP
28 changes: 28 additions & 0 deletions test/ffi/test-ffi-callback-abort-cross-thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { test } = require('node:test');
const { assertAborts } = require('./ffi-callback-test-common');

test('ffi aborts on cross-thread callback invocation', () => {
const workerSource = `
const { workerData } = require('node:worker_threads');
const ffi = require('node:ffi');
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
const { functions } = ffi.dlopen(libraryPath, fixtureSymbols);
functions.call_int_callback(workerData, 21);
`;
assertAborts(
`'use strict';
const { Worker } = require('node:worker_threads');
const ffi = require('node:ffi');
const { fixtureSymbols, libraryPath } = require(${JSON.stringify(require.resolve('./ffi-test-common'))});
const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
const callback = lib.registerCallback(
{ arguments: ['i32'], return: 'i32' },
(value) => value * 2,
);
new Worker(${JSON.stringify(workerSource)}, { eval: true, workerData: callback });`,
/Callbacks can only be invoked on the system thread they were created on/,
);
});
9 changes: 9 additions & 0 deletions test/ffi/test-ffi-callback-abort-fractional-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { test } = require('node:test');
const { assertCallbackAborts } = require('./ffi-callback-test-common');

test('ffi aborts on fractional callback return values', () => {
assertCallbackAborts('return 1.5;', /Callback returned invalid value for declared FFI type/);
});
9 changes: 9 additions & 0 deletions test/ffi/test-ffi-callback-abort-out-of-range-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { test } = require('node:test');
const { assertCallbackAborts } = require('./ffi-callback-test-common');

test('ffi aborts on out-of-range callback return values', () => {
assertCallbackAborts('return 2 ** 40;', /Callback returned invalid value for declared FFI type/);
});
9 changes: 9 additions & 0 deletions test/ffi/test-ffi-callback-abort-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { test } = require('node:test');
const { assertCallbackAborts } = require('./ffi-callback-test-common');

test('ffi aborts when a callback returns a promise', () => {
assertCallbackAborts('return Promise.resolve(1);', /Callbacks cannot return promises/);
});
9 changes: 9 additions & 0 deletions test/ffi/test-ffi-callback-abort-throw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const { test } = require('node:test');
const { assertCallbackAborts } = require('./ffi-callback-test-common');

test('ffi aborts when a callback throws', () => {
assertCallbackAborts('throw new Error("boom");', /Callbacks cannot throw an exception/);
});
84 changes: 84 additions & 0 deletions test/ffi/test-ffi-callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const assert = require('node:assert');
const { test } = require('node:test');
const ffi = require('node:ffi');
const { cString, fixtureSymbols, libraryPath } = require('./ffi-test-common');

function getLibrary() {
return ffi.dlopen(libraryPath, fixtureSymbols);
}

test('ffi callbacks can be registered and invoked', () => {
const { lib, functions: symbols } = getLibrary();
const seen = [];
const intCallback = lib.registerCallback(
{ arguments: ['i32'], return: 'i32' },
(value) => value * 2,
);
const stringCallback = lib.registerCallback(
{ arguments: ['pointer'], return: 'void' },
(ptr) => seen.push(ffi.toString(ptr)),
);
const binaryCallback = lib.registerCallback(
{ arguments: ['i32', 'i32'], return: 'i32' },
(a, b) => a + b,
);

try {
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);

const nullPointerCallback = lib.registerCallback({ return: 'pointer' }, () => null);
const undefinedPointerCallback = lib.registerCallback({ return: 'pointer' }, () => undefined);
try {
assert.strictEqual(symbols.call_pointer_callback_is_null(nullPointerCallback), 1);
assert.strictEqual(symbols.call_pointer_callback_is_null(undefinedPointerCallback), 1);
} finally {
lib.unregisterCallback(nullPointerCallback);
lib.unregisterCallback(undefinedPointerCallback);
}
} finally {
lib.unregisterCallback(intCallback);
lib.unregisterCallback(stringCallback);
lib.unregisterCallback(binaryCallback);
lib.close();
}
});

test('ffi callback ref and unref APIs work', () => {
const { lib, functions: symbols } = getLibrary();
let called = false;
const values = [];
const voidCallback = lib.registerCallback(() => {
called = true;
});
const countingCallback = lib.registerCallback(
{ arguments: ['i32'], return: 'i32' },
(value) => {
values.push(value);
return 0;
},
);

try {
lib.unrefCallback(voidCallback);
lib.refCallback(voidCallback);
symbols.call_void_callback(voidCallback);
symbols.call_callback_multiple_times(countingCallback, 5);

assert.strictEqual(called, true);
assert.deepStrictEqual(values, [0, 1, 2, 3, 4]);

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/);
} finally {
lib.close();
}
});
Loading
Loading