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:
| Platform | suffix value |
|---|
| macOS | dylib |
| Linux | so |
| Windows | dll |
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
32-bit signed integer. Maps to C int.
64-bit floating-point number. Maps to C double.
Null-terminated C string. JavaScript strings are automatically converted.
No return value. Use for functions declared void in C.
Raw pointer value. Use when the native function takes or returns an opaque pointer.
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:
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.