Commit 6c59f359573 for nodejs

commit 6c59f3595735d7c666e9d70edab8db3d61aa791b
Author: Soul Lee <alus20x@gmail.com>
Date:   Sat Sep 26 02:34:13 2026 +0900

    ffi: allocate string argument storage lazily

    Reserve temporary string storage in InvokeFunction only when the
    first string argument is encountered. Calls without string arguments
    avoid allocating and freeing an unused vector buffer.

    Reserve capacity for all arguments before saving the first string
    pointer so subsequent strings cannot invalidate it. Add coverage for
    multiple strings, mixed Buffer arguments, and errors during argument
    conversion.

    Signed-off-by: Soul Lee <alus20x@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66178
    Reviewed-By: Daeyeon Jeong <daeyeon.dev@gmail.com>
    Reviewed-By: Anna Henningsen <anna@addaleax.net>

diff --git a/src/node_ffi.cc b/src/node_ffi.cc
index 495e59ba5cc..732657a368e 100644
--- a/src/node_ffi.cc
+++ b/src/node_ffi.cc
@@ -602,7 +602,6 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
   std::vector<uint64_t> values(expected_args, 0);
   std::vector<void*> ffi_args(expected_args, nullptr);
   std::vector<std::string> strings;
-  strings.reserve(expected_args);

   for (unsigned int i = 0; i < expected_args; i++) {
     FFIArgumentCategory res;
@@ -624,6 +623,10 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
         return;
       }

+      if (strings.empty()) {
+        // Keep string pointers stable as subsequent arguments are converted.
+        strings.reserve(expected_args);
+      }
       strings.push_back(*str);
       values[i] = reinterpret_cast<uint64_t>(strings.back().c_str());
       ffi_args[i] = &values[i];
diff --git a/test/ffi/test-ffi-invoke-string-storage.js b/test/ffi/test-ffi-invoke-string-storage.js
new file mode 100644
index 00000000000..c54e55b37d4
--- /dev/null
+++ b/test/ffi/test-ffi-invoke-string-storage.js
@@ -0,0 +1,57 @@
+// Flags: --expose-internals
+'use strict';
+
+const common = require('../common');
+common.skipIfFFIMissing();
+
+const assert = require('node:assert');
+const { internalBinding } = require('internal/test/binding');
+const { DynamicLibrary, kSbInvokeSlow } = internalBinding('ffi');
+// Capture the native method before node:ffi installs argument conversions.
+const getFunction = DynamicLibrary.prototype.getFunction;
+const ffi = require('node:ffi');
+const { libraryPath, cString } = require('./ffi-test-common');
+
+const lib = new ffi.DynamicLibrary(libraryPath);
+const rawConcat = getFunction.call(lib, 'string_concat', {
+  arguments: ['pointer', 'pointer'],
+  return: 'pointer',
+});
+// String and Buffer arguments cannot use the scalar Fast API entrypoint.
+// On SharedBuffer platforms, select its native fallback explicitly.
+const concat = rawConcat[kSbInvokeSlow] ?? rawConcat;
+const free = lib.getFunction('free_string', {
+  arguments: ['pointer'],
+  return: 'void',
+});
+
+try {
+  for (const [left, right] of [
+    ['', ''],
+    ['hello ', 'world'],
+    ['a'.repeat(128), 'b'.repeat(128)],
+    ['\u03b1', '\u03b2'],
+  ]) {
+    for (const [a, b] of [
+      [left, right],
+      [cString(left), right],
+      [left, cString(right)],
+      [cString(left), cString(right)],
+    ]) {
+      const result = concat(a, b);
+      try {
+        assert.strictEqual(ffi.toString(result), left + right);
+      } finally {
+        free(result);
+      }
+    }
+  }
+  assert.throws(() => concat('hello', 'bad\0string'), {
+    code: 'ERR_INVALID_ARG_VALUE',
+  });
+  assert.throws(() => concat('hello', 42), {
+    code: 'ERR_INVALID_ARG_VALUE',
+  });
+} finally {
+  lib.close();
+}