Skip to content

Commit d3db2f6

Browse files
ShogunPandabengl
andcommitted
ffi: add experimental fast FFI call API
Add fast API trampolines for AArch64 and x86_64. IsJitMemorySupported() maps an RW page, writes a ret instruction (0xD65F03C0 AArch64 / 0xC3 x86_64), and mprotects it to RX. A successful RX transition is treated as the support signal. The page is deliberately not executed: this check may run during normal operation (when an FFI function is first created), and executing freshly written code from a capability probe could SIGSEGV/SIGKILL the process on systems that block executable memory. The real trampoline emitter performs the same mprotect at creation time and falls back to libffi when it is rejected. The result is computed once via std::call_once and cached for the lifetime of the process, so concurrent callers never observe a provisional value. Wired into CreateFastFFIMetadata() for early nullptr bail-out when JIT memory is unavailable. Windows stub returns false (no trampolines yet on this branch). 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 that mirror the trampoline emitters (AArch64 and x86_64 SysV). Platforms without an emitter, including Win64, are reported ineligible. - Buffer and float args cannot coexist, and buffer args additionally consume an extra GP register slot on both supported ABIs. The arg/arg-name lengths are checked before the per-arg loop so a malformed signature cannot index out of bounds. Returns nullptr from CreateFastFFIMetadata() for ineligible signatures, falling back to libffi. Co-authored-by: Bryan English <bryan@bryanenglish.com> Signed-off-by: Paolo Insogna <paolo@cowtech.it> Signed-off-by: Bryan English <bryan@bryanenglish.com> Assisted-By: OpenAI:GPT-5.5 <openai/gpt-5.5>
1 parent d179c7e commit d3db2f6

50 files changed

Lines changed: 3483 additions & 130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const add = functions.add_f64;
2121
function main({ n }) {
2222
bench.start();
2323
for (let i = 0; i < n; ++i)
24-
add(1.5, 2.5);
24+
add(20.5, 21.5);
2525
bench.end(n);
2626

2727
lib.close();

benchmark/ffi/add-f32.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_f32: { result: 'f32', parameters: ['f32', 'f32'] },
17+
});
18+
19+
const add = functions.add_f32;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20.5, 21.5);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-i16.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_i16: { result: 'i16', parameters: ['i16', 'i16'] },
17+
});
18+
19+
const add = functions.add_i16;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20, 22);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-i64.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_i64: { result: 'i64', parameters: ['i64', 'i64'] },
17+
});
18+
19+
const add = functions.add_i64;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20n, 22n);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-i8.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_i8: { result: 'i8', parameters: ['i8', 'i8'] },
17+
});
18+
19+
const add = functions.add_i8;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20, 22);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-u16.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_u16: { result: 'u16', parameters: ['u16', 'u16'] },
17+
});
18+
19+
const add = functions.add_u16;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20, 22);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-u64.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_u64: { result: 'u64', parameters: ['u64', 'u64'] },
17+
});
18+
19+
const add = functions.add_u64;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20n, 22n);
25+
bench.end(n);
26+
27+
lib.close();
28+
}

benchmark/ffi/add-u8.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
add_u8: { result: 'u8', parameters: ['u8', 'u8'] },
17+
});
18+
19+
const add = functions.add_u8;
20+
21+
function main({ n }) {
22+
bench.start();
23+
for (let i = 0; i < n; ++i)
24+
add(20, 22);
25+
bench.end(n);
26+
27+
lib.close();
28+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
first_byte: { result: 'u8', parameters: ['pointer'] },
17+
});
18+
19+
const fn = functions.first_byte;
20+
const bytes = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
21+
const pointer = ffi.getRawPointer(bytes);
22+
23+
function main({ n }) {
24+
bench.start();
25+
for (let i = 0; i < n; ++i)
26+
fn(pointer);
27+
bench.end(n);
28+
29+
lib.close();
30+
}

benchmark/ffi/buffer-first-byte.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const ffi = require('node:ffi');
5+
const { libraryPath, ensureFixtureLibrary } = require('./common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e7],
9+
}, {
10+
flags: ['--experimental-ffi'],
11+
});
12+
13+
ensureFixtureLibrary();
14+
15+
const { lib, functions } = ffi.dlopen(libraryPath, {
16+
first_byte: { result: 'u8', parameters: ['buffer'] },
17+
});
18+
19+
const fn = functions.first_byte;
20+
const bytes = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
21+
22+
function main({ n }) {
23+
bench.start();
24+
for (let i = 0; i < n; ++i)
25+
fn(bytes);
26+
bench.end(n);
27+
28+
lib.close();
29+
}

0 commit comments

Comments
 (0)