Skip to content

Commit e64878b

Browse files
dmazzellaaduh95
authored andcommitted
ffi: support function calls from pointers
Add getFunctionFromPointer() to create callables from native addresses with explicit signatures. Reuse function preparation without populating named symbol caches. Keep the associated library alive and invalidate pointer callables when it is closed. Add coverage for conversions, vtables, permissions and garbage collection, and document pointer lifetime requirements. Assisted-by: GitHub Copilot Signed-off-by: Damiano Mazzella <damianomazzella@gmail.com> PR-URL: #66458 Fixes: #66272 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent ce1da53 commit e64878b

9 files changed

Lines changed: 535 additions & 24 deletions

File tree

‎doc/api/ffi.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,64 @@ console.log(add(20, 22));
440440
console.log(add.pointer);
441441
```
442442

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

445503
* `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 rawToFunction = DynamicLibrary.prototype.toFunction;
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.toFunction = function toFunction(pointer, signature) {
185+
const raw = FunctionPrototypeCall(rawToFunction, 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: 108 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const {
106106
sizeof(decltype(function_wrappers_)::value_type),
107107
"std::unordered_map<std::string, v8::Global<v8::Function>>");
108108

109+
tracker->TrackFieldWithSize(
110+
"pointer_functions",
111+
pointer_functions_ == nullptr
112+
? 0
113+
: sizeof(std::unordered_set<FFIFunction*>) +
114+
pointer_functions_->size() * sizeof(FFIFunction*),
115+
"std::unordered_set<FFIFunction*>");
116+
109117
// FFIFunctionInfo instances and their sb_backing ArrayBuffers are
110118
// owned by V8 function wrappers and reachable only via weak references,
111119
// so they are deliberately not counted here.
@@ -117,6 +125,14 @@ void DynamicLibrary::Close() {
117125
fn->ptr = nullptr;
118126
}
119127

128+
if (pointer_functions_ != nullptr) {
129+
for (FFIFunction* fn : *pointer_functions_) {
130+
fn->closed = true;
131+
fn->ptr = nullptr;
132+
}
133+
pointer_functions_->clear();
134+
}
135+
120136
// Closing the library invalidates all registered callbacks. Node.js does not
121137
// track or revoke callback pointers that have already been handed to native
122138
// code. If native code calls a callback pointer after `close()` or
@@ -158,7 +174,10 @@ Maybe<void*> DynamicLibrary::ResolveSymbol(Environment* env,
158174
}
159175

160176
Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
161-
Environment* env, const std::string& name, Local<Object> signature) {
177+
Environment* env,
178+
const std::string& name,
179+
Local<Object> signature,
180+
void* ptr) {
162181
std::shared_ptr<FFIFunction> fn;
163182
FunctionSignature parsed;
164183

@@ -167,22 +186,22 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
167186
}
168187
// Look up the cache only after parsing: the signature's getters run user
169188
// code that may close the library, which clears `functions_`.
170-
auto existing = functions_.find(name);
189+
const bool from_pointer = ptr != nullptr;
190+
auto existing = from_pointer ? functions_.end() : functions_.find(name);
171191
auto [return_type, args, return_type_name, arg_type_names] =
172192
std::move(parsed);
173193

174194
bool should_cache_symbol = false;
175195
bool should_cache_function = false;
176196

177197
if (existing == functions_.end()) {
178-
void* ptr;
179-
180-
if (!ResolveSymbol(env, name).To(&ptr)) {
181-
return {};
198+
if (!from_pointer) {
199+
if (!ResolveSymbol(env, name).To(&ptr)) {
200+
return {};
201+
}
202+
should_cache_symbol = symbols_.find(name) == symbols_.end();
182203
}
183204

184-
should_cache_symbol = symbols_.find(name) == symbols_.end();
185-
186205
fn = std::make_shared<FFIFunction>();
187206
fn->ptr = ptr;
188207
fn->args = std::move(args);
@@ -221,7 +240,7 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
221240
}
222241
#endif
223242

224-
should_cache_function = true;
243+
should_cache_function = !from_pointer;
225244
} else {
226245
fn = existing->second;
227246

@@ -309,7 +328,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
309328
const std::string& name,
310329
const std::shared_ptr<FFIFunction>& fn) {
311330
Isolate* isolate = env->isolate();
312-
Local<Context> context = env->context();
313331

314332
// Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and
315333
// on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already
@@ -324,20 +342,41 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
324342
function_wrappers_.erase(cached);
325343
}
326344

345+
Local<Function> ret;
346+
if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) {
347+
return {};
348+
}
349+
function_wrappers_.emplace(name, Global<Function>(isolate, ret))
350+
.first->second.SetWeak();
351+
return ret;
352+
}
353+
354+
MaybeLocal<Function> DynamicLibrary::BuildFunction(
355+
Environment* env,
356+
const std::string& name,
357+
const std::shared_ptr<FFIFunction>& fn,
358+
bool optimize) {
359+
Isolate* isolate = env->isolate();
360+
Local<Context> context = env->context();
327361
auto info = FFIFunctionInfo::Create(env, fn, this);
362+
if (!info) {
363+
return {};
364+
}
328365

329366
DCHECK_EQ(fn->args.size(), fn->arg_type_names.size());
330367

331368
// Try the generated Fast API path first. If metadata creation rejects the
332369
// signature, fall back to SharedBuffer for supported scalar shapes, then to
333370
// the generic libffi invoker.
334-
std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
335-
if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate)
336-
.MoveTo(&info->fast_metadata)) {
337-
return {};
371+
if (optimize) {
372+
std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
373+
if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate)
374+
.MoveTo(&info->fast_metadata)) {
375+
return {};
376+
}
338377
}
339378
bool use_fast_api = info->fast_metadata != nullptr;
340-
bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn);
379+
bool use_sb = optimize && !use_fast_api && IsSBEligibleSignature(*fn);
341380
bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn);
342381
// Signatures that need JS-side conversion or validation use a wrapper, as
343382
// do all fast signatures on platforms without a native library guard.
@@ -532,14 +571,6 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
532571
}
533572
}
534573

