current commit
260828.1 · ffi: enable module by default
'use strict';
const common = require('../common');
const { spawnSyncAndAssert } = require('../common/child_process');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const { Worker } = require('node:worker_threads');

common.skipIfFFIMissing();

test('ffi cannot be loaded without node: prefix', () => {
  assert.throws(() => {
    require('ffi');
  }, {
    code: 'MODULE_NOT_FOUND',
    message: /Cannot find module 'ffi'/,
  });
});

test('ffi builtin is unavailable when disabled', () => {
  const { stdout, stderr, status, signal } = spawnSync(process.execPath, [
    '--no-experimental-ffi',
    '-e',
    'require("node:ffi")',
  ], {
    encoding: 'utf8',
  });

  assert.strictEqual(stdout, '');
  assert.match(stderr, /No such built-in module: node:ffi/);
  assert.notStrictEqual(status, 0);
  assert.strictEqual(signal, null);
});

test('ffi builtin is listed by default', () => {
  for (const flags of [[], ['--experimental-ffi']]) {
    spawnSyncAndAssert(process.execPath, [
      ...flags,
      '-p',
      'require("node:module").builtinModules.includes("node:ffi")',
    ], { stdout: 'true\n' });
  }

  spawnSyncAndAssert(process.execPath, [
    '--no-experimental-ffi',
    '-p',
    'require("node:module").builtinModules.includes("node:ffi")',
  ], { stdout: 'false\n' });
});

test('ffi can be imported from ESM', () => {
  const { stdout, stderr, status, signal } = spawnSync(process.execPath, [
    '--input-type=module',
    '-e',
    'import * as ffi from "node:ffi"; console.log(typeof ffi.dlopen);',
  ], {
    encoding: 'utf8',
  });

  assert.strictEqual(stdout.trim(), 'function');
  assert.match(stderr, /ExperimentalWarning: FFI is an experimental feature/);
  assert.strictEqual(status, 0);
  assert.strictEqual(signal, null);
});

test('DynamicLibrary requires new', () => {
  const { stdout, stderr, status, signal } = spawnSync(process.execPath, [
    '-e',
    'const ffi = require("node:ffi"); ffi.DynamicLibrary("missing");',
  ], {
    encoding: 'utf8',
  });

  assert.strictEqual(stdout, '');
  assert.match(stderr, /Class constructor DynamicLibrary cannot be invoked without 'new'/);
  assert.notStrictEqual(status, 0);
  assert.strictEqual(signal, null);
});

