Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions benchmark/ffi/invoke-function.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
'use strict';

const assert = require('node:assert');
const common = require('../common.js');
const { libraryPath, ensureFixtureLibrary } = require('./common.js');

// Measure the invocation (call) path for signatures that bypass V8 Fast API
// and use libffi through FFIFunction::Invoke(). On x86-64 System V with
// libffi >= 3.7, Invoke() reuses a precomputed call plan that avoids repeating
// argument-placement work on every call. This benchmark quantifies the
// per-call benefit.
//
// Signatures chosen to bypass both V8 Fast API and keep native work minimal:
// - call_int_callback (null): 'function' type forces the generic path; null
// pointer triggers the early return in C so native computation is negligible.
// From libffi's perspective this is a register-only plan (2 pointer-sized
// args both fit in GP registers on x86-64 System V).
// - sum_8_i32: 8 GP args exceed the x86-64 Fast API register cap (6), forcing
// the generic path. From libffi's perspective 6 args go in registers and 2
// spill to the stack, exercising a stack-spilled plan.

const bench = common.createBenchmark(main, {
n: [1e7],
symbol: ['call_int_callback', 'sum_8_i32'],
}, {
flags: ['--experimental-ffi', '--no-warnings'],
});

ensureFixtureLibrary();

function main({ n, symbol }) {
const ffi = require('node:ffi');

if (symbol === 'call_int_callback') {
// 'function' type bypasses Fast API (IsFastCallEligible rejects it).
// Pass 0n (null function pointer) so the native function returns -1
// immediately without invoking any callback, keeping per-call overhead
// dominated by the FFI call machinery itself.
const { lib, functions } = ffi.dlopen(libraryPath, {
call_int_callback: { return: 'i32', arguments: ['function', 'i32'] },
});

try {
// Verify the null-pointer early return.
assert.strictEqual(functions.call_int_callback(0n, 7), -1);

bench.start();
for (let i = 0; i < n; ++i)
functions.call_int_callback(0n, 21);
bench.end(n);
} finally {
lib.close();
}
} else {
// 8 integer args exceed the x86-64 SysV GP register cap (6), which makes
// CreateFastFFIMetadata reject the signature. Calls go through the
// SharedBuffer or generic invoker into FFIFunction::Invoke().
const { lib, functions } = ffi.dlopen(libraryPath, {
sum_8_i32: {
return: 'i32',
arguments: [
'i32', 'i32', 'i32', 'i32',
'i32', 'i32', 'i32', 'i32',
],
},
});

const fn = functions.sum_8_i32;

assert.strictEqual(fn(1, 2, 3, 4, 5, 6, 7, 8), 36);

bench.start();
for (let i = 0; i < n; ++i)
fn(1, 2, 3, 4, 5, 6, 7, 14);
bench.end(n);

lib.close();
}
}
24 changes: 21 additions & 3 deletions src/ffi/fast.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,13 +192,31 @@ bool SignatureNeedsFastBufferInvoke(const FFIFunction& fn) {
IsBufferTypeName(fn.arg_type_names[0]));
}

namespace {

std::shared_ptr<FFIFunction> CloneForFastMetadata(
const std::shared_ptr<FFIFunction>& fn) {
// Fast metadata only needs the native target and signature. In particular,
// its temporary clone must not borrow the original function's cif or plan.
auto clone = std::make_shared<FFIFunction>();
clone->closed = fn->closed;
clone->ptr = fn->ptr;
clone->args = fn->args;
clone->return_type = fn->return_type;
clone->arg_type_names = fn->arg_type_names;
clone->return_type_name = fn->return_type_name;
return clone;
}

} // namespace

std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// The primary Fast API entrypoint receives pointer-compatible values as
// BigInts after the JS wrapper has converted strings, nullish values, and
// memory-backed objects. A secondary entrypoint handles the monomorphic
// memory-backed case without extracting the pointer in JS.
auto clone = std::make_shared<FFIFunction>(*fn);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsBufferTypeName(name)) {
name = "pointer";
Expand All@@ -209,10 +227,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(

std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
const std::shared_ptr<FFIFunction>& fn) {
// Reuse the same native target and libffi metadata, but describe the JS
// Reuse the same native target and signature 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);
auto clone = CloneForFastMetadata(fn);
for (std::string& name : clone->arg_type_names) {
if (IsPointerTypeName(name)) {
name = "buffer";
Expand Down
37 changes: 27 additions & 10 deletions src/node_ffi.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,17 @@ 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);
}
Expand DownExpand Up@@ -146,14 +157,12 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(

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)});
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,
Expand All@@ -178,6 +187,14 @@ Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
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;
Expand DownExpand Up@@ -550,7 +567,7 @@ void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
}

ffi_call(&fn->cif, FFI_FN(fn->ptr), result, ffi_args.data());
fn->Invoke(result, ffi_args.data());

// Return result back to Javascript
ToJSReturnValue(env, args, fn->return_type, result);
Expand DownExpand Up@@ -609,7 +626,7 @@ void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
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());
fn->Invoke(result, ffi_args.data());

if (result != nullptr) {
WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
Expand Down
29 changes: 25 additions & 4 deletions src/node_ffi.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,20 +14,41 @@
#include <unordered_map>
#include <vector>

// libffi only accelerates reusable call plans on x86-64 System V. Other
// targets implement the API by calling ffi_call(), which adds no benefit.
#if defined(FFI_VERSION_NUMBER) && FFI_VERSION_NUMBER >= 30700 && \
defined(__x86_64__) && !defined(__ILP32__) && !defined(X86_WIN64) && \
!defined(_WIN32)
#define NODE_FFI_HAS_FAST_CALL_PLAN 1
#endif

namespace node::ffi {

class DynamicLibrary;
struct FFIFunction;

struct FFIFunction {
bool closed;
FFIFunction() = default;
FFIFunction(const FFIFunction&) = delete;
FFIFunction& operator=(const FFIFunction&) = delete;
FFIFunction(FFIFunction&&) = delete;
FFIFunction& operator=(FFIFunction&&) = delete;

void* ptr;
ffi_cif cif;
bool closed = false;

void* ptr = nullptr;
ffi_cif cif = {};
std::vector<ffi_type*> args;
ffi_type* return_type;
ffi_type* return_type = nullptr;
std::vector<std::string> arg_type_names;
std::string return_type_name;
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
// The plan borrows cif, so it must remain uniquely owned by this instance.
std::unique_ptr<ffi_call_plan, decltype(&ffi_call_plan_free)> call_plan{
nullptr, ffi_call_plan_free};
#endif

void Invoke(void* result, void** values);
};

class FFIFunctionInfo final : public BaseObject {
Expand Down
Loading