node/lib/internal/ffi-shared-buffer.js
Bryan English 528f8b2bae
ffi: add shared-buffer fast path for numeric and pointer signatures
Adds an ArrayBuffer-based invocation path for FFI functions whose
signatures are composed entirely of numeric types (i8..i64, u8..u64,
f32, f64, bool, char) and/or pointer types. The JS wrapper packs
arguments directly into a per-function AB via primordial DataView
setters and the C++ invoker (`InvokeFunctionSB`) reads them without
going through V8's `FunctionCallbackInfo`. Results are returned the same
way.

Pointer arguments use runtime dispatch: BigInt, null, and undefined take
the fast path, while Buffer, ArrayBuffer, ArrayBufferView, and String
fall back transparently to the classic `InvokeFunction` path via a
stashed `_invokeSlow` function. Signatures containing
non-numeric/non-pointer types also bypass the fast path.

The fast path is disabled on big-endian platforms.

Callers do not opt in, and the fast path is transparent in every way
users should rely on. One observable change: function wrappers returned
by `library.getFunction`, `library.getFunctions`, and `library.functions`
now have `.length` equal to the declared parameter count rather than
`0`. Code that relied on the previous value will need to be updated.

Adds microbenchmarks covering the common FFI call shapes so future
changes to the invoker can be evaluated:

- add-i32.js: 2-arg integer
- add-f64.js: 2-arg float
- many-args.js: 6-arg integer
- pointer-bigint.js: 1-arg pointer (BigInt)
- sum-buffer.js: pointer + length (Buffer)

A `common.js` helper resolves the fixture-library path from
`test/ffi/fixture_library` without pulling in the test harness, and
throws a clear message if the fixture hasn't been built yet.

Also adds `sum_6_i32` to the fixture library for the many-args case.

Signed-off-by: Bryan English <bryan@bryanenglish.com>
PR-URL: https://github.com/nodejs/node/pull/62918
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
2026-05-05 16:19:54 +02:00

635 lines
22 KiB
JavaScript