test('ffi exports expected API surface', () => {
  const ffi = require('node:ffi');
  const expected = [
    'DynamicLibrary',
    'dlclose',
    'dlopen',
    'dlsym',
    'exportArrayBuffer',
    'exportArrayBufferView',
    'exportBuffer',
    'exportString',
    'getCurrentEventLoop',
    'getFloat32',
    'getFloat64',
    'getInt16',
    'getInt32',
    'getInt64',
    'getInt8',
    'getRawPointer',
    'getUint16',
    'getUint32',
    'getUint64',
    'getUint8',
    'setFloat32',
    'setFloat64',
    'setInt16',
    'setInt32',
    'setInt64',
    'setInt8',
    'setUint16',
    'setUint32',
    'setUint64',
    'setUint8',
    'suffix',
    'toArrayBuffer',
    'toBuffer',
    'toString',
    'types',
  ];

  assert.deepStrictEqual(Object.keys(ffi).sort(), expected);
  assert.strictEqual(typeof ffi.DynamicLibrary, 'function');
  assert.strictEqual(typeof ffi.dlopen, 'function');
  assert.strictEqual(typeof ffi.dlclose, 'function');
  assert.strictEqual(typeof ffi.dlsym, 'function');
  assert.strictEqual(typeof ffi.exportArrayBuffer, 'function');
  assert.strictEqual(typeof ffi.exportArrayBufferView, 'function');
  assert.strictEqual(typeof ffi.exportString, 'function');
  assert.strictEqual(typeof ffi.exportBuffer, 'function');
  assert.strictEqual(typeof ffi.getRawPointer, 'function');
  assert.strictEqual(typeof ffi.getInt8, 'function');
  assert.strictEqual(typeof ffi.getUint8, 'function');
  assert.strictEqual(typeof ffi.getInt16, 'function');
  assert.strictEqual(typeof ffi.getUint16, 'function');
  assert.strictEqual(typeof ffi.getInt32, 'function');
  assert.strictEqual(typeof ffi.getUint32, 'function');
  assert.strictEqual(typeof ffi.getInt64, 'function');
  assert.strictEqual(typeof ffi.getUint64, 'function');
  assert.strictEqual(typeof ffi.getFloat32, 'function');
  assert.strictEqual(typeof ffi.getFloat64, 'function');
  assert.strictEqual(typeof ffi.getCurrentEventLoop, 'function');
  assert.strictEqual(typeof ffi.setInt8, 'function');
  assert.strictEqual(typeof ffi.setUint8, 'function');
  assert.strictEqual(typeof ffi.setInt16, 'function');
  assert.strictEqual(typeof ffi.setUint16, 'function');
  assert.strictEqual(typeof ffi.setInt32, 'function');
  assert.strictEqual(typeof ffi.setUint32, 'function');
  assert.strictEqual(typeof ffi.setInt64, 'function');
  assert.strictEqual(typeof ffi.setUint64, 'function');
  assert.strictEqual(typeof ffi.setFloat32, 'function');
  assert.strictEqual(typeof ffi.setFloat64, 'function');
  assert.strictEqual(typeof ffi.toString, 'function');
  assert.strictEqual(typeof ffi.toBuffer, 'function');
  assert.strictEqual(typeof ffi.toArrayBuffer, 'function');
  assert.strictEqual(typeof ffi.types, 'object');
});

test('ffi.suffix matches the current platform', () => {
  const ffi = require('node:ffi');
  const expected = process.platform === 'win32' ? 'dll' :
    process.platform === 'darwin' ? 'dylib' : 'so';

  assert.strictEqual(ffi.suffix, expected);
});

test('ffi.types exports canonical type constants', () => {
  const ffi = require('node:ffi');
  const expected = {
    __proto__: null,
    VOID: 'void',
    POINTER: 'pointer',
    BUFFER: 'buffer',
    ARRAY_BUFFER: 'arraybuffer',
    FUNCTION: 'function',
    BOOL: 'bool',
    CHAR: 'char',
    STRING: 'string',
    FLOAT: 'float',
    DOUBLE: 'double',
    INT_8: 'int8',
    UINT_8: 'uint8',
    INT_16: 'int16',
    UINT_16: 'uint16',
    INT_32: 'int32',
    UINT_32: 'uint32',
    INT_64: 'int64',
    UINT_64: 'uint64',
    FLOAT_32: 'float32',
    FLOAT_64: 'float64',
  };

  assert.deepStrictEqual(ffi.types, expected);
  assert.strictEqual(Object.isFrozen(ffi.types), true);
});

test('ffi.getCurrentEventLoop returns the current thread event loop address', async () => {
  const ffi = require('node:ffi');
  const mainLoop = ffi.getCurrentEventLoop();

  assert.strictEqual(typeof mainLoop, 'bigint');
  assert.ok(mainLoop > 0n);
  assert.strictEqual(ffi.getCurrentEventLoop(), mainLoop);

  const workerLoop = await new Promise((resolve, reject) => {
    const worker = new Worker(`
      const { parentPort } = require('node:worker_threads');
      const ffi = require('node:ffi');
      const loop = ffi.getCurrentEventLoop();

      parentPort.postMessage([loop, ffi.getCurrentEventLoop()]);
    `, { eval: true });

    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });

  assert.strictEqual(typeof workerLoop[0], 'bigint');
  assert.ok(workerLoop[0] > 0n);
  assert.strictEqual(workerLoop[0], workerLoop[1]);
  assert.notStrictEqual(workerLoop[0], mainLoop);
});