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);