535-
// A strong handle would root the callable, which holds the library object
536-
// through FFIFunctionInfo, so neither could ever be collected. Weaken the
537-
// stored handle instead, so the cache lasts exactly as long as user code
538-
// keeps a reference. SetWeak() runs after the move into the map because
539-
// moving a handle relocates the underlying slot.
540-
function_wrappers_.emplace(name, Global<Function>(isolate, ret))
541-
.first->second.SetWeak();
542-
543574
return ret;
544575
}
545576

@@ -921,6 +952,59 @@ void DynamicLibrary::GetFunction(const FunctionCallbackInfo<Value>& args) {
921952
args.GetReturnValue().Set(ret);
922953
}
923954

955+
void DynamicLibrary::ToFunction(const FunctionCallbackInfo<Value>& args) {
956+
Environment* env = Environment::GetCurrent(args);
957+
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
958+
959+
if (args.Length() < 1 || !args[0]->IsBigInt()) {
960+
THROW_ERR_INVALID_ARG_TYPE(env, "Function pointer must be a bigint");
961+
return;
962+
}
963+
bool lossless;
964+
uint64_t address = args[0].As<BigInt>()->Uint64Value(&lossless);
965+
if (!lossless || address == 0 ||
966+
address > static_cast<uint64_t>(std::numeric_limits<uintptr_t>::max())) {
967+
THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer");
968+
return;
969+
}
970+
if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
971+
THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
972+
return;
973+
}
974+
975+
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
976+
if (lib->is_closed()) {
977+
THROW_ERR_FFI_LIBRARY_CLOSED(env);
978+
return;
979+
}
980+
Local<Object> signature = args[1].As<Object>();
981+
PreparedFunction prepared;
982+
void* ptr = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
983+
if (!lib->PrepareFunction(env, "<pointer>", signature, ptr).To(&prepared)) {
984+
return;
985+
}
986+
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
987+
if (lib->is_closed()) {
988+
THROW_ERR_FFI_LIBRARY_CLOSED(env);
989+
return;
990+
}
991+
992+
auto fn = std::move(prepared.fn);
993+
if (lib->pointer_functions_ == nullptr) {
994+
lib->pointer_functions_ =
995+
std::make_shared<std::unordered_set<FFIFunction*>>();
996+
}
997+
fn->pointer_registry = lib->pointer_functions_;
998+
lib->pointer_functions_->insert(fn.get());
999+
Local<Function> ret;
1000+
if (!lib->BuildFunction(env, "<pointer>", fn, false).ToLocal(&ret)) {
1001+
lib->pointer_functions_->erase(fn.get());
1002+
fn->ptr = nullptr;
1003+
return;
1004+
}
1005+
args.GetReturnValue().Set(ret);
1006+
}
1007+
9241008
void DynamicLibrary::GetFunctions(const FunctionCallbackInfo<Value>& args) {
9251009
Environment* env = Environment::GetCurrent(args);
9261010
Isolate* isolate = env->isolate();
@@ -1397,6 +1481,7 @@ Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
13971481
SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close);
13981482
SetProtoDispose(isolate, tmpl, DynamicLibrary::Close);
13991483
SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction);
1484+
SetProtoMethod(isolate, tmpl, "toFunction", DynamicLibrary::ToFunction);
14001485
SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions);
14011486
SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol);
14021487
SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols);

‎src/node_ffi.h‎

Lines changed: 16 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 = {};
@@ -136,6 +143,7 @@ class DynamicLibrary : public BaseObject {
136143

137144
static void GetPath(const v8::FunctionCallbackInfo<v8::Value>& args);
138145
static void GetFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
146+
static void ToFunction(const v8::FunctionCallbackInfo<v8::Value>& args);
139147
static void GetFunctions(const v8::FunctionCallbackInfo<v8::Value>& args);
140148
static void GetSymbol(const v8::FunctionCallbackInfo<v8::Value>& args);
141149
static void GetSymbols(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -159,11 +167,17 @@ class DynamicLibrary : public BaseObject {
159167
};
160168
v8::Maybe<PreparedFunction> PrepareFunction(Environment* env,
161169
const std::string& name,
162-
v8::Local<v8::Object> signature);
170+
v8::Local<v8::Object> signature,
171+
void* ptr = nullptr);
163172
v8::MaybeLocal<v8::Function> CreateFunction(
164173
Environment* env,
165174
const std::string& name,
166175
const std::shared_ptr<FFIFunction>& fn);
176+
v8::MaybeLocal<v8::Function> BuildFunction(
177+
Environment* env,
178+
const std::string& name,
179+
const std::shared_ptr<FFIFunction>& fn,
180+
bool optimize);
167181
static void CleanupHook(void* arg);
168182
static void CleanupFunctionInfo(
169183
const v8::WeakCallbackInfo<FFIFunctionInfo>& data);
@@ -179,6 +193,7 @@ class DynamicLibrary : public BaseObject {
179193
// which keeps the map from rooting the library through the wrapper's
180194
// FFIFunctionInfo.
181195
std::unordered_map<std::string, v8::Global<v8::Function>> function_wrappers_;
196+
std::shared_ptr<std::unordered_set<FFIFunction*>> pointer_functions_;
182197
std::unordered_map<void*, std::unique_ptr<FFICallback>> callbacks_;
183198
};
184199

0 commit comments

Comments
 (0)