mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
The FFI fast-call wrappers key per-function metadata on raw FFI
functions using two per-isolate Symbols (kFastArguments /
kFastBufferInvoke) that were declared in src/env_properties.h.
Everything in env_properties.h is allocated while the startup
snapshot is built, so each Symbol advances the isolate's
identity-hash RNG before Object.prototype / Function.prototype
receive their snapshot identity hashes. In the snapshot produced
for Node 26.4.0+ this shifted those hashes so a function map (a
function whose `length` was redefined) and a plain-object map (an
object literal with an accessor) collide in V8's 64-slot
NormalizedMapCache. Every store into such objects then misses
the inline cache, and the repro reported in the linked issue is
roughly 7x slower.
Create the two Symbols lazily in the FFI binding's Initialize, on
the first run of internalBinding('ffi') at runtime, instead of
declaring them in env_properties.h. They are therefore not
allocated during snapshot serialization and no longer bias the
snapshot's prototype identity hashes. Their export, property
layout, and the fast-call feature behavior are unchanged.
Refs: https://github.com/nodejs/node/issues/66011
Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
Assisted-by: Pi
PR-URL: https://github.com/nodejs/node/pull/66015
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
1461 lines
49 KiB
C++
1461 lines
49 KiB
C++
#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_binding.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::Object;
|
|
using v8::PropertyAttribute;
|
|
using v8::ReadOnly;
|
|
using v8::Signature;
|
|
using v8::String;
|
|
using v8::TryCatch;
|
|
using v8::Value;
|
|
|
|
namespace ffi {
|
|
|
|
void FFIFunction::Invoke(void* result, void** values) {
|
|
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
|
|
if (call_plan != nullptr) {
|
|
ffi_call_plan_invoke(call_plan.get(), FFI_FN(ptr), result, values);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
ffi_call(&cif, FFI_FN(ptr), result, values);
|
|
}
|
|
|
|
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*>");
|
|
|
|
tracker->TrackFieldWithSize(
|
|
"function_wrappers",
|
|
function_wrappers_.size() *
|
|
sizeof(decltype(function_wrappers_)::value_type),
|
|
"std::unordered_map<std::string, v8::Global<v8::Function>>");
|
|
|
|
// 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();
|
|
function_wrappers_.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>();
|
|
fn->ptr = ptr;
|
|
fn->args = std::move(args);
|
|
fn->return_type = return_type;
|
|
fn->arg_type_names = std::move(arg_type_names);
|
|
fn->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 {};
|
|
}
|
|
|
|
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
|
|
// Allocation failure is non-fatal. Invoke() falls back to ffi_call().
|
|
ffi_call_plan* call_plan = ffi_call_plan_alloc(&fn->cif);
|
|
if (call_plan != nullptr) {
|
|
fn->call_plan.reset(call_plan);
|
|
}
|
|
#endif
|
|
|
|
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();
|
|
|
|
// Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and
|
|
// on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already
|
|
// handed out for this symbol. `PrepareFunction()` rejects a request that uses
|
|
// a different signature, so a hit always describes the same signature. An
|
|
// empty handle means the wrapper was collected; fall through and rebuild.
|
|
auto cached = function_wrappers_.find(name);
|
|
if (cached != function_wrappers_.end()) {
|
|
if (!cached->second.IsEmpty()) {
|
|
return cached->second.Get(isolate);
|
|
}
|
|
function_wrappers_.erase(cached);
|
|
}
|
|
|
|
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.
|
|
std::shared_ptr<FFIFunction> 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 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.
|
|
bool needs_fast_argument_wrapper =
|
|
use_fast_api && (SignatureNeedsRawPointerConversions(*fn) ||
|
|
SignatureNeedsFastIntegerValidation(*fn) ||
|
|
!info->fast_metadata->guards_library);
|
|
// 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(),
|
|
0,
|
|
v8::ConstructorBehavior::kThrow);
|
|
}
|
|
|
|
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(),
|
|
0,
|
|
v8::ConstructorBehavior::kThrow)
|
|
.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_fast_argument_wrapper || 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, &fn->closed, isolate);
|
|
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>();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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<Function>(isolate, ret))
|
|
.first->second.SetWeak();
|
|
|
|
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();
|
|
}
|
|
|
|
// On the internal path args[1] carries the library's bytes, for a library
|
|
// that lives somewhere the dynamic loader cannot open by path (a virtual
|
|
// file system). Materialize them into a private, self-cleaning image - the
|
|
// same mechanism process.dlopen() uses for such native addons - and load
|
|
// that, while still reporting the library's own path in `library.path` and
|
|
// any error.
|
|
binding::AddonImage image;
|
|
if (args.Length() > 1 && !args[1]->IsUndefined()) {
|
|
if (!args[1]->IsArrayBufferView()) {
|
|
THROW_ERR_INVALID_ARG_TYPE(
|
|
env, "Library binary must be a Buffer, TypedArray, or DataView");
|
|
return;
|
|
}
|
|
// Loading from bytes materializes them into an image in the temporary
|
|
// directory, so this needs write access there on top of the FFI
|
|
// permission checked above. The check does not depend on whether the
|
|
// image actually reaches the file system on this platform (Linux uses an
|
|
// anonymous memfd): what a program must be granted should not vary by
|
|
// platform.
|
|
THROW_IF_INSUFFICIENT_PERMISSIONS(
|
|
env,
|
|
permission::PermissionScope::kFileSystemWrite,
|
|
binding::AddonImage::TempDir());
|
|
ArrayBufferViewContents<char> binary(args[1]);
|
|
if (!image.Materialize(binary.data(), binary.length())) {
|
|
THROW_ERR_FFI_CALL_FAILED(
|
|
env, "dlopen failed: %s: %s", image.errmsg().c_str(), library_path);
|
|
return;
|
|
}
|
|
library_path = image.path().c_str();
|
|
}
|
|
|
|
CHECK(lib->is_closed());
|
|
// Open the library
|
|
const bool opened = uv_dlopen(library_path, &lib->lib_) == 0;
|
|
image.AfterOpen(opened,
|
|
opened ? static_cast<void*>(lib->lib_.handle) : nullptr);
|
|
if (!opened) {
|
|
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));
|
|
}
|
|
|
|
fn->Invoke(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;
|
|
|
|
fn->Invoke(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);
|
|
|
|
// libffi always points `args[i]` at its own storage for the value of
|
|
// argument `i`, so the slot pointers themselves are never null. A NULL
|
|
// pointer argument surfaces as the BigInt `0n` via ToJSArgument.
|
|
for (size_t i = 0; i < expected_args; i++) {
|
|
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->SetPrototypeV2(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->SetPrototypeV2(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;
|
|
}
|
|
|
|
// The callback function may already have been collected after a previous
|
|
// unrefCallback() call. The persistent handle is empty in that case, and
|
|
// ClearWeak() on an empty handle dereferences a null slot. There is also no
|
|
// function left to make strong again.
|
|
if (existing->second->fn.IsEmpty()) {
|
|
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;
|
|
}
|
|
|
|
// The callback function may already have been collected by a previous
|
|
// unrefCallback() call. The persistent handle is empty in that case, and
|
|
// SetWeak() on an empty handle dereferences a null slot.
|
|
if (existing->second->fn.IsEmpty()) {
|
|
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->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "DynamicLibrary"));
|
|
tmpl->InstanceTemplate()->SetInternalFieldCount(
|
|
DynamicLibrary::kInternalFieldCount);
|
|
Local<Signature> signature = Signature::New(isolate, tmpl);
|
|
|
|
tmpl->InstanceTemplate()->SetAccessorProperty(
|
|
env->path_string(),
|
|
FunctionTemplate::New(
|
|
isolate, DynamicLibrary::GetPath, Local<Value>(), signature),
|
|
Local<FunctionTemplate>(),
|
|
attributes);
|
|
|
|
tmpl->InstanceTemplate()->SetAccessorProperty(
|
|
FIXED_ONE_BYTE_STRING(isolate, "symbols"),
|
|
FunctionTemplate::New(
|
|
isolate, DynamicLibrary::GetSymbols, Local<Value>(), signature),
|
|
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(
|
|
isolate, DynamicLibrary::GetFunctions, Local<Value>(), signature),
|
|
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;
|
|
}
|
|
|
|
void GetCurrentEventLoop(const FunctionCallbackInfo<Value>& args) {
|
|
Environment* env = Environment::GetCurrent(args);
|
|
Isolate* isolate = env->isolate();
|
|
|
|
THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
|
|
|
|
args.GetReturnValue().Set(BigInt::NewFromUnsigned(
|
|
isolate,
|
|
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(env->event_loop()))));
|
|
}
|
|
|
|
// 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,
|
|
SetConstructorFunctionFlag::NONE);
|
|
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, "getCurrentEventLoop", GetCurrentEventLoop);
|
|
|
|
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 JavaScript fast paths use `uintptrMax` to reject pointer BigInts that
|
|
// would otherwise be silently truncated by V8 or, on 32-bit platforms, by
|
|
// `ReadFFIArgFromBuffer`'s `memcpy(..., type->size, ...)`. 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. These are created here
|
|
// (at runtime, on first `internalBinding('ffi')`) instead of being declared
|
|
// in env_properties.h, so they are not allocated while the startup snapshot
|
|
// is being built. Allocating Symbols during snapshot serialization advances
|
|
// the isolate's identity-hash RNG and shifts the identity hashes baked into
|
|
// the snapshot for Object.prototype / Function.prototype, which can make a
|
|
// function map and a plain-object map collide in V8's NormalizedMapCache.
|
|
if (env->ffi_fast_arguments_symbol().IsEmpty()) {
|
|
env->set_ffi_fast_arguments_symbol(v8::Symbol::New(
|
|
isolate, FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_arguments_symbol")));
|
|
}
|
|
if (env->ffi_fast_buffer_invoke_symbol().IsEmpty()) {
|
|
env->set_ffi_fast_buffer_invoke_symbol(v8::Symbol::New(
|
|
isolate,
|
|
FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_buffer_invoke_symbol")));
|
|
}
|
|
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
|