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();
+}