'use strict';
const {
DataView,
DataViewPrototypeGetBigInt64,
DataViewPrototypeGetBigUint64,
DataViewPrototypeGetFloat32,
DataViewPrototypeGetFloat64,
DataViewPrototypeGetInt16,
DataViewPrototypeGetInt32,
DataViewPrototypeGetInt8,
DataViewPrototypeGetUint16,
DataViewPrototypeGetUint32,
DataViewPrototypeGetUint8,
DataViewPrototypeSetBigInt64,
DataViewPrototypeSetBigUint64,
DataViewPrototypeSetFloat32,
DataViewPrototypeSetFloat64,
DataViewPrototypeSetInt16,
DataViewPrototypeSetInt32,
DataViewPrototypeSetInt8,
DataViewPrototypeSetUint16,
DataViewPrototypeSetUint32,
DataViewPrototypeSetUint8,
FunctionPrototypeCall,
NumberIsInteger,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectKeys,
ReflectApply,
TypeError,
} = primordials;
const {
codes: {
ERR_INTERNAL_ASSERTION,
},
} = require('internal/errors');
const {
DynamicLibrary,
charIsSigned,
kSbInvokeSlow,
kSbParams,
kSbResult,
kSbSharedBuffer,
uintptrMax,
} = internalBinding('ffi');
// Validator fields (`min`, `max`, `label`) must mirror `ToFFIArgument` in
// `src/ffi/types.cc` so the fast and slow paths produce identical errors.
const sI8 = DataViewPrototypeSetInt8;
const gI8 = DataViewPrototypeGetInt8;
const sU8 = DataViewPrototypeSetUint8;
const gU8 = DataViewPrototypeGetUint8;
const sI16 = DataViewPrototypeSetInt16;
const gI16 = DataViewPrototypeGetInt16;
const sU16 = DataViewPrototypeSetUint16;
const gU16 = DataViewPrototypeGetUint16;
const sI32 = DataViewPrototypeSetInt32;
const gI32 = DataViewPrototypeGetInt32;
const sU32 = DataViewPrototypeSetUint32;
const gU32 = DataViewPrototypeGetUint32;
const sI64 = DataViewPrototypeSetBigInt64;
const gI64 = DataViewPrototypeGetBigInt64;
const sU64 = DataViewPrototypeSetBigUint64;
const gU64 = DataViewPrototypeGetBigUint64;
const sF32 = DataViewPrototypeSetFloat32;
const gF32 = DataViewPrototypeGetFloat32;
const sF64 = DataViewPrototypeSetFloat64;
const gF64 = DataViewPrototypeGetFloat64;
const sbTypeInfo = {
__proto__: null,
i8: { set: sI8, get: gI8, kind: 'int', min: -128, max: 127, label: 'an int8' },
int8: { set: sI8, get: gI8, kind: 'int', min: -128, max: 127, label: 'an int8' },
char: charIsSigned ?
{ set: sI8, get: gI8, kind: 'int', min: -128, max: 127, label: 'an int8' } :
{ set: sU8, get: gU8, kind: 'int', min: 0, max: 255, label: 'a uint8' },
u8: { set: sU8, get: gU8, kind: 'int', min: 0, max: 255, label: 'a uint8' },
uint8: { set: sU8, get: gU8, kind: 'int', min: 0, max: 255, label: 'a uint8' },
bool: { set: sU8, get: gU8, kind: 'int', min: 0, max: 255, label: 'a uint8' },
i16: { set: sI16, get: gI16, kind: 'int', min: -32768, max: 32767, label: 'an int16' },
int16: { set: sI16, get: gI16, kind: 'int', min: -32768, max: 32767, label: 'an int16' },
u16: { set: sU16, get: gU16, kind: 'int', min: 0, max: 65535, label: 'a uint16' },
uint16: { set: sU16, get: gU16, kind: 'int', min: 0, max: 65535, label: 'a uint16' },
i32: { set: sI32, get: gI32, kind: 'int', min: -2147483648, max: 2147483647, label: 'an int32' },
int32: { set: sI32, get: gI32, kind: 'int', min: -2147483648, max: 2147483647, label: 'an int32' },
u32: { set: sU32, get: gU32, kind: 'int', min: 0, max: 4294967295, label: 'a uint32' },
uint32: { set: sU32, get: gU32, kind: 'int', min: 0, max: 4294967295, label: 'a uint32' },
i64: { set: sI64, get: gI64, kind: 'i64', label: 'an int64' },
int64: { set: sI64, get: gI64, kind: 'i64', label: 'an int64' },
u64: { set: sU64, get: gU64, kind: 'u64', label: 'a uint64' },
uint64: { set: sU64, get: gU64, kind: 'u64', label: 'a uint64' },
f32: { set: sF32, get: gF32, kind: 'float', label: 'a float' },
float: { set: sF32, get: gF32, kind: 'float', label: 'a float' },
float32: { set: sF32, get: gF32, kind: 'float', label: 'a float' },
f64: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
double: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
float64: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
pointer: { set: sU64, get: gU64, kind: 'pointer' },
ptr: { set: sU64, get: gU64, kind: 'pointer' },
function: { set: sU64, get: gU64, kind: 'pointer' },
buffer: { set: sU64, get: gU64, kind: 'pointer' },
arraybuffer: { set: sU64, get: gU64, kind: 'pointer' },
string: { set: sU64, get: gU64, kind: 'pointer' },
str: { set: sU64, get: gU64, kind: 'pointer' },
};
const U64_MAX = 0xFFFFFFFFFFFFFFFFn;
const I64_MAX = 0x7FFFFFFFFFFFFFFFn;
const I64_MIN = -0x8000000000000000n;
// Builds the exact error shape the non-SB implementation (the native FFI
// invoker) produces.
function throwFFIArgError(msg) {
// eslint-disable-next-line no-restricted-syntax
const err = new TypeError(msg);
err.code = 'ERR_INVALID_ARG_VALUE';
throw err;
}
function throwFFIArgCountError(expected, actual) {
throwFFIArgError(
`Invalid argument count: expected ${expected}, got ${actual}`);
}
// Validation and error messages must mirror `ToFFIArgument` in
// `src/ffi/types.cc`.
function writeNumericArg(view, info, offset, arg, index) {
const kind = info.kind;
if (kind === 'int') {
if (typeof arg !== 'number' || !NumberIsInteger(arg) ||
arg < info.min || arg > info.max) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
info.set(view, offset, arg, true);
return;
}
if (kind === 'i64') {
if (typeof arg !== 'bigint' || arg < I64_MIN || arg > I64_MAX) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
sI64(view, offset, arg, true);
return;
}
if (kind === 'u64') {
if (typeof arg !== 'bigint' || arg < 0n || arg > U64_MAX) {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
sU64(view, offset, arg, true);
return;
}
if (kind === 'float') {
if (typeof arg !== 'number') {
throwFFIArgError(`Argument ${index} must be ${info.label}`);
}
info.set(view, offset, arg, true);
return;
}
/* c8 ignore start */
// Unreachable: caller filters out non-numeric kinds.
throw new ERR_INTERNAL_ASSERTION(
`FFI: writeNumericArg reached with unexpected kind="${kind}"`);
/* c8 ignore stop */
}
// Returns true on fast-path success, false when the caller must fall back
// to the slow path (strings, Buffers, ArrayBuffers, ArrayBufferViews).
function writePointerArg(view, offset, arg, index) {
if (typeof arg === 'bigint') {
// Bound by `uintptrMax` (not `U64_MAX`) to mirror the slow path: on
// 32-bit platforms a BigInt that doesn't fit in `uintptr_t` would be
// silently truncated by `ReadFFIArgFromBuffer`'s
// `memcpy(..., type->size, ...)`.
if (arg < 0n || arg > uintptrMax) {
throwFFIArgError(
`Argument ${index} must be a non-negative pointer bigint`);
}
sU64(view, offset, arg, true);
return true;
}
if (arg === null || arg === undefined) {
sU64(view, offset, 0n, true);
return true;
}
return false;
}
// The `pointer` descriptor mirrors the raw function's so user code that
// reassigns `.pointer` keeps working through the wrapper.
function inheritMetadata(wrapper, rawFn, nargs) {
ObjectDefineProperty(wrapper, 'name', {
__proto__: null, value: rawFn.name, configurable: true,
});
ObjectDefineProperty(wrapper, 'length', {
__proto__: null, value: nargs, configurable: true,
});
ObjectDefineProperty(wrapper, 'pointer', {
__proto__: null, value: rawFn.pointer,
writable: true, configurable: true, enumerable: true,
});
return wrapper;
}
// Reentrancy: the ArrayBuffer is per-function, but `InvokeFunctionSB` copies
// arguments out of it into invocation-local storage before `ffi_call` and
// reads the return value back only after, so nested/reentrant calls into
// the same function are safe.
function wrapWithSharedBuffer(rawFn, parameters, resultType) {
if (rawFn === undefined || rawFn === null) return rawFn;
const buffer = rawFn[kSbSharedBuffer];
if (buffer === undefined) return rawFn;
// Callers without explicit signature info (the `functions` accessor
// patch below) rely on the `kSbParams` / `kSbResult` metadata attached
// by the native `CreateFunction`.
if (parameters === undefined) parameters = rawFn[kSbParams];
if (resultType === undefined) resultType = rawFn[kSbResult];
// `CreateFunction` always attaches these for SB-eligible functions.
// Missing here means the native side and this wrapper are out of sync.
/* c8 ignore start */
if (parameters === undefined || resultType === undefined) {
throw new ERR_INTERNAL_ASSERTION(
'FFI: shared-buffer raw function is missing kSbParams or kSbResult');
}
/* c8 ignore stop */
const slowInvoke = rawFn[kSbInvokeSlow];
const view = new DataView(buffer);
let retGetter = null;
if (resultType !== 'void') {
const retInfo = sbTypeInfo[resultType];
/* c8 ignore start */
if (retInfo === undefined) {
throw new ERR_INTERNAL_ASSERTION(
`FFI: shared-buffer type table missing entry for result type "${resultType}"`);
}
/* c8 ignore stop */
retGetter = retInfo.get;
}
const nargs = parameters.length;
const argInfos = [];
const argOffsets = [];
let anyPointer = false;
for (let i = 0; i < nargs; i++) {
const info = sbTypeInfo[parameters[i]];
/* c8 ignore start */
if (info === undefined) {
throw new ERR_INTERNAL_ASSERTION(
`FFI: shared-buffer type table missing entry for parameter type "${parameters[i]}"`);
}
/* c8 ignore stop */
// Push the `sbTypeInfo` entry directly (entries with the same `kind`
// share a shape, keeping `writeNumericArg`'s call sites
// low-polymorphism) and store offsets in a parallel array to avoid
// per-arg object cloning.
argInfos.push(info);
argOffsets.push(8 * (i + 1));
if (info.kind === 'pointer') anyPointer = true;
}
let wrapper;
if (anyPointer) {
// Pointer signatures need a per-arg runtime type check and fall back
// to the native slow-path invoker for non-BigInt pointer arguments,
// so arity specialization wouldn't buy much here.
/* c8 ignore start */
if (slowInvoke === undefined) {
throw new ERR_INTERNAL_ASSERTION(
'FFI: shared-buffer raw function with pointer arguments is ' +
'missing kSbInvokeSlow');
}
/* c8 ignore stop */
wrapper = function(...args) {
if (args.length !== nargs) {
throwFFIArgCountError(nargs, args.length);
}
for (let i = 0; i < nargs; i++) {
const info = argInfos[i];
const offset = argOffsets[i];
if (info.kind === 'pointer') {
if (!writePointerArg(view, offset, args[i], i)) {
return ReflectApply(slowInvoke, undefined, args);
}
} else {
writeNumericArg(view, info, offset, args[i], i);
}
}
rawFn();
return retGetter === null ? undefined : retGetter(view, 0, true);
};
} else {
// Arity specialization avoids the per-call `Array` allocation of
// `...args`; the void/non-void split removes a per-call branch on
// `retGetter`.
wrapper = buildNumericWrapper(
rawFn, view, argInfos, argOffsets, nargs, retGetter);
}
return inheritMetadata(wrapper, rawFn, nargs);
}
// Specialized for nargs 0..6 with a generic rest-params fallback for 7+.
// Per-arg type info and offsets are captured into closure locals so the
// hot path reads a single variable per arg. This is admittedly pretty weird
// but it's the result of lots of perf-hunting.
function buildNumericWrapper(
rawFn, view, argInfos, argOffsets, nargs, retGetter) {
// `IsSBEligibleSignature` on the native side rejects 0-arg signatures,
// so this branch is unreachable today. It's kept as defense-in-depth
// for when that filter changes or for programmatic callers that hand a
// 0-arg signature through `wrapWithSharedBuffer` directly.
/* c8 ignore start */
if (nargs === 0) {
if (retGetter === null) {
return function() {
if (arguments.length !== 0) {
throwFFIArgCountError(0, arguments.length);
}
rawFn();
};
}
return function() {
if (arguments.length !== 0) {
throwFFIArgCountError(0, arguments.length);
}
rawFn();
return retGetter(view, 0, true);
};
}
/* c8 ignore stop */
if (nargs === 1) {
const i0 = argInfos[0];
const o0 = argOffsets[0];
if (retGetter === null) {
return function(a0) {
if (arguments.length !== 1) {
throwFFIArgCountError(1, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
rawFn();
};
}
return function(a0) {
if (arguments.length !== 1) {
throwFFIArgCountError(1, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
rawFn();
return retGetter(view, 0, true);
};
}
if (nargs === 2) {
const i0 = argInfos[0];
const i1 = argInfos[1];
const o0 = argOffsets[0];
const o1 = argOffsets[1];
if (retGetter === null) {
return function(a0, a1) {
if (arguments.length !== 2) {
throwFFIArgCountError(2, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
rawFn();
};
}
return function(a0, a1) {
if (arguments.length !== 2) {
throwFFIArgCountError(2, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
rawFn();
return retGetter(view, 0, true);
};
}
if (nargs === 3) {
const i0 = argInfos[0];
const i1 = argInfos[1];
const i2 = argInfos[2];
const o0 = argOffsets[0];
const o1 = argOffsets[1];
const o2 = argOffsets[2];
if (retGetter === null) {
return function(a0, a1, a2) {
if (arguments.length !== 3) {
throwFFIArgCountError(3, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
rawFn();
};
}
return function(a0, a1, a2) {
if (arguments.length !== 3) {
throwFFIArgCountError(3, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
rawFn();
return retGetter(view, 0, true);
};
}
if (nargs === 4) {
const i0 = argInfos[0];
const i1 = argInfos[1];
const i2 = argInfos[2];
const i3 = argInfos[3];
const o0 = argOffsets[0];
const o1 = argOffsets[1];
const o2 = argOffsets[2];
const o3 = argOffsets[3];
if (retGetter === null) {
return function(a0, a1, a2, a3) {
if (arguments.length !== 4) {
throwFFIArgCountError(4, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
rawFn();
};
}
return function(a0, a1, a2, a3) {
if (arguments.length !== 4) {
throwFFIArgCountError(4, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
rawFn();
return retGetter(view, 0, true);
};
}
if (nargs === 5) {
const i0 = argInfos[0];
const i1 = argInfos[1];
const i2 = argInfos[2];
const i3 = argInfos[3];
const i4 = argInfos[4];
const o0 = argOffsets[0];
const o1 = argOffsets[1];
const o2 = argOffsets[2];
const o3 = argOffsets[3];
const o4 = argOffsets[4];
if (retGetter === null) {
return function(a0, a1, a2, a3, a4) {
if (arguments.length !== 5) {
throwFFIArgCountError(5, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
writeNumericArg(view, i4, o4, a4, 4);
rawFn();
};
}
return function(a0, a1, a2, a3, a4) {
if (arguments.length !== 5) {
throwFFIArgCountError(5, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
writeNumericArg(view, i4, o4, a4, 4);
rawFn();
return retGetter(view, 0, true);
};
}
if (nargs === 6) {
const i0 = argInfos[0];
const i1 = argInfos[1];
const i2 = argInfos[2];
const i3 = argInfos[3];
const i4 = argInfos[4];
const i5 = argInfos[5];
const o0 = argOffsets[0];
const o1 = argOffsets[1];
const o2 = argOffsets[2];
const o3 = argOffsets[3];
const o4 = argOffsets[4];
const o5 = argOffsets[5];
if (retGetter === null) {
return function(a0, a1, a2, a3, a4, a5) {
if (arguments.length !== 6) {
throwFFIArgCountError(6, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
writeNumericArg(view, i4, o4, a4, 4);
writeNumericArg(view, i5, o5, a5, 5);
rawFn();
};
}
return function(a0, a1, a2, a3, a4, a5) {
if (arguments.length !== 6) {
throwFFIArgCountError(6, arguments.length);
}
writeNumericArg(view, i0, o0, a0, 0);
writeNumericArg(view, i1, o1, a1, 1);
writeNumericArg(view, i2, o2, a2, 2);
writeNumericArg(view, i3, o3, a3, 3);
writeNumericArg(view, i4, o4, a4, 4);
writeNumericArg(view, i5, o5, a5, 5);
rawFn();
return retGetter(view, 0, true);
};
}
// 7+ args: further specialization is diminishing returns and bloats
// this builder.
if (retGetter === null) {
return function(...args) {
if (args.length !== nargs) {
throwFFIArgCountError(nargs, args.length);
}
for (let i = 0; i < nargs; i++) {
writeNumericArg(view, argInfos[i], argOffsets[i], args[i], i);
}
rawFn();
};
}
return function(...args) {
if (args.length !== nargs) {
throwFFIArgCountError(nargs, args.length);
}
for (let i = 0; i < nargs; i++) {
writeNumericArg(view, argInfos[i], argOffsets[i], args[i], i);
}
rawFn();
return retGetter(view, 0, true);
};
}
// Accept-set mirrors the native `ParseFunctionSignature` in
// `src/ffi/types.cc`. `ParseFunctionSignature` additionally throws when
// multiple aliases are set at once. The wrapper runs before the native
// call, so those conflicts still surface from the native side regardless
// of which alias we happen to read here.
function sigParams(sig) {
return sig.parameters ?? sig.arguments ?? [];
}
function sigResult(sig) {
return sig.result ?? sig.return ?? sig.returns ?? 'void';
}
// The native invoker for SB-eligible symbols is `InvokeFunctionSB`, which
// reads arguments from the shared buffer populated by
// `wrapWithSharedBuffer`. These patches make sure every path that surfaces
// a raw SB-eligible function to user code (`getFunction`, `getFunctions`,
// and the `functions` accessor) returns the wrapper instead.
const rawGetFunction = DynamicLibrary.prototype.getFunction;
const rawGetFunctions = DynamicLibrary.prototype.getFunctions;
DynamicLibrary.prototype.getFunction = function getFunction(name, sig) {
// Native `DynamicLibrary::GetFunction` validates `sig`, so by the time
// we have `raw` we know `sig` is a valid object.
const raw = FunctionPrototypeCall(rawGetFunction, this, name, sig);
return wrapWithSharedBuffer(raw, sigParams(sig), sigResult(sig));
};
DynamicLibrary.prototype.getFunctions = function getFunctions(definitions) {
// Native `GetFunctions` switches on `args.Length() > 0`. Zero args
// returns every cached function, one arg requires an object. Forwarding
// `undefined` would fail the object check, so drop it when omitted.
const raw = definitions === undefined ?
FunctionPrototypeCall(rawGetFunctions, this) :
FunctionPrototypeCall(rawGetFunctions, this, definitions);
if (raw === undefined || raw === null) return raw;
const keys = ObjectKeys(raw);
const out = { __proto__: null };
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
// No `definitions`: native side returned every cached function, so we
// wrap using each function's own `kSbParams` / `kSbResult` metadata
// (same fallback as the `functions` accessor).
if (definitions === undefined) {
out[name] = wrapWithSharedBuffer(raw[name]);
} else {
const sig = definitions[name];
out[name] = wrapWithSharedBuffer(raw[name], sigParams(sig), sigResult(sig));
}
}
return out;
};
{
// The native side installs `functions` as an accessor returning raw
// functions. Rewrap each access so `lib.functions.foo(...)` goes through
// the SB wrapper instead of invoking the fast path against an
// uninitialized buffer.
const functionsDescriptor =
ObjectGetOwnPropertyDescriptor(DynamicLibrary.prototype, 'functions');
/* c8 ignore start */
if (functionsDescriptor === undefined || !functionsDescriptor.get) {
// Missing getter means the native and JS sides are out of sync; silently
// skipping the patch would expose the fast-path-against-uninitialized-buffer
// footgun this whole block exists to prevent.
throw new ERR_INTERNAL_ASSERTION(
'FFI: DynamicLibrary.prototype.functions accessor not found or has no getter');
}
/* c8 ignore stop */
const origGetter = functionsDescriptor.get;
ObjectDefineProperty(DynamicLibrary.prototype, 'functions', {
__proto__: null,
configurable: true,
enumerable: functionsDescriptor.enumerable,
get() {
const raw = FunctionPrototypeCall(origGetter, this);
if (raw === undefined || raw === null) return raw;
const wrapped = { __proto__: null };
const keys = ObjectKeys(raw);
for (let i = 0; i < keys.length; i++) {
const name = keys[i];
wrapped[name] = wrapWithSharedBuffer(raw[name]);
}
return wrapped;
},
});
}
module.exports = {
wrapWithSharedBuffer,
};