current commit
261104.12 · ffi: support function calls from pointers
// Flags: --expose-gc
'use strict';
const { skipIfFFIMissing } = require('../common');
skipIfFFIMissing();

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

test('ffi function pointer callables retain their library through GC', async (t) => {
  let library = ffi.dlopen(libraryPath);
  const ref = new WeakRef(library.lib);
  const fn = library.lib.toFunction(library.lib.getSymbol('add_i32'), {
    arguments: ['i32', 'i32'], return: 'i32',
  });
  library = null;
  try {
    for (let index = 0; index < 5; index++) {
      await gcUntil('ffi function pointer retains library', () => true, 1);
      t.assert.notStrictEqual(ref.deref(), undefined);
      t.assert.strictEqual(fn(20, 22), 42);
    }
  } finally {
    ref.deref()?.close();
  }
  t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
});

test('ffi function pointer registry does not retain callables', async (t) => {
  const { lib } = ffi.dlopen(libraryPath);
  try {
    const address = lib.getSymbol('add_i32');
    const refs = [];
    for (let index = 0; index < 100; index++) {
      let fn = lib.toFunction(address, {
        arguments: ['i32', 'i32'], return: 'i32',
      });
      refs.push(new WeakRef(fn));
      fn = null;
    }
    await gcUntil('ffi function pointer callables are collected',
                  () => refs.every((ref) => ref.deref() === undefined));
    const fn = lib.toFunction(address, {
      arguments: ['i32', 'i32'], return: 'i32',
    });
    t.assert.strictEqual(fn(20, 22), 42);
    lib.close();
    t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
  } finally {
    lib.close();
  }
});

test('ffi pointer callable construction ignores throwing inherited setters', async (t) => {
  const { lib } = ffi.dlopen(libraryPath);
  t.after(() => lib.close());
  const address = lib.getSymbol('add_i32');
  const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer');
  let fn;
  let setterCalls = 0;
  const failure = new Error('pointer setter failed');
  try {
    Object.defineProperty(Function.prototype, 'pointer', {
      configurable: true,
      get() { return undefined; },
      set() {
        setterCalls++;
        throw failure;
      },
    });
    fn = lib.toFunction(address, {
      arguments: ['i32', 'i32'], return: 'i32',
    });
    t.assert.strictEqual(setterCalls, 0);
    t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address);
    t.assert.strictEqual(fn(20, 22), 42);
  } finally {
    if (original === undefined) {
      delete Function.prototype.pointer;
    } else {
      Object.defineProperty(Function.prototype, 'pointer', original);
    }
  }
  try {
    const ref = new WeakRef(fn);
    fn = null;
    await gcUntil('ffi pointer callable with inherited setter is collected',
                  () => ref.deref() === undefined);
  } finally {
    lib.close();
  }
});

test('ffi pointer callable construction ignores inherited setters that close the library', async (t) => {
  const { lib } = ffi.dlopen(libraryPath);
  t.after(() => lib.close());
  const address = lib.getSymbol('add_i32');
  const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer');
  let fn;
  let setterCalls = 0;
  try {
    Object.defineProperty(Function.prototype, 'pointer', {
      configurable: true,
      get() { return undefined; },
      set() {
        setterCalls++;
        lib.close();
      },
    });
    fn = lib.toFunction(address, {
      arguments: ['i32', 'i32'], return: 'i32',
    });
    t.assert.strictEqual(setterCalls, 0);
    t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address);
    t.assert.strictEqual(fn(20, 22), 42);
  } finally {
    if (original === undefined) {
      delete Function.prototype.pointer;
    } else {
      Object.defineProperty(Function.prototype, 'pointer', original);
    }
  }
  lib.close();
  t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
  const ref = new WeakRef(fn);
  fn = null;
  await gcUntil('ffi pointer callable from closed library is collected',
                () => ref.deref() === undefined);
});

test('ffi unrefCallback releases callback function', async (t) => {
  const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);
  t.after(() => lib.close());

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

  lib.unrefCallback(pointer);
  callback = null;

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

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

  lib.unregisterCallback(pointer);
});

test('ffi unrefCallback zero-fills narrow callback return', async (t) => {
  const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);
  t.after(() => lib.close());

  let callback = () => 1;
  const ref = new WeakRef(callback);
  const pointer = lib.registerCallback(
    { arguments: ['i8'], return: 'i8' },
    callback,
  );

  lib.unrefCallback(pointer);
  callback = null;

  await gcUntil('ffi unrefCallback zero-fills narrow callback return', () => {
    return ref.deref() === undefined;
  });

  t.assert.strictEqual(symbols.call_int8_callback(pointer, 21), 0);
  lib.unregisterCallback(pointer);
});

test('ffi refCallback retains callback function', async (t) => {
  const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
  t.after(() => lib.close());

  let callback = () => 1;
  const ref = new WeakRef(callback);
  const pointer = lib.registerCallback({ return: 'i32' }, callback);

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

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

  lib.unregisterCallback(pointer);
});

test('callback ref/unref throw after callback function is collected', async (t) => {
  const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
  t.after(() => lib.close());

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

  lib.unrefCallback(pointer);
  callback = null;

  await gcUntil(
    'callback ref/unref throw after callback function is collected',
    () => ref.deref() === undefined,
  );

  t.assert.throws(() => lib.unrefCallback(pointer), {
    code: 'ERR_INVALID_ARG_VALUE',
    message: /Callback not found/,
  });
  t.assert.throws(() => lib.refCallback(pointer), {
    code: 'ERR_INVALID_ARG_VALUE',
    message: /Callback not found/,
  });

  lib.unregisterCallback(pointer);
});

test('callback ref/unref/unregister throw when library is closed', (t) => {
  const { lib } = ffi.dlopen(libraryPath, fixtureSymbols);
  const callback = lib.registerCallback(() => {});

  lib.close();

  t.assert.throws(() => lib.unregisterCallback(callback), /Library is closed/);
  t.assert.throws(() => lib.refCallback(callback), /Library is closed/);
  t.assert.throws(() => lib.unrefCallback(callback), /Library is closed/);
});