Commit 70074c6f988 for nodejs
commit 70074c6f988328425cbed56dc47214eaa89cff75
Author: Trivikram Kamat <trivikr.dev@gmail.com>
Date: Mon Nov 2 21:14:37 2026 -0800
ffi: make Fast API functions collectable
V8 strongly caches instantiations of low-serial FunctionTemplates,
which pinned Fast API functions and, through them, their
DynamicLibrary and trampoline. FunctionTemplate::New() has no opt-out,
so add an ObjectTemplate property to make these templates uncacheable,
then delete it from the new function.
Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/67177
Fixes: https://github.com/nodejs/node/issues/67176
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/src/env_properties.h b/src/env_properties.h
index 9f634ec13f9..bb1d8394935 100644
--- a/src/env_properties.h
+++ b/src/env_properties.h
@@ -416,6 +416,7 @@
V(fdclose_constructor_template, v8::ObjectTemplate) \
V(ffi_dynamic_library_constructor_template, v8::FunctionTemplate) \
V(ffi_function_constructor_template, v8::FunctionTemplate) \
+ V(ffi_uncacheable_placeholder_template, v8::ObjectTemplate) \
V(filehandlereadwrap_template, v8::ObjectTemplate) \
V(fips_indicator_event_template, v8::DictionaryTemplate) \
V(free_list_statistics_template, v8::DictionaryTemplate) \
diff --git a/src/node_ffi.cc b/src/node_ffi.cc
index 81b698a7530..f963913884c 100644
--- a/src/node_ffi.cc
+++ b/src/node_ffi.cc
@@ -37,6 +37,7 @@ using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Object;
+using v8::ObjectTemplate;
using v8::PropertyAttribute;
using v8::ReadOnly;
using v8::Signature;
@@ -277,6 +278,32 @@ BaseObjectPtr<FFIFunctionInfo> FFIFunctionInfo::Create(
return MakeWeakBaseObject<FFIFunctionInfo>(env, obj, std::move(fn), library);
}
+// V8 strongly caches instantiations of low-serial FunctionTemplates, which
+// would root the function and, through FFIFunctionInfo, its library and
+// trampoline. An ObjectTemplate property is the only public way to make a
+// FunctionTemplate uncacheable (see v8::Template::Set()), so add one and delete
+// it from the instance. It is shared because its own instantiation is cached.
+static MaybeLocal<Function> GetUncachedFunction(Environment* env,
+ Local<FunctionTemplate> tmpl) {
+ Local<Context> context = env->context();
+ IsolateData* isolate_data = env->isolate_data();
+
+ Local<ObjectTemplate> placeholder =
+ isolate_data->ffi_uncacheable_placeholder_template();
+ if (placeholder.IsEmpty()) {
+ placeholder = ObjectTemplate::New(env->isolate());
+ isolate_data->set_ffi_uncacheable_placeholder_template(placeholder);
+ }
+ tmpl->Set(env->pointer_string(), placeholder);
+
+ Local<Function> ret;
+ if (!tmpl->GetFunction(context).ToLocal(&ret) ||
+ !ret->Delete(context, env->pointer_string()).FromMaybe(false)) {
+ return MaybeLocal<Function>();
+ }
+ return ret;
+}
+
MaybeLocal<Function> DynamicLibrary::CreateFunction(
Environment* env,
const std::string& name,
@@ -340,7 +367,7 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
v8::ConstructorBehavior::kThrow,
v8::SideEffectType::kHasSideEffect,
&info->fast_metadata->c_function);
- maybe_ret = tmpl->GetFunction(context);
+ maybe_ret = GetUncachedFunction(env, tmpl);
} else {
// Non-Fast signatures either use the SharedBuffer invoker, where JS writes
// argument slots before calling with no arguments, or the generic invoker
@@ -492,7 +519,7 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
v8::SideEffectType::kHasSideEffect,
&info->fast_buffer_metadata->c_function);
Local<Function> fast_buffer_invoke;
- if (!tmpl->GetFunction(context).ToLocal(&fast_buffer_invoke)) {
+ if (!GetUncachedFunction(env, tmpl).ToLocal(&fast_buffer_invoke)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
diff --git a/test/ffi/test-ffi-dynamic-library.js b/test/ffi/test-ffi-dynamic-library.js
index 7dcbf175a86..138e1ccb410 100644
--- a/test/ffi/test-ffi-dynamic-library.js
+++ b/test/ffi/test-ffi-dynamic-library.js
@@ -260,6 +260,56 @@ test('a dropped function wrapper is collectable', async () => {
}
});
+for (const symbol of [
+ 'multiply_f64', // Fast API, no JS wrapper
+ 'add_i32', // Fast API, JS wrapper
+ 'get_counter', // Fast API, no arguments
+ 'pointer_to_usize', // Fast API, JS wrapper and Buffer invoker
+]) {
+ test(`dropping ${symbol} releases the function and its library`, async () => {
+ let lib = new ffi.DynamicLibrary(libraryPath);
+ let fn = lib.getFunction(symbol, fixtureSymbols[symbol]);
+ const fnRef = new WeakRef(fn);
+ const libRef = new WeakRef(lib);
+ lib = null;
+ fn = null;
+
+ await gcUntil(`dropping ${symbol} releases the function and its library`, () => {
+ return fnRef.deref() === undefined && libRef.deref() === undefined;
+ });
+ });
+}
+
+test('an optimized Fast API call target can be collected', async () => {
+ const lib = new ffi.DynamicLibrary(libraryPath);
+
+ try {
+ function callMultiply(fn) {
+ return fn(20, 22);
+ }
+ eval('%PrepareFunctionForOptimization(callMultiply)');
+
+ // Collect a function that optimized code has called, then make sure a new
+ // function for the same symbol works through a fresh trampoline.
+ let fn = lib.getFunction('multiply_f64', fixtureSymbols.multiply_f64);
+ assert.strictEqual(callMultiply(fn), 440);
+ assert.strictEqual(callMultiply(fn), 440);
+ eval('%OptimizeFunctionOnNextCall(callMultiply)');
+ assert.strictEqual(callMultiply(fn), 440);
+
+ const ref = new WeakRef(fn);
+ fn = null;
+ await gcUntil('an optimized Fast API call target can be collected', () => {
+ return ref.deref() === undefined;
+ });
+
+ fn = lib.getFunction('multiply_f64', fixtureSymbols.multiply_f64);
+ assert.strictEqual(callMultiply(fn), 440);
+ } finally {
+ lib.close();
+ }
+});
+
test('FFI functions keep their owning library alive', async () => {
let lib = new ffi.DynamicLibrary(libraryPath);
const addI32 = lib.getFunction('add_i32', fixtureSymbols.add_i32);