current commit
261003.15 · ffi: accept safe integer numbers for 64-bit arguments
'use strict';

const {
  ArrayBufferPrototypeGetDetached,
  ArrayPrototypeIncludes,
  BigInt,
  DataViewPrototypeGetBuffer,
  NumberIsInteger,
  NumberIsSafeInteger,
  ObjectDefineProperty,
  ReflectApply,
  SafeWeakMap,
  StringPrototypeIncludes,
  TypeError,
  TypedArrayPrototypeGetBuffer,
} = primordials;

const {
  Buffer,
} = require('buffer');

const {
  isArrayBuffer,
  isArrayBufferView,
  isDataView,
} = require('internal/util/types');

const {
  charIsSigned,
  getRawPointer,
  kFastArguments,
  kFastBufferInvoke,
  uintptrMax,
} = internalBinding('ffi');

const {
  codes: {
    ERR_FFI_LIBRARY_CLOSED,
  },
} = require('internal/errors');

const U64_MAX = 0xFFFFFFFFFFFFFFFFn;
const I64_MAX = 0x7FFFFFFFFFFFFFFFn;
const I64_MIN = -0x8000000000000000n;
const fastLibraryStates = new SafeWeakMap();

// These ranges mirror ToFFIArgument in src/ffi/types.cc. V8's Fast API
// exposes narrow integers as 32-bit values and uses truncating BigInt
// conversions, so the public FFI ranges must be checked before the raw call.
const fastIntegerTypeInfo = {
  __proto__: null,
  char: charIsSigned ?
    { kind: 'number', min: -128, max: 127, label: 'an int8' } :
    { kind: 'number', min: 0, max: 255, label: 'a uint8' },
  int8: { kind: 'number', min: -128, max: 127, label: 'an int8' },
  uint8: { kind: 'number', min: 0, max: 255, label: 'a uint8' },
  int16: { kind: 'number', min: -32768, max: 32767, label: 'an int16' },
  uint16: { kind: 'number', min: 0, max: 65535, label: 'a uint16' },
  int32: { kind: 'number', min: -2147483648, max: 2147483647, label: 'an int32' },
  uint32: { kind: 'number', min: 0, max: 4294967295, label: 'a uint32' },
  int64: { kind: 'bigint', min: I64_MIN, max: I64_MAX, label: 'an int64' },
  uint64: { kind: 'bigint', min: 0n, max: U64_MAX, label: 'a uint64' },

  // Alternative spellings.
  i8: { kind: 'number', min: -128, max: 127, label: 'an int8' },
  u8: { kind: 'number', min: 0, max: 255, label: 'a uint8' },
  bool: { kind: 'number', min: 0, max: 255, label: 'a uint8' },
  i16: { kind: 'number', min: -32768, max: 32767, label: 'an int16' },
  u16: { kind: 'number', min: 0, max: 65535, label: 'a uint16' },
  i32: { kind: 'number', min: -2147483648, max: 2147483647, label: 'an int32' },
  u32: { kind: 'number', min: 0, max: 4294967295, label: 'a uint32' },
  i64: { kind: 'bigint', min: I64_MIN, max: I64_MAX, label: 'an int64' },
  u64: { kind: 'bigint', min: 0n, max: U64_MAX, label: 'a uint64' },
};

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}`);
}

function validateAndConvertFastIntegerArg(type, value, index) {
  const info = fastIntegerTypeInfo[type];
  if (info === undefined) return value;

  // The native Fast API expects BigInt for 64-bit integer arguments.
  if (info.kind === 'bigint' && typeof value === 'number') {
    if (!NumberIsSafeInteger(value) || (info.min === 0n && value < 0)) {
      throwFFIArgError(`Argument ${index} must be ${info.label}`);
    }
    return BigInt(value);
  }

  const validType = info.kind === 'number' ?
    typeof value === 'number' && NumberIsInteger(value) :
    typeof value === 'bigint';
  if (!validType || value < info.min || value > info.max) {
    throwFFIArgError(`Argument ${index} must be ${info.label}`);
  }

  return value;
}

function needsRawPointerConversion(type) {
  return type === 'buffer' || type === 'arraybuffer';
}

function needsPointerLikeConversion(type) {
  return type === 'pointer' || type === 'function' || type === 'buffer' ||
         type === 'arraybuffer' || type === 'ptr';
}

function needsStringPointerConversion(type) {
  return type === 'string' || type === 'str' || needsPointerLikeConversion(type);
}

function needsNullPointerConversion(type) {
  return needsPointerLikeConversion(type) || type === 'string' || type === 'str' ||
         needsRawPointerConversion(type);
}

function needsPointerConversion(type) {
  return needsRawPointerConversion(type) ||
         needsNullPointerConversion(type) || needsStringPointerConversion(type);
}

function validateFastPointerArg(type, value, index) {
  if (needsPointerConversion(type) && typeof value === 'bigint' &&
      (value < 0n || value > uintptrMax)) {
    throwFFIArgError(
      `Argument ${index} must be a non-negative pointer bigint`);
  }
}

function hasStringPointerArg(type, value) {
  return typeof value === 'string' && needsStringPointerConversion(type);
}

function hasPointerMemoryArg(type, value) {
  return (needsRawPointerConversion(type) || needsStringPointerConversion(type)) &&
         (isArrayBufferView(value) || isArrayBuffer(value));
}

function enterStringConversion(state) {
  if (state.buffers[state.depth] === undefined) {
    state.buffers[state.depth] = [];
  }
  state.depth++;
}

function exitStringConversion(state) {
  state.depth--;
}

function getStringConversionPointer(state, value, index) {
  const size = value.length * 3 + 1;
  const buffers = state.buffers[state.depth - 1];
  let entry = buffers[index];
  if (StringPrototypeIncludes(value, '\0')) {
    throwFFIArgError(`Argument ${index} must not contain null bytes`);
  }
  if (entry === undefined || entry.buffer.length < size) {
    const buffer = Buffer.allocUnsafe(size);
    entry = {
      __proto__: null,
      buffer,
      pointer: getRawPointer(buffer),
    };
    buffers[index] = entry;
  }

  const buffer = entry.buffer;
  const written = buffer.write(value, 0, size - 1, 'utf8');
  buffer[written] = 0;
  return entry.pointer;
}

function getRawPointerArg(value, index) {
  let buffer;
  let isView = false;
  if (isArrayBuffer(value)) {
    buffer = value;
  } else if (isArrayBufferView(value)) {
    isView = true;
    buffer = isDataView(value) ?
      DataViewPrototypeGetBuffer(value) :
      TypedArrayPrototypeGetBuffer(value);
  }

  if (buffer !== undefined && isArrayBuffer(buffer) &&
      ArrayBufferPrototypeGetDetached(buffer)) {
    throwFFIArgError(isView ?
      `Argument ${index} is an ArrayBufferView backed by a detached ArrayBuffer` :
      `Argument ${index} is a detached ArrayBuffer`);
  }

  return getRawPointer(value);
}

function convertPointerArg(type, value, stringState, index) {
  validateFastPointerArg(type, value, index);
  if (needsNullPointerConversion(type) &&
      (value === null || value === undefined)) {
    return 0n;
  }
  if (hasStringPointerArg(type, value)) {
    return getStringConversionPointer(stringState, value, index);
  }
  if (hasPointerMemoryArg(type, value)) {
    return getRawPointerArg(value, index);
  }
  // Pointer-like values (e.g. BigInt addresses) are passed through, matching
  // ToFFIArgument in src/ffi/types.cc and the single-argument fast path.
  return value;
}

function getFastArgumentIndexes(argumentsTypes) {
  let indexes = null;
  for (let i = 0; i < argumentsTypes.length; i++) {
    if (fastIntegerTypeInfo[argumentsTypes[i]] === undefined &&
        !needsPointerConversion(argumentsTypes[i])) {
      continue;
    }
    if (indexes === null) {
      indexes = [];
    }
    indexes.push(i);
  }
  return indexes;
}

function convertFastArg(type, value, stringState, index) {
  value = validateAndConvertFastIntegerArg(type, value, index);

  return needsPointerConversion(type) ?
    convertPointerArg(type, value, stringState, index) : value;
}

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;
}

function markFastLibraryClosed(owner) {
  const state = fastLibraryStates.get(owner);
  if (state !== undefined) {
    state.closed = true;
  }
}

function throwIfFastLibraryClosed(state) {
  if (state.closed) {
    throw new ERR_FFI_LIBRARY_CLOSED();
  }
}

// Concise methods do not have [[Construct]], unlike function expressions.
// Keep wrappers non-constructible to match the native FFI functions.
function wrapWithRawPointerConversions(rawFn, argumentTypes, owner) {
  if (rawFn === undefined || rawFn === null) {
    return rawFn;
  }
  if (argumentTypes === undefined) {
    argumentTypes = rawFn[kFastArguments];
  }
  if (argumentTypes === undefined) {
    return rawFn;
  }

  let state = fastLibraryStates.get(owner);
  if (state === undefined) {
    state = { __proto__: null, closed: false };
    fastLibraryStates.set(owner, state);
  }

  const indexes = getFastArgumentIndexes(argumentTypes) ?? [];

  const stringState = {
    __proto__: null,
    buffers: [],
    depth: 0,
  };

  const nargs = argumentTypes.length;
  let wrapper;
  if (nargs === 1 && indexes.length === 1 && indexes[0] === 0) {
    const t0 = argumentTypes[0];
    const string0 = needsStringPointerConversion(t0);
    const memory0 = needsRawPointerConversion(t0) || string0;
    const fastBufferInvoke = needsPointerLikeConversion(t0) ?
      rawFn[kFastBufferInvoke] : undefined;
    wrapper = { invoke(a0) {
      throwIfFastLibraryClosed(state);
      if (arguments.length !== 1) {
        throwFFIArgCountError(1, arguments.length);
      }
      let arg = validateAndConvertFastIntegerArg(t0, a0, 0);
      validateFastPointerArg(t0, a0, 0);
      if (needsNullPointerConversion(t0) &&
          (arg === null || arg === undefined)) {
        arg = 0n;
      } else if (string0 && typeof arg === 'string') {
        enterStringConversion(stringState);
        try {
          return rawFn(getStringConversionPointer(stringState, arg, 0));
        } finally {
          exitStringConversion(stringState);
        }
      } else if (memory0 && (isArrayBufferView(arg) || isArrayBuffer(arg))) {
        if (fastBufferInvoke !== undefined) {
          return fastBufferInvoke(arg);
        }
        arg = getRawPointerArg(arg, 0);
      }
      return rawFn(arg);
    } }.invoke;
  } else if (nargs === 2) {
    const c0 = ArrayPrototypeIncludes(indexes, 0);
    const c1 = ArrayPrototypeIncludes(indexes, 1);
    const t0 = argumentTypes[0];
    const t1 = argumentTypes[1];
    wrapper = { invoke(a0, a1) {
      throwIfFastLibraryClosed(state);
      if (arguments.length !== 2) {
        throwFFIArgCountError(2, arguments.length);
      }
      const stringCall = (c0 && hasStringPointerArg(t0, a0)) ||
                         (c1 && hasStringPointerArg(t1, a1));
      if (stringCall) enterStringConversion(stringState);
      try {
        return rawFn(c0 ? convertFastArg(t0, a0, stringState, 0) : a0,
                     c1 ? convertFastArg(t1, a1, stringState, 1) : a1);
      } finally {
        if (stringCall) exitStringConversion(stringState);
      }
    } }.invoke;
  } else if (nargs === 3) {
    const c0 = ArrayPrototypeIncludes(indexes, 0);
    const c1 = ArrayPrototypeIncludes(indexes, 1);
    const c2 = ArrayPrototypeIncludes(indexes, 2);
    const t0 = argumentTypes[0];
    const t1 = argumentTypes[1];
    const t2 = argumentTypes[2];
    wrapper = { invoke(a0, a1, a2) {
      throwIfFastLibraryClosed(state);
      if (arguments.length !== 3) {
        throwFFIArgCountError(3, arguments.length);
      }
      const stringCall = (c0 && hasStringPointerArg(t0, a0)) ||
                         (c1 && hasStringPointerArg(t1, a1)) ||
                         (c2 && hasStringPointerArg(t2, a2));
      if (stringCall) enterStringConversion(stringState);
      try {
        return rawFn(c0 ? convertFastArg(t0, a0, stringState, 0) : a0,
                     c1 ? convertFastArg(t1, a1, stringState, 1) : a1,
                     c2 ? convertFastArg(t2, a2, stringState, 2) : a2);
      } finally {
        if (stringCall) exitStringConversion(stringState);
      }
    } }.invoke;
  } else {
    wrapper = { invoke(...args) {
      throwIfFastLibraryClosed(state);
      if (args.length !== nargs) {
        throwFFIArgCountError(nargs, args.length);
      }
      let stringCall = false;
      for (let i = 0; i < indexes.length; i++) {
        const index = indexes[i];
        if (hasStringPointerArg(argumentTypes[index], args[index])) {
          stringCall = true;
          break;
        }
      }
      if (stringCall) enterStringConversion(stringState);
      try {
        for (let i = 0; i < indexes.length; i++) {
          const index = indexes[i];
          args[index] = convertFastArg(
            argumentTypes[index], args[index], stringState, index);
        }
        return ReflectApply(rawFn, undefined, args);
      } finally {
        if (stringCall) exitStringConversion(stringState);
      }
    } }.invoke;
  }

  return inheritMetadata(wrapper, rawFn, nargs);
}

module.exports = {
  convertPointerArg,
  hasPointerMemoryArg,
  hasStringPointerArg,
  markFastLibraryClosed,
  wrapWithRawPointerConversions,
};