Native FFI
js/native-ffi.md
fino:ffi loads native libraries and binds C ABI functions. It is a sharp
runtime extension surface for advanced applications and built-in modules that
need system calls, native codecs, crypto libraries, database clients, or
platform APIs before a higher-level fino:* wrapper exists.
Prefer public Fino modules when they cover the job. A wrong FFI signature, pointer lifetime, callback lifetime, struct layout, or ownership assumption can crash the process.
Load Symbols
import { dlopen } from 'fino:ffi';
const libc = dlopen(null, {
getpid: { parameters: [], result: 'i32' },
});
console.log(libc.symbols.getpid());
path may be null to resolve symbols from the current process. The symbol map
declares parameter and result descriptors used by the native binding layer.
Bind Function Pointers
Use ffiFunction(pointer, definition) for addresses returned by native APIs or
stored in C structures. It accepts a pointer buffer, a view over that buffer,
or a BigInt address, using the same signature options as dlopen().
import { dlopen, ffiFunction } from 'fino:ffi';
const libc = dlopen(null, { getpid: { parameters: [], result: 'i32' } });
const getpid = ffiFunction(libc.pointers.getpid, { parameters: [], result: 'i32' });
console.log(getpid());
libc.close();
A bound pointer does not retain its library or callback owner. Keep that owner
alive and open until every call, including asynchronous calls, has completed.
Use fast: false when the native function synchronously calls back into JS.
Null pointers and malformed signatures are rejected when binding; a non-null
address still needs to identify a live function with the declared ABI.
Work With Pointers
Pointer helpers read, write, copy, offset, and view native memory:
import { Pointer } from 'fino:ffi';
const bytes = new Uint8Array(8);
const ptr = Pointer.of(bytes);
Pointer.writeU32(ptr, 0, 42);
console.log(Pointer.readU32(ptr, 0));
Buffers passed to native code must remain alive for as long as native code may
read them. Native memory viewed through Pointer.view() must outlive the
returned buffer unless onRelease owns cleanup.
Callbacks And Blocking Work
FfiCallback creates native-callable JS callbacks. Closing a callback revokes
JavaScript invocation. Acquire callback.lease() when native work needs to keep
its executable pointer alive independently: subsequent calls return zero after
revocation. Release the lease only after the native API acknowledges completion
and its last callback invocation has returned. Resolving a Promise inside the
handler does not establish that condition. Ordinary leases must finish before
the owning Realm shuts down.
On macOS, callback.block() creates an Objective-C block and supplies its
implicit block argument automatically. Apple APIs that copy the block own the
callback code until their final native release, even after Realm shutdown.
Closing the returned block releases your reference; closing the callback
revokes JavaScript invocation through every copy. The optional resource list
transfers owned native references to the block, each paired with a synchronous
C void(void*) destructor. These destructors must never call JavaScript.
Use FfiResource for an owned allocation or native object:
using memory = new FfiResource(libc.symbols.malloc(64), libc.pointers.free);
Pointer.writeU8(memory.pointer, 0, 42);
Its destructor runs once on explicit disposal or during Realm teardown, before Realm completion is reported. A scheduled Realm can move between reactor threads, so the destructor must not call JavaScript or require thread affinity. Keep the library containing it loaded until release. Ownership roots are released explicitly or at teardown; garbage collection is not a cleanup signal.
Mark long-running symbols async: true so they run on the native
blocking pool instead of stalling the JS event loop.
The full fino:ffi surface — types, struct layout, callbacks, and pointer
helpers — is documented in the generated API reference.