Files
node/test/ffi/test-ffi-fast-buffer.js
T
Trivikram Kamat 2b350bb8e4 ffi: accept pointer BigInts in multi-argument fast calls
convertPointerArg ended with an unconditional getRawPointer call for
buffer and arraybuffer types, rejecting BigInt addresses that the
single-argument fast path and ToFFIArgument both accept. Drop the
fallback; hasPointerMemoryArg already converts memory-backed values,
and null, undefined, and strings are handled earlier.

Signed-off-by: Trivikram Kamat <16024985+trivikr@users.noreply.github.com>
Assisted-by: claude:opus-5
PR-URL: https://github.com/nodejs/node/pull/64964
Fixes: https://github.com/nodejs/node/issues/64963
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
2026-08-04 17:15:37 +00:00

169 lines
5.0 KiB
JavaScript

// Flags: --experimental-ffi --expose-internals
'use strict';
const common = require('../common');
common.skipIfFFIMissing();
const assert = require('node:assert');
const { test } = require('node:test');
const { internalBinding } = require('internal/test/binding');
const {
kSbSharedBuffer,
} = internalBinding('ffi');
const ffi = require('node:ffi');
const { libraryPath } = require('./ffi-test-common');
test('fast FFI accepts buffer and arraybuffer arguments natively', () => {
const lib = new ffi.DynamicLibrary(libraryPath);
const functions = {
first_byte_buffer: lib.getFunction('first_byte', {
arguments: ['buffer'],
return: 'u8',
}),
first_byte_arraybuffer: lib.getFunction('first_byte', {
arguments: ['arraybuffer'],
return: 'u8',
}),
pointer_to_usize: lib.getFunction('pointer_to_usize', {
arguments: ['pointer'],
return: 'u64',
}),
sum_buffer: {
arguments: ['buffer', 'u64'],
return: 'u64',
},
};
functions.sum_buffer = lib.getFunction('sum_buffer', functions.sum_buffer);
try {
const bytes = Buffer.from([1, 2, 3, 4]);
const ab = Uint8Array.from([5, 6, 7, 8]).buffer;
assert.strictEqual(functions.first_byte_buffer(bytes), 1);
assert.strictEqual(functions.first_byte_arraybuffer(ab), 5);
assert.strictEqual(
functions.pointer_to_usize(bytes), ffi.getRawPointer(bytes));
assert.strictEqual(functions.sum_buffer(bytes, BigInt(bytes.length)), 10n);
if (process.arch === 'arm64') {
assert.strictEqual(functions.first_byte_buffer[kSbSharedBuffer], undefined);
assert.strictEqual(functions.sum_buffer[kSbSharedBuffer], undefined);
assert.strictEqual(functions.pointer_to_usize[kSbSharedBuffer], undefined);
}
} finally {
lib.close();
}
});
test('fast FFI buffer arguments reject invalid values', () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
first_byte: { arguments: ['buffer'], return: 'u8' },
});
try {
assert.throws(() => functions.first_byte(123), {
code: 'ERR_INVALID_ARG_VALUE',
});
} finally {
lib.close();
}
});
test('fast FFI string buffers survive reentrant callbacks', {
// Bundled libffi callbacks crash on SmartOS.
skip: common.isSunOS,
}, () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
safe_strlen: { arguments: ['string'], return: 'i32' },
string_survives_callback: {
arguments: ['string', 'pointer'],
return: 'i32',
},
});
let nestedLength;
const callback = lib.registerCallback(() => {
nestedLength = functions.safe_strlen('inner string');
});
try {
assert.strictEqual(
functions.string_survives_callback('outer string', callback), 1);
assert.strictEqual(nestedLength, 12);
} finally {
lib.unregisterCallback(callback);
lib.close();
}
});
test('optimized buffer signatures preserve pointer-like conversions', () => {
const lib = new ffi.DynamicLibrary(libraryPath);
const asBuffer = lib.getFunction('pointer_to_usize', {
arguments: ['buffer'],
return: 'u64',
});
const asArrayBuffer = lib.getFunction('pointer_to_usize', {
arguments: ['arraybuffer'],
return: 'u64',
});
function callBuffer(value) {
return asBuffer(value);
}
function callArrayBuffer(value) {
return asArrayBuffer(value);
}
try {
for (let i = 0; i < 100_000; i++) {
assert.strictEqual(callBuffer(0n), 0n);
assert.strictEqual(callArrayBuffer(0n), 0n);
}
for (const call of [callBuffer, callArrayBuffer]) {
assert.strictEqual(call(null), 0n);
assert.strictEqual(call(undefined), 0n);
assert.notStrictEqual(call('ffi'), 0n);
const bytes = Buffer.alloc(1);
assert.strictEqual(call(bytes), ffi.getRawPointer(bytes));
}
} finally {
lib.close();
}
});
test('multi-argument buffer signatures accept pointer BigInts', () => {
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_buffer: { arguments: ['buffer', 'u64'], return: 'u64' },
fill_buffer: { arguments: ['arraybuffer', 'u64', 'u32'], return: 'void' },
});
try {
const bytes = Buffer.from([1, 2, 3, 4]);
const pointer = ffi.getRawPointer(bytes);
const length = BigInt(bytes.length);
// The two-argument wrapper must treat a raw address like the buffer it
// came from, matching both the single-argument fast path and the slow
// paths in src/ffi/types.cc.
assert.strictEqual(functions.sum_buffer(pointer, length), 10n);
assert.strictEqual(functions.sum_buffer(bytes, length), 10n);
assert.strictEqual(functions.sum_buffer(0n, length), 0n);
assert.strictEqual(functions.sum_buffer(null, length), 0n);
// The three-argument wrapper must forward the address to real memory
// instead of rejecting it.
functions.fill_buffer(pointer, length, 7);
assert.deepStrictEqual(bytes, Buffer.from([7, 7, 7, 7]));
// Still accepted once the call has been optimized.
for (let i = 0; i < 100_000; i++) {
assert.strictEqual(functions.sum_buffer(pointer, length), 28n);
}
} finally {
lib.close();
}
});