API reference

node:ffi

11/12Coverage9/12Strict (zero caveats)

Node's experimental node:ffi module (v26.1.0+) — call C functions from shared libraries, or from the process itself, with plain-object signatures, and peek/poke native memory through bigint pointers. Ported from Node's own lib/ffi.js and node_ffi.cc over libffi: a signature is an ordinary value checked when a function is requested, calls and callbacks go through libffi call interfaces and closures, and argument checks, error codes and the symbols/functions order are Node's. 64-bit integers and pointers travel as bigint, strings pass as NUL-terminated UTF-8, and Buffers/typed arrays/ArrayBuffers pass their data pointer.

Exact Works, with caveats Not supported

ffi.dlopen(path, definitions?)

Exact
dlopen(path: string | null, definitions?: Record<string, { arguments?: string[], return?: string }>): { lib: DynamicLibrary, functions: Record<string, Function> }

Loads a shared library (or the current process image for null) and resolves every function in definitions in one step. The definitions are ordinary values, checked like Node's; a failing definition closes the library before the error propagates. The path may be any runtime string. Failures throw an Error carrying dlerror().

Returns

{ lib, functions } — The library handle plus one callable per definition.

const { lib, functions } = ffi.dlopen(null, {
  strlen: { arguments: ['string'], return: 'uint64' },
});
See it in a full example →

new DynamicLibrary(path) / lib.close() / ffi.dlclose(lib)

Works, with caveats
new DynamicLibrary(path: string | null); close(): void; dlclose(lib: DynamicLibrary): void

Opens a library without resolving anything yet; lib.path reads the path back. close() and [Symbol.dispose]() release the handle and are idempotent, as in Node; afterwards every call and lookup throws ERR_FFI_LIBRARY_CLOSED.

Returns

DynamicLibrary — A library handle.

const lib = new DynamicLibrary(`./libtest.${ffi.suffix}`);
lib.close();
See it in a full example →
⚠ Differences from standard
  • console.log(lib) shows DynamicLibrary {} where Node shows its own accessors ({ path: [Getter], symbols: [Getter] })
  • using declarations are not parsed yet

lib.getFunction(name, signature) / lib.getFunctions(definitions)

Exact
getFunction(name: string, signature: { arguments?: string[], return?: string }): Function; getFunctions(definitions: object): Record<string, Function>

Binds one native symbol (or a map of them) to a directly-callable function. The signature is an ordinary object; type names accept every Node alias spelling (i32, ptr, str, double, …) and the ffi.types constants. The result is a real function object, as in Node: typeof 'function', name and length, an own pointer property (the raw address as a bigint), one identity per symbol per library, and util.inspect's [Function: abs] { pointer: … }.

Returns

Function — A callable lowered to a direct C-ABI call.

const strchr = lib.getFunction('strchr', { arguments: ['string', 'int32'], return: 'string' });
See it in a full example →

lib.getSymbol(name) / ffi.dlsym(lib, name)

Exact
getSymbol(name: string): bigint; dlsym(lib: DynamicLibrary, name: string): bigint

Resolves a symbol to its raw address as a bigint. A missing symbol throws a catchable Error carrying dlerror().

Returns

bigint — The symbol's address.

const addr = lib.getSymbol('strlen');
See it in a full example →

Typed calls + marshalling

Exact
functions.name(...args)

Arguments convert per the declared type: numbers for ints up to 32 bits and floats, bigint for int64/uint64 and pointers, strings pass NUL-terminated UTF-8, and Buffer/typed-array/ArrayBuffer arguments pass their backing data pointer. Returns widen back the same way; pointer returns are bigint addresses and a string return is pointer-like too — the raw char* as a bigint, not an auto-marshalled JS string (read it with ffi.toString), exactly as Node exposes it. function-typed values pass as raw bigint addresses; creating one from a closure needs registerCallback. Arguments are validated exactly as Node does, for direct calls and calls through any: in-range integers (-0 rejected only by the 32-bit kinds), an in-range bigint for 64-bit types, any number for floats, and a bigint in [0, 2^64), string, buffer/view/ArrayBuffer, null or undefined for the pointer family; bool is uint8 and char the platform char. A wrong argument count throws.

Returns

number | bigint | string | void — Per the signature's return type.

functions.pow(2, 16);        // 65536
functions.strlen('hello');   // 5n
See it in a full example →

lib.functions / lib.symbols / lib.getFunctions() / lib.getSymbols()

Exact
functions: Record<string, Function>; symbols: Record<string, bigint>; getFunctions(): Record<string, Function>; getSymbols(): Record<string, bigint>

