Skip to content

Commit 0fac5bc

Browse files
committed
ffi: support function calls from pointers
Add getFunctionFromPointer() to create callables from native addresses with explicit signatures. Reuse function preparation without populating named symbol caches. Keep the associated library alive and invalidate pointer callables when it is closed. Add coverage for conversions, vtables, permissions and garbage collection, and document pointer lifetime requirements. Assisted-by: GitHub Copilot Signed-off-by: Damiano Mazzella <damianomazzella@gmail.com>
1 parent 463711a commit 0fac5bc

9 files changed

Lines changed: 504 additions & 22 deletions

File tree

‎doc/api/ffi.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,64 @@ console.log(add(20, 22));
433433
console.log(add.pointer);
434434
```
435435

436+
### `library.getFunctionFromPointer(pointer, signature)`
437+
438+
* `pointer` {bigint}
439+
* `signature` {Object}
440+
* Returns: {Function}
441+
442+
Creates a callable JavaScript wrapper for a native function address. The pointer
443+
must be a nonzero, non-negative `bigint` that fits the platform's pointer width.
444+
It can come from a resolved symbol, a native function's return value, or a
445+
function pointer stored in native memory, such as a vtable slot.
446+
447+
```cjs
448+
const { DynamicLibrary, suffix } = require('node:ffi');
449+
450+
const lib = new DynamicLibrary(`./mylib.${suffix}`);
451+
try {
452+
const address = lib.getSymbol('add_i32');
453+
const add = lib.getFunctionFromPointer(address, {
454+
arguments: ['int32', 'int32'],
455+
return: 'int32',
456+
});
457+
console.log(add(20, 22));
458+
console.log(add.pointer === address);
459+
} finally {
460+
lib.close();
461+
}
462+
```
463+
464+
Argument and return conversions follow the same rules as `getFunction()`.
465+
This method supports fixed signatures using the platform's default calling
466+
convention. Explicit calling convention selection and structures passed or
467+
returned by value are not supported.
468+
469+
Each call creates a distinct wrapper. Multiple signatures can be associated
470+
with the same address, but the caller is responsible for their correctness.
471+
These wrappers do not appear in `library.functions`, `library.getFunctions()`,
472+
`library.symbols`, or `library.getSymbols()` unless a symbol was separately
473+
resolved by name. They use the generic libffi call path rather than Fast API
474+
or SharedBuffer invokers.
475+
476+
The wrapper keeps the associated library alive. Calling it after
477+
`library.close()` throws `ERR_FFI_LIBRARY_CLOSED`. FFI permission is required
478+
when creating the wrapper, including after permission has been revoked on an
479+
already-open library.
480+
481+
**The associated library does not establish ownership or validity of the
482+
address.** Node.js cannot determine whether the pointer refers to executable
483+
code, matches the signature, or is still valid. Passing a data pointer, using
484+
an incorrect signature, or calling code that has been unloaded can crash the
485+
process or corrupt memory.
486+
487+
The caller must keep the actual code and any native object used by the call
488+
alive. For example, associating a COM vtable method with `ole32.dll` does not
489+
retain the COM object or the module implementing that method. The caller must
490+
manage its native references and lifetime. A wrapper for a callback pointer
491+
also becomes unsafe if that callback is unregistered, even if the associated
492+
library remains open.
493+
436494
### `library.getFunctions([definitions])`
437495

438496
* `definitions` {Object}

‎lib/ffi.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ ObjectDefineProperty(DynamicLibrary.prototype, 'constructor', {
152152
});
153153

154154
const rawGetFunction = DynamicLibrary.prototype.getFunction;
155+
const rawGetFunctionFromPointer = DynamicLibrary.prototype.getFunctionFromPointer;
155156
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
156157
const rawClose = DynamicLibrary.prototype.close;
157158

@@ -180,6 +181,11 @@ DynamicLibrary.prototype.getFunction = function getFunction(name, signature) {
180181
return wrapFFIFunction(raw, this);
181182
};
182183

184+
DynamicLibrary.prototype.getFunctionFromPointer = function getFunctionFromPointer(pointer, signature) {
185+
const raw = FunctionPrototypeCall(rawGetFunctionFromPointer, this, pointer, signature);
186+
return wrapFFIFunction(raw, this);
187+
};
188+
183189
DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) {
184190
const raw = definitions === undefined ?
185191
FunctionPrototypeCall(rawGetFunctions, this) :

‎src/node_ffi.cc‎

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const {
9090
sizeof(decltype(function_wrappers_)::value_type),
9191
"std::unordered_map<std::string, v8::Global<v8::Function>>");
9292

93+
tracker->TrackFieldWithSize(
94+
"pointer_functions",
95+
pointer_functions_ == nullptr
96+
? 0
97+
: sizeof(std::unordered_set<FFIFunction*>) +
98+
pointer_functions_->size() * sizeof(FFIFunction*),
99+
"std::unordered_set<FFIFunction*>");
100+
93101
// FFIFunctionInfo instances and their sb_backing ArrayBuffers are
94102
// owned by V8 function wrappers and reachable only via weak references,
95103
// so they are deliberately not counted here.
@@ -101,6 +109,14 @@ void DynamicLibrary::Close() {
101109
fn->ptr = nullptr;
102110
}
103111

112+
if (pointer_functions_ != nullptr) {
113+
for (FFIFunction* fn : *pointer_functions_) {
114+
fn->closed = true;
115+
fn->ptr = nullptr;
116+
}
117+
pointer_functions_->clear();
118+
}
119+
104120
// Closing the library invalidates all registered callbacks. Node.js does not
105121
// track or revoke callback pointers that have already been handed to native
106122
// code. If native code calls a callback pointer after `close()` or
@@ -142,7 +158,10 @@ Maybe<void*> DynamicLibrary::ResolveSymbol(Environment* env,
142158
}
143159

144160
Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
145-
Environment* env, const std::string& name, Local<Object> signature) {
161+
Environment* env,
162+
const std::string& name,
163+
Local<Object> signature,
164+
void* ptr) {
146165
std::shared_ptr<FFIFunction> fn;
147166
FunctionSignature parsed;
148167

@@ -151,22 +170,22 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
151170
}
152171
// Look up the cache only after parsing: the signature's getters run user
153172
// code that may close the library, which clears `functions_`.
154-
auto existing = functions_.find(name);
173+
const bool from_pointer = ptr != nullptr;
174+
auto existing = from_pointer ? functions_.end() : functions_.find(name);
155175
auto [return_type, args, return_type_name, arg_type_names] =
156176
std::move(parsed);
157177

158178
bool should_cache_symbol = false;
159179
bool should_cache_function = false;
160180

161181
if (existing == functions_.end()) {
162-
void* ptr;
163-
164-
if (!ResolveSymbol(env, name).To(&ptr)) {
165-
return {};
182+
if (!from_pointer) {
183+
if (!ResolveSymbol(env, name).To(&ptr)) {
184+
return {};
185+
}
186+
should_cache_symbol = symbols_.find(name) == symbols_.end();
166187
}
167188

168-
should_cache_symbol = symbols_.find(name) == symbols_.end();
169-
170189
fn = std::make_shared<FFIFunction>();
171190
fn->ptr = ptr;
172191
fn->args = std::move(args);
@@ -205,7 +224,7 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
205224
}
206225
#endif
207226

208-
should_cache_function = true;
227+
should_cache_function = !from_pointer;
209228
} else {
210229
fn = existing->second;
211230

@@ -267,7 +286,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
267286
const std::string& name,
268287
const std::shared_ptr<FFIFunction>& fn) {
269288
Isolate* isolate = env->isolate();
270-
Local<Context> context = env->context();
271289

272290
// Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and
273291
// on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already
@@ -282,17 +300,38 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
282300
function_wrappers_.erase(cached);
283301
}
284302

303+
Local<Function> ret;
304+
if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) {
305+
return {};
306+
}
307+
function_wrappers_.emplace(name, Global<Function>(isolate, ret))
308+
.first->second.SetWeak();
309+
return ret;
310+
}
311+
312+
MaybeLocal<Function> DynamicLibrary::BuildFunction(
313+
Environment* env,
314+
const std::string& name,
315+
const std::shared_ptr<FFIFunction>& fn,
316+
bool optimize) {
317+
Isolate* isolate = env->isolate();
318+
Local<Context> context = env->context();
285319
auto info = FFIFunctionInfo::Create(env, fn, this);
320+
if (!info) {
321+
return {};
322+
}
286323

287324
DCHECK_EQ(fn->args.size(), fn->arg_type_names.size());
288325

289326
// Try the generated Fast API path first. If metadata creation rejects the
290327
// signature, fall back to SharedBuffer for supported scalar shapes, then to
291328
// the generic libffi invoker.
292-
std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
293-
info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate);
329+
if (optimize) {
330+
std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
331+
info->fast_metadata = CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate);
332+
}
294333
bool use_fast_api = info->fast_metadata != nullptr;
295-
bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn);
334+
bool use_sb = optimize && !use_fast_api && IsSBEligibleSignature(*fn);
296335
bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn);
297336
// Signatures that need JS-side conversion or validation use a wrapper, as
298337
// do all fast signatures on platforms without a native library guard.
@@ -485,14 +524,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
485524
}
486525
}
487526

488-
// A strong handle would root the callable, which holds the library object
489-
// through FFIFunctionInfo, so neither could ever be collected. Weaken the
490-
// stored handle instead, so the cache lasts exactly as long as user code
491-
// keeps a reference. SetWeak() runs after the move into the map because
492-
// moving a handle relocates the underlying slot.
493-
function_wrappers_.emplace(name, Global<Function>(isolate, ret))
494-
.first->second.SetWeak();
495-
496527
return ret;
497528
}
498529

@@ -607,6 +638,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
607638
std::vector<uint64_t> values(expected_args, 0);
608639
std::vector<void*> ffi_args(expected_args, nullptr);
609640
std::vector<std::string> strings;
641+
strings.reserve(expected_args);
610642

611643
for (unsigned int i = 0; i < expected_args; i++) {
612644
FFIArgumentCategory res;
@@ -861,6 +893,68 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo<Value>& args) {
861893
args.GetReturnValue().Set(ret);
862894
}
863895

896+
void DynamicLibrary::GetFunctionFromPointer(
897+
const FunctionCallbackInfo<Value>& args) {
898+
Environment* env = Environment::GetCurrent(args);
899+
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
900+
901+
if (args.Length() < 1 || !args[0]->IsBigInt()) {
902+
THROW_ERR_INVALID_ARG_TYPE(env, "Function pointer must be a bigint");
903+
return;
904+
}
905+
bool lossless;
906+
uint64_t address = args[0].As<BigInt>()->Uint64Value(&lossless);
907+
if (!lossless || address == 0 ||
908+
address > static_cast<uint64_t>(
909+
std::numeric_limits<uintptr_t>::max())) {
910+
THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer");
911+
return;
912+
}
913+
if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
914+
THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
915+
return;
916+
}
917+
918+
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
919+
if (lib->is_closed()) {
920+
THROW_ERR_FFI_LIBRARY_CLOSED(env);
921+
return;
922+
}
923+
Local<Object> signature = args[1].As<Object>();
924+
PreparedFunction prepared;
925+
void* ptr = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
926+
if (!lib->PrepareFunction(env, "<pointer>", signature, ptr).To(&prepared)) {
927+
return;
928+
}
929+
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
930+
if (lib->is_closed()) {
931+
THROW_ERR_FFI_LIBRARY_CLOSED(env);
932+
return;
933+
}
934+
935+
auto fn = std::move(prepared.fn);
936+
if (lib->pointer_functions_ == nullptr) {
937+
lib->pointer_functions_ =
938+
std::make_shared<std::unordered_set<FFIFunction*>>();
939+
}
940+
fn->pointer_registry = lib->pointer_functions_;
941+
fn->closed = true;
942+
lib->pointer_functions_->insert(fn.get());
943+
Local<Function> ret;
944+
if (!lib->BuildFunction(env, "<pointer>", fn, false).ToLocal(&ret)) {
945+
lib->pointer_functions_->erase(fn.get());
946+
fn->ptr = nullptr;
947+
return;
948+
}
949+
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
950+
if (lib->is_closed()) {
951+
THROW_ERR_FFI_LIBRARY_CLOSED(env);
952+
return;
953+
}
954+
fn->closed = false;
955+
args.GetReturnValue().Set(ret);
956+
}
957+
864958
void DynamicLibrary::GetFunctions(const FunctionCallbackInfo<Value>& args) {
865959
Environment* env = Environment::GetCurrent(args);
866960
Isolate* isolate = env->isolate();
@@ -1324,6 +1418,10 @@ Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
13241418
SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close);
13251419
SetProtoDispose(isolate, tmpl, DynamicLibrary::Close);
13261420
SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction);
1421+
SetProtoMethod(isolate,
1422+
tmpl,
1423+
"getFunctionFromPointer",
1424+
DynamicLibrary::GetFunctionFromPointer);
13271425
SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions);
13281426
SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol);
13291427
SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols);

‎src/node_ffi.h‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <string>
1313
#include <thread>
1414
#include <unordered_map>
15+
#include <unordered_set>
1516
#include <vector>
1617

1718
// libffi only accelerates reusable call plans on x86-64 System V. Other
@@ -29,12 +30,18 @@ struct FFIFunction;
2930

3031
struct FFIFunction {
3132
FFIFunction() = default;
33+
~FFIFunction() {
34+
if (auto registry = pointer_registry.lock()) {
35+
registry->erase(this);
36+
}
37+
}
3238
FFIFunction(const FFIFunction&) = delete;
3339
FFIFunction& operator=(const FFIFunction&) = delete;
3440
FFIFunction(FFIFunction&&) = delete;
3541
FFIFunction& operator=(FFIFunction&&) = delete;
3642

3743
bool closed = false;
44+
std::weak_ptr<std::unordered_set<FFIFunction*>> pointer_registry;
3845

3946
void* ptr = nullptr;
4047
ffi_cif cif = {};
@@ -133,6 +140,8 @@ class DynamicLibrary : public BaseObject {
133140

134141
static void GetPath(const v8::FunctionCallbackInfo<v8::Value>& args);
135142
static void GetFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
143+
static void GetFunctionFromPointer(
144+
const v8::FunctionCallbackInfo<v8::Value>& args);
136145
static void GetFunctions(const v8::FunctionCallbackInfo<v8::Value>& args);
137146
static void GetSymbol(const v8::FunctionCallbackInfo<v8::Value>& args);
138147
static void GetSymbols(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -156,11 +165,17 @@ class DynamicLibrary : public BaseObject {
156165
};
157166
v8::Maybe<PreparedFunction> PrepareFunction(Environment* env,
158167
const std::string& name,
159-
v8::Local<v8::Object> signature);
168+
v8::Local<v8::Object> signature,
169+
void* ptr = nullptr);
160170
v8::MaybeLocal<v8::Function> CreateFunction(
161171
Environment* env,
162172
const std::string& name,
163173
const std::shared_ptr<FFIFunction>& fn);
174+
v8::MaybeLocal<v8::Function> BuildFunction(
175+
Environment* env,
176+
const std::string& name,
177+
const std::shared_ptr<FFIFunction>& fn,
178+
bool optimize);
164179
static void CleanupFunctionInfo(
165180
const v8::WeakCallbackInfo<FFIFunctionInfo>& data);
166181
bool is_closed() const;
@@ -175,6 +190,7 @@ class DynamicLibrary : public BaseObject {
175190
// which keeps the map from rooting the library through the wrapper's
176191
// FFIFunctionInfo.
177192
std::unordered_map<std::string, v8::Global<v8::Function>> function_wrappers_;
193+
std::shared_ptr<std::unordered_set<FFIFunction*>> pointer_functions_;
178194
std::unordered_map<void*, std::unique_ptr<FFICallback>> callbacks_;
179195
};
180196

0 commit comments

Comments
 (0)