Commit bea4d26011c for nodejs
commit bea4d26011cdea85f125b06fbf7124fb38c9f591
Author: Damiano Mazzella <damianomazzella@gmail.com>
Date: Wed Nov 4 20:25:06 2026 +0100
ffi: support function calls from pointers
Add getFunctionFromPointer() to create callables from native addresses
with explicit signatures.
Reuse function preparation without populating named symbol caches.
Keep the associated library alive and invalidate pointer callables
when it is closed.
Add coverage for conversions, vtables, permissions and garbage
collection, and document pointer lifetime requirements.
Assisted-by: GitHub Copilot
Signed-off-by: Damiano Mazzella <damianomazzella@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66458
Fixes: https://github.com/nodejs/node/issues/66272
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
diff --git a/doc/api/ffi.md b/doc/api/ffi.md
index d505ca745df..3a8adb9378f 100644
--- a/doc/api/ffi.md
+++ b/doc/api/ffi.md
@@ -440,6 +440,64 @@ console.log(add(20, 22));
console.log(add.pointer);
```
+### `library.toFunction(pointer, signature)`
+
+* `pointer` {bigint}
+* `signature` {Object}
+* Returns: {Function}
+
+Creates a callable JavaScript wrapper for a native function address. The pointer
+must be a nonzero, non-negative `bigint` that fits the platform's pointer width.
+It can come from a resolved symbol, a native function's return value, or a
+function pointer stored in native memory, such as a vtable slot.
+
+```cjs
+const { DynamicLibrary, suffix } = require('node:ffi');
+
+const lib = new DynamicLibrary(`./mylib.${suffix}`);
+try {
+ const address = lib.getSymbol('add_i32');
+ const add = lib.toFunction(address, {
+ arguments: ['int32', 'int32'],
+ return: 'int32',
+ });
+ console.log(add(20, 22));
+ console.log(add.pointer === address);
+} finally {
+ lib.close();
+}
+```
+
+Argument and return conversions follow the same rules as `getFunction()`.
+This method supports fixed signatures using the platform's default calling
+convention. Explicit calling convention selection and structures passed or
+returned by value are not supported.
+
+Each call creates a distinct wrapper. Multiple signatures can be associated
+with the same address, but the caller is responsible for their correctness.
+These wrappers do not appear in `library.functions`, `library.getFunctions()`,
+`library.symbols`, or `library.getSymbols()` unless a symbol was separately
+resolved by name. They use the generic libffi call path rather than Fast API
+or SharedBuffer invokers.
+
+The wrapper keeps the associated library alive. Calling it after
+`library.close()` throws `ERR_FFI_LIBRARY_CLOSED`. FFI permission is required
+when creating the wrapper, including after permission has been revoked on an
+already-open library.
+
+**The associated library does not establish ownership or validity of the
+address.** Node.js cannot determine whether the pointer refers to executable
+code, matches the signature, or is still valid. Passing a data pointer, using
+an incorrect signature, or calling code that has been unloaded can crash the
+process or corrupt memory.
+
+The caller must keep the actual code and any native object used by the call
+alive. For example, associating a COM vtable method with `ole32.dll` does not
+retain the COM object or the module implementing that method. The caller must
+manage its native references and lifetime. A wrapper for a callback pointer
+also becomes unsafe if that callback is unregistered, even if the associated
+library remains open.
+
### `library.getFunctions([definitions])`
* `definitions` {Object}
diff --git a/lib/ffi.js b/lib/ffi.js
index 82663a9710d..f3295ad0ee7 100644
--- a/lib/ffi.js
+++ b/lib/ffi.js
@@ -152,6 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', {
});
const rawGetFunction = DynamicLibrary.prototype.getFunction;
+const rawToFunction = DynamicLibrary.prototype.toFunction;
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
const rawClose = DynamicLibrary.prototype.close;
@@ -180,6 +181,11 @@ DynamicLibrary.prototype.getFunction = function getFunction(name, signature) {
return wrapFFIFunction(raw, this);
};
+DynamicLibrary.prototype.toFunction = function toFunction(pointer, signature) {
+ const raw = FunctionPrototypeCall(rawToFunction, this, pointer, signature);
+ return wrapFFIFunction(raw, this);
+};
+
DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) {
const raw = definitions === undefined ?
FunctionPrototypeCall(rawGetFunctions, this) :
diff --git a/src/node_ffi.cc b/src/node_ffi.cc
index f963913884c..317af551294 100644
--- a/src/node_ffi.cc
+++ b/src/node_ffi.cc
@@ -106,6 +106,14 @@ void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const {
sizeof(decltype(function_wrappers_)::value_type),
"std::unordered_map<std::string, v8::Global<v8::Function>>");
+ tracker->TrackFieldWithSize(
+ "pointer_functions",
+ pointer_functions_ == nullptr
+ ? 0
+ : sizeof(std::unordered_set<FFIFunction*>) +
+ pointer_functions_->size() * sizeof(FFIFunction*),
+ "std::unordered_set<FFIFunction*>");
+
// FFIFunctionInfo instances and their sb_backing ArrayBuffers are
// owned by V8 function wrappers and reachable only via weak references,
// so they are deliberately not counted here.
@@ -117,6 +125,14 @@ void DynamicLibrary::Close() {
fn->ptr = nullptr;
}
+ if (pointer_functions_ != nullptr) {
+ for (FFIFunction* fn : *pointer_functions_) {
+ fn->closed = true;
+ fn->ptr = nullptr;
+ }
+ pointer_functions_->clear();
+ }
+
// Closing the library invalidates all registered callbacks. Node.js does not
// track or revoke callback pointers that have already been handed to native
// code. If native code calls a callback pointer after `close()` or
@@ -158,7 +174,10 @@ Maybe<void*> DynamicLibrary::ResolveSymbol(Environment* env,
}
Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
- Environment* env, const std::string& name, Local<Object> signature) {
+ Environment* env,
+ const std::string& name,
+ Local<Object> signature,
+ void* ptr) {
std::shared_ptr<FFIFunction> fn;
FunctionSignature parsed;
@@ -167,7 +186,8 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
}
// Look up the cache only after parsing: the signature's getters run user
// code that may close the library, which clears `functions_`.
- auto existing = functions_.find(name);
+ const bool from_pointer = ptr != nullptr;
+ auto existing = from_pointer ? functions_.end() : functions_.find(name);
auto [return_type, args, return_type_name, arg_type_names] =
std::move(parsed);
@@ -175,14 +195,13 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
bool should_cache_function = false;
if (existing == functions_.end()) {
- void* ptr;
-
- if (!ResolveSymbol(env, name).To(&ptr)) {
- return {};
+ if (!from_pointer) {
+ if (!ResolveSymbol(env, name).To(&ptr)) {
+ return {};
+ }
+ should_cache_symbol = symbols_.find(name) == symbols_.end();
}
- should_cache_symbol = symbols_.find(name) == symbols_.end();
-
fn = std::make_shared<FFIFunction>();
fn->ptr = ptr;
fn->args = std::move(args);
@@ -221,7 +240,7 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
}
#endif
- should_cache_function = true;
+ should_cache_function = !from_pointer;
} else {
fn = existing->second;
@@ -309,7 +328,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
const std::string& name,
const std::shared_ptr<FFIFunction>& fn) {
Isolate* isolate = env->isolate();
- Local<Context> context = env->context();
// Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and
// on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already
@@ -324,20 +342,41 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
function_wrappers_.erase(cached);
}
+ Local<Function> ret;
+ if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) {
+ return {};
+ }
+ function_wrappers_.emplace(name, Global<Function>(isolate, ret))
+ .first->second.SetWeak();
+ return ret;
+}
+
+MaybeLocal<Function> DynamicLibrary::BuildFunction(
+ Environment* env,
+ const std::string& name,
+ const std::shared_ptr<FFIFunction>& fn,
+ bool optimize) {
+ Isolate* isolate = env->isolate();
+ Local<Context> context = env->context();
auto info = FFIFunctionInfo::Create(env, fn, this);
+ if (!info) {
+ return {};
+ }
DCHECK_EQ(fn->args.size(), fn->arg_type_names.size());
// Try the generated Fast API path first. If metadata creation rejects the
// signature, fall back to SharedBuffer for supported scalar shapes, then to
// the generic libffi invoker.
- std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
- if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate)
- .MoveTo(&info->fast_metadata)) {
- return {};
+ if (optimize) {
+ std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
+ if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate)
+ .MoveTo(&info->fast_metadata)) {
+ return {};
+ }
}
bool use_fast_api = info->fast_metadata != nullptr;
- bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn);
+ bool use_sb = optimize && !use_fast_api && IsSBEligibleSignature(*fn);
bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn);
// Signatures that need JS-side conversion or validation use a wrapper, as
// do all fast signatures on platforms without a native library guard.
@@ -532,14 +571,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
}
}
- // A strong handle would root the callable, which holds the library object
- // through FFIFunctionInfo, so neither could ever be collected. Weaken the
- // stored handle instead, so the cache lasts exactly as long as user code
- // keeps a reference. SetWeak() runs after the move into the map because
- // moving a handle relocates the underlying slot.
- function_wrappers_.emplace(name, Global<Function>(isolate, ret))
- .first->second.SetWeak();
-
return ret;
}
@@ -921,6 +952,59 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}
+void DynamicLibrary::ToFunction(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
+
+ if (args.Length() < 1 || !args[0]->IsBigInt()) {
+ THROW_ERR_INVALID_ARG_TYPE(env, "Function pointer must be a bigint");
+ return;
+ }
+ bool lossless;
+ uint64_t address = args[0].As<BigInt>()->Uint64Value(&lossless);
+ if (!lossless || address == 0 ||
+ address > static_cast<uint64_t>(std::numeric_limits<uintptr_t>::max())) {
+ THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer");
+ return;
+ }
+ if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
+ THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
+ return;
+ }
+
+ DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
+ if (lib->is_closed()) {
+ THROW_ERR_FFI_LIBRARY_CLOSED(env);
+ return;
+ }
+ Local<Object> signature = args[1].As<Object>();
+ PreparedFunction prepared;
+ void* ptr = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
+ if (!lib->PrepareFunction(env, "<pointer>", signature, ptr).To(&prepared)) {
+ return;
+ }
+ THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
+ if (lib->is_closed()) {
+ THROW_ERR_FFI_LIBRARY_CLOSED(env);
+ return;
+ }
+
+ auto fn = std::move(prepared.fn);
+ if (lib->pointer_functions_ == nullptr) {
+ lib->pointer_functions_ =
+ std::make_shared<std::unordered_set<FFIFunction*>>();
+ }
+ fn->pointer_registry = lib->pointer_functions_;
+ lib->pointer_functions_->insert(fn.get());
+ Local<Function> ret;
+ if (!lib->BuildFunction(env, "<pointer>", fn, false).ToLocal(&ret)) {
+ lib->pointer_functions_->erase(fn.get());
+ fn->ptr = nullptr;
+ return;
+ }
+ args.GetReturnValue().Set(ret);
+}
+
void DynamicLibrary::GetFunctions(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
@@ -1397,6 +1481,7 @@ Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close);
SetProtoDispose(isolate, tmpl, DynamicLibrary::Close);
SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction);
+ SetProtoMethod(isolate, tmpl, "toFunction", DynamicLibrary::ToFunction);
SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions);
SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol);
SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols);
diff --git a/src/node_ffi.h b/src/node_ffi.h
index 4b5df70124d..86737273e28 100644
--- a/src/node_ffi.h
+++ b/src/node_ffi.h
@@ -12,6 +12,7 @@
#include <string>
#include <thread>
#include <unordered_map>
+#include <unordered_set>
#include <vector>
// libffi only accelerates reusable call plans on x86-64 System V. Other
@@ -29,12 +30,18 @@ struct FFIFunction;
struct FFIFunction {
FFIFunction() = default;
+ ~FFIFunction() {
+ if (auto registry = pointer_registry.lock()) {
+ registry->erase(this);
+ }
+ }
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;
bool closed = false;
+ std::weak_ptr<std::unordered_set<FFIFunction*>> pointer_registry;
void* ptr = nullptr;
ffi_cif cif = {};
@@ -136,6 +143,7 @@ class DynamicLibrary : public BaseObject {
static void GetPath(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void ToFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetFunctions(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSymbol(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSymbols(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -159,11 +167,17 @@ class DynamicLibrary : public BaseObject {
};
v8::Maybe<PreparedFunction> PrepareFunction(Environment* env,
const std::string& name,
- v8::Local<v8::Object> signature);
+ v8::Local<v8::Object> signature,
+ void* ptr = nullptr);
v8::MaybeLocal<v8::Function> CreateFunction(
Environment* env,
const std::string& name,
const std::shared_ptr<FFIFunction>& fn);
+ v8::MaybeLocal<v8::Function> BuildFunction(
+ Environment* env,
+ const std::string& name,
+ const std::shared_ptr<FFIFunction>& fn,
+ bool optimize);
static void CleanupHook(void* arg);
static void CleanupFunctionInfo(
const v8::WeakCallbackInfo<FFIFunctionInfo>& data);
@@ -179,6 +193,7 @@ class DynamicLibrary : public BaseObject {
// which keeps the map from rooting the library through the wrapper's
// FFIFunctionInfo.
std::unordered_map<std::string, v8::Global<v8::Function>> function_wrappers_;
+ std::shared_ptr<std::unordered_set<FFIFunction*>> pointer_functions_;
std::unordered_map<void*, std::unique_ptr<FFICallback>> callbacks_;
};
diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c
index 3e24b7cd3d3..d04b55a2078 100644
--- a/test/ffi/fixture_library/ffi_test_library.c
+++ b/test/ffi/fixture_library/ffi_test_library.c
@@ -11,6 +11,43 @@
// Integer operations.
+typedef int32_t (*PointerUnaryFunction)(int32_t);
+
+FFI_EXPORT uint32_t get_pointer_size(void) {
+ return sizeof(uintptr_t);
+}
+
+static int32_t pointer_increment(int32_t value) {
+ return value + 1;
+}
+
+FFI_EXPORT PointerUnaryFunction get_function_pointer(void) {
+ return pointer_increment;
+}
+
+typedef struct PointerTestObject PointerTestObject;
+typedef int32_t (*PointerTestMethod)(PointerTestObject*, int32_t);
+
+struct PointerTestObject {
+ const PointerTestMethod* vtable;
+ int32_t base;
+};
+
+static int32_t pointer_object_add(PointerTestObject* self, int32_t value) {
+ return self->base + value;
+}
+
+FFI_EXPORT PointerTestObject* get_pointer_test_object(void) {
+ static const PointerTestMethod vtable[] = {pointer_object_add};
+ static PointerTestObject object = {vtable, 40};
+ return &object;
+}
+
+FFI_EXPORT uint64_t pointer_string_lengths(const char* first,
+ const char* second) {
+ return strlen(first) + strlen(second);
+}
+
FFI_EXPORT void noop_void(void) {}
FFI_EXPORT int8_t add_i8(int8_t a, int8_t b) {
diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js
index 338a728bd22..3dbd1a905d1 100644
--- a/test/ffi/test-ffi-calls.js
+++ b/test/ffi/test-ffi-calls.js
@@ -11,6 +11,180 @@ function getLibrary() {
return ffi.dlopen(libraryPath, fixtureSymbols);
}
+test('ffi function pointers are independent callables with library guards', () => {
+ const { lib } = ffi.dlopen(libraryPath);
+ try {
+ const signature = { arguments: ['i32', 'i32'], return: 'i32' };
+ const address = lib.getSymbol('add_i32');
+ const first = lib.toFunction(address, signature);
+ const second = lib.toFunction(address, signature);
+ assert.notStrictEqual(first, second);
+ assert.strictEqual(first.pointer, address);
+ assert.strictEqual(first(20, 22), 42);
+ assert.strictEqual(second(-10, 52), 42);
+ assert.deepStrictEqual(Object.keys(lib.functions), []);
+ assert.deepStrictEqual(Object.keys(lib.getFunctions()), []);
+ const named = lib.getFunction('add_i32', signature);
+ assert.notStrictEqual(named, first);
+ assert.strictEqual(lib.toFunction(named.pointer, signature)(1, 2), 3);
+ assert.throws(() => first(1), { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.throws(() => first(1, 2, 3), { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.throws(() => first(1, 2n), { code: 'ERR_INVALID_ARG_VALUE' });
+ const different = lib.toFunction(address, {
+ arguments: ['f64'], return: 'f64',
+ });
+ assert.notStrictEqual(different, first);
+ lib.close();
+ for (const fn of [first, second, different]) {
+ assert.throws(() => fn(), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+ }
+ assert.throws(() => lib.toFunction(address, signature), {
+ code: 'ERR_FFI_LIBRARY_CLOSED',
+ });
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi calls function pointers returned by C and stored in vtables', () => {
+ const { lib, functions } = ffi.dlopen(libraryPath, {
+ get_function_pointer: { arguments: [], return: 'pointer' },
+ get_pointer_test_object: { arguments: [], return: 'pointer' },
+ get_pointer_size: { arguments: [], return: 'u32' },
+ });
+ try {
+ const increment = lib.toFunction(functions.get_function_pointer(), {
+ arguments: ['i32'], return: 'i32',
+ });
+ assert.strictEqual(increment(41), 42);
+ const readPointer = functions.get_pointer_size() === 8 ?
+ (address) => ffi.getUint64(address) : (address) => BigInt(ffi.getUint32(address));
+ const object = functions.get_pointer_test_object();
+ const vtable = readPointer(object);
+ const method = lib.toFunction(readPointer(vtable), {
+ arguments: ['pointer', 'i32'], return: 'i32',
+ });
+ assert.strictEqual(method(object, 2), 42);
+ lib.close();
+ assert.throws(() => method(object, 2), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi function pointers reuse scalar, buffer and string conversions', () => {
+ const { lib } = ffi.dlopen(libraryPath);
+ try {
+ for (const [name, signature, values, expected] of [
+ ['add_i8', { arguments: ['i8', 'i8'], return: 'i8' }, [120, 10], -126],
+ ['add_u64', { arguments: ['u64', 'u64'], return: 'u64' }, [20n, 22n], 42n],
+ ['add_f32', { arguments: ['f32', 'f32'], return: 'f32' }, [1.25, 2.75], 4],
+ ['multiply_f64', { arguments: ['f64', 'f64'], return: 'f64' }, [6, 7], 42],
+ ['noop_void', { arguments: [], return: 'void' }, [], undefined],
+ ['pointer_string_lengths', { arguments: ['string', 'string'], return: 'u64' },
+ ['first', 'second'], 11n],
+ ]) {
+ const fn = lib.toFunction(lib.getSymbol(name), signature);
+ assert.strictEqual(fn(...values), expected);
+ }
+ const identity = lib.toFunction(lib.getSymbol('identity_pointer'), {
+ arguments: ['pointer'], return: 'pointer',
+ });
+ const buffer = Buffer.from([1, 2, 3]);
+ assert.strictEqual(identity(buffer), ffi.getRawPointer(buffer));
+ const view = new Uint8Array(buffer.buffer, buffer.byteOffset + 1, 1);
+ assert.strictEqual(identity(view), ffi.getRawPointer(view));
+ assert.strictEqual(identity(null), 0n);
+ assert.strictEqual(identity(undefined), 0n);
+ const lengths = lib.toFunction(lib.getSymbol('pointer_string_lengths'), {
+ arguments: ['string', 'string'], return: 'u64',
+ });
+ assert.throws(() => lengths('first\0', 'second'), { code: 'ERR_INVALID_ARG_VALUE' });
+ assert.strictEqual(lengths('first', 'second'), 11n);
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi function pointers reject invalid addresses and signatures', () => {
+ const { lib } = ffi.dlopen(libraryPath);
+ try {
+ const signature = { arguments: ['i32', 'i32'], return: 'i32' };
+ const address = lib.getSymbol('add_i32');
+ for (const value of [undefined, null, 1, '1', {},
+ { valueOf: common.mustNotCall() }]) {
+ assert.throws(() => lib.toFunction(value, signature), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ });
+ }
+ const pointerSize = lib.getFunction('get_pointer_size', {
+ arguments: [], return: 'u32',
+ })();
+ for (const value of [0n, -1n, 2n ** BigInt(pointerSize * 8), 2n ** 64n]) {
+ assert.throws(() => lib.toFunction(value, signature), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ });
+ }
+ for (const value of [undefined, null, 1, 'signature', []]) {
+ assert.throws(() => lib.toFunction(address, value), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ });
+ }
+ assert.throws(() => lib.toFunction(address, { return: 'unknown' }), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ });
+ assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42);
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi function pointer signature getters preserve errors and closure', () => {
+ for (const property of ['return', 'arguments']) {
+ const { lib } = ffi.dlopen(libraryPath);
+ try {
+ const address = lib.getSymbol('add_i32');
+ const failure = new Error('pointer signature getter failed');
+ const signature = { arguments: ['i32', 'i32'], return: 'i32' };
+ Object.defineProperty(signature, property, { get() { throw failure; } });
+ assert.throws(() => lib.toFunction(address, signature),
+ (error) => error === failure);
+ Object.defineProperty(signature, property, {
+ get() {
+ lib.close();
+ return property === 'arguments' ? ['i32', 'i32'] : 'i32';
+ },
+ });
+ assert.throws(() => lib.toFunction(address, signature), {
+ code: 'ERR_FFI_LIBRARY_CLOSED',
+ });
+ } finally {
+ lib.close();
+ }
+ }
+});
+
+test('ffi function pointer guards survive optimized JavaScript call sites', () => {
+ const { lib } = ffi.dlopen(libraryPath);
+ const fn = lib.toFunction(lib.getSymbol('add_i32'), {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ function call(first, second) {
+ return fn(first, second);
+ }
+ try {
+ eval('%PrepareFunctionForOptimization(call)');
+ assert.strictEqual(call(20, 22), 42);
+ eval('%OptimizeFunctionOnNextCall(call)');
+ assert.strictEqual(call(20, 22), 42);
+ assert.throws(() => call(1, {}), { code: 'ERR_INVALID_ARG_VALUE' });
+ lib.close();
+ assert.throws(() => call(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+ } finally {
+ lib.close();
+ }
+});
+
test('ffi calls support integer arithmetic and char semantics', () => {
const { lib, functions: symbols } = getLibrary();
try {
diff --git a/test/ffi/test-ffi-permission-drop.js b/test/ffi/test-ffi-permission-drop.js
index 275499503e6..a20ff8e6705 100644
--- a/test/ffi/test-ffi-permission-drop.js
+++ b/test/ffi/test-ffi-permission-drop.js
@@ -29,6 +29,20 @@ function openLibrary() {
}
{
+ const { lib } = ffi.dlopen(libraryPath);
+ const address = lib.getSymbol('add_i32');
+ const signature = { arguments: ['i32', 'i32'], return: 'i32' };
+ assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42);
+ assert.throws(() => lib.toFunction(address, {
+ get arguments() {
+ process.permission.drop('ffi');
+ return ['i32', 'i32'];
+ },
+ return: 'i32',
+ }), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' }));
+ assert.throws(() => lib.toFunction(address, signature),
+ common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' }));
+ lib.close();
process.permission.drop('ffi');
assert.ok(!process.permission.has('ffi'));
assert.throws(() => {
diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js
index 4daa26a9e35..37758ea61af 100644
--- a/test/ffi/test-ffi-weakref-calls.js
+++ b/test/ffi/test-ffi-weakref-calls.js
@@ -8,6 +8,127 @@ const test = require('node:test');
const ffi = require('node:ffi');
const { fixtureSymbols, libraryPath } = require('./ffi-test-common');
+test('ffi function pointer callables retain their library through GC', async (t) => {
+ let library = ffi.dlopen(libraryPath);
+ const ref = new WeakRef(library.lib);
+ const fn = library.lib.toFunction(library.lib.getSymbol('add_i32'), {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ library = null;
+ try {
+ for (let index = 0; index < 5; index++) {
+ await gcUntil('ffi function pointer retains library', () => true, 1);
+ t.assert.notStrictEqual(ref.deref(), undefined);
+ t.assert.strictEqual(fn(20, 22), 42);
+ }
+ } finally {
+ ref.deref()?.close();
+ }
+ t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+});
+
+test('ffi function pointer registry does not retain callables', async (t) => {
+ const { lib } = ffi.dlopen(libraryPath);
+ try {
+ const address = lib.getSymbol('add_i32');
+ const refs = [];
+ for (let index = 0; index < 100; index++) {
+ let fn = lib.toFunction(address, {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ refs.push(new WeakRef(fn));
+ fn = null;
+ }
+ await gcUntil('ffi function pointer callables are collected',
+ () => refs.every((ref) => ref.deref() === undefined));
+ const fn = lib.toFunction(address, {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ t.assert.strictEqual(fn(20, 22), 42);
+ lib.close();
+ t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi pointer callable construction ignores throwing inherited setters', async (t) => {
+ const { lib } = ffi.dlopen(libraryPath);
+ t.after(() => lib.close());
+ const address = lib.getSymbol('add_i32');
+ const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer');
+ let fn;
+ let setterCalls = 0;
+ const failure = new Error('pointer setter failed');
+ try {
+ Object.defineProperty(Function.prototype, 'pointer', {
+ configurable: true,
+ get() { return undefined; },
+ set() {
+ setterCalls++;
+ throw failure;
+ },
+ });
+ fn = lib.toFunction(address, {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ t.assert.strictEqual(setterCalls, 0);
+ t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address);
+ t.assert.strictEqual(fn(20, 22), 42);
+ } finally {
+ if (original === undefined) {
+ delete Function.prototype.pointer;
+ } else {
+ Object.defineProperty(Function.prototype, 'pointer', original);
+ }
+ }
+ try {
+ const ref = new WeakRef(fn);
+ fn = null;
+ await gcUntil('ffi pointer callable with inherited setter is collected',
+ () => ref.deref() === undefined);
+ } finally {
+ lib.close();
+ }
+});
+
+test('ffi pointer callable construction ignores inherited setters that close the library', async (t) => {
+ const { lib } = ffi.dlopen(libraryPath);
+ t.after(() => lib.close());
+ const address = lib.getSymbol('add_i32');
+ const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer');
+ let fn;
+ let setterCalls = 0;
+ try {
+ Object.defineProperty(Function.prototype, 'pointer', {
+ configurable: true,
+ get() { return undefined; },
+ set() {
+ setterCalls++;
+ lib.close();
+ },
+ });
+ fn = lib.toFunction(address, {
+ arguments: ['i32', 'i32'], return: 'i32',
+ });
+ t.assert.strictEqual(setterCalls, 0);
+ t.assert.strictEqual(Object.getOwnPropertyDescriptor(fn, 'pointer').value, address);
+ t.assert.strictEqual(fn(20, 22), 42);
+ } finally {
+ if (original === undefined) {
+ delete Function.prototype.pointer;
+ } else {
+ Object.defineProperty(Function.prototype, 'pointer', original);
+ }
+ }
+ lib.close();
+ t.assert.throws(() => fn(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' });
+ const ref = new WeakRef(fn);
+ fn = null;
+ await gcUntil('ffi pointer callable from closed library is collected',
+ () => ref.deref() === undefined);
+});
+
test('ffi unrefCallback releases callback function', async (t) => {
const { lib, functions: symbols } = ffi.dlopen(libraryPath, fixtureSymbols);
t.after(() => lib.close());
diff --git a/typings/internalBinding/ffi.d.ts b/typings/internalBinding/ffi.d.ts
index bebf2afa24d..8ecadbeeb53 100644
--- a/typings/internalBinding/ffi.d.ts
+++ b/typings/internalBinding/ffi.d.ts
@@ -40,6 +40,7 @@ declare namespace InternalFFIBinding {
close(): void;
getFunction(name: string, signature: FunctionSignature): FFIFunction;
+ toFunction(pointer: bigint, signature: FunctionSignature): FFIFunction;
getFunctions(): Record<string, FFIFunction>;
getFunctions(
definitions: Record<string, FunctionSignature>,