Skip to main content
Ant’s Foreign Function Interface (FFI) lets you call functions in native shared libraries directly from JavaScript — without writing a C extension or a binding layer. You import dlopen, suffix, and FFIType from the ant:ffi built-in module, open the library by path, declare each function’s argument and return types, and start calling them as if they were ordinary JavaScript functions.
ant:ffi is specific to Ant and is not available in Node.js or Bun. Scripts that use it will not run on other runtimes.

Import the FFI module

import { dlopen, suffix, FFIType } from 'ant:ffi';
suffix gives you the platform-correct shared library file extension so your code works without branching:
Platformsuffix value
macOSdylib
Linuxso
Windowsdll

Open a shared library

dlopen<T>(path) opens the library at the given path and returns a typed handle. You pass a TypeScript interface as the type parameter to get autocomplete and type-checking on the functions you declare.
interface libC {
  putchar(c: number): number;
  printf(format: string, ...args: unknown[]): number;
}

const libc = dlopen<libC>(libcName);
The type parameter is optional in plain JavaScript — you can omit it and still call functions by name at runtime.

Declare function signatures

Before calling a function, tell Ant its argument types and return type using lib.define(name, { args, returns }). The types come from the FFIType enum.
libc.define('putchar', {
  args: [FFIType.int],
  returns: FFIType.int
});

libc.define('printf', {
  args: [FFIType.string, FFIType.spread],
  returns: FFIType.int
});

FFIType values

FFIType.int
enum value
32-bit signed integer. Maps to C int.
FFIType.double
enum value
64-bit floating-point number. Maps to C double.
FFIType.string
enum value
Null-terminated C string. JavaScript strings are automatically converted.
FFIType.void
enum value
No return value. Use for functions declared void in C.
FFIType.pointer
enum value
Raw pointer value. Use when the native function takes or returns an opaque pointer.
FFIType.spread
enum value
Variadic argument list. Place at the end of an args array for functions like printf that accept variable arguments.

Call functions

After defining a function you can call it in two ways. By property — treat the declared function as a method directly on the library handle:
libc.putchar(65);                          // prints 'A'
libc.printf('Hello FFI! I see %d\n', 42); // prints 'Hello FFI! I see 42'
By name — use lib.call(name, ...args) when you want to reference the function dynamically:
libc.call('putchar', 66); // prints 'B'
Both forms are equivalent at runtime. The property form is preferred when you have TypeScript types because it provides type-checking and autocomplete.

Full example: printf and putchar

This is the complete printf.ts example from the Ant repository. It detects the correct libc name for the current platform, declares two functions, and calls them both ways.
import { dlopen, suffix, FFIType } from 'ant:ffi';

let libcName: string;
if (process.platform === 'darwin') {
  libcName = `libSystem.${suffix}`;
} else if (process.platform === 'linux') {
  libcName = `libc.${suffix}`;
} else if (process.platform === 'win32') {
  libcName = `msvcrt.${suffix}`;
} else throw new Error(`Unsupported platform: ${process.platform}`);

interface libC {
  putchar(c: number): number;
  printf(format: string, ...args: unknown[]): number;
}

const libc = dlopen<libC>(libcName);

libc.define('putchar', {
  args: [FFIType.int],
  returns: FFIType.int
});

libc.define('printf', {
  args: [FFIType.string, FFIType.spread],
  returns: FFIType.int
});

console.log(libc);

console.log('calling putchar(65):');
libc.putchar(65); // 'A'

console.log('\ncalling printf:');
libc.printf('Hello FFI! I see %d\n', 42);

console.log('calling putchar(66):');
libc.call('putchar', 66); // 'B'
Run it with:
ant printf.ts

Example: calling a third-party library

The same pattern applies to any installed shared library. This example queries the SQLite version string by opening libsqlite3 and calling sqlite3_libversion, which takes no arguments and returns a string.
import { dlopen, suffix, FFIType } from 'ant:ffi';

const sqlite3 = dlopen(`libsqlite3.${suffix}`);

sqlite3.define('sqlite3_libversion', {
  args: [],
  returns: FFIType.string
});

console.log(`SQLite 3 version: ${sqlite3.call('sqlite3_libversion')}`);
When declaring a function with no arguments, pass an empty array: args: []. Omitting the args key entirely will throw an error at define time.
Ant does not verify that the types you pass to define match the actual native function signature. Passing the wrong types can corrupt memory or crash the process. Always check the C header for the correct signature before declaring a function.