@ 261105.16
current
'use strict';

const {
  BigInt,
  DataView,
  DataViewPrototypeGetBigInt64,
  DataViewPrototypeGetBigUint64,
  DataViewPrototypeGetFloat32,
  DataViewPrototypeGetFloat64,
  DataViewPrototypeGetInt16,
  DataViewPrototypeGetInt32,
  DataViewPrototypeGetInt8,
  DataViewPrototypeGetUint16,
  DataViewPrototypeGetUint32,
  DataViewPrototypeGetUint8,
  DataViewPrototypeSetBigInt64,
  DataViewPrototypeSetBigUint64,
  DataViewPrototypeSetFloat32,
  DataViewPrototypeSetFloat64,
  DataViewPrototypeSetInt16,
  DataViewPrototypeSetInt32,
  DataViewPrototypeSetInt8,
  DataViewPrototypeSetUint16,
  DataViewPrototypeSetUint32,
  DataViewPrototypeSetUint8,
  NumberIsInteger,
  NumberIsSafeInteger,
  ObjectDefineProperty,
  ReflectApply,
  TypeError,
} = primordials;

const assert = require('internal/assert');

const {
  charIsSigned,
  kSbArguments,
  kSbInvokeSlow,
  kSbReturn,
  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,
  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' },
  int8: { set: sI8, get: gI8, kind: 'int', min: -128, max: 127, label: 'an int8' },
  uint8: { set: sU8, get: gU8, kind: 'int', min: 0, max: 255, label: 'a uint8' },
  int16: { set: sI16, get: gI16, kind: 'int', min: -32768, max: 32767, label: 'an int16' },
  uint16: { set: sU16, get: gU16, kind: 'int', min: 0, max: 65535, label: 'a uint16' },
  int32: { set: sI32, get: gI32, kind: 'int', min: -2147483648, max: 2147483647, label: 'an int32' },
  uint32: { set: sU32, get: gU32, kind: 'int', min: 0, max: 4294967295, label: 'a uint32' },
  int64: { set: sI64, get: gI64, kind: 'i64', label: 'an int64' },
  uint64: { set: sU64, get: gU64, kind: 'u64', label: 'a uint64' },
  float32: { set: sF32, get: gF32, kind: 'float', label: 'a float' },
  float64: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
  pointer: { set: sU64, get: gU64, kind: 'pointer' },
  string: { set: sU64, get: gU64, kind: 'pointer' },
  buffer: { set: sU64, get: gU64, kind: 'pointer' },
  arraybuffer: { set: sU64, get: gU64, kind: 'pointer' },
  function: { set: sU64, get: gU64, kind: 'pointer' },

  // Alternative spellings.
  i8: { set: sI8, get: gI8, kind: 'int', min: -128, max: 127, label: 'an int8' },
  u8: { 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' },
  u16: { 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' },
  u32: { set: sU32, get: gU32, kind: 'int', min: 0, max: 4294967295, label: 'a uint32' },
  i64: { set: sI64, get: gI64, kind: 'i64', label: 'an int64' },
  u64: { 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' },
  f64: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
  double: { set: sF64, get: gF64, kind: 'float', label: 'a double' },
  ptr: { 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 === 'number') {
      if (!NumberIsSafeInteger(arg)) {
        throwFFIArgError(`Argument ${index} must be ${info.label}`);
      }
      arg = BigInt(arg);
    } else 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 === 'number') {
      if (!NumberIsSafeInteger(arg) || arg < 0) {
        throwFFIArgError(`Argument ${index} must be ${info.label}`);
      }
      arg = BigInt(arg);
    } else 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;
  }

  // Unreachable: caller filters out non-numeric kinds.
  /* c8 ignore next */
  assert.fail(`FFI: writeNumericArg reached with unexpected kind="${kind}"`);
}

// 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.
// Concise methods do not have [[Construct]], unlike function expressions, so
// use them below to match the native FFI functions' non-constructible behavior.
function wrapWithSharedBuffer(rawFn, signature) {
  if (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 `kSbArguments` / `kSbReturn` metadata attached
  // by the native `CreateFunction`.
  let argumentTypes, returnType;
  if (signature === undefined) {
    argumentTypes = rawFn[kSbArguments];
    returnType = rawFn[kSbReturn];

    // `CreateFunction` always attaches these for SB-eligible functions.
    // Missing here means the native side and this wrapper are out of sync.
    assert(argumentTypes !== undefined && returnType !== undefined,
           'FFI: shared-buffer raw function is missing kSbArguments or kSbReturn');
  } else {
    argumentTypes = signature.arguments ?? [];
    returnType = signature.return ?? 'void';
  }

  const slowInvoke = rawFn[kSbInvokeSlow];
  const view = new DataView(buffer);
  let retGetter = null;
  if (returnType !== 'void') {
    const retInfo = sbTypeInfo[returnType];
    assert(retInfo !== undefined,
           `FFI: shared-buffer type table missing entry for return type "${returnType}"`);
    retGetter = retInfo.get;
  }

  const nargs = argumentTypes.length;
  const argInfos = [];
  const argOffsets = [];
  let anyPointer = false;
  for (let i = 0; i < nargs; i++) {
    const info = sbTypeInfo[argumentTypes[i]];
    assert(info !== undefined,
           `FFI: shared-buffer type table missing entry for argument type "${argumentTypes[i]}"`);
    // 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.
    assert(slowInvoke !== undefined,
           'FFI: shared-buffer raw function with pointer arguments is missing kSbInvokeSlow');
    wrapper = { invoke(...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);
    } }.invoke;
  } 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 { invoke() {
        if (arguments.length !== 0) {
          throwFFIArgCountError(0, arguments.length);
        }
        rawFn();
      } }.invoke;
    }
    return { invoke() {
      if (arguments.length !== 0) {
        throwFFIArgCountError(0, arguments.length);
      }
      rawFn();
      return retGetter(view, 0, true);
    } }.invoke;
  }
  /* c8 ignore stop */
  if (nargs === 1) {
    const i0 = argInfos[0];
    const o0 = argOffsets[0];
    if (retGetter === null) {
      return { invoke(a0) {
        if (arguments.length !== 1) {
          throwFFIArgCountError(1, arguments.length);
        }
        writeNumericArg(view, i0, o0, a0, 0);
        rawFn();
      } }.invoke;
    }
    return { invoke(a0) {
      if (arguments.length !== 1) {
        throwFFIArgCountError(1, arguments.length);
      }
      writeNumericArg(view, i0, o0, a0, 0);
      rawFn();
      return retGetter(view, 0, true);
    } }.invoke;
  }
  if (nargs === 2) {
    const i0 = argInfos[0];
    const i1 = argInfos[1];
    const o0 = argOffsets[0];
    const o1 = argOffsets[1];
    if (retGetter === null) {
      return { invoke(a0, a1) {
        if (arguments.length !== 2) {
          throwFFIArgCountError(2, arguments.length);
        }
        writeNumericArg(view, i0, o0, a0, 0);
        writeNumericArg(view, i1, o1, a1, 1);
        rawFn();
      } }.invoke;
    }
    return { invoke(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);
    } }.invoke;
  }
  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 { invoke(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();
      } }.invoke;
    }
    return { invoke(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);
    } }.invoke;
  }
  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 { invoke(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();
      } }.invoke;
    }
    return { invoke(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);
    } }.invoke;
  }
  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 { invoke(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();
      } }.invoke;
    }
    return { invoke(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);
    } }.invoke;
  }
  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 { invoke(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();
      } }.invoke;
    }
    return { invoke(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);
    } }.invoke;
  }
  // 7+ args: further specialization is diminishing returns and bloats
  // this builder.
  if (retGetter === null) {
    return { invoke(...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();
    } }.invoke;
  }
  return { invoke(...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);
  } }.invoke;
}

module.exports = {
  wrapWithSharedBuffer,
};