Improve the performances of your Phoenix app with Rust: in both back and front
Tags: elixir, rust, phoenix, webassembly
Published: 2020-06-10 22:52:02

I know, Phoenix and Elixir are quite performant and quite fast. But sometimes, it may be necessary to improve a little bit the performances of your app. Rust is the solution in both your back-end and front-end code.

Here I will show you how to do that with very basic examples.

First let's install all required tools for Rust:
```bash
# Install rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install rust-wasm
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 
# Install cargo generate
cargo install cargo-generate
```
I assume you have `elixir` (with the mix `phx_new` archive) and `node` installed.

# Add Rust in our back-end code

In Erlang, we have the NIF functions which are callable functions implemented in C/C++ or even Rust directly from our Erlang/Elixir code. Thanks to rustler, making those functions became very easy !

We will start by making a new Phoenix project.
```bash
mix phx.new rust_phoenix --live --no-ecto
# if asked, install all deps
cd rust_phoenix
mix phx.server
```
You shall see the default live page which search in the HexDocs.
We will change that page to make a simple calculator, change the `lib/rust_phoenix_web/live/page_live.html.leex` file :
```html
<form phx-change="change" phx-submit="compute">
    <input type="number" name="a" value="<%= @a %>" list="results" autocomplete="off"/>
    <input type="number" name="b" value="<%= @b %>" list="results" autocomplete="off"/>
    <h2>The result is: <%= @result %></h2>
    <button type="submit">Compute</button>
</form>
```
And then let's change the back-end code of our live page in `lib/rust_phoenix_web/live/page_live.ex` :
```elixir
defmodule RustPhoenixWeb.PageLive do
  use RustPhoenixWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    result = 0
    {:ok, assign(socket, a: 0, b: 0, result: result)}
  end

  @impl true
  def handle_event(
        "compute",
        _,
        %{assigns: %{a: a, b: b}} = socket
      ) do
    result = a + b
    {:noreply, assign(socket, result: result)}
  end

  @impl true
  def handle_event(
        "change",
        %{"a" => a, "b" => b},
        socket
      ) do
    {a, _} = Integer.parse(a)
    {b, _} = Integer.parse(b)
    {:noreply, assign(socket, a: a, b: b)}
  end
end
```
Ok, easy. We just have two variables and we will compute the result when clicking on the "Compute" button.

It could be better but we will let it like that. When looking on our browser we see the both input and when typing `1` and `2` and then clicking on the compute button : we got `3`. Awesome !

Let's now add our Rust project to make our computation in native code.
Add the `rustler` deps, in our `mix.exs` file add:
```elixir
defp deps do
  [
    # your deps
    {:rustler, "~> 0.21.1"}
  ]
end
```
We have to register the `rustler` compiler, in our `mix.exs` add:
```elixir
def project do
  [
    # ...
    # I did just add :rustler here
    compilers: [:phoenix, :gettext, :rustler] ++ Mix.compilers(),
  ]
end
```
And we will generate our awesome NIF project, when asked give the module name, I decided to give `RustPhoenix.RustHelloWorld`.
```bash
mix rustler.new 
```
And now in `native/rustphoenix_rusthelloworld` we got our project. Take a look at `native/rustphoenix_rusthelloworld/src/lib.rs` file, here what it looks like:
```rust
#[macro_use]
extern crate rustler;

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

mod atoms {
    rustler_atoms! {
        atom ok;
    }
}

rustler::rustler_export_nifs! {
    "Elixir.RustPhoenix.RustHelloWorld",
    [
        ("add", 2, add)
    ],
    None
}

fn add<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let num1: i64 = args[0].decode()?;
    let num2: i64 = args[1].decode()?;

    Ok((atoms::ok(), num1 + num2).encode(env))
}
```
_(Add the `#[macro_use] extern crate rustler;` at the beginning of the file if it is missing.)_

Let's add the glue to use our module in our Elixir project.
First we have to register our crate, in our `mix.exs` file add:
```elixir
def project do
  [
    # ...
    # I did just add :rustler here
    rustler_crates: [
      rustphoenix_rusthelloworld: [
        mode: (if Mix.env() == :prod, do: :release, else: :debug)
      ]
    ],
  ]
end
```
Then we will create our glue module, create the file `lib/rust_phoenix/rust_hello_world.ex` and write :
```elixir
defmodule RustPhoenix.RustHelloWorld do
  use Rustler, otp_app: :rust_phoenix, crate: :rustphoenix_rusthelloworld

  def add(_arg1, _arg2), do: :erlang.nif_error(:nif_not_loaded)
end
```
We did have exported `("add", 2, add)` from our `lib.rs` file, so we have to declare the `add/2` function in our module. All functions will be override by Rustler when the NIF functions will be correctly loaded.

