node:ffi
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.
ffi.dlopen(path, definitions?)
ExactLoads 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' },
});new DynamicLibrary(path) / lib.close() / ffi.dlclose(lib)
Works, with caveatsOpens 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.
ReturnsDynamicLibrary — A library handle.
const lib = new DynamicLibrary(`./libtest.${ffi.suffix}`);
lib.close();- 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)
ExactBinds 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: … }.
ReturnsFunction — A callable lowered to a direct C-ABI call.
const strchr = lib.getFunction('strchr', { arguments: ['string', 'int32'], return: 'string' });lib.getSymbol(name) / ffi.dlsym(lib, name)
ExactResolves a symbol to its raw address as a bigint. A missing symbol throws a catchable Error carrying dlerror().
Returnsbigint — The symbol's address.
const addr = lib.getSymbol('strlen');Typed calls + marshalling
ExactArguments 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.
Returnsnumber | bigint | string | void — Per the signature's return type.
functions.pow(2, 16); // 65536
functions.strlen('hello'); // 5nlib.functions / lib.symbols / lib.getFunctions() / lib.getSymbols()
ExactNode'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.
Returnsobject — Callable wrappers or bigint addresses, keyed by name.
lib.getSymbol('getpid');
Object.keys(lib.symbols); // ['strlen', 'getpid', ...]
lib.functions.strlen('hi'); // 2nlib.registerCallback / unregisterCallback / refCallback / unrefCallback
Works, with caveatsHands 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.
Returnsbigint — 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);- 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
ExactRead 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.
Returnsnumber | bigint | void — The read value; setters return nothing.
ffi.setInt32(p, 0, 1234);
ffi.getInt32(p, 0); // 1234ffi.toString / toBuffer / toArrayBuffer / getRawPointer
ExactBring 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.
Returnsstring | Buffer | ArrayBuffer | bigint — Per helper.
const s = ffi.toString(ptr);
const view = ffi.toBuffer(ptr, 16, false); // zero-copyffi.exportString / exportBuffer / exportArrayBuffer / exportArrayBufferView
ExactCopy 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.
Returnsvoid — Nothing.
ffi.exportString('written from TS', ptr, 32);ffi.suffix / ffi.types
Exactsuffix 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.
Returnsstring — The constant string.
const path = `libsqlite3.${ffi.suffix}`;