Node's accumulators of everything previously resolved through the library: functions holds the callable typed wrappers (dlopen definitions plus getFunction registrations), symbols every resolved address as a bigint. Each read is a fresh null-prototype object in Node's own enumeration order — Node keeps these tables in a std::unordered_map, and so does the runtime here (libc++ on macOS, libstdc++ on Linux, identical to Node by construction; an MSVC-STL port on Windows, checked against Node 26.10 there). Every resolution is recorded, runtime-computed names included; after close() the accessors throw ERR_FFI_LIBRARY_CLOSED.

Returns

object — Callable wrappers or bigint addresses, keyed by name.

lib.getSymbol('getpid');
Object.keys(lib.symbols); // ['strlen', 'getpid', ...]
lib.functions.strlen('hi'); // 2n
See it in a full example →

lib.registerCallback / unregisterCallback / refCallback / unrefCallback

Works, with caveats
registerCallback(signature: { arguments?: string[], return?: string }, callback: Function): bigint; unregisterCallback(pointer: bigint): void; refCallback(pointer: bigint): void; unrefCallback(pointer: bigint): void

Hands a closure to C as a raw function pointer: the returned bigint is a real C-callable address (a libffi closure) whose invocations marshal the C arguments to the callback and its return value back out; any number may be live at once. unregisterCallback frees it.

Returns

bigint — The C-callable function-pointer address.

const cmp = lib.registerCallback(
  { arguments: ['pointer', 'pointer'], return: 'int32' },
  (a: bigint, b: bigint): number => ffi.getInt32(a) - ffi.getInt32(b)
);
functions.qsort(ptr, 5n, 4n, cmp);
See it in a full example →
⚠ Differences from standard
  • refCallback/unrefCallback are validated no-ops, and library.close() does not invalidate callbacks — unregister explicitly; an exception a callback throws is reported as uncaught once the foreign call returns

ffi.getInt8…getFloat64 / ffi.setInt8…setFloat64

Exact
getInt32(pointer: bigint, offset?: number): number; setInt32(pointer: bigint, offset: number, value: number): void; getInt64(...): bigint; setInt64(..., value: bigint): void

Read and write native memory at a bigint address plus a byte offset. All ten widths in both directions: 8/16/32-bit values are numbers, 64-bit values are bigint (exact — values above 2^53 round-trip), float32 narrows through double. Getter offsets are optional (default 0); setter offsets are required, matching Node. No bounds checking — the module's documented unsafe posture.

Returns

number | bigint | void — The read value; setters return nothing.

ffi.setInt32(p, 0, 1234);
ffi.getInt32(p, 0); // 1234
See it in a full example →

ffi.toString / toBuffer / toArrayBuffer / getRawPointer

Exact
toString(pointer: bigint): string | null; toBuffer(pointer: bigint, length: number, copy?: boolean): Buffer; toArrayBuffer(pointer: bigint, length: number, copy?: boolean): ArrayBuffer; getRawPointer(src: Buffer | ArrayBuffer | ArrayBufferView): bigint

Bring native memory into JS values. toString reads a NUL-terminated UTF-8 C string (null for 0n). toBuffer/toArrayBuffer take length bytes — copied by default, or as a zero-copy view with copy: false (the caller keeps the memory valid, Node's contract). getRawPointer goes the other way: the backing data pointer of a Buffer/ArrayBuffer/view as a bigint address.

Returns

string | Buffer | ArrayBuffer | bigint — Per helper.

const s = ffi.toString(ptr);
const view = ffi.toBuffer(ptr, 16, false); // zero-copy
See it in a full example →

ffi.exportString / exportBuffer / exportArrayBuffer / exportArrayBufferView

Exact
exportString(str: string, pointer: bigint, length: number, encoding?: 'utf8'): void; exportBuffer(src: Buffer, pointer: bigint, length: number): void; exportArrayBuffer(src: ArrayBuffer, pointer: bigint, length: number): void; exportArrayBufferView(src: ArrayBufferView, pointer: bigint, length: number): void

Copy JS data into caller-provided native memory. exportString encodes with any Buffer encoding (utf8 by default; utf16le/ucs2 get a 2-byte terminator) and, as in Node, throws ERR_OUT_OF_RANGE when length cannot hold the encoded string plus its terminator, ERR_INVALID_ARG_TYPE for a bigint length, and ERR_UNKNOWN_ENCODING for an unknown encoding. The byte-export forms copy the source's bytes and throw a RangeError when length is smaller than the source's byteLength.

Returns

void — Nothing.

ffi.exportString('written from TS', ptr, 32);
See it in a full example →

ffi.suffix / ffi.types

Exact
suffix: 'dylib' | 'so' | 'dll'; types: Record<string, string>

suffix is the host's shared-library filename suffix ('dylib' on macOS, 'so' on Linux, 'dll' on Windows), a compile-time constant. ffi.types holds the type-name string constants (ffi.types.INT_32 === 'int32', …) usable anywhere a type name goes in a signature.

Returns

string — The constant string.

const path = `libsqlite3.${ffi.suffix}`;
See it in a full example →