From c3af01b588efbb4c92484d48a09934def94fc853 Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 09:51:37 +0200 Subject: [PATCH 1/4] 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 --- doc/api/ffi.md | 58 +++++++ lib/ffi.js | 6 + src/node_ffi.cc | 144 +++++++++++++--- src/node_ffi.h | 18 +- test/ffi/fixture_library/ffi_test_library.c | 37 +++++ test/ffi/test-ffi-calls.js | 174 ++++++++++++++++++++ test/ffi/test-ffi-permission-drop.js | 14 ++ test/ffi/test-ffi-weakref-calls.js | 78 +++++++++ typings/internalBinding/ffi.d.ts | 1 + 9 files changed, 506 insertions(+), 24 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index d3c01f6e052..8e9fe8528e9 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.getFunctionFromPointer(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.getFunctionFromPointer(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..109b836fd0f 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -152,6 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { }); const rawGetFunction = DynamicLibrary.prototype.getFunction; +const rawGetFunctionFromPointer = DynamicLibrary.prototype.getFunctionFromPointer; 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.getFunctionFromPointer = function getFunctionFromPointer(pointer, signature) { + const raw = FunctionPrototypeCall(rawGetFunctionFromPointer, 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 7581da4e7c6..46de52bb651 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -91,6 +91,14 @@ void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const { sizeof(decltype(function_wrappers_)::value_type), "std::unordered_map>"); + tracker->TrackFieldWithSize( + "pointer_functions", + pointer_functions_ == nullptr + ? 0 + : sizeof(std::unordered_set) + + pointer_functions_->size() * sizeof(FFIFunction*), + "std::unordered_set"); + // 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. @@ -102,6 +110,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 @@ -143,7 +159,10 @@ Maybe DynamicLibrary::ResolveSymbol(Environment* env, } Maybe DynamicLibrary::PrepareFunction( - Environment* env, const std::string& name, Local signature) { + Environment* env, + const std::string& name, + Local signature, + void* ptr) { std::shared_ptr fn; FunctionSignature parsed; @@ -152,7 +171,8 @@ Maybe 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); @@ -160,14 +180,13 @@ Maybe 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(); fn->ptr = ptr; fn->args = std::move(args); @@ -206,7 +225,7 @@ Maybe DynamicLibrary::PrepareFunction( } #endif - should_cache_function = true; + should_cache_function = !from_pointer; } else { fn = existing->second; @@ -268,7 +287,6 @@ MaybeLocal DynamicLibrary::CreateFunction( const std::string& name, const std::shared_ptr& fn) { Isolate* isolate = env->isolate(); - Local 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 @@ -283,20 +301,41 @@ MaybeLocal DynamicLibrary::CreateFunction( function_wrappers_.erase(cached); } + Local ret; + if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) { + return {}; + } + function_wrappers_.emplace(name, Global(isolate, ret)) + .first->second.SetWeak(); + return ret; +} + +MaybeLocal DynamicLibrary::BuildFunction( + Environment* env, + const std::string& name, + const std::shared_ptr& fn, + bool optimize) { + Isolate* isolate = env->isolate(); + Local 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 fast_fn = CloneWithRawPointerArgNames(fn); - if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate) - .MoveTo(&info->fast_metadata)) { - return {}; + if (optimize) { + std::shared_ptr 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. @@ -491,14 +530,6 @@ MaybeLocal 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(isolate, ret)) - .first->second.SetWeak(); - return ret; } @@ -613,6 +644,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo& args) { std::vector values(expected_args, 0); std::vector ffi_args(expected_args, nullptr); std::vector strings; + strings.reserve(expected_args); for (unsigned int i = 0; i < expected_args; i++) { FFIArgumentCategory res; @@ -867,6 +899,68 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } +void DynamicLibrary::GetFunctionFromPointer( + const FunctionCallbackInfo& 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()->Uint64Value(&lossless); + if (!lossless || address == 0 || + address > static_cast( + std::numeric_limits::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(args.This()); + if (lib->is_closed()) { + THROW_ERR_FFI_LIBRARY_CLOSED(env); + return; + } + Local signature = args[1].As(); + PreparedFunction prepared; + void* ptr = reinterpret_cast(static_cast(address)); + if (!lib->PrepareFunction(env, "", 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>(); + } + fn->pointer_registry = lib->pointer_functions_; + fn->closed = true; + lib->pointer_functions_->insert(fn.get()); + Local ret; + if (!lib->BuildFunction(env, "", fn, false).ToLocal(&ret)) { + lib->pointer_functions_->erase(fn.get()); + fn->ptr = nullptr; + return; + } + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + if (lib->is_closed()) { + THROW_ERR_FFI_LIBRARY_CLOSED(env); + return; + } + fn->closed = false; + args.GetReturnValue().Set(ret); +} + void DynamicLibrary::GetFunctions(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); @@ -1339,6 +1433,10 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); + SetProtoMethod(isolate, + tmpl, + "getFunctionFromPointer", + DynamicLibrary::GetFunctionFromPointer); 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 7758380138f..4b5cfb40f8c 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -12,6 +12,7 @@ #include #include #include +#include #include // 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> pointer_registry; void* ptr = nullptr; ffi_cif cif = {}; @@ -133,6 +140,8 @@ class DynamicLibrary : public BaseObject { static void GetPath(const v8::FunctionCallbackInfo& args); static void GetFunction(const v8::FunctionCallbackInfo& args); + static void GetFunctionFromPointer( + const v8::FunctionCallbackInfo& args); static void GetFunctions(const v8::FunctionCallbackInfo& args); static void GetSymbol(const v8::FunctionCallbackInfo& args); static void GetSymbols(const v8::FunctionCallbackInfo& args); @@ -156,11 +165,17 @@ class DynamicLibrary : public BaseObject { }; v8::Maybe PrepareFunction(Environment* env, const std::string& name, - v8::Local signature); + v8::Local signature, + void* ptr = nullptr); v8::MaybeLocal CreateFunction( Environment* env, const std::string& name, const std::shared_ptr& fn); + v8::MaybeLocal BuildFunction( + Environment* env, + const std::string& name, + const std::shared_ptr& fn, + bool optimize); static void CleanupFunctionInfo( const v8::WeakCallbackInfo& data); bool is_closed() const; @@ -175,6 +190,7 @@ class DynamicLibrary : public BaseObject { // which keeps the map from rooting the library through the wrapper's // FFIFunctionInfo. std::unordered_map> function_wrappers_; + std::shared_ptr> pointer_functions_; std::unordered_map> callbacks_; }; diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index 3e24b7cd3d3..25682c3b19e 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 0dbddb244b7..169b6f5c7a1 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.getFunctionFromPointer(address, signature); + const second = lib.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(lib.getSymbol(name), signature); + assert.strictEqual(fn(...values), expected); + } + const identity = lib.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(value, signature), { + code: 'ERR_INVALID_ARG_VALUE', + }); + } + for (const value of [undefined, null, 1, 'signature', []]) { + assert.throws(() => lib.getFunctionFromPointer(address, value), { + code: 'ERR_INVALID_ARG_TYPE', + }); + } + assert.throws(() => lib.getFunctionFromPointer(address, { return: 'unknown' }), { + code: 'ERR_INVALID_ARG_VALUE', + }); + assert.strictEqual(lib.getFunctionFromPointer(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.getFunctionFromPointer(address, signature), + (error) => error === failure); + Object.defineProperty(signature, property, { + get() { + lib.close(); + return property === 'arguments' ? ['i32', 'i32'] : 'i32'; + }, + }); + assert.throws(() => lib.getFunctionFromPointer(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.getFunctionFromPointer(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..2f8c1102e6b 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.getFunctionFromPointer(address, signature)(20, 22), 42); + assert.throws(() => lib.getFunctionFromPointer(address, { + get arguments() { + process.permission.drop('ffi'); + return ['i32', 'i32']; + }, + return: 'i32', + }), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); + assert.throws(() => lib.getFunctionFromPointer(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..5f7670900f0 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -8,6 +8,84 @@ 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.getFunctionFromPointer(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.getFunctionFromPointer(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.getFunctionFromPointer(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 failed pointer callable construction releases its registration', async (t) => { + const { lib } = ffi.dlopen(libraryPath); + const original = Object.getOwnPropertyDescriptor(Function.prototype, 'pointer'); + let escaped; + const failure = new Error('pointer setter failed'); + try { + Object.defineProperty(Function.prototype, 'pointer', { + configurable: true, + get() { return undefined; }, + set() { + escaped = this; + throw failure; + }, + }); + t.assert.throws(() => lib.getFunctionFromPointer(lib.getSymbol('add_i32'), { + arguments: ['i32', 'i32'], return: 'i32', + }), (error) => error === failure); + } finally { + if (original === undefined) { + delete Function.prototype.pointer; + } else { + Object.defineProperty(Function.prototype, 'pointer', original); + } + } + try { + t.assert.throws(() => escaped(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + const ref = new WeakRef(escaped); + escaped = null; + await gcUntil('ffi failed pointer callable is collected', () => ref.deref() === undefined); + } finally { + lib.close(); + } +}); + 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 0294d1987b0..cc025bd44b1 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; + getFunctionFromPointer(pointer: bigint, signature: FunctionSignature): FFIFunction; getFunctions(): Record; getFunctions( definitions: Record, From 16f2ed4fe0456704820af1febf0ab47799256546 Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 11:07:09 +0200 Subject: [PATCH 2/4] ffi: fix formatting for pointer callables Apply clang-format corrections to pointer callable bindings and the FFI test fixture to satisfy the format-cpp CI check. Signed-off-by: Damiano Mazzella --- src/node_ffi.cc | 9 ++++----- test/ffi/fixture_library/ffi_test_library.c | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 46de52bb651..547e4e2fe26 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -911,8 +911,7 @@ void DynamicLibrary::GetFunctionFromPointer( bool lossless; uint64_t address = args[0].As()->Uint64Value(&lossless); if (!lossless || address == 0 || - address > static_cast( - std::numeric_limits::max())) { + address > static_cast(std::numeric_limits::max())) { THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer"); return; } @@ -1434,9 +1433,9 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); SetProtoMethod(isolate, - tmpl, - "getFunctionFromPointer", - DynamicLibrary::GetFunctionFromPointer); + tmpl, + "getFunctionFromPointer", + DynamicLibrary::GetFunctionFromPointer); SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions); SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol); SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols); diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index 25682c3b19e..d04b55a2078 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -44,7 +44,7 @@ FFI_EXPORT PointerTestObject* get_pointer_test_object(void) { } FFI_EXPORT uint64_t pointer_string_lengths(const char* first, - const char* second) { + const char* second) { return strlen(first) + strlen(second); } From f8c3373a0a542c4ca77572b87f34a043712e48c1 Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 13:47:55 +0200 Subject: [PATCH 3/4] test: cover library closure during pointer callable creation Verify that closing the library in the pointer setter rejects callable creation, leaves the escaped callable unusable, and allows its collection. Assisted-by: GitHub Copilot Signed-off-by: Damiano Mazzella --- test/ffi/test-ffi-weakref-calls.js | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index 5f7670900f0..4777afe8743 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -86,6 +86,38 @@ test('ffi failed pointer callable construction releases its registration', async } }); +test('ffi pointer callable construction detects library closure', 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 escaped; + try { + Object.defineProperty(Function.prototype, 'pointer', { + configurable: true, + get() { return undefined; }, + set() { + escaped = this; + lib.close(); + }, + }); + t.assert.throws(() => lib.getFunctionFromPointer(address, { + arguments: ['i32', 'i32'], return: 'i32', + }), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + } finally { + if (original === undefined) { + delete Function.prototype.pointer; + } else { + Object.defineProperty(Function.prototype, 'pointer', original); + } + } + t.assert.throws(() => escaped(20, 22), { code: 'ERR_FFI_LIBRARY_CLOSED' }); + const ref = new WeakRef(escaped); + escaped = 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()); From 470a278a3c3cbe4fe412ebfee26e147950892ccf Mon Sep 17 00:00:00 2001 From: Damiano Mazzella Date: Fri, 2 Oct 2026 16:22:56 +0200 Subject: [PATCH 4/4] ffi: rename getFunctionFromPointer to toFunction Signed-off-by: Damiano Mazzella --- doc/api/ffi.md | 4 ++-- lib/ffi.js | 6 ++--- src/node_ffi.cc | 8 ++----- src/node_ffi.h | 3 +-- test/ffi/test-ffi-calls.js | 36 ++++++++++++++-------------- test/ffi/test-ffi-permission-drop.js | 6 ++--- test/ffi/test-ffi-weakref-calls.js | 10 ++++---- typings/internalBinding/ffi.d.ts | 2 +- 8 files changed, 35 insertions(+), 40 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 8e9fe8528e9..3520cdaf8d6 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -440,7 +440,7 @@ console.log(add(20, 22)); console.log(add.pointer); ``` -### `library.getFunctionFromPointer(pointer, signature)` +### `library.toFunction(pointer, signature)` * `pointer` {bigint} * `signature` {Object} @@ -457,7 +457,7 @@ const { DynamicLibrary, suffix } = require('node:ffi'); const lib = new DynamicLibrary(`./mylib.${suffix}`); try { const address = lib.getSymbol('add_i32'); - const add = lib.getFunctionFromPointer(address, { + const add = lib.toFunction(address, { arguments: ['int32', 'int32'], return: 'int32', }); diff --git a/lib/ffi.js b/lib/ffi.js index 109b836fd0f..f3295ad0ee7 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -152,7 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', { }); const rawGetFunction = DynamicLibrary.prototype.getFunction; -const rawGetFunctionFromPointer = DynamicLibrary.prototype.getFunctionFromPointer; +const rawToFunction = DynamicLibrary.prototype.toFunction; const rawGetFunctions = DynamicLibrary.prototype.getFunctions; const rawClose = DynamicLibrary.prototype.close; @@ -181,8 +181,8 @@ DynamicLibrary.prototype.getFunction = function getFunction(name, signature) { return wrapFFIFunction(raw, this); }; -DynamicLibrary.prototype.getFunctionFromPointer = function getFunctionFromPointer(pointer, signature) { - const raw = FunctionPrototypeCall(rawGetFunctionFromPointer, this, pointer, signature); +DynamicLibrary.prototype.toFunction = function toFunction(pointer, signature) { + const raw = FunctionPrototypeCall(rawToFunction, this, pointer, signature); return wrapFFIFunction(raw, this); }; diff --git a/src/node_ffi.cc b/src/node_ffi.cc index 547e4e2fe26..c6f78f3d262 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -899,8 +899,7 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(ret); } -void DynamicLibrary::GetFunctionFromPointer( - const FunctionCallbackInfo& args) { +void DynamicLibrary::ToFunction(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); @@ -1432,10 +1431,7 @@ Local DynamicLibrary::GetConstructorTemplate( SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close); SetProtoDispose(isolate, tmpl, DynamicLibrary::Close); SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction); - SetProtoMethod(isolate, - tmpl, - "getFunctionFromPointer", - DynamicLibrary::GetFunctionFromPointer); + 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 4b5cfb40f8c..d46abba5516 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -140,8 +140,7 @@ class DynamicLibrary : public BaseObject { static void GetPath(const v8::FunctionCallbackInfo& args); static void GetFunction(const v8::FunctionCallbackInfo& args); - static void GetFunctionFromPointer( - const v8::FunctionCallbackInfo& args); + static void ToFunction(const v8::FunctionCallbackInfo& args); static void GetFunctions(const v8::FunctionCallbackInfo& args); static void GetSymbol(const v8::FunctionCallbackInfo& args); static void GetSymbols(const v8::FunctionCallbackInfo& args); diff --git a/test/ffi/test-ffi-calls.js b/test/ffi/test-ffi-calls.js index 169b6f5c7a1..fa2d02ef8b2 100644 --- a/test/ffi/test-ffi-calls.js +++ b/test/ffi/test-ffi-calls.js @@ -16,8 +16,8 @@ test('ffi function pointers are independent callables with library guards', () = try { const signature = { arguments: ['i32', 'i32'], return: 'i32' }; const address = lib.getSymbol('add_i32'); - const first = lib.getFunctionFromPointer(address, signature); - const second = lib.getFunctionFromPointer(address, signature); + 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); @@ -26,11 +26,11 @@ test('ffi function pointers are independent callables with library guards', () = assert.deepStrictEqual(Object.keys(lib.getFunctions()), []); const named = lib.getFunction('add_i32', signature); assert.notStrictEqual(named, first); - assert.strictEqual(lib.getFunctionFromPointer(named.pointer, signature)(1, 2), 3); + 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.getFunctionFromPointer(address, { + const different = lib.toFunction(address, { arguments: ['f64'], return: 'f64', }); assert.notStrictEqual(different, first); @@ -38,7 +38,7 @@ test('ffi function pointers are independent callables with library guards', () = for (const fn of [first, second, different]) { assert.throws(() => fn(), { code: 'ERR_FFI_LIBRARY_CLOSED' }); } - assert.throws(() => lib.getFunctionFromPointer(address, signature), { + assert.throws(() => lib.toFunction(address, signature), { code: 'ERR_FFI_LIBRARY_CLOSED', }); } finally { @@ -53,7 +53,7 @@ test('ffi calls function pointers returned by C and stored in vtables', () => { get_pointer_size: { arguments: [], return: 'u32' }, }); try { - const increment = lib.getFunctionFromPointer(functions.get_function_pointer(), { + const increment = lib.toFunction(functions.get_function_pointer(), { arguments: ['i32'], return: 'i32', }); assert.strictEqual(increment(41), 42); @@ -61,7 +61,7 @@ test('ffi calls function pointers returned by C and stored in vtables', () => { (address) => ffi.getUint64(address) : (address) => BigInt(ffi.getUint32(address)); const object = functions.get_pointer_test_object(); const vtable = readPointer(object); - const method = lib.getFunctionFromPointer(readPointer(vtable), { + const method = lib.toFunction(readPointer(vtable), { arguments: ['pointer', 'i32'], return: 'i32', }); assert.strictEqual(method(object, 2), 42); @@ -84,10 +84,10 @@ test('ffi function pointers reuse scalar, buffer and string conversions', () => ['pointer_string_lengths', { arguments: ['string', 'string'], return: 'u64' }, ['first', 'second'], 11n], ]) { - const fn = lib.getFunctionFromPointer(lib.getSymbol(name), signature); + const fn = lib.toFunction(lib.getSymbol(name), signature); assert.strictEqual(fn(...values), expected); } - const identity = lib.getFunctionFromPointer(lib.getSymbol('identity_pointer'), { + const identity = lib.toFunction(lib.getSymbol('identity_pointer'), { arguments: ['pointer'], return: 'pointer', }); const buffer = Buffer.from([1, 2, 3]); @@ -96,7 +96,7 @@ test('ffi function pointers reuse scalar, buffer and string conversions', () => assert.strictEqual(identity(view), ffi.getRawPointer(view)); assert.strictEqual(identity(null), 0n); assert.strictEqual(identity(undefined), 0n); - const lengths = lib.getFunctionFromPointer(lib.getSymbol('pointer_string_lengths'), { + 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' }); @@ -113,7 +113,7 @@ test('ffi function pointers reject invalid addresses and signatures', () => { const address = lib.getSymbol('add_i32'); for (const value of [undefined, null, 1, '1', {}, { valueOf: common.mustNotCall() }]) { - assert.throws(() => lib.getFunctionFromPointer(value, signature), { + assert.throws(() => lib.toFunction(value, signature), { code: 'ERR_INVALID_ARG_TYPE', }); } @@ -121,19 +121,19 @@ test('ffi function pointers reject invalid addresses and signatures', () => { arguments: [], return: 'u32', })(); for (const value of [0n, -1n, 2n ** BigInt(pointerSize * 8), 2n ** 64n]) { - assert.throws(() => lib.getFunctionFromPointer(value, signature), { + assert.throws(() => lib.toFunction(value, signature), { code: 'ERR_INVALID_ARG_VALUE', }); } for (const value of [undefined, null, 1, 'signature', []]) { - assert.throws(() => lib.getFunctionFromPointer(address, value), { + assert.throws(() => lib.toFunction(address, value), { code: 'ERR_INVALID_ARG_TYPE', }); } - assert.throws(() => lib.getFunctionFromPointer(address, { return: 'unknown' }), { + assert.throws(() => lib.toFunction(address, { return: 'unknown' }), { code: 'ERR_INVALID_ARG_VALUE', }); - assert.strictEqual(lib.getFunctionFromPointer(address, signature)(20, 22), 42); + assert.strictEqual(lib.toFunction(address, signature)(20, 22), 42); } finally { lib.close(); } @@ -147,7 +147,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => 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.getFunctionFromPointer(address, signature), + assert.throws(() => lib.toFunction(address, signature), (error) => error === failure); Object.defineProperty(signature, property, { get() { @@ -155,7 +155,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => return property === 'arguments' ? ['i32', 'i32'] : 'i32'; }, }); - assert.throws(() => lib.getFunctionFromPointer(address, signature), { + assert.throws(() => lib.toFunction(address, signature), { code: 'ERR_FFI_LIBRARY_CLOSED', }); } finally { @@ -166,7 +166,7 @@ test('ffi function pointer signature getters preserve errors and closure', () => test('ffi function pointer guards survive optimized JavaScript call sites', () => { const { lib } = ffi.dlopen(libraryPath); - const fn = lib.getFunctionFromPointer(lib.getSymbol('add_i32'), { + const fn = lib.toFunction(lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }); function call(first, second) { diff --git a/test/ffi/test-ffi-permission-drop.js b/test/ffi/test-ffi-permission-drop.js index 2f8c1102e6b..a20ff8e6705 100644 --- a/test/ffi/test-ffi-permission-drop.js +++ b/test/ffi/test-ffi-permission-drop.js @@ -32,15 +32,15 @@ function openLibrary() { const { lib } = ffi.dlopen(libraryPath); const address = lib.getSymbol('add_i32'); const signature = { arguments: ['i32', 'i32'], return: 'i32' }; - assert.strictEqual(lib.getFunctionFromPointer(address, signature)(20, 22), 42); - assert.throws(() => lib.getFunctionFromPointer(address, { + 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.getFunctionFromPointer(address, signature), + assert.throws(() => lib.toFunction(address, signature), common.expectsError({ code: 'ERR_ACCESS_DENIED', permission: 'FFI' })); lib.close(); process.permission.drop('ffi'); diff --git a/test/ffi/test-ffi-weakref-calls.js b/test/ffi/test-ffi-weakref-calls.js index 4777afe8743..b1635084926 100644 --- a/test/ffi/test-ffi-weakref-calls.js +++ b/test/ffi/test-ffi-weakref-calls.js @@ -11,7 +11,7 @@ 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.getFunctionFromPointer(library.lib.getSymbol('add_i32'), { + const fn = library.lib.toFunction(library.lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }); library = null; @@ -33,7 +33,7 @@ test('ffi function pointer registry does not retain callables', async (t) => { const address = lib.getSymbol('add_i32'); const refs = []; for (let index = 0; index < 100; index++) { - let fn = lib.getFunctionFromPointer(address, { + let fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }); refs.push(new WeakRef(fn)); @@ -41,7 +41,7 @@ test('ffi function pointer registry does not retain callables', async (t) => { } await gcUntil('ffi function pointer callables are collected', () => refs.every((ref) => ref.deref() === undefined)); - const fn = lib.getFunctionFromPointer(address, { + const fn = lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }); t.assert.strictEqual(fn(20, 22), 42); @@ -66,7 +66,7 @@ test('ffi failed pointer callable construction releases its registration', async throw failure; }, }); - t.assert.throws(() => lib.getFunctionFromPointer(lib.getSymbol('add_i32'), { + t.assert.throws(() => lib.toFunction(lib.getSymbol('add_i32'), { arguments: ['i32', 'i32'], return: 'i32', }), (error) => error === failure); } finally { @@ -101,7 +101,7 @@ test('ffi pointer callable construction detects library closure', async (t) => { lib.close(); }, }); - t.assert.throws(() => lib.getFunctionFromPointer(address, { + t.assert.throws(() => lib.toFunction(address, { arguments: ['i32', 'i32'], return: 'i32', }), { code: 'ERR_FFI_LIBRARY_CLOSED' }); } finally { diff --git a/typings/internalBinding/ffi.d.ts b/typings/internalBinding/ffi.d.ts index cc025bd44b1..b0f51b8f2c9 100644 --- a/typings/internalBinding/ffi.d.ts +++ b/typings/internalBinding/ffi.d.ts @@ -40,7 +40,7 @@ declare namespace InternalFFIBinding { close(): void; getFunction(name: string, signature: FunctionSignature): FFIFunction; - getFunctionFromPointer(pointer: bigint, signature: FunctionSignature): FFIFunction; + toFunction(pointer: bigint, signature: FunctionSignature): FFIFunction; getFunctions(): Record; getFunctions( definitions: Record,