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}
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.