blob: c1d8b9deeea87863c0294a8ae3813b4f35d00f70 [file] [edit]
#if HAVE_FFI
#include "node_ffi.h"
#include <climits>
#include <cstring>
#include <limits>
#include <memory>
#include "base_object-inl.h"
#include "env-inl.h"
#include "ffi/data.h"
#include "ffi/fast.h"
#include "ffi/types.h"
#include "node_errors.h"
namespace node {
using v8::Array;
using v8::ArrayBuffer;
using v8::BigInt;
using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::DontEnum;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::HandleScope;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Null;
using v8::Object;
using v8::PropertyAttribute;
using v8::ReadOnly;
using v8::String;
using v8::TryCatch;
using v8::Value;
namespace ffi {
void FFIFunctionInfo::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("sb_backing", sb_backing);
}
DynamicLibrary::DynamicLibrary(Environment* env, Local<Object> object)
: BaseObject(env, object) {
MakeWeak();
}
DynamicLibrary::~DynamicLibrary() {
this->Close();
}
bool DynamicLibrary::is_closed() const {
return static_cast<void*>(lib_.handle) == nullptr;
}
void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackFieldWithSize("path", path_.capacity() + 1, "std::string");
size_t symbols_size = 0;
for (const auto& [name, ptr] : symbols_) {
symbols_size += name.capacity() + 1;
symbols_size += sizeof(ptr);
symbols_size += sizeof(decltype(symbols_)::value_type);
}
tracker->TrackFieldWithSize(
"symbols", symbols_size, "std::unordered_map<std::string, void*>");
// 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.
}
void DynamicLibrary::Close() {
for (auto& [name, fn] : functions_) {
fn->closed = true;
fn->ptr = nullptr;
}
// 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
// `unregisterCallback()`, the behavior is undefined, not allowed, and
// dangerous: it can crash the process, produce incorrect output, or corrupt
// memory.
if (!is_closed()) {
uv_dlclose(&lib_);
lib_ = {};
}
symbols_.clear();
functions_.clear();
callbacks_.clear();
}
Maybe<void*> DynamicLibrary::ResolveSymbol(Environment* env,
const std::string& name) {
if (is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return {};
}
auto existing = symbols_.find(name);
void* ptr;
if (existing != symbols_.end()) {
ptr = existing->second;
} else {
if (uv_dlsym(&lib_, name.c_str(), &ptr) != 0) {
THROW_ERR_FFI_CALL_FAILED(env, "dlsym failed: %s", uv_dlerror(&lib_));
return {};
}
}
return Just(ptr);
}
Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
Environment* env, const std::string& name, Local<Object> signature) {
std::shared_ptr<FFIFunction> fn;
auto existing = functions_.find(name);
FunctionSignature parsed;
if (!ParseFunctionSignature(env, name, signature).To(&parsed)) {
return {};
}
auto [return_type, args, return_type_name, arg_type_names] =
std::move(parsed);
bool should_cache_symbol = false;
bool should_cache_function = false;
if (existing == functions_.end()) {
void* ptr;
if (!ResolveSymbol(env, name).To(&ptr)) {
return {};
}
should_cache_symbol = symbols_.find(name) == symbols_.end();
fn = std::make_shared<FFIFunction>(
FFIFunction{.closed = false,
.ptr = ptr,
.cif = {},
.args = args,
.return_type = return_type,
.arg_type_names = std::move(arg_type_names),
.return_type_name = std::move(return_type_name)});
ffi_status status = ffi_prep_cif(&fn->cif,
FFI_DEFAULT_ABI,
fn->args.size(),
fn->return_type,
fn->args.data());
if (status != FFI_OK) {
const char* msg = "ffi_prep_cif failed";
switch (status) {
case FFI_BAD_TYPEDEF:
msg = "ffi_prep_cif failed: bad typedef";
break;
case FFI_BAD_ABI:
msg = "ffi_prep_cif failed: bad ABI";
break;
default:
msg = "ffi_prep_cif failed: unknown error";
break;
}
THROW_ERR_FFI_CALL_FAILED(env, msg);
return {};
}
should_cache_function = true;
} else {
fn = existing->second;
if (!SignaturesMatch(*fn, return_type, args)) {
THROW_ERR_INVALID_ARG_VALUE(
env,
"Function %s"
" was already requested with a different signature",
name);
return {};
}
}
return Just(PreparedFunction{fn, should_cache_symbol, should_cache_function});
}
FFIFunctionInfo::FFIFunctionInfo(Environment* env,
Local<Object> object,
std::shared_ptr<FFIFunction> fn,
DynamicLibrary* library)
: BaseObject(env, object), fn(std::move(fn)) {
// Keep the DynamicLibrary instance alive as long as any of its functions are
// alive
object->SetInternalField(FFIFunctionInfo::kLibrary, library->object());
}
Local<FunctionTemplate> FFIFunctionInfo::GetConstructorTemplate(
IsolateData* isolate_data) {
Local<FunctionTemplate> tmpl =
isolate_data->ffi_function_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = isolate_data->isolate();
tmpl = MakeLazilyInitializedJSTemplate(isolate_data, kInternalFieldCount);
Local<String> classname = FIXED_ONE_BYTE_STRING(isolate, "FFIFunctionInfo");
tmpl->SetClassName(classname);
auto instance = tmpl->InstanceTemplate();
instance->SetInternalFieldCount(FFIFunctionInfo::kInternalFieldCount);
isolate_data->set_ffi_function_constructor_template(tmpl);
}
return tmpl;
}
BaseObjectPtr<FFIFunctionInfo> FFIFunctionInfo::Create(
Environment* env,
std::shared_ptr<FFIFunction> fn,
DynamicLibrary* library) {
Local<Object> obj;
if (!GetConstructorTemplate(env->isolate_data())
->InstanceTemplate()
->NewInstance(env->context())
.ToLocal(&obj)) {
return nullptr;
}
return MakeWeakBaseObject<FFIFunctionInfo>(env, obj, std::move(fn), library);
}
MaybeLocal<Function> DynamicLibrary::CreateFunction(
Environment* env,
const std::string& name,
const std::shared_ptr<FFIFunction>& fn) {
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
auto info = FFIFunctionInfo::Create(env, fn, this);
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.
info->fast_metadata = CreateFastFFIMetadata(*fn);
bool use_fast_api = info->fast_metadata != nullptr;
bool use_sb = !use_fast_api && IsSBEligibleSignature(*fn);
bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn);
// Fast API signatures that still accept JS pointer-like values need a JS
// wrapper with the native type names attached as hidden metadata.
bool needs_raw_pointer_conversions =
use_fast_api && SignatureNeedsRawPointerConversions(*fn);
// A single pointer-like parameter can get a separate Buffer-aware Fast API
// entrypoint so Buffer calls avoid JS pointer extraction.
bool needs_fast_buffer_invoke =
use_fast_api && SignatureNeedsFastBufferInvoke(*fn);
MaybeLocal<Function> maybe_ret;
if (use_fast_api) {
// V8 calls this FunctionTemplate through `fast_metadata->c_function` when
// the optimized Fast API call path is available. The normal callback stays
// attached as a fallback for V8 deopts and unsupported call sites.
Local<FunctionTemplate> tmpl =
FunctionTemplate::New(isolate,
DynamicLibrary::InvokeFunction,
info->object(),
Local<v8::Signature>(),
fn->args.size(),
v8::ConstructorBehavior::kThrow,
v8::SideEffectType::kHasSideEffect,
&info->fast_metadata->c_function);
maybe_ret = tmpl->GetFunction(context);
} else {
// Non-Fast signatures either use the SharedBuffer invoker, where JS writes
// argument slots before calling with no arguments, or the generic invoker
// that converts each JS argument in C++.
maybe_ret = Function::New(context,
use_sb ? DynamicLibrary::InvokeFunctionSB
: DynamicLibrary::InvokeFunction,
info->object());
}
Local<Function> ret;
if (!maybe_ret.ToLocal(&ret)) {
return MaybeLocal<Function>();
}
Local<Value> name_str;
if (!ToV8Value(env->context(), name, isolate).ToLocal(&name_str)) {
return MaybeLocal<Function>();
}
ret->SetName(name_str.As<String>());
if (!ret->Set(
context,
env->pointer_string(),
BigInt::NewFromUnsigned(
isolate,
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(fn->ptr))))
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
// Internal properties are keyed by per-isolate Symbols (see
// `env_properties.h`) to keep them out of string-key reflection, and the
// `ReadOnly | DontEnum | DontDelete` attribute set blocks user code from
// reading, modifying, or deleting them.
PropertyAttribute internal_attrs =
static_cast<PropertyAttribute>(ReadOnly | DontEnum | DontDelete);
if (use_sb) {
// SharedBuffer layout is intentionally fixed-width: slot 0 stores the
// return value and slots 1..N store argument payloads. The JS wrapper and
// InvokeFunctionSB share this exact layout.
size_t sb_size = 8 * (fn->args.size() + 1);
Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, sb_size);
// The shared_ptr to the backing store keeps the memory alive while
// FFIFunctionInfo still references it.
info->sb_backing = ab->GetBackingStore();
if (!ret->DefineOwnProperty(
context, env->ffi_sb_shared_buffer_symbol(), ab, internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
// Signatures with pointer args also expose a slow-path invoker bound
// to the same FFIFunctionInfo. The JS wrapper routes through it when a
// pointer argument is anything other than a BigInt, null, or undefined
// (strings, Buffers, ArrayBuffers, and ArrayBufferViews).
if (has_ptr_args) {
Local<Function> slow_fn;
if (!Function::New(
context, DynamicLibrary::InvokeFunction, info->object())
.ToLocal(&slow_fn)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
env->ffi_sb_invoke_slow_symbol(),
slow_fn,
internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
}
// Attach the original signature type names so the JS wrapper can
// rebuild the signature from a raw function when the caller did not
// pass arguments and return explicitly. The `lib.functions` accessor
// path relies on this.
Local<Value> args_arr;
if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(&args_arr)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
env->ffi_sb_arguments_symbol(),
args_arr,
internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
Local<Value> return_name;
if (!ToV8Value(context, fn->return_type_name, isolate)
.ToLocal(&return_name)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
env->ffi_sb_return_symbol(),
return_name,
internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
}
if (needs_raw_pointer_conversions || needs_fast_buffer_invoke) {
// Fast API wrappers need only the parameter type names. Result conversion
// is still handled by V8's CFunction metadata, unlike the SharedBuffer path
// which must also know how to read slot 0.
Local<Value> arguments_arr;
if (!ToV8Value(context, fn->arg_type_names, isolate)
.ToLocal(&arguments_arr)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
env->ffi_fast_arguments_symbol(),
arguments_arr,
internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
}
if (needs_fast_buffer_invoke) {
// Build an alternate CFunction that describes the pointer-like argument as
// a V8 buffer value. The JS wrapper dispatches here only when the runtime
// argument is Buffer/ArrayBuffer-backed memory.
std::shared_ptr<FFIFunction> fast_buffer_fn =
CloneWithFastBufferArgNames(fn);
info->fast_buffer_metadata = CreateFastFFIMetadata(*fast_buffer_fn);
if (info->fast_buffer_metadata != nullptr) {
// Store the secondary invoker on the primary raw function under a hidden
// Symbol. Keeping it separate avoids overloading SharedBuffer slow-path
// metadata for Fast API routing.
Local<FunctionTemplate> tmpl =
FunctionTemplate::New(isolate,
DynamicLibrary::InvokeFunction,
info->object(),
Local<v8::Signature>(),
fn->args.size(),
v8::ConstructorBehavior::kThrow,
v8::SideEffectType::kHasSideEffect,
&info->fast_buffer_metadata->c_function);
Local<Function> fast_buffer_invoke;
if (!tmpl->GetFunction(context).ToLocal(&fast_buffer_invoke)) {
return MaybeLocal<Function>();
}
if (!ret->DefineOwnProperty(context,
env->ffi_fast_buffer_invoke_symbol(),
fast_buffer_invoke,
internal_attrs)
.FromMaybe(false)) {
return MaybeLocal<Function>();
}
}
}
return ret;
}
void DynamicLibrary::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args.IsConstructCall()) {
return THROW_ERR_CONSTRUCT_CALL_REQUIRED(
env,
"Class constructor DynamicLibrary cannot be invoked without 'new'");
}
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
#ifndef _WIN32
if (args.Length() < 1 || (!args[0]->IsString() && !args[0]->IsNull())) {
THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string or null");
return;
}
#else
if (args.Length() < 1 || !args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string");
return;
}
#endif
const char* library_path = nullptr;
DynamicLibrary* lib = new DynamicLibrary(env, args.This());
if (args[0]->IsString()) {
Utf8Value filename(env->isolate(), args[0]);
if (ThrowIfContainsNullBytes(env, filename, "Library path")) {
return;
}
lib->path_ = filename.ToString();
library_path = lib->path_.c_str();
}
CHECK(lib->is_closed());
// Open the library
if (uv_dlopen(library_path, &lib->lib_) != 0) {
THROW_ERR_FFI_CALL_FAILED(env, "dlopen failed: %s", uv_dlerror(&lib->lib_));
return;
}
}
void DynamicLibrary::Close(const FunctionCallbackInfo<Value>& args) {
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
// Closing a library from one of its active callbacks is unsupported and
// dangerous. Callbacks must return before the owning library is closed.
lib->Close();
}
void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
FFIFunctionInfo* info = Unwrap<FFIFunctionInfo>(args.Data());
CHECK_NOT_NULL(info);
FFIFunction* fn = info->fn.get();
if (fn == nullptr || fn->closed || fn->ptr == nullptr) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
// Convert arguments
unsigned int expected_args = fn->args.size();
unsigned int provided_args = args.Length();
if (provided_args != expected_args) {
THROW_ERR_INVALID_ARG_VALUE(env,
"Invalid argument count: expected %s, got %s",
expected_args,
provided_args);
return;
}
std::vector<uint64_t> values(expected_args, 0);
std::vector<void*> ffi_args(expected_args, nullptr);
std::vector<std::string> strings;
strings.reserve(expected_args);
for (unsigned int i = 0; i < expected_args; i++) {
FFIArgumentCategory res;
if (!ToFFIArgument(env, i, fn->args[i], args[i], &values[i]).To(&res)) {
return;
}
// The argument is a string, we need to copy
if (res == FFIArgumentCategory::String) {
Utf8Value str(env->isolate(), args[i]);
if (*str == nullptr) {
THROW_ERR_INVALID_ARG_TYPE(env, "Argument %s must be a string", i);
return;
}
if (ThrowIfContainsNullBytes(env, str, "Argument " + std::to_string(i))) {
return;
}
strings.push_back(*str);
values[i] = reinterpret_cast<uint64_t>(strings.back().c_str());
ffi_args[i] = &values[i];
} else {
ffi_args[i] = &values[i];
}
}
void* result = nullptr;
if (fn->return_type->type != FFI_TYPE_VOID) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}
ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
free(result);
}
void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
FFIFunctionInfo* info = Unwrap<FFIFunctionInfo>(args.Data());
CHECK_NOT_NULL(info);
FFIFunction* fn = info->fn.get();
if (fn == nullptr || fn->closed || fn->ptr == nullptr) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
// Arguments reach the native invoker through the shared buffer, not
// through V8. The JS wrapper always calls the raw function as `rawFn()`
// so any non-zero argument count indicates that user code reached the
// raw SB function directly and is about to read stale buffer contents.
if (args.Length() != 0) {
THROW_ERR_INVALID_ARG_VALUE(
env,
"SB-invoked FFI functions receive arguments through the shared "
"buffer, not as JavaScript arguments");
return;
}
// A failure of either CHECK means the SB invoker ran against a function
// that `CreateFunction` did not set up for the fast path, which is a
// contract violation. They stay enabled in Release because each FFI call
// is already dominated by `ffi_call` itself.
CHECK(info->sb_backing);
CHECK_EQ(info->sb_backing->ByteLength(), 8u * (info->fn->args.size() + 1));
uint8_t* buffer = static_cast<uint8_t*>(info->sb_backing->Data());
unsigned int nargs = fn->args.size();
// Layout is 8 bytes per slot. The return value lives at offset 0 and
// argument i lives at offset 8*(i+1).
std::vector<uint64_t> values(nargs, 0);
std::vector<void*> ffi_args(nargs, nullptr);
for (unsigned int i = 0; i < nargs; i++) {
ReadFFIArgFromBuffer(fn->args[i], buffer, 8 * (i + 1), &values[i]);
ffi_args[i] = &values[i];
}
// The storage must cover both the ffi_arg width that libffi uses for
// promoted small integer returns and the 8 bytes needed for non-promoted
// SB-eligible returns like f64, i64, and u64. `sizeof(ffi_arg)` is only
// 4 on 32-bit ARM, so take the max.
constexpr size_t kSBResultStorageSize =
sizeof(ffi_arg) > 8 ? sizeof(ffi_arg) : 8;
alignas(8) uint8_t result_storage[kSBResultStorageSize] = {0};
void* result = (fn->return_type != &ffi_type_void) ? result_storage : nullptr;
ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
}
}
// This is the function that will be called by libffi when a callback
// is invoked from a dlopen library. It converts the arguments to JavaScript
// values and calls the original JavaScript callback function.
// It also handles the return value and exceptions properly.
// Note that since this function is called from native code, it must not throw
// exceptions or return promises, as there is no defined way to propagate them
// back to the caller.
// If such cases occur, the process will be aborted to avoid undefined behavior.
void DynamicLibrary::InvokeCallback(ffi_cif* cif,
void* ret,
void** args,
void* user_data) {
FFICallback* cb = static_cast<FFICallback*>(user_data);
// It is unsupported and dangerous for a callback to unregister itself or
// close its owning library while executing. The current invocation must
// return before teardown APIs are used.
if (cb->owner->is_closed() || cb->ptr == nullptr) {
if (ret != nullptr && cb->return_type->size > 0) {
std::memset(ret, 0, GetFFIReturnValueStorageSize(cb->return_type));
}
return;
}
if (std::this_thread::get_id() != cb->thread_id) {
FPrintF(stderr,
"Callbacks can only be invoked on the system thread they were "
"created on\n");
ABORT();
}
Environment* env = cb->env;
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Local<Context> context = env->context();
if (cb->fn.IsEmpty()) {
if (ret != nullptr && cb->return_type->size > 0) {
std::memset(ret, 0, GetFFIReturnValueStorageSize(cb->return_type));
}
return;
}
size_t expected_args = cb->args.size();
LocalVector<Value> callback_args(isolate, expected_args);
for (size_t i = 0; i < expected_args; i++) {
if (args[i] == nullptr) {
callback_args[i] = Null(isolate);
continue;
} else {
callback_args[i] = ToJSArgument(isolate, cb->args[i], args[i]);
}
}
TryCatch try_catch(isolate);
Local<Function> callback = Local<Function>::New(isolate, cb->fn);
MaybeLocal<Value> result = callback->Call(
context, Undefined(isolate), expected_args, callback_args.data());
// Handle exceptions by crashing (can't propagate across FFI boundary)
if (try_catch.HasCaught()) {
FPrintF(stderr, "Callbacks cannot throw an exception\n");
ABORT();
}
Local<Value> result_val;
if (!result.ToLocal(&result_val)) {
if (try_catch.HasCaught()) {
FPrintF(stderr, "Callbacks cannot return an exception\n");
ABORT();
}
return;
}
if (result_val->IsPromise()) {
FPrintF(stderr, "Callbacks cannot return promises\n");
ABORT();
}
if (!ToFFIReturnValue(result_val, cb->return_type, ret)) {
FPrintF(stderr, "Callback returned invalid value for declared FFI type\n");
ABORT();
}
}
void DynamicLibrary::GetPath(const FunctionCallbackInfo<Value>& args) {
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
Local<Value> path;
if (!ToV8Value(lib->env()->context(), lib->path_, args.GetIsolate())
.ToLocal(&path)) {
return;
}
args.GetReturnValue().Set(path);
}
void DynamicLibrary::GetFunction(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
if (args.Length() < 1 || !args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Function name must be a string");
return;
}
if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
return;
}
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
Utf8Value name(isolate, args[0]);
if (ThrowIfContainsNullBytes(env, name, "Function name")) {
return;
}
PreparedFunction prepared;
Local<Object> signature = args[1].As<Object>();
if (!lib->PrepareFunction(env, *name, signature).To(&prepared)) {
return;
}
auto [fn, should_cache_symbol, should_cache_function] = prepared;
if (should_cache_symbol) {
lib->symbols_.emplace(*name, fn->ptr);
}
if (should_cache_function) {
lib->functions_.emplace(*name, fn);
}
MaybeLocal<Function> maybe_ret = lib->CreateFunction(env, *name, fn);
Local<Function> ret;
if (!maybe_ret.ToLocal(&ret)) {
return;
}
args.GetReturnValue().Set(ret);
}
void DynamicLibrary::GetFunctions(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
Local<Object> functions = Object::New(isolate);
if (!functions->SetPrototype(context, Null(isolate)).FromMaybe(false)) {
return;
}
if (args.Length() > 0) {
if (!args[0]->IsObject() || args[0]->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Functions signatures must be an object");
return;
}
Local<Object> signatures = args[0].As<Object>();
Local<Array> keys;
if (!signatures->GetOwnPropertyNames(context).ToLocal(&keys)) {
return;
}
std::vector<ResolvedFunction> pending;
pending.reserve(keys->Length());
for (uint32_t i = 0; i < keys->Length(); i++) {
Local<Value> key;
Local<Value> signature;
if (!keys->Get(context, i).ToLocal(&key)) {
return;
}
Utf8Value name(isolate, key);
if (ThrowIfContainsNullBytes(env, name, "Function name")) {
return;
}
if (!signatures->Get(env->context(), key).ToLocal(&signature)) {
return;
}
if (!signature->IsObject() || signature->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(
env, "Signature of function %s must be an object", name);
return;
}
PreparedFunction prepared;
Local<Object> signature_object = signature.As<Object>();
if (!lib->PrepareFunction(env, *name, signature_object).To(&prepared)) {
return;
}
auto [fn, should_cache_symbol, should_cache_function] = prepared;
pending.push_back(ResolvedFunction{
.name = *name,
.fn = fn,
.should_cache_symbol = should_cache_symbol,
.should_cache_function = should_cache_function,
});
}
for (const auto& item : pending) {
if (item.should_cache_symbol) {
lib->symbols_.emplace(item.name, item.fn->ptr);
}
if (item.should_cache_function) {
lib->functions_.emplace(item.name, item.fn);
}
}
for (const auto& item : pending) {
MaybeLocal<Function> maybe_ret =
lib->CreateFunction(env, item.name, item.fn);
Local<Function> ret;
if (!maybe_ret.ToLocal(&ret)) {
return;
}
Local<Value> name_string;
if (!ToV8Value(env->context(), item.name, env->isolate())
.ToLocal(&name_string)) {
return;
}
if (!functions->Set(context, name_string.As<String>(), ret)
.FromMaybe(false)) {
return;
}
}
} else {
for (const auto& entry : lib->functions_) {
MaybeLocal<Function> maybe_fn =
lib->CreateFunction(env, entry.first, entry.second);
Local<Function> fn;
if (!maybe_fn.ToLocal(&fn)) {
return;
}
Local<Value> name_string;
if (!ToV8Value(env->context(), entry.first, env->isolate())
.ToLocal(&name_string)) {
return;
}
if (!functions->Set(context, name_string.As<String>(), fn)
.FromMaybe(false)) {
return;
}
}
}
args.GetReturnValue().Set(functions);
}
void DynamicLibrary::GetSymbol(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
if (args.Length() < 1 || !args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Symbol name must be a string");
return;
}
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
Utf8Value name(isolate, args[0]);
if (ThrowIfContainsNullBytes(env, name, "Symbol name")) {
return;
}
void* ptr;
if (!lib->ResolveSymbol(env, *name).To(&ptr)) {
return;
}
lib->symbols_.emplace(*name, ptr);
args.GetReturnValue().Set(BigInt::NewFromUnsigned(
isolate, static_cast<uint64_t>(reinterpret_cast<uintptr_t>(ptr))));
}
void DynamicLibrary::GetSymbols(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
Local<Object> symbols = Object::New(isolate);
if (!symbols->SetPrototype(context, Null(isolate)).FromMaybe(false)) {
return;
}
for (const auto& entry : lib->symbols_) {
Local<Value> symbol_key;
if (!ToV8Value(env->context(), entry.first, env->isolate())
.ToLocal(&symbol_key)) {
return;
}
if (!symbols
->Set(context,
symbol_key.As<String>(),
BigInt::NewFromUnsigned(
isolate,
static_cast<uint64_t>(
reinterpret_cast<uintptr_t>(entry.second))))
.FromMaybe(false)) {
return;
}
}
args.GetReturnValue().Set(symbols);
}
void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
ffi_type* return_type = &ffi_type_void;
std::vector<ffi_type*> callback_args;
Local<Function> fn;
if (args.Length() < 1) {
THROW_ERR_INVALID_ARG_TYPE(
env, "First argument must be a function or a signature object");
return;
}
if (args[0]->IsFunction()) {
fn = args[0].As<Function>();
} else {
if (!args[0]->IsObject() || args[0]->IsArray()) {
THROW_ERR_INVALID_ARG_TYPE(
env, "First argument must be a function or a signature object");
return;
}
if (args.Length() < 2 || !args[1]->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a function");
return;
}
FunctionSignature parsed;
if (!ParseFunctionSignature(env, "<callback>", args[0].As<Object>())
.To(&parsed)) {
return;
}
return_type = parsed.return_type;
callback_args = std::move(parsed.args);
fn = args[1].As<Function>();
}
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
auto callback = std::unique_ptr<FFICallback>(
new FFICallback{.owner = lib,
.env = env,
.thread_id = std::this_thread::get_id(),
.fn = Global<Function>(isolate, fn),
.closure = nullptr,
.ptr = nullptr,
.cif = {},
.args = std::move(callback_args),
.return_type = return_type});
callback->closure = static_cast<ffi_closure*>(
ffi_closure_alloc(sizeof(ffi_closure), &callback->ptr));
if (callback->closure == nullptr) {
THROW_ERR_FFI_CALL_FAILED(env, "ffi_closure_alloc failed");
return;
}
ffi_status status;
status = ffi_prep_cif(&callback->cif,
FFI_DEFAULT_ABI,
callback->args.size(),
callback->return_type,
callback->args.data());
if (status != FFI_OK) {
const char* msg = "ffi_prep_cif failed";
switch (status) {
case FFI_BAD_TYPEDEF:
msg = "ffi_prep_cif failed: bad typedef";
break;
case FFI_BAD_ABI:
msg = "ffi_prep_cif failed: bad ABI";
break;
default:
msg = "ffi_prep_cif failed: unknown error";
break;
}
THROW_ERR_FFI_CALL_FAILED(env, msg);
return;
}
status = ffi_prep_closure_loc(callback->closure,
&callback->cif,
DynamicLibrary::InvokeCallback,
callback.get(),
callback->ptr);
if (status != FFI_OK) {
const char* msg = "ffi_prep_closure_loc failed";
switch (status) {
case FFI_BAD_TYPEDEF:
msg = "ffi_prep_closure_loc failed: bad typedef";
break;
case FFI_BAD_ABI:
msg = "ffi_prep_closure_loc failed: bad ABI";
break;
default:
msg = "ffi_prep_closure_loc failed: unknown error";
break;
}
THROW_ERR_FFI_CALL_FAILED(env, msg);
return;
}
auto ret = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(callback->ptr));
lib->callbacks_.emplace(callback->ptr, std::move(callback));
args.GetReturnValue().Set(BigInt::NewFromUnsigned(isolate, ret));
}
void DynamicLibrary::UnregisterCallback(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
if (args.Length() < 1 || !args[0]->IsBigInt()) {
THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
return;
}
uintptr_t raw_ptr;
if (!GetValidatedPointerAddress(env, args[0], "first argument")
.To(&raw_ptr)) {
return;
}
void* ptr = reinterpret_cast<void*>(raw_ptr);
auto existing = lib->callbacks_.find(ptr);
if (existing == lib->callbacks_.end()) {
THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
return;
}
// This releases the callback trampoline immediately. If foreign code still
// retains and invokes the pointer afterwards, the behavior is undefined, not
// allowed, and dangerous: it can crash the process, produce incorrect
// output, or corrupt memory. Unregistering a callback while it is currently
// executing is also unsupported and dangerous.
lib->callbacks_.erase(existing);
}
void DynamicLibrary::RefCallback(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
if (args.Length() < 1 || !args[0]->IsBigInt()) {
THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
return;
}
uintptr_t raw_ptr;
if (!GetValidatedPointerAddress(env, args[0], "first argument")
.To(&raw_ptr)) {
return;
}
void* ptr = reinterpret_cast<void*>(raw_ptr);
auto existing = lib->callbacks_.find(ptr);
if (existing == lib->callbacks_.end()) {
THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
return;
}
existing->second->fn.ClearWeak();
}
void DynamicLibrary::UnrefCallback(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
if (lib->is_closed()) {
THROW_ERR_FFI_LIBRARY_CLOSED(env);
return;
}
if (args.Length() < 1 || !args[0]->IsBigInt()) {
THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
return;
}
uintptr_t raw_ptr;
if (!GetValidatedPointerAddress(env, args[0], "first argument")
.To(&raw_ptr)) {
return;
}
void* ptr = reinterpret_cast<void*>(raw_ptr);
auto existing = lib->callbacks_.find(ptr);
if (existing == lib->callbacks_.end()) {
THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
return;
}
existing->second->fn.SetWeak();
}
Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
Environment* env) {
Local<FunctionTemplate> tmpl =
env->ffi_dynamic_library_constructor_template();
if (tmpl.IsEmpty()) {
Isolate* isolate = env->isolate();
enum PropertyAttribute attributes =
static_cast<PropertyAttribute>(ReadOnly | DontDelete);
tmpl = NewFunctionTemplate(isolate, DynamicLibrary::New);
tmpl->InstanceTemplate()->SetInternalFieldCount(
DynamicLibrary::kInternalFieldCount);
tmpl->InstanceTemplate()->SetAccessorProperty(
FIXED_ONE_BYTE_STRING(isolate, "path"),
FunctionTemplate::New(env->isolate(), DynamicLibrary::GetPath),
Local<FunctionTemplate>(),
attributes);
tmpl->InstanceTemplate()->SetAccessorProperty(
FIXED_ONE_BYTE_STRING(isolate, "symbols"),
FunctionTemplate::New(env->isolate(), DynamicLibrary::GetSymbols),
Local<FunctionTemplate>(),
attributes);
// `functions` lives on the prototype template rather than the instance
// template so `lib/ffi.js` can replace it via `Object.defineProperty`
// on the prototype. The attribute set omits `DontDelete` for the same
// reason.
tmpl->PrototypeTemplate()->SetAccessorProperty(
FIXED_ONE_BYTE_STRING(isolate, "functions"),
FunctionTemplate::New(env->isolate(), DynamicLibrary::GetFunctions),
Local<FunctionTemplate>(),
static_cast<PropertyAttribute>(ReadOnly));
SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close);
SetProtoDispose(isolate, tmpl, DynamicLibrary::Close);
SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction);
SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions);
SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol);
SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols);
SetProtoMethod(
isolate, tmpl, "registerCallback", DynamicLibrary::RegisterCallback);
SetProtoMethod(isolate,
tmpl,
"unregisterCallback",
DynamicLibrary::UnregisterCallback);
SetProtoMethod(isolate, tmpl, "refCallback", DynamicLibrary::RefCallback);
SetProtoMethod(
isolate, tmpl, "unrefCallback", DynamicLibrary::UnrefCallback);
env->set_ffi_dynamic_library_constructor_template(tmpl);
}
return tmpl;
}
// Module initialization.
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
// Create the DynamicLibrary template
Local<FunctionTemplate> dl_tmpl = DynamicLibrary::GetConstructorTemplate(env);
SetConstructorFunction(context, target, "DynamicLibrary", dl_tmpl);
SetMethod(context, target, "toString", ToString);
SetMethod(context, target, "toBuffer", ToBuffer);
SetMethod(context, target, "toArrayBuffer", ToArrayBuffer);
SetMethod(context, target, "exportBytes", ExportBytes);
SetMethod(context, target, "getRawPointer", GetRawPointer);
SetMethod(context, target, "getInt8", GetInt8);
SetMethod(context, target, "getUint8", GetUint8);
SetMethod(context, target, "getInt16", GetInt16);
SetMethod(context, target, "getUint16", GetUint16);
SetMethod(context, target, "getInt32", GetInt32);
SetMethod(context, target, "getUint32", GetUint32);
SetMethod(context, target, "getInt64", GetInt64);
SetMethod(context, target, "getUint64", GetUint64);
SetMethod(context, target, "getFloat32", GetFloat32);
SetMethod(context, target, "getFloat64", GetFloat64);
SetMethod(context, target, "setInt8", SetInt8);
SetMethod(context, target, "setUint8", SetUint8);
SetMethod(context, target, "setInt16", SetInt16);
SetMethod(context, target, "setUint16", SetUint16);
SetMethod(context, target, "setInt32", SetInt32);
SetMethod(context, target, "setUint32", SetUint32);
SetMethod(context, target, "setInt64", SetInt64);
SetMethod(context, target, "setUint64", SetUint64);
SetMethod(context, target, "setFloat32", SetFloat32);
SetMethod(context, target, "setFloat64", SetFloat64);
// ToFFIType maps `char` to sint8 or uint8 based on `CHAR_MIN < 0` at C++
// build time. Exposing the same decision to JS lets the shared-buffer
// wrapper's range check match `ToFFIArgument` on every platform.
Isolate* isolate = env->isolate();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "charIsSigned"),
Boolean::New(isolate, CHAR_MIN < 0))
.Check();
// The shared-buffer fast path uses `uintptrMax` to reject pointer BigInts
// that would otherwise be silently truncated by `ReadFFIArgFromBuffer`'s
// `memcpy(..., type->size, ...)` on 32-bit platforms. The slow path
// rejects the same values through `ToFFIArgument`.
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "uintptrMax"),
v8::BigInt::NewFromUnsigned(
isolate,
static_cast<uint64_t>(std::numeric_limits<uintptr_t>::max())))
.Check();
// Per-isolate Symbols used by `lib/internal/ffi-shared-buffer.js` to key
// shared-buffer internal state on raw FFI functions.
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kSbSharedBuffer"),
env->ffi_sb_shared_buffer_symbol())
.Check();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kSbInvokeSlow"),
env->ffi_sb_invoke_slow_symbol())
.Check();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kSbArguments"),
env->ffi_sb_arguments_symbol())
.Check();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kSbReturn"),
env->ffi_sb_return_symbol())
.Check();
// Fast API wrappers use separate metadata Symbols so pointer-conversion
// routing does not depend on SharedBuffer internals.
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kFastArguments"),
env->ffi_fast_arguments_symbol())
.Check();
target
->Set(context,
FIXED_ONE_BYTE_STRING(isolate, "kFastBufferInvoke"),
env->ffi_fast_buffer_invoke_symbol())
.Check();
}
} // namespace ffi
} // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(ffi, node::ffi::Initialize)
#endif // HAVE_FFI