Skip to content

Commit 56ffab4

Browse files
committed
deps: patch v8 fast-call to support no-receiver mode
Adds CFunctionInfo::HasReceiver = kNo. When set, V8's TurboFan and Turboshaft fast-call lowering omits the JS receiver from the C call — the C function pointer is invoked with user args only, no receiver in the first parameter register. For Node's FFI fast-call path this means the receiver-strip JIT stub is no longer needed: dlsym'd target functions can be registered directly with V8 as the C address. Eliminates ~7 instructions per call (AArch64) plus V8's own receiver-into-arg0 setup. Yields +3-24% over the prior validators+stub path on FFI microbenchmarks (largest gains on many-args and pointer-bigint), and beats the pre-fix silent-truncation thin wrapper on every numeric and pointer benchmark while preserving strict validation. The change is gated by an enum on CFunctionInfo (default kYes, backward-compatible). Existing fast-call users (DOM bindings, V8 internals) are unaffected. Patches in deps/v8 cover the API header, the constructor, the GetFastApiCallTarget overload-matching, and the js-call-reducer input-layout setup; the simplified-lowering and turboshaft graph-builder loops already iterate from input 0 over ArgumentCount() inputs and pick up the new layout automatically. Signed-off-by: Bryan English <bryan@bryanenglish.com>
1 parent c308d4d commit 56ffab4

7 files changed

Lines changed: 132 additions & 137 deletions

File tree

deps/v8/include/v8-fast-api-calls.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,28 @@ class V8_EXPORT CFunctionInfo {
308308
kBigInt = 1, // Use BigInts to represent 64 bit integers.
309309
};
310310

311+
// Whether the C function takes a JS receiver as its first argument.
312+
// Most fast-call C functions do (matching how V8 wires up FunctionTemplate
313+
// callbacks). Embedders that want to register a plain C function pointer
314+
// — e.g. an FFI dispatcher that has no use for the receiver — can set this
315+
// to kNo. In that mode V8 omits the receiver from the C call: arg_info[0]
316+
// is the first user argument, ArgumentCount() returns the user-arg count,
317+
// and the JS receiver value is discarded by the lowering instead of being
318+
// passed in the first parameter register.
319+
enum class HasReceiver : uint8_t {
320+
kYes = 0,
321+
kNo = 1,
322+
};
323+
311324
// Construct a struct to hold a CFunction's type information.
312325
// |return_info| describes the function's return type.
313326
// |arg_info| is an array of |arg_count| CTypeInfos describing the
314327
// arguments. Only the last argument may be of the special type
315328
// CTypeInfo::kCallbackOptionsType.
316329
CFunctionInfo(const CTypeInfo& return_info, unsigned int arg_count,
317330
const CTypeInfo* arg_info,
318-
Int64Representation repr = Int64Representation::kNumber);
331+
Int64Representation repr = Int64Representation::kNumber,
332+
HasReceiver has_receiver = HasReceiver::kYes);
319333

320334
const CTypeInfo& ReturnInfo() const { return return_info_; }
321335

