Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

fumo:ffi

fumo:ffi provides intentionally unsafe Win64 native access.

It is available only when:

  1. Settings → Script Engine → Insecure scripting is enabled.
  2. The package manifest includes the ffi capability.
  3. The package is reloaded after the setting changes.
import { ffi, NativeType } from "fumo:ffi";

Native types

const NativeType = {
  void: "void",
  bool: "bool",
  int8: "int8",
  uint8: "uint8",
  int16: "int16",
  uint16: "uint16",
  int32: "int32",
  uint32: "uint32",
  int64: "int64",
  uint64: "uint64",
  pointer: "pointer",
  cstring: "cstring",
  float32: "float32",
  float64: "float64"
};

float32 and float64 are currently supported by read and write, not by bound function signatures. Bound calls accept at most eight integer, pointer, or C-string arguments.

Loading and binding

const kernel32 = ffi.openModule("kernel32.dll");

const getCurrentProcessId = kernel32.bind("GetCurrentProcessId", {
  abi: "win64",
  returns: NativeType.uint32,
  args: []
});

console.log("PID", getCurrentProcessId());

ffi.module(name) is an alias for ffi.openModule(name). Existing modules are reused through GetModuleHandle; otherwise the engine calls LoadLibrary.

An opened module has:

interface NativeModule {
  readonly name: string;
  readonly base: bigint;
  bind(name: string, signature?: NativeSignature): NativeFunction;
}

interface NativeSignature {
  abi?: "win64";
  returns?: NativeType;
  args?: NativeType[];
}

Pointer and 64-bit return values are JavaScript bigint values. Native calls are surrounded by a Windows exception boundary, but not every native failure is recoverable.

Allocating memory

const block = ffi.alloc(64);
try {
  ffi.write(block, NativeType.uint32, 0x12345678);
  console.log(ffi.read(block, NativeType.uint32));
} finally {
  ffi.free(block);
}

alloc uses read/write VirtualAlloc pages. Package-owned allocations are released automatically when the package unloads. free accepts only an address returned by that package’s ffi.alloc.

Reading and writing memory

ffi.read(address: bigint, type: NativeType, maxLength?: number): unknown;
ffi.write(address: bigint, type: NativeType, value: unknown): void;

read and write accept arbitrary process addresses. cstring reads stop at the first null byte and default to a 4 KiB maximum. The optional maximum is clamped to 1 MiB. C-string writes are not currently supported.

Invalid memory accesses normally throw a JavaScript error containing the Windows exception code.

Warning

Signatures are not verified. Calling an address with the wrong argument or return types can corrupt registers, the stack, heap state, or game state. Only enable insecure mode for code you fully trust.