Skip to content

ffi: Fast API functions and their DynamicLibrary are pinned by V8's template instantiation cache #67176

Open

Description

@trivikr

Version

main

Platform

macOS

Subsystem

ffi

What steps will reproduce the bug?

import { DynamicLibrary } from 'node:ffi';
import { setImmediate as tick } from 'node:timers/promises';

const cases = {
  'Fast API, [f64, f64] (no JS wrapper)':
    ['copysign', { arguments: ['f64', 'f64'], return: 'f64' }],
  'Fast API, [i32, i32] (JS wrapper)':
    ['kill', { arguments: ['i32', 'i32'], return: 'i32' }],
  'Fast API, zero args':
    ['getpid', { arguments: [], return: 'i32' }],
  'Shared buffer ([function, pointer])':
    ['atexit', { arguments: ['function', 'pointer'], return: 'i32' }],
};

for (const [label, [symbol, signature]] of Object.entries(cases)) {
  let lib = new DynamicLibrary('/usr/lib/libSystem.B.dylib');
  let fn = lib.getFunction(symbol, signature);
  const fnRef = new WeakRef(fn);
  const libRef = new WeakRef(lib);
  fn = null;
  lib = null;

  for (let i = 0; i < 20; i++) {
    await tick();
    globalThis.gc();
  }
  console.log(label);
  console.log('  function collected:', fnRef.deref() === undefined);
  console.log('  library collected: ', libRef.deref() === undefined);
}

Run with

node --no-warnings --expose-gc repro.mjs

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior? Why is that the expected behavior?

Fast API, [f64, f64] (no JS wrapper)
  function collected: true
  library collected:  true
Fast API, [i32, i32] (JS wrapper)
  function collected: true
  library collected:  true
Fast API, zero args
  function collected: true
  library collected:  true
Shared buffer ([function, pointer])
  function collected: true
  library collected:  true

After all user references are dropped, the function and its DynamicLibrary should both be collectable. Collection should then dlclose the library and unmap the per-function executable trampoline page.

What do you see instead?

Fast API, [f64, f64] (no JS wrapper)
  function collected: false
  library collected:  false
Fast API, [i32, i32] (JS wrapper)
  function collected: true
  library collected:  false
Fast API, zero args
  function collected: false
  library collected:  false
Shared buffer ([function, pointer])
  function collected: true
  library collected:  true

In the [i32, i32] case, only the JS wrapper is collected; the raw function stays alive.

Additional information

No response

Activity

  1. self-assigned this
    on Nov 1, 2026
  2. added
    ffiIssues and PRs related to experimental Foreign Function Interface support.
    on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

ffiIssues and PRs related to experimental Foreign Function Interface support.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions