Skip to content

Commit 91c8e5f

Browse files
committed
fixup! ffi: add shared-buffer fast path for numeric and pointer signatures
Raise JS coverage on `lib/internal/ffi-shared-buffer.js` from 83.14% to 99.84% so the nodejs/node `coverage-linux-without-intl` workflow's 95% threshold is met (the workflow was failing at 94.98%). Two kinds of changes: * Mark genuinely-unreachable defensive blocks with `/* c8 ignore */`. These are `ERR_INTERNAL_ASSERTION` throws that trigger only when the native and JS sides disagree on the shared-buffer ABI, plus the `nargs === 0` branch in `buildNumericWrapper` which is dead today because `IsSBEligibleSignature` rejects 0-arg signatures on the native side. * Add fixtures and tests to exercise every arity of the void-return specialization ladder (1, 3, 4, 5, plus the 7+ rest-params fallback; 2 and 6 were already covered), and to hit the `throwFFIArgCountError` branch in each value-return specialization (1..6) and in the pointer-dispatch wrapper. No behavior change in the FFI runtime path itself.
1 parent 48cee03 commit 91c8e5f

4 files changed

Lines changed: 197 additions & 6 deletions

File tree

lib/internal/ffi-shared-buffer.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,11 @@ function writeNumericArg(view, info, offset, arg, index) {
159159
return;
160160
}
161161

162+
/* c8 ignore start */
162163
// Unreachable: caller filters out non-numeric kinds.
163164
throw new ERR_INTERNAL_ASSERTION(
164165
`FFI: writeNumericArg reached with unexpected kind="${kind}"`);
166+
/* c8 ignore stop */
165167
}
166168