@@ -327,6 +341,8 @@ class V8_EXPORT CFunctionInfo {
327341

328342
Int64Representation GetInt64Representation() const { return repr_; }
329343

344+
bool HasReceiverArg() const { return has_receiver_ == HasReceiver::kYes; }
345+
330346
// |index| must be less than ArgumentCount().
331347
// Note: if the last argument passed on construction of CFunctionInfo
332348
// has type CTypeInfo::kCallbackOptionsType, it is not included in
@@ -342,6 +358,7 @@ class V8_EXPORT CFunctionInfo {
342358
private:
343359
const CTypeInfo return_info_;
344360
const Int64Representation repr_;
361+
const HasReceiver has_receiver_;
345362
const unsigned int arg_count_;
346363
const CTypeInfo* arg_info_;
347364
};

deps/v8/src/api/api.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11992,9 +11992,11 @@ CFunction::CFunction(const void* address, const CFunctionInfo* type_info)
1199211992

1199311993
CFunctionInfo::CFunctionInfo(const CTypeInfo& return_info,
1199411994
unsigned int arg_count, const CTypeInfo* arg_info,
11995-
Int64Representation repr)
11995+
Int64Representation repr,
11996+
HasReceiver has_receiver)
1199611997
: return_info_(return_info),
1199711998
repr_(repr),
11999+
has_receiver_(has_receiver),
1199812000
arg_count_(arg_count),
1199912001
arg_info_(arg_info) {
1200012002
DCHECK(repr == Int64Representation::kNumber ||

deps/v8/src/compiler/fast-api-calls.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,14 @@ FastApiCallFunction GetFastApiCallTarget(
385385
function_template_info.c_signatures(broker);
386386
const size_t overloads_count = signatures.size();
387387

388-
// Only considers entries whose type list length matches arg_count.
388+
// Only considers entries whose type list length matches arg_count. For
389+
// signatures registered with HasReceiver=kNo, the C-side ArgumentCount
390+
// already excludes the receiver, so we don't subtract it here.
389391
for (size_t i = 0; i < overloads_count; i++) {
390392
const CFunctionInfo* c_signature = signatures[i];
391-
const size_t len = c_signature->ArgumentCount() - kReceiver;
393+
const size_t len =
394+
c_signature->ArgumentCount() -
395+
(c_signature->HasReceiverArg() ? kReceiver : 0);
392396
bool optimize_to_fast_call =
393397
(len == arg_count) &&
394398
fast_api_call::CanOptimizeFastSignature(c_signature);

deps/v8/src/compiler/js-call-reducer.cc

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,9 @@ class FastApiCallReducerAssembler : public JSCallReducerAssembler {
662662
// arguments, so extract c_argument_count from the first function.
663663
const int c_argument_count =
664664
static_cast<int>(c_function_.signature->ArgumentCount());
665-
CHECK_GE(c_argument_count, kReceiver);
665+
if (c_function_.signature->HasReceiverArg()) {
666+
CHECK_GE(c_argument_count, kReceiver);
667+
}
666668

667669
const int slow_arg_count =
668670
// Arguments for CallApiCallbackOptimizedXXX builtin including
@@ -677,11 +679,16 @@ class FastApiCallReducerAssembler : public JSCallReducerAssembler {
677679
base::SmallVector<Node*, kInlineSize> inputs(value_input_count +
678680
kEffectAndControl);
679681
int cursor = 0;
680-
inputs[cursor++] = n.receiver();
682+
const bool has_receiver_arg =
683+
c_function_.signature->HasReceiverArg();
684+
if (has_receiver_arg) {
685+
inputs[cursor++] = n.receiver();
686+
}
681687

682688
// TODO(turbofan): Consider refactoring CFunctionInfo to distinguish
683689
// between receiver and arguments, simplifying this (and related) spots.
684-
int js_args_count = c_argument_count - kReceiver;
690+
int js_args_count =
691+
c_argument_count - (has_receiver_arg ? kReceiver : 0);
685692
for (int i = 0; i < js_args_count; ++i) {
686693
if (i < n.ArgumentCount()) {
687694
inputs[cursor++] = n.Argument(i);

src/ffi/fastcall/cfunction_info.cc

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -91,30 +91,25 @@ CFunctionInfoBundle& CFunctionInfoBundle::operator=(
9191
}
9292

9393
CFunctionInfoBundle BuildCFunctionInfo(const FFIFunction& fn) {
94-
using T = v8::CTypeInfo::Type;
9594
static_assert(std::is_trivially_destructible_v<v8::CTypeInfo>,
9695
"CTypeInfo must be trivially destructible for placement-new "
9796
"array without explicit element destruction");
9897
CFunctionInfoBundle b;
9998

100-
// V8 wants the receiver as arg[0]. Total CTypeInfo entries = args + 1.
101-
// CTypeInfo has no default constructor, so we allocate raw storage and
102-
// placement-new each element individually.
103-
size_t n = fn.args.size() + 1;
104-
void* raw = ::operator new[](n * sizeof(v8::CTypeInfo));
105-
b.arg_types = static_cast<v8::CTypeInfo*>(raw);
106-
// Placement-new the receiver slot.
107-
new (&b.arg_types[0]) v8::CTypeInfo(T::kV8Value);
108-
// Placement-new the arg slots with a placeholder; overwritten below.
109-
for (size_t i = 1; i < n; ++i) {
110-
new (&b.arg_types[i]) v8::CTypeInfo(T::kVoid);
111-
}
112-
113-
b.arg_classes.reserve(fn.args.size());
114-
for (size_t i = 0; i < fn.args.size(); ++i) {
115-
auto m = MapArgType(fn.args[i]);
116-
b.arg_types[i + 1] = v8::CTypeInfo(m.ctype);
117-
b.arg_classes.push_back(m.cls);
99+
// We register the C function with HasReceiver=kNo, so V8 does not pass a
100+
// JS receiver in the first parameter register. The CTypeInfo[] holds only
101+
// user-arg types — no receiver slot. CTypeInfo has no default constructor,
102+
// so we allocate raw storage and placement-new each element.
103+
size_t n = fn.args.size();
104+
if (n > 0) {
105+
void* raw = ::operator new[](n * sizeof(v8::CTypeInfo));
106+
b.arg_types = static_cast<v8::CTypeInfo*>(raw);
107+
b.arg_classes.reserve(n);
108+
for (size_t i = 0; i < n; ++i) {
109+
auto m = MapArgType(fn.args[i]);
110+
new (&b.arg_types[i]) v8::CTypeInfo(m.ctype);
111+
b.arg_classes.push_back(m.cls);
112+
}
118113
}
119114

120115
auto rm = MapResultType(fn.return_type);
@@ -125,7 +120,8 @@ CFunctionInfoBundle BuildCFunctionInfo(const FFIFunction& fn) {
125120
return_info,
126121
static_cast<unsigned int>(n),
127122
b.arg_types,
128-
v8::CFunctionInfo::Int64Representation::kBigInt);
123+
v8::CFunctionInfo::Int64Representation::kBigInt,
124+
v8::CFunctionInfo::HasReceiver::kNo);
129125

130126
return b;
131127
}

src/node_ffi.cc

Lines changed: 76 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -319,118 +319,85 @@ MaybeLocal<Function> DynamicLibrary::CreateFunction(
319319
if (fc_ok) {
320320
auto bundle = node::ffi::fastcall::BuildCFunctionInfo(*fn);
321321
{
322-
auto stub = node::ffi::fastcall::EmitForwarder(
323-
isolate, fn->ptr, bundle.arg_classes, bundle.result_class);
324-
if (!stub.has_value()) {
325-
// Warn at most once per process: a single FFI app may register hundreds
326-
// of functions, and the failure cause (mprotect / SELinux / hardened
327-
// runtime) is the same for every call. Repeated warnings would flood
328-
// logs without adding signal.
329-
static std::atomic<bool> warned{false};
330-
if (!warned.exchange(true, std::memory_order_acq_rel)) {
331-
if (ProcessEmitWarning(env,
332-
"FFI fast-call stub emission failed for an eligible "
333-
"signature; falling back to libffi for this and possibly "
334-
"future calls. This usually indicates a JIT memory or "
335-
"permission issue (mprotect/SELinux/hardened runtime).")
336-
.IsNothing()) {
337-
return MaybeLocal<Function>();
338-
}
322+
// Register the dlsym'd target function pointer directly with V8 as the
323+
// C function. With CFunctionInfo built using HasReceiver=kNo, V8's fast-
324+
// call lowering omits the JS receiver from the C call — so no receiver-
325+
// strip stub is needed. The target's plain C signature matches the
326+
// CFunctionInfo exactly.
327+
v8::CFunction cfun(fn->ptr, bundle.info);
328+
v8::Local<v8::FunctionTemplate> tmpl = v8::FunctionTemplate::New(
329+
isolate,
330+
DynamicLibrary::InvokeFunction,
331+
data,
332+
v8::Local<v8::Signature>(),
333+
static_cast<int>(fn->args.size()),
334+
v8::ConstructorBehavior::kThrow,
335+
v8::SideEffectType::kHasSideEffect,
336+
&cfun);
337+
v8::Local<v8::Function> fc_fn;
338+
if (tmpl->GetFunction(context).ToLocal(&fc_fn)) {
339+
bool metadata_ok = true;
340+
341+
v8::Local<v8::ArrayBuffer> alive_ab = AliveBuffer(isolate);
342+
if (!fc_fn->DefineOwnProperty(
343+
context, env->ffi_fastcall_alive_symbol(),
344+
alive_ab, internal_attrs)
345+
.FromMaybe(false)) {
346+
metadata_ok = false;
339347
}
340-
// fall through to libffi path
341-
} else {
342-
v8::CFunction cfun(stub->entry, bundle.info);
343-
v8::Local<v8::FunctionTemplate> tmpl = v8::FunctionTemplate::New(
344-
isolate,
345-
DynamicLibrary::InvokeFunction,
346-
data,
347-
v8::Local<v8::Signature>(),
348-
static_cast<int>(fn->args.size()),
349-
v8::ConstructorBehavior::kThrow,
350-
v8::SideEffectType::kHasSideEffect,
351-
&cfun);
352-
v8::Local<v8::Function> fc_fn;
353-
if (tmpl->GetFunction(context).ToLocal(&fc_fn)) {
354-
// Build all V8 values before touching `info` fields, so that on
355-
// any failure we can free the stub and return cleanly without
356-
// leaking JIT memory.
357-
bool metadata_ok = true;
358-
359-
// Alive ArrayBuffer.
360-
v8::Local<v8::ArrayBuffer> alive_ab = AliveBuffer(isolate);
361-
if (!fc_fn->DefineOwnProperty(
362-
context, env->ffi_fastcall_alive_symbol(),
363-
alive_ab, internal_attrs)
364-
.FromMaybe(false)) {
365-
metadata_ok = false;
366-
}
367-
368-
// Build the slow-invoke v8::Function for the wrapper's pointer
369-
// fallback path.
370-
Local<Function> slow_fn;
371-
if (metadata_ok &&
372-
!Function::New(context, DynamicLibrary::InvokeFunction, data)
373-
.ToLocal(&slow_fn)) {
374-
metadata_ok = false;
375-
}
376-
if (metadata_ok &&
377-
!fc_fn->DefineOwnProperty(
378-
context, env->ffi_fastcall_invoke_slow_symbol(),
379-
slow_fn, internal_attrs)
380-
.FromMaybe(false)) {
381-
metadata_ok = false;
382-
}
383-
384-
// Attach params and result type names.
385-
Local<Value> params_arr;
386-
if (metadata_ok &&
387-
!ToV8Value(context, fn->arg_type_names, isolate)
388-
.ToLocal(&params_arr)) {
389-
metadata_ok = false;
390-
}
391-
if (metadata_ok &&
392-
!fc_fn->DefineOwnProperty(
393-
context, env->ffi_fastcall_params_symbol(),
394-
params_arr, internal_attrs)
395-
.FromMaybe(false)) {
396-
metadata_ok = false;
397-
}
398-
Local<Value> result_str;
399-
if (metadata_ok &&
400-
!ToV8Value(context, fn->return_type_name, isolate)
401-
.ToLocal(&result_str)) {
402-
metadata_ok = false;
403-
}
404-
if (metadata_ok &&
405-
!fc_fn->DefineOwnProperty(
406-
context, env->ffi_fastcall_result_symbol(),
407-
result_str, internal_attrs)
408-
.FromMaybe(false)) {
409-
metadata_ok = false;
410-
}
411-
412-
if (!metadata_ok) {
413-
// Free the stub to avoid a JIT memory leak.
414-
node::ffi::fastcall::JitMemory::Get(isolate)
415-
->Free(*stub);
416-
return MaybeLocal<Function>();
417-
}
418-
419-
// All metadata attached successfully. Populate fast-call state.
420-
info->fast = std::make_unique<FastCallState>(
421-
isolate, slow_fn, stub->entry, stub->alloc_size,
422-
std::make_unique<node::ffi::fastcall::CFunctionInfoBundle>(
423-
std::move(bundle)));
424-
ret = fc_fn;
425-
} else {
426-
// Template-to-function failed (V8 has a pending exception). Free the
427-
// stub since we won't use it; mirror the metadata-fail path and
428-
// propagate the empty MaybeLocal so the caller surfaces the V8
429-
// exception.
430-
node::ffi::fastcall::JitMemory::Get(isolate)
431-
->Free(*stub);
348+
349+
Local<Function> slow_fn;
350+
if (metadata_ok &&
351+
!Function::New(context, DynamicLibrary::InvokeFunction, data)
352+
.ToLocal(&slow_fn)) {
353+
metadata_ok = false;
354+
}
355+
if (metadata_ok &&
356+
!fc_fn->DefineOwnProperty(
357+
context, env->ffi_fastcall_invoke_slow_symbol(),
358+
slow_fn, internal_attrs)
359+
.FromMaybe(false)) {
360+
metadata_ok = false;
361+
}
362+
363+
Local<Value> params_arr;
364+
if (metadata_ok &&
365+
!ToV8Value(context, fn->arg_type_names, isolate)
366+
.ToLocal(&params_arr)) {
367+
metadata_ok = false;
368+
}
369+
if (metadata_ok &&
370+
!fc_fn->DefineOwnProperty(
371+
context, env->ffi_fastcall_params_symbol(),
372+
params_arr, internal_attrs)
373+
.FromMaybe(false)) {
374+
metadata_ok = false;
375+
}
376+
Local<Value> result_str;
377+
if (metadata_ok &&
378+
!ToV8Value(context, fn->return_type_name, isolate)
379+
.ToLocal(&result_str)) {
380+
metadata_ok = false;
381+
}
382+
if (metadata_ok &&
383+
!fc_fn->DefineOwnProperty(
384+
context, env->ffi_fastcall_result_symbol(),
385+
result_str, internal_attrs)
386+
.FromMaybe(false)) {
387+
metadata_ok = false;
388+
}
389+
390+
if (!metadata_ok) {
432391
return MaybeLocal<Function>();
433392
}
393+
394+
info->fast = std::make_unique<FastCallState>(
395+
isolate, slow_fn, /*stub=*/nullptr, /*alloc_size=*/0,
396+
std::make_unique<node::ffi::fastcall::CFunctionInfoBundle>(
397+
std::move(bundle)));
398+
ret = fc_fn;
399+
} else {
400+
return MaybeLocal<Function>();
434401
}
435402
}
436403
}

test/cctest/test_ffi_fastcall_cfunction.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ TEST(FFIFastCallCFunction, NumericSignature) {
3333
{"i32", "i32"});
3434
auto bundle = BuildCFunctionInfo(fn);
3535
// Receiver + 2 args = 3 CTypeInfo entries.
36-
EXPECT_EQ(bundle.info->ArgumentCount(), 3u);
36+
// No-receiver mode: ArgumentCount counts user args only (no leading
37+
// v8::Value receiver slot).
38+
EXPECT_EQ(bundle.info->ArgumentCount(), 2u);
3739
EXPECT_EQ(bundle.arg_classes.size(), 2u);
3840
EXPECT_EQ(bundle.arg_classes[0], ArgClass::kGP);
3941
EXPECT_EQ(bundle.arg_classes[1], ArgClass::kGP);

0 commit comments

Comments
 (0)