UnFFI
Runtimes

Node.js (koffi)

Using unffi with the koffi backend on Node.js — structs, output params, opaque handles, wide strings.

Node.js has no stable built-in FFI before v26.3.0. unffi uses koffi — a pre-compiled native addon, no build step. It installs as an optional peer dependency.

npm install unffi koffi

If koffi is missing, dlopen throws with a clear install hint. When running on Node ≥26.3.0 with --experimental-ffi, unffi prefers native node:ffi automatically — fall back to koffi explicitly by importing from 'unffi' and using koffi-specific types.

Quick start

import { ,  } from 'unffi'

const  = await ('./libmath.dylib', {
  :  { : [., .], : . },
  : { : [.],        : . },
})

const sum  = ..(3, 4)
const sum: number
const root = ..(2.0)
const root: number

Core primitives (t.i32, t.cstring, t.buffer, t.fn(...), t.bool) work identically to the native backend.

Structs

import { dlopen, t } from 'unffi'

const Point = t.koffi.struct({ x: t.f64, y: t.f64 })

await using lib = await dlopen('./geom.dylib', {
  translate: { args: [Point, t.f64, t.f64], returns: Point },
  distance:  { args: [Point], returns: t.f64 },
})

lib.symbols.translate({ x: 1.0, y: 2.0 }, 10.0, 20.0)
// → { x: 11.0, y: 22.0 }

lib.symbols.distance({ x: 3.0, y: 4.0 })
// → 5.0

Field values accept unffi CType tokens or raw koffi type strings.

Arrays

const Hash32 = t.koffi.array(t.u8, 32)   // uint8_t[32]

await using lib = await dlopen('./crypto.dylib', {
  sha256: { args: [t.buffer, t.i32], returns: Hash32 },
})

const hash = lib.symbols.sha256(new Uint8Array(64), 64)
// → Uint8Array(32) [...]

Output parameters

C functions that write through a pointer:

const Point = t.koffi.struct({ x: t.f64, y: t.f64 })

await using lib = await dlopen('./geom.dylib', {
  get_origin: { args: [t.koffi.out(Point)], returns: t.void },
  get_bounds: { args: [t.koffi.out(Point), t.koffi.out(Point)], returns: t.void },
})

const [origin] = lib.symbols.get_origin()
// origin = { x: 0, y: 0 }

const [min, max] = lib.symbols.get_bounds()

t.koffi.out(type) handles pointer wrapping internally. Use t.koffi.inout(type) when the C function reads and writes.

Pointer-sized integers

t.koffi.uintptr   // uintptr_t → bigint
t.koffi.intptr    // intptr_t  → bigint

Opaque handles

const DB = t.koffi.opaque('DB')
const DBPtr = t.koffi.pointer(DB)

await using lib = await dlopen('./db.dylib', {
  db_open:  { args: [t.cstring], returns: DBPtr },
  db_exec:  { args: [DBPtr, t.cstring], returns: t.i32 },
  db_close: { args: [DBPtr], returns: t.void },
})

const db = lib.symbols.db_open('./data.db')
lib.symbols.db_exec(db, 'SELECT 1')
lib.symbols.db_close(db)

Windows wide strings

t.koffi.str16   // UTF-16 wchar_t* — for Win32 APIs

Async / nonblocking calls

koffi runs async calls on its own worker pool:

import { ,  } from 'unffi'

const  = await ('./codec.dylib', {
  : { : [., .], : ., : true },
})

const outLen = await ..(new (1024), 1024)
const outLen: number

Escape hatch

Direct access to the raw koffi module:

const koffi = t.koffi.raw

Installation

koffi is an optional peer dependency. Install it alongside unffi:

npm install unffi koffi

On this page