Gleam FFI

FFI stands for Foreign Function Interface. It lets Gleam call functions written in other languages — Erlang for the BEAM target and JavaScript for the browser/Node.js target. FFI bridges the gap between Gleam's type-safe world and existing code in the broader ecosystem.

When to Use FFI


FFI Use Cases
──────────────────────────────────────────────────
✓ Using Erlang standard library functions not yet in gleam_stdlib
✓ Calling Elixir library functions
✓ Integrating with npm packages (JS target)
✓ Accessing browser APIs (window, document, fetch)
✓ Using platform-specific optimized code
✓ Wrapping a C extension via Erlang NIFs

The @external Attribute


Syntax
──────────────────────────────────────────────────
@external(target, "module", "function")
pub fn gleam_name(param: Type) -> ReturnType

target = "erlang"     ← for Erlang/BEAM target
target = "javascript" ← for JS target

Erlang FFI


// Call erlang:phash2 for hashing
@external(erlang, "erlang", "phash2")
pub fn hash(term: a) -> Int

// Get node name
@external(erlang, "erlang", "node")
pub fn node_name() -> atom.Atom

// List all running processes
@external(erlang, "erlang", "processes")
pub fn all_processes() -> List(process.Pid)

// Format a term as a string (Erlang debugging)
@external(erlang, "io_lib", "format")
fn erlang_format(fmt: charlist, args: List(a)) -> charlist

JavaScript FFI


// Browser APIs
@external(javascript, "window", "alert")
pub fn alert(message: String) -> Nil

@external(javascript, "JSON", "stringify")
pub fn json_stringify(value: a) -> String

// Fetch API wrapper
@external(javascript, "./fetch_ffi.mjs", "fetchUrl")
pub fn fetch_url(url: String) -> Promise(String)

// Local module
@external(javascript, "./crypto_ffi.mjs", "sha256")
pub fn sha256(input: String) -> String

Writing FFI Helper Files

Complex JavaScript interactions go in a separate .mjs file:


// src/storage_ffi.mjs
export function getItem(key) {
  return localStorage.getItem(key);
}

export function setItem(key, value) {
  localStorage.setItem(key, value);
  return undefined;
}

export function removeItem(key) {
  localStorage.removeItem(key);
  return undefined;
}

// src/storage.gleam
@external(javascript, "./storage_ffi.mjs", "getItem")
fn js_get(key: String) -> String

@external(javascript, "./storage_ffi.mjs", "setItem")
fn js_set(key: String, value: String) -> Nil

@external(javascript, "./storage_ffi.mjs", "removeItem")
fn js_remove(key: String) -> Nil

// Public Gleam API — type-safe wrapper
pub fn get(key: String) -> Option(String) {
  case js_get(key) {
    "" -> None
    v  -> Some(v)
  }
}

pub fn set(key: String, value: String) -> Nil {
  js_set(key, value)
}

Dual-Target FFI

Write one Gleam function that has different implementations on each target using two @external declarations:


// src/platform/time.gleam

@external(erlang, "erlang", "system_time")
fn erlang_now(unit: atom.Atom) -> Int

@external(javascript, "Date", "now")
fn js_now() -> Int

// Pure Gleam fallback if no external declared
pub fn now_ms() -> Int {
  // Gleam picks the right @external at compile time
  // based on the --target flag
  0
}

In practice, packages that support both targets use one @external per target and no fallback body.

Type Safety in FFI


FFI Type Safety Rules
──────────────────────────────────────────────────
What Gleam guarantees:
  ✓ Gleam → FFI: argument types checked at compile time
  ✓ Return type annotation is trusted

What Gleam CANNOT guarantee:
  ✗ Runtime behavior of the external function
  ✗ That the external function actually returns the declared type
  ✗ That the external module exists at runtime

Your responsibility:
  → Validate external return values when in doubt
  → Wrap raw FFI in safe Gleam functions
  → Write tests for FFI wrappers

Safe FFI Wrapping Pattern


// Raw FFI — private, minimal, no validation
@external(erlang, "os", "getenv")
fn os_getenv(name: charlist) -> String

// Safe wrapper — public, validated, typed
pub fn get_env(name: String) -> Option(String) {
  let charlist_name = erlang.to_charlist(name)
  let raw = os_getenv(charlist_name)
  case string.length(raw) {
    0 -> None
    _ -> Some(raw)
  }
}

Practical Example — Crypto FFI

// src/crypto.gleam — Erlang target

import gleam/erlang/atom

@external(erlang, "crypto", "hash")
fn erlang_hash(algorithm: atom.Atom, data: BitArray) -> BitArray

pub fn sha256(input: String) -> BitArray {
  let alg = atom.create_from_string("sha256")
  erlang_hash(alg, <>)
}

pub fn md5(input: String) -> BitArray {
  let alg = atom.create_from_string("md5")
  erlang_hash(alg, <>)
}

Key Points


FFI Essentials
──────────────────────────────────────────────────
1. @external(target, "module", "function") declares FFI
2. Erlang target: call any Erlang/Elixir module
3. JavaScript target: call globals, objects, or .mjs files
4. Gleam checks argument types; trusts the return type annotation
5. Wrap raw FFI in a public Gleam function for safety
6. Use dual @external declarations for dual-target packages
7. Put complex JS logic in .mjs helper files, not inline

FFI makes Gleam pragmatic. You gain the full benefit of the BEAM and JavaScript ecosystems without waiting for native Gleam ports. Write a thin, well-tested wrapper and the rest of your codebase stays pure Gleam — type-safe, predictable, and portable.

Leave a Comment

Your email address will not be published. Required fields are marked *