blob: 7e8d182a7bdc8786fba20db1b1e158911e4ae658 [file] [edit]
#if HAVE_FFI
#include "ffi/fast.h"
#include "env-inl.h"
#include "ffi/jit_memory.h"
#include "ffi/types.h"
#include "node_errors.h"
#include "node_ffi.h"
#include <climits>
#include <limits>
namespace node::ffi {
using v8::CFunctionInfo;
using v8::CTypeInfo;
using v8::FastApiCallbackOptions;
bool IsTypeName(std::string_view type,
std::initializer_list<std::string_view> names) {
// Signature parsing accepts several public aliases for the same ABI type.
// The fast path normalizes them by checking the original type name against
// each alias set before selecting a FastFFIType.
for (std::string_view name : names) {
if (type == name) {
return true;
}
}
return false;
}
bool FastScalarTypeFromName(std::string_view type, FastFFIType* out) {
// Scalars can be passed directly through V8 Fast API CFunction metadata.
// Pointer-like types are represented as uintptr-sized unsigned integers;
// JavaScript wrappers handle strings and object-to-pointer conversions.
if (type == "void") {
*out = FastFFIType::kVoid;
} else if (type == "bool") {
*out = FastFFIType::kBool;
} else if (IsTypeName(type, {"i8", "int8"})) {
*out = FastFFIType::kInt8;
} else if (IsTypeName(type, {"u8", "uint8"})) {
*out = FastFFIType::kUint8;
} else if (type == "char") {
*out = CHAR_MIN < 0 ? FastFFIType::kInt8 : FastFFIType::kUint8;
} else if (IsTypeName(type, {"i16", "int16"})) {
*out = FastFFIType::kInt16;
} else if (IsTypeName(type, {"u16", "uint16"})) {
*out = FastFFIType::kUint16;
} else if (IsTypeName(type, {"i32", "int32"})) {
*out = FastFFIType::kInt32;
} else if (IsTypeName(type, {"u32", "uint32"})) {
*out = FastFFIType::kUint32;
} else if (IsTypeName(type, {"i64", "int64"})) {
*out = FastFFIType::kInt64;
} else if (IsTypeName(type, {"u64", "uint64"})) {
*out = FastFFIType::kUint64;
} else if (IsTypeName(type, {"f32", "float", "float32"})) {
*out = FastFFIType::kFloat32;
} else if (IsTypeName(type, {"f64", "double", "float64"})) {
*out = FastFFIType::kFloat64;
} else if (IsTypeName(type, {"buffer", "arraybuffer"})) {
*out = FastFFIType::kPointer;
} else if (IsTypeName(type,
{"pointer", "ptr", "string", "str", "function"})) {
*out = FastFFIType::kPointer;
} else {
return false;
}
return true;
}
bool FastArgTypeFromName(std::string_view type, FastFFIType* out) {
// Buffer and ArrayBuffer parameters are special only for arguments: V8 passes
// the original JS value so the generated trampoline can extract its backing
// store address immediately before calling the native target.
if (IsTypeName(type, {"buffer", "arraybuffer"})) {
*out = FastFFIType::kBuffer;
return true;
}
return FastScalarTypeFromName(type, out);
}
bool NeedsBigIntRepresentation(FastFFIType type) {
// V8's Fast API has one int64 representation per CFunction. Any pointer or
// 64-bit integer in the signature forces BigInt to avoid precision loss.
return type == FastFFIType::kInt64 || type == FastFFIType::kUint64 ||
type == FastFFIType::kPointer;
}
CTypeInfo::Type ToV8Type(FastFFIType type, bool is_return) {
// Map the internal FFI type model onto the limited set of CFunctionInfo
// types that V8 can call directly. Narrow integer arguments are widened in
// the V8 signature and then narrowed again by the trampoline.
switch (type) {
case FastFFIType::kVoid:
return CTypeInfo::Type::kVoid;
case FastFFIType::kBool:
return CTypeInfo::Type::kBool;
case FastFFIType::kUint8:
return CTypeInfo::Type::kUint32;
case FastFFIType::kInt8:
case FastFFIType::kInt16:
return CTypeInfo::Type::kInt32;
case FastFFIType::kUint16:
return CTypeInfo::Type::kUint32;
case FastFFIType::kInt32:
return CTypeInfo::Type::kInt32;
case FastFFIType::kUint32:
return CTypeInfo::Type::kUint32;
case FastFFIType::kInt64:
return CTypeInfo::Type::kInt64;
case FastFFIType::kUint64:
case FastFFIType::kPointer:
return CTypeInfo::Type::kUint64;
case FastFFIType::kBuffer:
return CTypeInfo::Type::kV8Value;
case FastFFIType::kFloat32:
return CTypeInfo::Type::kFloat32;
case FastFFIType::kFloat64:
return CTypeInfo::Type::kFloat64;
}
UNREACHABLE();
}
uintptr_t PointerFromValue(v8::Local<v8::Value> value) {
// ArrayBufferViews point at the backing store plus their byte offset, while
// ArrayBuffer and SharedArrayBuffer point at the start of their store.
if (value->IsArrayBufferView()) {
v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
void* data = view->Buffer()->Data();
return reinterpret_cast<uintptr_t>(static_cast<char*>(data) +
view->ByteOffset());
}
if (value->IsArrayBuffer()) {
return reinterpret_cast<uintptr_t>(value.As<v8::ArrayBuffer>()->Data());
}
if (value->IsSharedArrayBuffer()) {
std::shared_ptr<v8::BackingStore> store =
value.As<v8::SharedArrayBuffer>()->GetBackingStore();
return reinterpret_cast<uintptr_t>(store->Data());
}
return std::numeric_limits<uintptr_t>::max();
}
bool SignatureNeedsRawPointerConversions(const FFIFunction& fn) {
// These public types accept JS values that are not raw BigInt pointers. When
// a signature contains them, JS wraps the fast function to perform the
// conversion before V8 enters the CFunction trampoline.
for (const std::string& name : fn.arg_type_names) {
if (name == "buffer" || name == "arraybuffer" || name == "string" ||
name == "str") {
return true;
}
}
return false;
}
bool IsPointerTypeName(const std::string& name) {
// `pointer`, `ptr`, and `function` all use the same uintptr ABI slot; only
// the public type spelling differs.
return name == "pointer" || name == "ptr" || name == "function";
}
bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
// The secondary buffer invoke is only generated for the hot monomorphic case
// where a single pointer-like argument can be satisfied by a Buffer or
// ArrayBuffer without allocating or caching a BigInt pointer in JS.
return fn.arg_type_names.size() == 1 &&
IsPointerTypeName(fn.arg_type_names[0]);
}
std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// argument as `buffer` so CreateFastFFIMetadata() emits a trampoline that
// receives a V8 value and calls node_ffi_fast_buffer_data().
auto clone = std::make_shared<FFIFunction>(*fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
}
}
return clone;
}
extern "C" uintptr_t node_ffi_fast_buffer_data(v8::Local<v8::Value> value,
FastApiCallbackOptions* options,
uint32_t index) {
// The generated trampoline treats this sentinel as "conversion failed" and
// returns zero after throwing, preventing the native target from seeing an
// invalid pointer value.
constexpr uintptr_t kInvalidBuffer = std::numeric_limits<uintptr_t>::max();
// Accept only memory-backed JS values in the native helper. Other pointer
// conversions, including strings, stay in the JS wrapper so their temporary
// lifetime is explicit.
if (value->IsArrayBufferView()) {
return PointerFromValue(value);
}
if (value->IsArrayBuffer() || value->IsSharedArrayBuffer()) {
return PointerFromValue(value);
}
v8::Isolate* isolate = options != nullptr ? options->isolate : nullptr;
if (isolate != nullptr) {
THROW_ERR_INVALID_ARG_VALUE(
isolate, "Argument %u must be a buffer or an ArrayBuffer", index);
}
return kInvalidBuffer;
}
FastFFIMetadata::~FastFFIMetadata() {
// Metadata owns executable memory through `trampoline`; releasing it here
// ties code lifetime to the V8 function's weak FFIFunctionInfo cleanup.
node_ffi_free_fast_trampoline(&trampoline);
}
bool IsFastCallSupported() {
// Fast call requires both a platform stub emitter and working JIT memory.
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__x86_64__)
return IsJitMemorySupported();
#else
return false;
#endif
}
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn) {
// Bail early if executable memory allocation doesn't work on this process
// (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.).
// The self-test runs once and caches the result.
if (!IsJitMemorySupported()) {
return nullptr;
}
// Check signature-level eligibility (type checks, register caps, platform
// support). Returning nullptr here lets the caller fall back to SharedBuffer
// or the generic libffi path.
const char* eligibility_reason;
if (!IsFastCallEligible(fn, &eligibility_reason)) {
return nullptr;
}
// Reject unsupported result types first. Returning nullptr means the caller
// can still fall back to SharedBuffer or the generic libffi path.
FastFFIType result;
if (!FastScalarTypeFromName(fn.return_type_name, &result)) {
return nullptr;
}
if (fn.args.size() != fn.arg_type_names.size()) {
return nullptr;
}
// Keep the initial Fast API implementation bounded to signatures V8 and the
// platform trampolines can describe without stack argument support.
if (fn.arg_type_names.size() > 8) {
return nullptr;
}
std::vector<FastFFIType> args;
args.reserve(fn.arg_type_names.size());
bool needs_bigint = NeedsBigIntRepresentation(result);
bool needs_callback_options = false;
// Normalize public argument names into FastFFIType values while collecting
// signature-wide flags required by V8 CFunctionInfo.
for (const std::string& name : fn.arg_type_names) {
FastFFIType type;
if (!FastArgTypeFromName(name, &type)) {
return nullptr;
}
if (type == FastFFIType::kVoid) {
return nullptr;
}
needs_bigint = needs_bigint || NeedsBigIntRepresentation(type);
needs_callback_options =
needs_callback_options || type == FastFFIType::kBuffer;
args.push_back(type);
}
auto metadata = std::make_unique<FastFFIMetadata>();
// The platform-specific trampoline is the executable entrypoint V8 calls.
// If the platform rejects the signature, the whole fast metadata object is
// discarded and the caller chooses another invocation path.
if (!node_ffi_create_fast_trampoline(
fn.ptr, args.data(), args.size(), result, &metadata->trampoline)) {
return nullptr;
}
metadata->arg_info.reserve(args.size() + 1);
// Fast API callbacks include the JS receiver as an implicit first argument,
// so the generated CFunctionInfo begins with a V8Value placeholder for it.
metadata->arg_info.emplace_back(CTypeInfo::Type::kV8Value);
for (FastFFIType arg : args) {
metadata->arg_info.emplace_back(ToV8Type(arg, false));
}
// Buffer arguments require FastApiCallbackOptions so the helper can throw on
// invalid inputs and report the correct argument index.
if (needs_callback_options) {
metadata->arg_info.emplace_back(CTypeInfo::kCallbackOptionsType);
}
// CFunctionInfo stores pointers into `arg_info`; both are held by
// FastFFIMetadata so they remain valid for the generated function lifetime.
metadata->c_function_info = std::make_unique<CFunctionInfo>(
CTypeInfo(ToV8Type(result, true)),
metadata->arg_info.size(),
metadata->arg_info.data(),
needs_bigint ? CFunctionInfo::Int64Representation::kBigInt
: CFunctionInfo::Int64Representation::kNumber);
metadata->c_function =
v8::CFunction(metadata->trampoline.code, metadata->c_function_info.get());
return metadata;
}
} // namespace node::ffi
#endif // HAVE_FFI