How to Use FFI in Deno: A Complete Guide to Calling Native Libraries

Deno’s FFI API lets you call native functions from shared libraries (.so, .dylib, .dll) directly using Deno.dlopen, which validates symbols, marshals arguments through the TurboCall engine, and requires the --allow-ffi permission flag.

Deno provides a powerful Foreign Function Interface (FFI) that bridges JavaScript and native code without writing C++ bindings or add-ons. As implemented in the denoland/deno repository, the FFI layer lives in the ext/ffi crate and exposes a high-level API through Deno.dlopen for loading dynamic libraries and calling their functions with automatic type marshalling.

How Deno FFI Works Under the Hood

The FFI implementation spans several Rust modules in ext/ffi/ and a JavaScript shim, working together to convert JavaScript calls into native machine instructions.

Core Architecture Components

  • Deno.dlopen – The entry point defined in ext/ffi/00_ffi.js that loads a dynamic library and returns a DynamicLibrary resource.
  • lib.rs – Implements the dlopen op, validates arguments, creates the library handle, and registers it with the resource table.
  • turbocall.rs – Generates low-level call stubs that marshal JavaScript values into native arguments and back at runtime.
  • symbol.rs – Looks up symbols in the loaded library and creates callable ForeignFunction wrappers.
  • repr.rs – Defines type representations (i32, u8, pointer, struct, etc.) used for argument and return value layout.
  • callback.rs – Enables native code to call back into JavaScript by creating stable function pointers that the host can invoke.

Execution Flow

  1. ValidationDeno.dlopen checks the supplied symbols map and forwards the request to the Rust op in lib.rs.
  2. Loading – Platform-specific helpers load the shared object (dlfcn.rs on Unix, winapi on Windows).
  3. Symbol Resolution – For each entry in the symbols map, symbol.rs creates a ForeignFunction descriptor using type information from repr.rs.
  4. Invocation – Calls from JavaScript are compiled on-the-fly using the TurboCall engine in turbocall.rs, which handles conversions and invokes the native pointer.
  5. Callbacks – If native code needs to call JavaScript, callback.rs registers a stable callback pointer routed through the runtime’s async dispatcher.

Required Permissions

Loading a library requires the --allow-ffi flag. The permission system validates this flag before the dlopen op executes in lib.rs.

Calling Simple Native Functions

To call a native function, define its C signature in the symbols map when opening the library.

// hello.c compiled as libhello.so / hello.dll / libhello.dylib:
// int add(int a, int b) { return a + b; }

const lib = Deno.dlopen(
  "./libhello.so",
  {
    "add": { parameters: ["i32", "i32"], result: "i32" },
  },
);

const result = lib.symbols.add(3, 4);
console.log("3 + 4 =", result); // → 7

lib.close(); // Free the library resource

The symbols map tells Deno the exact C signature. The lib.symbols.add function uses TurboCall to marshal arguments and invoke the native pointer. This pattern is tested in tests/unit/ffi_test.ts.

Working with Pointers and Buffers

Pass JavaScript buffers directly as pointers to native functions using the "pointer" type.

// buffer.c:
// void write_message(char *buf, size_t len) { memcpy(buf, "hello", len); }

const lib = Deno.dlopen("./libbuffer.so", {
  "write_message": { parameters: ["pointer", "usize"], result: "void" },
});

const buf = new Uint8Array(5);
lib.symbols.write_message(buf, buf.length);
console.log(new TextDecoder().decode(buf)); // → "hello"
lib.close();

When you pass a Uint8Array as a "pointer" parameter, Deno automatically extracts the underlying memory address. The "usize" type represents the buffer length. This marshalling logic lives in repr.rs and turbocall.rs.

Creating Callbacks for Native Code

Use Deno.UnsafeCallback to let native functions invoke JavaScript functions through function pointers.

// callback.c:
// typedef void (*cb_t)(int);
// void call_twice(cb_t cb) { cb(1); cb(2); }

const lib = Deno.dlopen("./libcallback.so", {
  "call_twice": { parameters: ["function"], result: "void" },
});

function rustCallback(value: number) {
  console.log("Callback received:", value);
}

const cbPtr = Deno.UnsafeCallback(
  { parameters: ["i32"], result: "void" },
  rustCallback
);

lib.symbols.call_twice(cbPtr.pointer);
Deno.UnsafeCallback.release(cbPtr);
lib.close();

Deno.UnsafeCallback creates a native function pointer that C code can store and call. The callback signature must match the parameters and result schema used in the symbols map. The implementation in callback.rs handles the lifetime management and routing back to the JavaScript runtime.

Dynamic Symbol Resolution

You can load a library without declaring symbols upfront and resolve them on demand.

const lib = Deno.dlopen("./libdynamic.so", null); // No symbols map

// Resolve symbol on first access
const add = lib.symbols["add"];
const sum = add(10, 20);
console.log(sum);

When the symbols map is null, Deno defers symbol resolution until the first property access. This dynamic lookup behavior is implemented in the dlopen op in lib.rs.

Summary

  • Use Deno.dlopen to load shared libraries (.so, .dylib, .dll) with a symbols map describing C function signatures.
  • Pass --allow-ffi when running scripts to grant permission for loading native libraries.
  • Map types correctly using "i32", "pointer", "usize", "void", and other specifiers defined in repr.rs.
  • Pass buffers as pointers by supplying Uint8Array instances directly to functions expecting "pointer" parameters.
  • Create callbacks with Deno.UnsafeCallback to expose JavaScript functions as native function pointers that C code can invoke.
  • Manage resources by calling lib.close() and Deno.UnsafeCallback.release() to free native resources.

Frequently Asked Questions

What permission is required to use Deno FFI?

You must run Deno with the --allow-ffi flag. The permission system checks this flag in lib.rs before executing the dlopen operation that loads the dynamic library into memory.

How do I handle strings when using FFI in Deno?

Pass strings as Uint8Array buffers using new TextEncoder().encode(str) and accept them as "pointer" parameters. For returning strings from native code, allocate a buffer in JavaScript, pass it as a writable pointer, and decode it with new TextDecoder().decode(buffer) after the native function writes to it.

Can native code call JavaScript functions through Deno FFI?

Yes. Use Deno.UnsafeCallback to create a stable native function pointer from a JavaScript function. Pass this pointer (accessed via .pointer) to native functions that accept callback arguments. The implementation in callback.rs routes these invocations back through Deno’s runtime.

What shared library formats does Deno support?

Deno supports platform-specific formats: .so (Linux), .dylib (macOS), and .dll (Windows). The lib.rs module uses dlfcn.rs on Unix systems and winapi on Windows to handle the platform-specific loading mechanics.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →