Skip to content

Custom Devices in Rust

Erlang can natively execute Rust functions with almost no overhead via NIF (Native Implemented Function).

Creating a Rust Device

Go to HyperBEAM/native directory and create a new Rust project.

cargo new dev_add_nif --lib && cd dev_add_nif

Step 1: Configure Cargo.toml

Change crate-type and add rustler dependency in Cargo.toml:

[package]
name = "dev_add_nif"
version = "0.1.0"
edition = "2021"
 
[lib]
crate-type = ["cdylib"]
 
[dependencies]
rustler = "0.36"

Step 2: Implement the Rust NIF

Create the NIF function in src/lib.rs:

use rustler::{Env, NifResult, Term, Encoder};
use rustler::types::atom::ok;
 
#[rustler::nif]
fn add<'a>(env: Env<'a>, a: i64, b: i64) -> NifResult<Term<'a>> {
    Ok((ok(), a + b).encode(env))
}
 
rustler::init!("dev_add_nif", [add]);

Step 3: Build the Rust Library

Compile the Rust code:

cargo build

For release builds with optimizations:

cargo build --release

Step 4: Configure rebar.config

Add the device to cargo_opts in HyperBEAM/rebar.config:

{cargo_opts, [
    {src_dir, "native/dev_add_nif"},
    {src_dir, "native/dev_snp_nif"}
]}.

Step 5: Create the Erlang NIF Module

Create HyperBEAM/src/dev_add_nif.erl:

-module(dev_add_nif).
-export([add/2]).
-on_load(init/0).
 
-include("include/cargo.hrl").
-include_lib("eunit/include/eunit.hrl").
 
init() ->
    ?load_nif_from_crate(dev_add_nif, 0).
 
add(_, _) ->
    erlang:nif_error(nif_not_loaded).

Step 6: Create the Erlang Device Module

Create HyperBEAM/src/dev_add.erl:

-module(dev_add).
-export([add/3]).
-include("include/hb.hrl").
-include_lib("eunit/include/eunit.hrl").
 
add(_M1, M2, _Opts) ->
    A = maps:get(<<"a">>, M2),
    B = maps:get(<<"b">>, M2),
    {ok, Sum} = dev_add_nif:add(A, B),
    {ok, #{ <<"sum">> => Sum }}.
 
add_test() ->
    M1 = #{ <<"device">> => <<"add@1.0">> },
    M2 = #{ <<"path">> => <<"add">>, <<"a">> => 2, <<"b">> => 3 },
    {ok, #{ <<"sum">> := 5 }} = hb_ao:resolve(M1, M2, #{}).

Step 7: Register the Device

Add the device to HyperBEAM/hb_opt.erl:

preloaded_devices => [
  ...
  #{<<"name">> => <<"add@1.0">>, <<"module">> => dev_add},
  ...
],

Step 8: Build and Test

Run the unit tests:

rebar3 eunit --module=dev_add

Test the device with WAO:

it("should test add@1.0", async () => {
  const res = await hb.send({ path: "/~add@1.0/add", a: 2, b: 3 })
  assert.equal(res.headers.get("sum"), "5")
})