Then let's change our live page to use our native code, edit `lib/rust_phoenix_web/live/page_live.ex` :
```elixir
defmodule RustPhoenixWeb.PageLive do
  use RustPhoenixWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, result} = RustPhoenix.RustHelloWorld.add(0, 0)
    {:ok, assign(socket, a: 0, b: 0, result: result)}
  end

  @impl true
  def handle_event(
        "compute",
        _,
        %{assigns: %{a: a, b: b}} = socket
      ) do
    {:ok, result} = RustPhoenix.RustHelloWorld.add(a, b)
    {:noreply, assign(socket, result: result)}
  end

  @impl true
  def handle_event(
        "change",
        %{"a" => a, "b" => b},
        socket
      ) do
    {a, _} = Integer.parse(a)
    {b, _} = Integer.parse(b)
    {:noreply, assign(socket, a: a, b: b)}
  end
end
```
Instead of doing `a + b`, we now do `RustPhoenix.RustHelloWorld.add(a, b)`.

Now, test, and everything should work !

# Add Rust in our front-end code

Let's go to our `assets` folder and create a `native` folder. Then generate our project and when asked type `helloworld`.
```bash
cd assets
mkdir native
cargo generate --git https://github.com/rustwasm/wasm-pack-template
```
In `assets/native/helloworld/src/lib.rs` we have our Rust code containing :
```rust
mod utils;

use wasm_bindgen::prelude::*;

#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, World!");
}
```
It simply define a `greet()` function calling the `alert` javascript function.
Now let's compile our project:
```bash
wasm-pack build
```
We should now have a `assets/native/helloworld/pkg` directory created with all our glues and generated wasm file.
Now we can add our package in our asset webpack config, so edit `assets/package.json` :
```javascript
"dependencies": {
  // ...
  "helloworld": "file:./native/helloworld/pkg"
},
```
And run:
```bash
npm install
```
We now have our Rust WASM code in our assets, ready to be used. The loading of WebAssembly code must be asynchronous and to achieve that, it's simple : let's modify our `assets/js/app.js` to add our new code:
```javascript
import ...
import("helloworld")
    .catch(console.error)
    .then(module => window.helloworld = module);
```
Now we can call our code, edit `lib/rust_phoenix_web/live/page_live.html.leex` and add:
```html
<button onClick="helloworld.greet()">Say Hello to Rust</button>
```
We now have to register the `application/wasm` MIME type required for our WebAssembly loader since Phoenix don't know it. Let's add it in our config file `config/config.exs` :
```elixir
config :mime, :types, %{"application/wasm" => ["wasm"]}
```
And to rebuild the MIME deps, run:
```elixir
mix deps.clean mime --build
```

Then when clicking on our button, it should pop-out our alert. Awesome !

# Conclusion

Here the performances are not quite amaizing, but we can have so much more of course.

Rust is a very good language with high performance and high reliability. Used in both back and client-side code assure a solid foundation. However, we should not abuse of all of that good things, keep using Javascript and Elixir and only use Rust to optimize some heavy part of your code which are too slow.

# Improvements

We can easily tell Webpack to watch for changes done in the native projects and recompile them. Take a look [here](https://github.com/wasm-tool/wasm-pack-plugin).

I actually don't know how to watch, re-compile and live-reload the Rust NIF functions. Maybe tricking with `nodemon` ([here](https://nodemon.io/)) could be a good idea.

# Docs and links

- The repository with all the code : [github.com/scorsi/rust_phoenix](https://github.com/scorsi/rust_phoenix).
- The Rust language main page on [rust-lang.org](https://www.rust-lang.org/).
- The Rust WASM documentation on [rustwasm.github.io](https://rustwasm.github.io/book/), you should specially look [here](https://rustwasm.github.io/book/game-of-life/hello-world.html).
- The Rustler documentation on [hexdocs.pm](https://hexdocs.pm/rustler/basics.html#content).
- Erlang NIF documentation on [erlang.org](https://erlang.org/doc/man/erl_nif.html).
- The typical Discord use-case on [blog.discord.com](https://blog.discord.com/using-rust-to-scale-elixir-for-11-million-concurrent-users-c6f19fc029d3).
- A good post on Rust NIF on [theguild.nl](https://www.theguild.nl/when-elixirs-performance-becomes-rust-y/).
- WebAssembly use-case examples on [webassembly.org](https://webassembly.org/docs/use-cases/).