1054 words, 6 min read

In addition to structs, you can pass maps and enums between Elixir and Rust when writing Native Implemented Functions (NIFs) with Rustler. This gives you flexibility when working with more dynamic or variant data.

Passing maps from Elixir to Rust

Elixir maps can be received in Rust as MapIterators or HashMaps, or decoded into Rust structs.

Example: reading keys from a map

use rustler::{MapIterator, NifResult, Term};

#[rustler::nif]
fn keys(map: Term) -> NifResult<Vec<String>> {
    MapIterator::new(map)
        .ok_or(rustler::Error::BadArg)?
        .map(|(k, _)| k.decode::<String>())
        .collect()
}
iex> MyNif.keys(%{"a" => 1, "b" => 2})
["a", "b"]

Example: decoding into a Rust struct

use rustler::{NifMap, NifResult, Term};

#[derive(NifMap)]
struct GreetUser {
    name: String,
    age: u32,
}

#[rustler::nif]
fn greet(user_term: Term) -> NifResult<String> {
    let user: GreetUser = user_term.decode()?;
    Ok(format!("Hello, {} (age {})", user.name, user.age))
}
iex> MyNif.greet(%{name: "Alice", age: 30})
"Hello, Alice (age 30)"

Passing enums from Elixir to Rust

Enums can be passed using tagged maps or atoms.

Rust enum:

#[derive(NifMap)]
struct Circle {
    radius: f64,
}

#[derive(NifMap)]
struct Rectangle {
    width: f64,
    height: f64,
}

enum Shape {
    Circle(Circle),
    Rectangle(Rectangle),
}

impl<'a> Decoder<'a> for Shape {
    fn decode(term: Term<'a>) -> NifResult<Self> {
        let key = Atom::from_str(term.get_env(), "type")?;
        let ty: String = term.map_get(key)?.decode()?;
        match ty.as_str() {
            "Circle" => Ok(Shape::Circle(term.decode()?)),
            "Rectangle" => Ok(Shape::Rectangle(term.decode()?)),
            _ => Err(Error::BadArg),
        }
    }
}

Function using the enum:

#[rustler::nif]
fn area(shape: Shape) -> f64 {
    match shape {
        Shape::Circle(Circle { radius }) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(Rectangle { width, height }) => width * height,
    }
}

Elixir usage:

iex> MyNif.area(%{type: "Circle", radius: 3.0})
28.274333882308138

iex> MyNif.area(%{type: "Rectangle", width: 4.0, height: 5.0})
20.0

Returning maps and enums from Rust to Elixir

To return dynamic structures like maps from Rust, construct them using Env and Rustler's term constructors.

Returning a map:

use rustler::{Env, Term, NifResult};

#[rustler::nif]
fn status_map<'a>(env: Env<'a>, code: u32) -> NifResult<Term<'a>> {
    let status = if code == 200 { "ok" } else { "error" };
    Term::map_new(env).map_put("status", status)
}
iex> MyNif.status_map(200)
%{"status" => "ok"}

Returning an enum as a tagged map

You can define a Rust enum and implement Encoder for it:

use rustler::{Encoder, Env, Term};

enum Response {
    Success { result: String },
    Error { reason: String },
}

// Elixir side:
//   %{"type" => "success", "result" => "done"}
//   %{"type" => "error", "reason" => "..."}
impl Encoder for Response {
    fn encode<'a>(&self, env: Env<'a>) -> Term<'a> {
        let pairs: Vec<(&str, &str)> = match self {
            Response::Success { result } => vec![("type", "success"), ("result", result)],
            Response::Error { reason } => vec![("type", "error"), ("reason", reason)],
        };

        let mut map = Term::map_new(env);
        for (k, v) in pairs {
            // String keys/values encode to Elixir binaries
            map = map.map_put(k, v).unwrap();
        }
        map
    }
}

#[rustler::nif]
fn status_map(code: u32) -> Response {
    if code == 200 {
        Response::Success { result: "done".to_string() }
    } else {
        Response::Error { reason: format!("unexpected code {}", code) }
    }
}

This allows you to return tagged maps like:

%{"type" => "success", "result" => "done"}
iex(1)> MyNif.status_map(200)
%{"result" => "done", "type" => "success"}
iex(2)> MyNif.status_map(500)
%{"reason" => "unexpected code 500", "type" => "error"}

Handling invalid input

Instead of calling .unwrap(), return a safe error from your NIF:

use rustler::{Atom, NifMap, Term};

rustler::atoms! {
    invalid_user_struct,
}

#[derive(NifMap)]
struct GreetUser {
    name: String,
}

#[rustler::nif]
fn safe_greet(user_term: Term) -> Result<String, Atom> {
    match user_term.decode::<GreetUser>() {
        Ok(u) => Ok(format!("Hello, {}", u.name)),
        Err(_) => Err(invalid_user_struct()),
    }
}

In Elixir:

iex> MyNif.safe_greet(%{name: "Bob"})
{:ok, "Hello, Bob"}

iex> MyNif.safe_greet(%{bad: "input"})
{:error, :invalid_user_struct}