From 0fac5bc6843324bbcea98f04947579c19204fc4f 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 | 140 +++++++++++++--- 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, 504 insertions(+), 22 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 342acfbec503..a389df1fc9b6 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -433,6 +433,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 5cd7c4b354ab..005fab89d3cc 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 4ef22a80a428..323178cf7aaa 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -90,6 +90,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. @@ -101,6 +109,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 @@ -142,7 +158,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; @@ -151,7 +170,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); @@ -159,14 +179,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); @@ -205,7 +224,7 @@ Maybe DynamicLibrary::PrepareFunction( } #endif - should_cache_function = true; + should_cache_function = !from_pointer; } else { fn = existing->second; @@ -267,7 +286,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 @@ -282,17 +300,38 @@ 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); - info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate); + if (optimize) { + std::shared_ptr fast_fn = CloneWithRawPointerArgNames(fn); + info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate); + } 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. @@ -485,14 +524,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; } @@ -607,6 +638,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; @@ -861,6 +893,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(); @@ -1324,6 +1418,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 7758380138fd..4b5cfb40f8cb 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 71c54a49fefe..6c5a81a582b7 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 bc3be0072db1..1f07951c7636 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 275499503e6b..2f8c1102e6b3 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 4daa26a9e355..5f7670900f00 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 0294d1987b03..cc025bd44b14 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 52a5f52b2cae37bfe15b50daf895b7c4917a6589 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 323178cf7aaa..17109fefbfc0 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -905,8 +905,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; } @@ -1419,9 +1418,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 6c5a81a582b7..34d6406a1f4b 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 f464a3117e34e8c15e94b599c8a852f62828ebaf 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 5f7670900f00..4777afe8743f 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 3da31d59b94f6f873b430fedbda6f78ca41b0d55 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 a389df1fc9b6..cf24422347e1 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -433,7 +433,7 @@ console.log(add(20, 22)); console.log(add.pointer); ``` -### `library.getFunctionFromPointer(pointer, signature)` +### `library.toFunction(pointer, signature)` * `pointer` {bigint} * `signature` {Object} @@ -450,7 +450,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 005fab89d3cc..246b714de0ae 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 17109fefbfc0..768d9bc46db4 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -893,8 +893,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, ""); @@ -1417,10 +1416,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 4b5cfb40f8cb..d46abba5516a 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 1f07951c7636..4a7ef2feb11f 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 2f8c1102e6b3..a20ff8e67053 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 4777afe8743f..b16350849265 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 cc025bd44b14..b0f51b8f2c9c 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,