Skip to content

ffi: getFunction() and the functions accessor rebuild wrappers on every call #64970

Description

@trivikr

Version

main

Platform

Linux

Subsystem

ffi

What steps will reproduce the bug?

repro.c

#include <stdint.h>

int32_t add2(int32_t a, int32_t b) {
  return a + b;
}

repro.js

import { DynamicLibrary, suffix } from 'node:ffi';
import { stat } from 'node:fs/promises';

const libPath = new URL(`./repro.${suffix}`, import.meta.url).pathname;
await stat(libPath);

const lib = new DynamicLibrary(libPath);
lib.getFunction('add2', { arguments: ['i32', 'i32'], return: 'i32' });

console.log('same wrapper on two reads:', lib.functions.add2 === lib.functions.add2);

// Idiomatic call through the accessor, which re-resolves on every iteration.
const before = process.memoryUsage.rss();
let sum = 0;
for (let i = 0; i < 20000; i++) sum += lib.functions.add2(20, 22);
const after = process.memoryUsage.rss();

console.log(`sum: ${sum}`);
console.log(`rss growth: ${((after - before) / 1024 / 1024).toFixed(1)} MiB`);

lib.close();

Commands to run:

$ cc -shared -fPIC -O2 -o repro.so repro.c

$ node --experimental-ffi repro.js

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

Always

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

Reading an already-resolved function off library.functions returns the same wrapper each time, and calling through the accessor in a loop does not grow memory.

What do you see instead?

same wrapper on two reads: false
sum: 840000
rss growth: 56.0 MiB

Each read builds a new wrapper, so lib.functions.add2 !== lib.functions.add2, and the loop allocates a fresh trampoline page plus a new FFIFunctionInfo per iteration, holding all of them until GC.

Additional information

No response

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