Skip to main content

Crate otter

Crate otter 

Source
Expand description

§Overview

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. The design is driven by two foundational principles:

  1. Functionalityotter’s 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.
  2. Speed — One of the prime motivations for writing NIFs is to achieve speed and memory efficiency not possible in the Erlang VM. otter’s datatypes are designed to “pay only for what you need” through controlled layering of enif_* calls, with extensive work to employ compile-time constraints rather than runtime ones.

§Quickstart

use otter::types::CallEnv;

// Declare the module: name, NIF table, and any atoms to pre-intern.
otter::init!("my_nif", [add], atoms = [ok, error]);

// A NIF that adds two integers, decoded straight to `i64` and encoded back.
#[otter::nif]
fn add(_env: CallEnv<'_>, a: i64, b: i64) -> i64 {
    a + b
}

§Features

A few highlights of the surface; the guided tour below maps the whole crate.

§Generative env/term lifetimes

Each term is branded by the env that produced it. Env<'id> is a sealed trait whose lifetime 'id is a generative brand — a fresh, non-escaping marker minted at every entry point. Every term type carries that brand (Integer<'id>, Binary<'id>, …), so a term from one env cannot be used with another: the compiler rejects it, with no per-operation runtime check. The BEAM treats a cross-env term as undefined behavior, and this construction makes that mistake impossible to write. (This is the generativity pattern.)

§Layered term resolution

Reading a term through the C API is a cascade: enif_term_type and the enif_is_* family reveal a type, enif_get_tuple hands back elements that are themselves opaque terms, and so on — a full decode can cost many calls into the VM, most of them unnecessary. otter mirrors that progression in its types, so you stop at exactly the depth you need: AnyTermTypedTerm → a concrete term type → a native Rust value.

Resolution is lazy. Constructing a term is free — one machine word plus a zero-sized brand marker — and nothing is read off the BEAM heap until an env-passing accessor asks for it:

// Constructors are associated functions taking an env:
let i = Integer::from_i64(env, 42);
let b = Binary::from_bytes(env, b"hello");
let m = Map::new(env);

// Accessors take an env:
let n = i.to_i64(env);          // Option<i64>
let bytes = b.as_bytes(env);    // &[u8], zero-copy into the BEAM heap

// Term inputs are taken as `impl Term<'id>` — pass concrete types directly,
// no `.encode()` needed; a term of the wrong brand fails to compile:
let m = m.put(env, key_atom, i);

§Honest codecs

Encoder/Decoder move between terms and native Rust types — integers, floats, bool, str/String, tuples, Vec<T>, HashMap<K, V>, and (with the bigint feature) arbitrary-precision integers. A decoder never lies: 300 into a u8 is an IntegerOverflow error, not a silently truncated 44, and a float will not quietly swallow an integer. A failed decode on a NIF argument becomes badarg; a failed encode on a return becomes badret.

§Faithful Erlang types

All eleven term types are present, and they mirror Erlang rather than a convenient subset of it. List is a real cons cell (Nil | Cell(head, tail)), so improper lists are first-class and a codec rejects a bad tail with a clean error instead of papering over it.

§The full send matrix

Message passing is a 2×2 of four free verbs: send_copy/send_move — copy a live term, or steal an OwnedEnvArena heap in O(1) — each available plain (NULL caller, off-thread) or _from (caller-attributed, in-NIF). You can build a message on a worker thread with no env in hand and move its whole heap into a process in a single send.

§Hot code upgrade without cross-build ABI assumptions

Every otter module is hot-code-upgradeable. Erlang upgrades a running system in place: a second build of a NIF library can load beside the first and inherit its live state (resources, priv_data). The two builds may come from different compilers and allocators and need not be byte-identical source, so the upgrade boundary is a foreign-ABI boundary.

Outside the raw feature, otter never assumes across that boundary that allocators or drop glue are compatible, that std datatypes share a layout, or that identical source compiles to a compatible layout; the safe path holds by construction. Code that relies on cross-build ABI compatibility belongs only behind raw, where the caller takes responsibility. The full treatment is in the project’s docs/UPGRADE.md.

§The raw escape hatch

Where the safe surface isn’t enough, the raw feature exposes the all-unsafe enif_ffi crate directly, widens the brand-bridging term constructors to pub, and accepts the _raw lifecycle callbacks in init! — a deliberate, opt-in boundary where you take responsibility for the ABI. See Cargo features.

§A guided tour

The crate is organized into a few logical areas:

§Cargo features

All features are off by default; otter always binds NIF 2.17 (OTP 26).

FeatureEffect
nif_2_18Enable the NIF 2.18 (OTP 29) additions.
bigintPull in the optional num-bigint dependency (compiled only when this feature is enabled) and add arbitrary-precision integers: types::BigInt (a re-export of num_bigint::BigInt), its Encoder/Decoder, and Integer::to_bigint/from_bigint.
rawExpose the raw, all-unsafe enif_ffi crate as the escape hatch, widen the brand-bridging term constructors to pub, and accept the _raw lifecycle callbacks in init!.

Re-exports§

pub use enif_ffi;raw

Modules§

alloc
An opt-in #[global_allocator] backed by the BEAM allocator.
codec
Conversions between Erlang terms and native Rust types — Encoder, Decoder, and CodecError.
priv_data
PrivData — the per-library private data the BEAM hands back via enif_priv_data.
resource
Resource types: Resource trait, ResourceArc<T>, Monitor.
select
I/O event multiplexing.
system
BEAM system information and thread introspection.
time
BEAM time functions.
types

Macros§

atom
Retrieve an atom pre-declared in the atoms = [...] list of init!.
enif_global_allocator
Install EnifAlloc as the #[global_allocator] of the current crate.
init
Declare the NIF library: its module name, exported NIFs, pre-interned atoms, resource types, and lifecycle callbacks. Invoke it exactly once per library.

Attribute Macros§

nif
Mark a Rust function as a NIF.
resource_impl
Optional marker attribute for an impl Resource for T block.