phoenix_rust_ports_and_nifs icon indicating copy to clipboard operation
phoenix_rust_ports_and_nifs copied to clipboard

Examples of Phoenix with Rust interfaces via both ports and nifs

This is an example project to show Elixir/Rust integration with NIFs and Ports.

This also takes place within the Phoenix Framework

This file has been created by doing the following:

mix phoenix.new phoenix_rustler_integration
cd phoenix_rustler_integration

Modify the web/mix.exs file so that rustler is a dependency:

  defp deps do
    [
     {:rustler, "~> 0.9.0"},
     {:phoenix, "~> 1.2.1"},
     {:phoenix_pubsub, "~> 1.0"},
     {:phoenix_ecto, "~> 3.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.6"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.11"},
     {:cowboy, "~> 1.0"}
     ]
  end

Get rustler as a dependency and create an example project

mix deps.get
mix rustler.new
Example

Create the elixir file in lib/ that will interface with the rustler Nif. Note that the @on_load line is commented out as it currently causes an elixir compilation error.

defmodule NifExample do
 use Rustler, otp_app: :phoenix_rust_ports_and_nifs, crate: :nifexample


 def add(_a, _b), do: exit(:nif_not_loaded)
end

Modify the web/mix.exs so that the :rustler term is included in the compilers list and the rustler crates are identified.

  def project do
    [app: :phoenix_rustler_integration,
     version: "0.0.1",
     elixir: "~> 1.2",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:rustler, :phoenix, :gettext] ++ Mix.compilers,
     rustler_crates: rustler_crates(),
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     aliases: aliases(),
     deps: deps()]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {PhoenixRustlerIntegration, []},
     applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
                    :phoenix_ecto, :postgrex]]
  end

  defp rustler_crates() do
    [nifexample: [
      path: "native/nifexample",
      mode: (if Mix.env == :prod, do: :release, else: :debug),
       ]]

  end

You should then be able to run iex -S mix and perform the following:

iex(1)> NifExample.add(1,1)
{:ok, 2}

More examples were added to show the basics of how to get information from Elixir in the form of Tuples and Maps. These are the testTuple() and testMap() functions:

    def printTuple(_tuple), do: exit(:nif_not_loaded)
    def testTuple() do
      tuple = {:im_an_atom, 1.0, 1, "string"}
      printTuple(tuple)
    end

    def printMap(_map), do: exit(:nif_not_loaded)

    def testMap() do
        map = %{"firstEntry" => 1,
        "secondEntry" => :second,
        :third => 3.0,
        4 => "fourthEntry",
        "fifthEntry" => "five"}
        printMap(map)
    end

These are a basic collection of several different types in the Tuple and Map.

This code is needed in Rust to access these with Rustler:

use rustler::types::map::NifMapIterator;
use rustler::types::atom::NifAtom;

rustler_export_nifs! {
    "Elixir.Example",
    [("add", 2, add),
     ("printTuple", 1, print_tuple),
     ("printMap",1, print_map)],
    None
}

fn print_tuple<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
//The types must be known beforehand to decode a tuple. The Turbofish is used here to set the types
    let tuple = (args[0]).decode::<(NifAtom, f64, i64, String)>()?; //Note that "?" is used here instead of "try!"
    println!("Atom: {:?}, Float: {}, Integer: {}, String: {}", tuple.0, tuple.1, tuple.2, tuple.3);
    Ok((atoms::ok().encode(env)))
}

fn print_map<'a>(env: NifEnv<'a>, args: &[NifTerm<'a>]) -> NifResult<NifTerm<'a>> {
//Maps can use the built-in NifMapIterator which makes iterating over the entries simple
    let mut map = NifMapIterator::new(args[0]).expect("Should be a map in the argument");
    for x in map {
        println!("{:?}", x); //Doing more here will likely require matching and knowledge of types in the map
    }
    Ok((atoms::ok().encode(env)))
}

These functions can be called by iex -S mix after modifying the Example.ex and the rust lib.rs as shown above. This results in:

iex(1)> NifExample.testTuple()
Atom: im_an_atom, Float: 1, Integer: 1, String: string
                                                      :ok
iex(2)> NifExample.testMap()
(4, <<"fourthEntry">>)
                      (third, 3.000000e+00)
                                           (<<"fifthEntry">>, <<"five">>)
                                                                         (<<"firstEntry">>, 1)
                                                                                              (<<"secondEntry">>, second)
      :ok

Notice also that the map entries are ordered differently than how they were put in Elixir. So building your interface will require match terms to properly pull data from the maps if this is how you want to use Rustler.

Port Example

This is a very basic port example. Pay close attention to both the Elixir and Rust code. Newlines are necessary for the read_line command in rust to work properly. So, Elixir must send a newline command for Rust to respond in this case.

So there's a gap following a printout of what was sent by elixir to Rust. There's not a newline in the rust output because the print! command is used instead of the println! command.

iex(1)> PortExample.test()
Input sent to rust: Hello World!

"Received in Rust: Hello World!"
true
iex(2)>