otter
otter — write fast and efficient Erlang NIFs in Rust
otter is built from the ground up to give Erlang and Elixir programmers an easy, efficient way to write NIFs in Rust. Its first priority is an API surface that faithfully captures the capabilities of the underlying erl_nif C API — and where the safe surface isn't enough, the raw API can be exposed by enabling the raw feature. The datatypes are designed so you can target speed and efficiency through controlled layering of enif_* calls, with extensive work to enforce constraints at compile time rather than runtime.
- Full NIF lifecycle —
load,upgrade, andunload, with upgrade-safepriv_data, so your module stays hot-code-upgradeable. - Faithful lists —
Listis a real cons cell (Node::Nil | Cell(head, tail)); improper lists are first-class, and codecs reject a bad tail with a clean error. - The full send matrix — copy or move (O(1) heap-steal) × attributed or off-thread, including stealing a message heap from inside a NIF.
- Decoders that don't lie —
300into au8is anIntegerOverflowerror, never a silently-truncated44; floats won't quietly swallow integers. - Compile-time env identity — a generative brand makes cross-env misuse a compile error and keeps terms one machine word, with no per-operation runtime env check.
otter is inspired by rustler; see docs/RUSTLER.md for a detailed comparison.
Status: 0.3.1. The full API surface is implemented, thoroughly tested end-to-end by otter_test, and security-audited by Claude. Feedback on the API shape, the Erlang-first philosophy, and the safety model is welcome — open an issue.
Note on Elixir. For now, otter ships no Elixir-specific tooling. Getting the Erlang-facing library right is the current priority; once the surface stabilizes, we will revisit building Elixir tooling on top of the otter framework or as an opt-in feature.
Quick start
This walks through a working NIF from an empty directory. It assumes rebar3,
cargo, and an OTP 27+ install are on your PATH (the rebar3 plugin uses OTP-27
doc syntax; the NIF runtime floor itself is OTP 26). The example uses an
application called my_app with a NIF crate called my_nifs.
1. Create the Erlang application.
$ rebar3 new app name=my_app
$ cd my_app
2. Add the plugin to rebar.config. rebar3_otter is published on hex.pm:
.
3. Scaffold the NIF crate.
$ rebar3 otter new --name my_nifs
This creates native/my_nifs/Cargo.toml (already depending on otter-nif from crates.io)
and native/my_nifs/src/lib.rs with a minimal NIF:
use ;
// Optional load hook. Atoms listed in `init!` are interned by the
// scaffolding before this runs, so a fresh crate has nothing to do here.
init!;
4. Register the crate and build hooks in rebar.config (the scaffolder
prints this for you):
.
.
5. Write the Erlang loader module src/my_nifs.erl. This is standard
Erlang and is yours to write — the plugin never generates Erlang source. The
module name must match the name passed to otter::init!:
%% Stub replaced at load time by the NIF implementation.
6. Build. The pre_compile hook invokes cargo (pulling otter-nif from
crates.io on the first build), locates the .so, and installs it into priv/native/.
$ rebar3 compile
===> Compiling Rust crate my_nifs
===> Installed .../my_app/priv/native/my_nifs.so
7. Verify.
$ rebar3 shell --eval 'io:format("~p~n", [my_nifs:hello()]), halt().'
world
To grow from here — more types, pre-declared atoms, an on_load callback,
resources, scheduling — see docs/USAGE.md. For a complete
working example app that builds against the published packages, see
otter-demo.
Components
| Crate / App | What it does |
|---|---|
otter-nif |
Core Rust library — types, codecs, environment, resources (imported as otter) |
otter-nif-macros |
Proc macros (#[otter::nif], otter::init!) — re-exported through otter-nif |
rebar3_otter |
rebar3 plugin — drives cargo build and cargo clean |
You only depend on otter-nif (imported as otter). The codegen macros are re-exported through it.
Features
- All 11 Erlang term types — Atom, Integer, Float, Binary/Bitstring, List, Tuple, Map, Pid, Port, Reference, Fun
- Four-level term resolution —
AnyTerm(zero cost) →TypedTerm(one NIF call) → concrete term type → native Rust value (i64,String,Vec<T>, … decoded directly). Pay only for what you use. - Compile-time lifetime safety —
Env/Termare traits with an invariant brand'idthat ties every term to its NIF call. Terms cannot escape. No runtime checks. - Native codecs —
Encoder/Decoderfor Rust primitives,String, tuples,Vec<T>,HashMap<K,V>(take and return them directly), plus optional arbitrary-precision integers via thebigintfeature - Pre-declared atoms —
init!'satoms = [...]+atom!for zero-cost atom retrieval, interned at load and re-interned on upgrade;Atom::internreturnsResult<_, AtomError> - Resource types — BEAM-managed Rust objects with destructors, monitors, and
selectstop callbacks, registered viainit!'sresources = [...] - Hot code upgrade — every
ottermodule is hot-upgradeable; a per-build ABI tag on resource type names keeps a different build from unsafely taking over, with an opt-in stable tag (andrawcallbacks) for state you carry across by hand - Message passing — four free verbs in a 2×2:
send_copy/send_move(copy a live term vs. steal anOwnedEnvArenaheap) × plain (NULL caller, off-thread) and_from(caller-attributed, in-NIF) - Dirty schedulers —
#[otter::nif(schedule = "DirtyCpu")]/"DirtyIo" - Result returns —
Result<T, Raised>whereOkencodes normally andErr(Raised)carries an already-pending exception out (raise viaenv.raise/env.badarg); an encode failure raisesbadret - BinaryBuf — growable binary buffer with
io::Writesupport - I/O select —
enif_select/enif_select_xfor async I/O integration - enif-backed global allocator — opt-in
enif_global_allocator!()routes Rust allocations through the BEAM allocator (enif_alloc/enif_free) - Panic safety — panics in NIF bodies, encoders, and callbacks are caught and converted to exceptions (with a
panic = "abort"build guard) - Feature flags —
bigint(arbitrary-precision integers),raw(the rawenif_*FFI escape hatch +_rawlifecycle callbacks),nif_2_18(OTP 29 additions); all off by default
Requirements
- OTP 27+ to build through the rebar3 plugin (it uses OTP-27 doc syntax); the NIF runtime floor is 2.17 / OTP 26. Optional
nif_2_18feature for OTP 29. - Rust edition 2021, MSRV 1.82 (
otter-nif-macros1.56). cargoonPATH.
Documentation
| Document | Contents |
|---|---|
| docs/USAGE.md | User-facing guide — setup, all types, atoms, resources, message passing, scheduling, select |
| docs/RESOURCES.md | Deep dive on the resource lifecycle |
| docs/UPGRADE.md | Hot-upgrade safety model and the no-cross-build-ABI invariant |
| docs/RUSTLER.md | Design comparison with rustler |
| docs/MIGRATION.md | Side-by-side rustler-to-otter migration guide |
| otter/DESIGN.md | Core library architecture and internals |
| otter_codegen/DESIGN.md | What the macros generate, argument/return type rules |
| rebar3_otter/DESIGN.md | Plugin providers, cargo integration, NIF loading path |
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.