167169
// Returns true on fast-path success, false when the caller must fall back
@@ -218,20 +220,24 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) {
218220
if (resultType === undefined) resultType = rawFn[kSbResult];
219221
// `CreateFunction` always attaches these for SB-eligible functions.
220222
// Missing here means the native side and this wrapper are out of sync.
223+
/* c8 ignore start */
221224
if (parameters === undefined || resultType === undefined) {
222225
throw new ERR_INTERNAL_ASSERTION(
223226
'FFI: shared-buffer raw function is missing kSbParams or kSbResult');
224227
}
228+
/* c8 ignore stop */
225229

226230
const slowInvoke = rawFn[kSbInvokeSlow];
227231
const view = new DataView(buffer);
228232
let retGetter = null;
229233
if (resultType !== 'void') {
230234
const retInfo = sbTypeInfo[resultType];
235+
/* c8 ignore start */
231236
if (retInfo === undefined) {
232237
throw new ERR_INTERNAL_ASSERTION(
233238
`FFI: shared-buffer type table missing entry for result type "${resultType}"`);
234239
}
240+
/* c8 ignore stop */
235241
retGetter = retInfo.get;
236242
}
237243

@@ -241,10 +247,12 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) {
241247
let anyPointer = false;
242248
for (let i = 0; i < nargs; i++) {
243249
const info = sbTypeInfo[parameters[i]];
250+
/* c8 ignore start */
244251
if (info === undefined) {
245252
throw new ERR_INTERNAL_ASSERTION(
246253
`FFI: shared-buffer type table missing entry for parameter type "${parameters[i]}"`);
247254
}
255+
/* c8 ignore stop */
248256
// Push the `sbTypeInfo` entry directly (entries with the same `kind`
249257
// share a shape, keeping `writeNumericArg`'s call sites
250258
// low-polymorphism) and store offsets in a parallel array to avoid
@@ -259,11 +267,13 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) {
259267
// Pointer signatures need a per-arg runtime type check and fall back
260268
// to the native slow-path invoker for non-BigInt pointer arguments,
261269
// so arity specialization wouldn't buy much here.
270+
/* c8 ignore start */
262271
if (slowInvoke === undefined) {
263272
throw new ERR_INTERNAL_ASSERTION(
264273
'FFI: shared-buffer raw function with pointer arguments is ' +
265274
'missing kSbInvokeSlow');
266275
}
276+
/* c8 ignore stop */
267277
wrapper = function(...args) {
268278
if (args.length !== nargs) {
269279
throwFFIArgCountError(nargs, args.length);
@@ -298,6 +308,11 @@ function wrapWithSharedBuffer(rawFn, parameters, resultType) {
298308
// but it's the result of lots of perf-hunting.
299309
function buildNumericWrapper(
300310
rawFn, view, argInfos, argOffsets, nargs, retGetter) {
311+
// `IsSBEligibleSignature` on the native side rejects 0-arg signatures,
312+
// so this branch is unreachable today. It's kept as defense-in-depth
313+
// for when that filter changes or for programmatic callers that hand a
314+
// 0-arg signature through `wrapWithSharedBuffer` directly.
315+
/* c8 ignore start */
301316
if (nargs === 0) {
302317
if (retGetter === null) {
303318
return function() {
@@ -315,6 +330,7 @@ function buildNumericWrapper(
315330
return retGetter(view, 0, true);
316331
};
317332
}
333+
/* c8 ignore stop */
318334
if (nargs === 1) {
319335
const i0 = argInfos[0];
320336
const o0 = argOffsets[0];
@@ -586,13 +602,15 @@ DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) {
586602
// uninitialized buffer.
587603
const functionsDescriptor =
588604
ObjectGetOwnPropertyDescriptor(DynamicLibrary.prototype, 'functions');
605+
/* c8 ignore start */
589606
if (functionsDescriptor === undefined || !functionsDescriptor.get) {
590607
// Missing getter means the native and JS sides are out of sync; silently
591608
// skipping the patch would expose the fast-path-against-uninitialized-buffer
592609
// footgun this whole block exists to prevent.
593610
throw new ERR_INTERNAL_ASSERTION(
594611
'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter');
595612
}
613+
/* c8 ignore stop */
596614
const origGetter = functionsDescriptor.get;
597615
ObjectDefineProperty(DynamicLibrary.prototype, 'functions', {
598616
__proto__: null,

src/node_builtins.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,10 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const {
139139
#ifndef OPENSSL_NO_QUIC
140140
"internal/quic/quic", "internal/quic/symbols", "internal/quic/stats",
141141
"internal/quic/state",
142-
#endif // !OPENSSL_NO_QUIC
142+
#endif // !OPENSSL_NO_QUIC
143143
#if !HAVE_FFI
144144
"internal/ffi-shared-buffer",
145-
#endif // !HAVE_FFI
145+
#endif // !HAVE_FFI
146146
"ffi", // Experimental.
147147
"quic", // Experimental.
148148
"sqlite", // Experimental.

test/ffi/fixture_library/ffi_test_library.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,39 @@ FFI_EXPORT void store_sum_2_i32(int32_t a, int32_t b) {
233233
global_scratch = a + b;
234234
}
235235

236+
FFI_EXPORT void store_i32(int32_t a) {
237+
global_scratch = a;
238+
}
239+
240+
FFI_EXPORT void store_sum_3_i32(int32_t a, int32_t b, int32_t c) {
241+
global_scratch = a + b + c;
242+
}
243+
244+
FFI_EXPORT void store_sum_4_i32(int32_t a, int32_t b, int32_t c, int32_t d) {
245+
global_scratch = a + b + c + d;
246+
}
247+
248+
FFI_EXPORT void store_sum_5_i32(
249+
int32_t a, int32_t b, int32_t c, int32_t d, int32_t e) {
250+
global_scratch = a + b + c + d + e;
251+
}
252+
236253
FFI_EXPORT void store_sum_6_i32(
237254
int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f) {
238255
global_scratch = a + b + c + d + e + f;
239256
}
240257

258+
FFI_EXPORT void store_sum_8_i32(int32_t a,
259+
int32_t b,
260+
int32_t c,
261+
int32_t d,
262+
int32_t e,
263+
int32_t f,
264+
int32_t g,
265+
int32_t h) {
266+
global_scratch = a + b + c + d + e + f + g + h;
267+
}
268+
241269
FFI_EXPORT int32_t get_scratch(void) {
242270
return global_scratch;
243271
}

test/ffi/test-ffi-shared-buffer.js

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,34 +484,179 @@ test('void-return 0-arg wrapper branch', () => {
484484
}
485485
});
486486

487-
test('void-return wrapper at arity 2 and 6 observes side effects', () => {
487+
test('void-return wrapper at every specialized arity observes side effects', () => {
488488
// The arity ladder has a separate void-return closure for each arity.
489489
// A wiring bug in a mid-arity void specialization would not be caught
490-
// by the 0-arg void test above, so exercise the side effects directly.
490+
// by the 0-arg void test above, so exercise the side effects directly
491+
// at every arity the ladder specializes (1..6) plus the 7+ rest-params
492+
// fallback.
491493
const { lib, functions } = ffi.dlopen(libraryPath, {
494+
store_i32: { result: 'void', parameters: ['i32'] },
492495
store_sum_2_i32: { result: 'void', parameters: ['i32', 'i32'] },
496+
store_sum_3_i32: { result: 'void', parameters: ['i32', 'i32', 'i32'] },
497+
store_sum_4_i32: {
498+
result: 'void',
499+
parameters: ['i32', 'i32', 'i32', 'i32'],
500+
},
501+
store_sum_5_i32: {
502+
result: 'void',
503+
parameters: ['i32', 'i32', 'i32', 'i32', 'i32'],
504+
},
493505
store_sum_6_i32: {
494506
result: 'void',
495507
parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'],
496508
},
509+
store_sum_8_i32: {
510+
result: 'void',
511+
parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'],
512+
},
497513
get_scratch: { result: 'i32', parameters: [] },
498514
});
499515
try {
516+
// Powers-of-two summands detect a dropped or duplicated slot at each
517+
// arity.
518+
assert.strictEqual(functions.store_i32(7), undefined);
519+
assert.strictEqual(functions.get_scratch(), 7);
520+
500521
assert.strictEqual(functions.store_sum_2_i32(10, 32), undefined);
501522
assert.strictEqual(functions.get_scratch(), 42);
502523

503-
// Powers-of-two summands detect a dropped or duplicated slot.
524+
assert.strictEqual(functions.store_sum_3_i32(1, 2, 4), undefined);
525+
assert.strictEqual(functions.get_scratch(), 7);
526+
527+
assert.strictEqual(functions.store_sum_4_i32(1, 2, 4, 8), undefined);
528+
assert.strictEqual(functions.get_scratch(), 15);
529+
530+
assert.strictEqual(functions.store_sum_5_i32(1, 2, 4, 8, 16), undefined);
531+
assert.strictEqual(functions.get_scratch(), 31);
532+
504533
assert.strictEqual(
505534
functions.store_sum_6_i32(1, 2, 4, 8, 16, 32), undefined);
506535
assert.strictEqual(functions.get_scratch(), 63);
507536

508-
// Validation still runs on the void-return branch.
537+
// 7+ args takes the generic rest-params void branch rather than a
538+
// per-arity specialization.
539+
assert.strictEqual(
540+
functions.store_sum_8_i32(1, 2, 4, 8, 16, 32, 64, 128), undefined);
541+
assert.strictEqual(functions.get_scratch(), 255);
542+
543+
// Validation still runs on every void-return branch, including the
544+
// rest-params fallback.
545+
assert.throws(
546+
() => functions.store_i32(1.5),
547+
{ code: 'ERR_INVALID_ARG_VALUE' });
509548
assert.throws(
510549
() => functions.store_sum_2_i32(1.5, 2),
511550
{ code: 'ERR_INVALID_ARG_VALUE' });
551+
assert.throws(
552+
() => functions.store_sum_3_i32(1, 1.5, 3),
553+
{ code: 'ERR_INVALID_ARG_VALUE' });
554+
assert.throws(
555+
() => functions.store_sum_4_i32(1, 2, 1.5, 4),
556+
{ code: 'ERR_INVALID_ARG_VALUE' });
557+
assert.throws(
558+
() => functions.store_sum_5_i32(1, 2, 3, 1.5, 5),
559+
{ code: 'ERR_INVALID_ARG_VALUE' });
512560
assert.throws(
513561
() => functions.store_sum_6_i32(1, 2, 3, 4, 5),
514562
{ code: 'ERR_INVALID_ARG_VALUE' });
563+
assert.throws(
564+
() => functions.store_sum_8_i32(1, 2, 3, 4, 5, 6, 7, 1.5),
565+
{ code: 'ERR_INVALID_ARG_VALUE' });
566+
567+
// Wrong arity hits the `throwFFIArgCountError` branch inside each
568+
// specialization (1..6 and the 7+ rest-params fallback).
569+
for (const [name, expected, badArgs] of [
570+
['store_i32', 1, []],
571+
['store_sum_2_i32', 2, [1]],
572+
['store_sum_3_i32', 3, [1, 2]],
573+
['store_sum_4_i32', 4, [1, 2, 3]],
574+
['store_sum_5_i32', 5, [1, 2, 3, 4]],
575+
['store_sum_6_i32', 6, [1, 2, 3, 4, 5]],
576+
['store_sum_8_i32', 8, [1, 2, 3, 4, 5, 6, 7]],
577+
]) {
578+
assert.throws(
579+
() => functions[name](...badArgs),
580+
{
581+
code: 'ERR_INVALID_ARG_VALUE',
582+
message: new RegExp(`expected ${expected}, got ${badArgs.length}`),
583+
});
584+
}
585+
} finally {
586+
lib.close();
587+
}
588+
});
589+
590+
test('value-return wrapper arity mismatch hits every specialized branch', () => {
591+
// `sum_7_i32` already exercises the 7+ rest-params branch elsewhere;
592+
// this test targets the per-arity `throwFFIArgCountError` call in the
593+
// value-return closures for arities 1..6 so each specialization's
594+
// argument-count guard runs at least once.
595+
const { lib, functions } = ffi.dlopen(libraryPath, {
596+
logical_not: { result: 'i32', parameters: ['i32'] },
597+
add_i32: { result: 'i32', parameters: ['i32', 'i32'] },
598+
sum_3_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32'] },
599+
sum_4_i32: { result: 'i32', parameters: ['i32', 'i32', 'i32', 'i32'] },
600+
sum_five_i32: {
601+
result: 'i32',
602+
parameters: ['i32', 'i32', 'i32', 'i32', 'i32'],
603+
},
604+
sum_6_i32: {
605+
result: 'i32',
606+
parameters: ['i32', 'i32', 'i32', 'i32', 'i32', 'i32'],
607+
},
608+
});
609+
try {
610+
for (const [name, expected, badArgs] of [
611+
['logical_not', 1, []],
612+
['add_i32', 2, [1]],
613+
['sum_3_i32', 3, [1, 2]],
614+
['sum_4_i32', 4, [1, 2, 3]],
615+
['sum_five_i32', 5, [1, 2, 3, 4]],
616+
['sum_6_i32', 6, [1, 2, 3, 4, 5]],
617+
]) {
618+
assert.throws(
619+
() => functions[name](...badArgs),
620+
{
621+
code: 'ERR_INVALID_ARG_VALUE',
622+
message: new RegExp(`expected ${expected}, got ${badArgs.length}`),
623+
});
624+
}
625+
626+
// Sanity-check that a correct call still returns a value at each
627+
// arity — a bug that swallowed the return on the value-return path
628+
// would be caught here.
629+
assert.strictEqual(functions.logical_not(0), 1);
630+
assert.strictEqual(functions.add_i32(1, 2), 3);
631+
assert.strictEqual(functions.sum_3_i32(1, 2, 4), 7);
632+
assert.strictEqual(functions.sum_4_i32(1, 2, 4, 8), 15);
633+
assert.strictEqual(functions.sum_five_i32(1, 2, 4, 8, 16), 31);
634+
assert.strictEqual(functions.sum_6_i32(1, 2, 4, 8, 16, 32), 63);
635+
} finally {
636+
lib.close();
637+
}
638+
});
639+
640+
test('pointer-dispatch wrapper rejects wrong-arity calls', () => {
641+
// Pointer signatures share a single rest-params wrapper rather than the
642+
// per-arity ladder, but it still has its own `throwFFIArgCountError`
643+
// branch that needs to be exercised.
644+
const { lib, functions } = ffi.dlopen(libraryPath, {
645+
identity_pointer: { result: 'pointer', parameters: ['pointer'] },
646+
});
647+
try {
648+
assert.throws(
649+
() => functions.identity_pointer(),
650+
{
651+
code: 'ERR_INVALID_ARG_VALUE',
652+
message: /expected 1, got 0/,
653+
});
654+
assert.throws(
655+
() => functions.identity_pointer(0n, 0n),
656+
{
657+
code: 'ERR_INVALID_ARG_VALUE',
658+
message: /expected 1, got 2/,
659+
});
515660
} finally {
516661
lib.close();
517662
}

0 commit comments

Comments
 (0)