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

> Learn how to use FFI in Deno to call native libraries directly. Discover Deno.dlopen, symbol validation, and argument marshalling with this complete guide.

- Repository: [Deno/deno](https://github.com/denoland/deno)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/denoland/deno/blob/main/ext/ffi/00_ffi.js) that loads a dynamic library and returns a `DynamicLibrary` resource.
- **[`lib.rs`](https://github.com/denoland/deno/blob/main/lib.rs)** – Implements the `dlopen` op, validates arguments, creates the library handle, and registers it with the resource table.
- **[`turbocall.rs`](https://github.com/denoland/deno/blob/main/turbocall.rs)** – Generates low-level call stubs that marshal JavaScript values into native arguments and back at runtime.
- **[`symbol.rs`](https://github.com/denoland/deno/blob/main/symbol.rs)** – Looks up symbols in the loaded library and creates callable `ForeignFunction` wrappers.
- **[`repr.rs`](https://github.com/denoland/deno/blob/main/repr.rs)** – Defines type representations (`i32`, `u8`, `pointer`, `struct`, etc.) used for argument and return value layout.
- **[`callback.rs`](https://github.com/denoland/deno/blob/main/callback.rs)** – Enables native code to call back into JavaScript by creating stable function pointers that the host can invoke.

### Execution Flow

1. **Validation** – `Deno.dlopen` checks the supplied symbols map and forwards the request to the Rust op in [`lib.rs`](https://github.com/denoland/deno/blob/main/lib.rs).
2. **Loading** – Platform-specific helpers load the shared object ([`dlfcn.rs`](https://github.com/denoland/deno/blob/main/dlfcn.rs) on Unix, `winapi` on Windows).
3. **Symbol Resolution** – For each entry in the symbols map, [`symbol.rs`](https://github.com/denoland/deno/blob/main/symbol.rs) creates a `ForeignFunction` descriptor using type information from [`repr.rs`](https://github.com/denoland/deno/blob/main/repr.rs).
4. **Invocation** – Calls from JavaScript are compiled on-the-fly using the TurboCall engine in [`turbocall.rs`](https://github.com/denoland/deno/blob/main/turbocall.rs), which handles conversions and invokes the native pointer.
5. **Callbacks** – If native code needs to call JavaScript, [`callback.rs`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/lib.rs).

## Calling Simple Native Functions

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

```typescript
// 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`](https://github.com/denoland/deno/blob/main/tests/unit/ffi_test.ts).

## Working with Pointers and Buffers

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

```typescript
// 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`](https://github.com/denoland/deno/blob/main/repr.rs) and [`turbocall.rs`](https://github.com/denoland/deno/blob/main/turbocall.rs).

## Creating Callbacks for Native Code

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

```typescript
// 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`](https://github.com/denoland/deno/blob/main/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.

```typescript
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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/lib.rs) module uses [`dlfcn.rs`](https://github.com/denoland/deno/blob/main/dlfcn.rs) on Unix systems and `winapi` on Windows to handle the platform-specific loading mechanics.