Skip to content

Commit 041c282

Browse files
committed
ffi: add fast-call signature eligibility check
IsFastCallEligible() validates at parse time whether a signature can use the fast-call path, covering: - Return and argument type eligibility (numeric, pointer; no structs) - Argument count cap (8, matching V8 fast-call limit) - Per-ABI register pressure limits (AArch64, x86_64 SysV, Win64) - AArch64 constraint: buffer/float args cannot coexist Returns nullptr from CreateFastFFIMetadata() for ineligible signatures, falling back to libffi. Signed-off-by: Bryan English <bryan@bryanenglish.com>
1 parent ffedb7d commit 041c282

2 files changed

Lines changed: 150 additions & 0 deletions

File tree

src/ffi/types.cc

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,144 @@ bool SignaturesMatch(const FFIFunction& fn,
230230
return true;
231231
}
232232

233+
namespace {
234+
235+
bool IsFastCallEligibleFFIType(ffi_type* type) {
236+
// Accept all numeric types, pointer, and void (void OK as return only).
237+
// Rejects struct types (not yet supported in fast-call path).
238+
return type == &ffi_type_void || type == &ffi_type_sint8 ||
239+
type == &ffi_type_uint8 || type == &ffi_type_sint16 ||
240+
type == &ffi_type_uint16 || type == &ffi_type_sint32 ||
241+
type == &ffi_type_uint32 || type == &ffi_type_sint64 ||
242+
type == &ffi_type_uint64 || type == &ffi_type_float ||
243+
type == &ffi_type_double || type == &ffi_type_pointer;
244+
}
245+
246+
bool IsFunctionTypeName(const std::string& name) {
247+
return name == "function";
248+
}
249+
250+
// Check if an FFI type occupies a floating-point register.
251+
bool IsFFITypeFloat(ffi_type* type) {
252+
return type == &ffi_type_float || type == &ffi_type_double;
253+
}
254+
255+
// Check if an FFI type name maps to a kBuffer (kV8Value) argument in the
256+
// fast-call path, which consumes an extra GP register for the helper call.
257+
bool IsBufferTypeName(const std::string& name) {
258+
return name == "buffer" || name == "arraybuffer";
259+
}
260+
261+
} // namespace
262+
263+
bool IsFastCallEligible(const FFIFunction& fn, const char** out_reason) {
264+
static const char* dummy = "";
265+
if (out_reason == nullptr) out_reason = &dummy;
266+
267+
// Check that a platform stub emitter exists for the current ABI.
268+
// Stub emitters cover AArch64 (Linux/macOS/FreeBSD/Windows) and
269+
// x86_64 (SysV: Linux/macOS/FreeBSD, Win64: Windows). Other platforms
270+
// fall back to libffi.
271+
#if !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__x86_64__)
272+
*out_reason = "no platform stub emitter";
273+
return false;
274+
#endif
275+
276+
// Check return type eligibility.
277+
if (!IsFastCallEligibleFFIType(fn.return_type)) {
278+
*out_reason = "unsupported return type";
279+
return false;
280+
}
281+
if (IsFunctionTypeName(fn.return_type_name)) {
282+
*out_reason = "return type is function";
283+
return false;
284+
}
285+
286+
// V8's fast-call lowering caps the C-side arg count. With HasReceiver=kNo
287+
// there's no implicit receiver in the count, so this is the user-arg cap.
288+
// V8's hard limit is 8 args; signatures over that fall back to libffi.
289+
if (fn.args.size() > 8) {
290+
*out_reason = "argument count exceeds V8 fast-call cap";
291+
return false;
292+
}
293+
294+
// Per-ABI register caps for arguments that must be passed in registers.
295+
// If an arg can't fit in a register, it goes on the stack — which the
296+
// current trampoline generators don't support.
297+
size_t gp_count = 0;
298+
size_t fp_count = 0;
299+
bool has_buffer_arg = false;
300+
for (size_t i = 0; i < fn.args.size(); ++i) {
301+
ffi_type* t = fn.args[i];
302+
const std::string& name = fn.arg_type_names[i];
303+
304+
if (!IsFastCallEligibleFFIType(t)) {
305+
*out_reason = "unsupported arg type";
306+
return false;
307+
}
308+
// `void` is fine as a return type but has no register slot, so it cannot
309+
// appear in `args`.
310+
if (t == &ffi_type_void) {
311+
*out_reason = "void cannot be an argument type";
312+
return false;
313+
}
314+
if (IsFunctionTypeName(name)) {
315+
*out_reason = "arg is function";
316+
return false;
317+
}
318+
319+
// `buffer`/`arraybuffer` args arrive as kV8Value in the V8 fast-call
320+
// signature, consuming an extra GP register for the helper call.
321+
if (IsBufferTypeName(name)) {
322+
has_buffer_arg = true;
323+
}
324+
325+
// Count register classes used by each argument.
326+
if (IsFFITypeFloat(t)) {
327+
fp_count++;
328+
} else {
329+
gp_count++;
330+
}
331+
}
332+
333+
// Platform-specific register pressure limits.
334+
#if defined(__aarch64__) || defined(_M_ARM64)
335+
// AArch64: 8 FP registers (v0-v7) + up to 7 GP registers per trampoline
336+
// constraint (the 8th GP slot is consumed by the helper call for buffer
337+
// args). Buffer args and float args can't coexist — the helper call would
338+
// clobber FP state.
339+
const size_t effective_gp = gp_count + (has_buffer_arg ? 1 : 0);
340+
if (has_buffer_arg && fp_count != 0) {
341+
*out_reason = "buffer and float args cannot coexist on AArch64";
342+
return false;
343+
}
344+
if (effective_gp > 7 || fp_count > 8) {
345+
*out_reason = "argument count exceeds AArch64 register limit";
346+
return false;
347+
}
348+
#elif defined(__x86_64__)
349+
#if defined(_WIN32)
350+
// x86_64 Win64: 4 register slots total (rcx, rdx, r8, r9 for GP;
351+
// xmm0-xmm3 for FP). GP + FP share the positional limit — once 4 slots
352+
// are consumed, remaining args go to the stack.
353+
if (gp_count + fp_count > 3) {
354+
*out_reason = "argument count exceeds x86_64 Win64 register limit";
355+
return false;
356+
}
357+
#else
358+
// x86_64 SysV: 6 GP registers (rdi, rsi, rdx, rcx, r8, r9) + 8 FP
359+
// registers (xmm0-xmm7).
360+
if (gp_count > 6 || fp_count > 8) {
361+
*out_reason = "argument count exceeds x86_64 SysV register limit";
362+
return false;
363+
}
364+
#endif // _WIN32
365+
#endif // __x86_64__
366+
367+
*out_reason = "";
368+
return true;
369+
}
370+
233371
bool IsSBEligibleFFIType(ffi_type* type) {
234372
return type == &ffi_type_void || type == &ffi_type_sint8 ||
235373
type == &ffi_type_uint8 || type == &ffi_type_sint16 ||

src/ffi/types.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ bool SignaturesMatch(const FFIFunction& fn,
5353
ffi_type* return_type,
5454
const std::vector<ffi_type*>& args);
5555

56+
// Returns true if `fn` can be invoked via the V8 fast-call path. On
57+
// false, `*out_reason` is set to a static string describing why
58+
// (never null after this returns; callers may pass nullptr to ignore).
59+
//
60+
// Eligibility checks: every arg type and the return type are
61+
// numeric-or-pointer, no `function`-typed args/return, arg count
62+
// within V8 fast-call cap (8), and register-passed arg counts within
63+
// per-ABI limits (AArch64: ≤ 7 GP + ≤ 8 FP, x86_64 SysV: ≤ 6 GP + ≤ 8 FP,
64+
// x86_64 Win64: GP+FP ≤ 3). Also checks that a platform stub emitter
65+
// exists for the current ABI.
66+
bool IsFastCallEligible(const FFIFunction& fn, const char** out_reason);
67+
5668
// True if the FFI type can be read from / written to a raw byte buffer
5769
// without needing V8 operations (conversion, allocation, etc.).
5870
bool IsSBEligibleFFIType(ffi_type* type);

0 commit comments

Comments
 (0)