Managing the Type Boundary: Gleam FFI for the BEAM
Learn how Gleam's FFI interacts with the BEAM, the risks of the trust boundary between static and dynamic types, and how to verify external function signatures using Dialyzer.
10 Apr 2026, 19:37 UTC

The Interop Problem: Static Types vs. Dynamic Terms
Gleam provides a strong, static type system, but it runs on the BEAM (the Erlang Virtual Machine), where data is represented as dynamic terms. To leverage the vast ecosystem of Erlang and Elixir libraries, Gleam uses a Foreign Function Interface (FFI). The core challenge is that the Gleam compiler cannot verify the internals of an Erlang module; it must trust that the developer's type declarations match the actual runtime behavior of the external code.
The takeaway for engineers is that the FFI is a trust boundary. While Gleam ensures type safety within your own code, the point where you call an external function is a potential site for runtime crashes if the signatures drift.
The Minimal Design: Zero-Conversion Stubs
Gleam's FFI is designed for maximum performance by avoiding runtime conversion overhead. When you declare an external function, the compiler generates a stub that passes BEAM terms directly between the caller and the target.
The smallest suitable design for an FFI call involves an @external attribute. This attribute tells the compiler the target language, the module name, and the function name. The function itself has no body.
// Example: Calling the Erlang 'os' module to get the system type
@external(erlang, "os", "system")
pub fn get_system_type() -> String
In this configuration, the Gleam compiler assumes that os:system/0 returns a value that can be treated as a Gleam String. Because there is no intermediate "adapter" layer, the BEAM simply passes the return value of the Erlang function back to the Gleam caller.
Trust and Data Boundaries
Because the compiler cannot inspect the Erlang source code, several trust assumptions are made at the boundary:
- Arity Trust: Gleam assumes the Erlang function accepts the exact number of arguments specified in the Gleam signature.
- Type Trust: Gleam assumes the returned BEAM term is compatible with the declared Gleam type.
- Existence Trust: The compiler does not check if the module or function actually exists until the code is executed on the VM.
Handling Opaque Types
When an Erlang library returns a complex internal state (like a database connection or a PID), you should use a custom type or an opaque type. Since you cannot pattern-match on an ExternalType in Gleam, you must treat it as a "black box" and pass it back to other external functions that know how to handle it.
Operational Checks and Verification
Since the type checker is blind to the external side, you must implement a multi-layered verification strategy.
1. Runtime Exercise
The primary safety net is a comprehensive test suite. Run your tests using gleam test. If an FFI signature is incorrect (e.g., wrong arity), the BEAM will raise an undef or badarg error at the call site.
2. Static Analysis of Generated Code
Gleam compiles to Erlang source before generating bytecode. You can run Dialyzer (the Erlang static analysis tool) on the generated .erl files located in the build/dev/erlang/ directory. Dialyzer can often detect arity mismatches or type inconsistencies that the Gleam compiler missed.
3. Binary vs. Charlist Check
A common failure mode is the "String Surprise." Erlang traditionally uses charlists (lists of integers) for strings, while Gleam uses UTF-8 binaries. If an external function returns a charlist, Gleam will treat it as a String, but runtime operations on that string may fail or produce unexpected results. Always verify if the target library returns binaries or lists.
Failure Modes and Design Shifts
| Failure Mode | Root Cause | Symptom |
|---|---|---|
| Signature Drift | Upstream library update changes function arity. | Runtime undef error. |
| Encoding Mismatch | Erlang charlist returned where Gleam expects binary. | Incorrect string manipulation/crashes. |
| Side-Effect Leak | FFI call modifies process dictionary. | Non-deterministic behavior in "pure" Gleam code. |
When to Change the Design
The current "trust-based" FFI design is sufficient for most integrations. However, you should move toward a more robust wrapper pattern (writing a small Erlang/Elixir shim) if:
- The external library returns highly dynamic shapes (e.g., tuples of varying lengths) that require validation before entering Gleam.
- You need to convert complex Erlang terms into Gleam's algebraic data types (ADTs).
- You are targeting WebAssembly (WASM), where the BEAM-specific
@external(erlang, ...)calls are not portable and must be replaced by JavaScript or WASM component interfaces.
Rollback and State
Since @external declarations are purely metadata for the compiler and do not modify the state of the BEAM VM or the filesystem during the build process, no rollback is required. To revert an FFI change, simply update the function signature and re-run gleam build.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.