Cookie Consent by Privacy Policies Generator Vegapit
Rust integration in the Elixir Phoenix framework
Rust Elixir Tue, 29 Jun 2021 15:12:11 +0000

Anyone looking to build fault-tolerant web applications should consider using the Phoenix framework. Written in Elixir and making full use of the Erlang Open Telecom Platform, it simplifies a lot of the configuration aspects of the application, while remaining very flexible for some low-level adjustments. On top of all these great features, Elixir and Rust enjoy a fairly mature interop library called Rustler. In this post, we will run through an example of how to integrate Rust functions in a Phoenix-powered website.

Project setup and configuration

The prerequisite to follow this tutorial is to have Elixir and Phoenix installed on your machine. The installation procedure is not covered here but all the relevant information to install it on your preferred platform are available on the official website. Once installed, we create a Phoenix codebase template in a new folder called rustphoenix using Mix, the Elixir project management tool:

mix phx.new rustphoenix --no-ecto --no-webpack --no-dashboard

The options passed to the command define that our project will not be using the database manager Ecto, nor the live dashboard, nor Webpack as our web asset manager. Next, let's setup a Rustler project within Phoenix. To do so, add the relevant version of Rustler to the deps function definition in the mix.exs file:

 defp deps do
    [
      # ...
      {:rustler, "~> 0.22.0"} # Add to the list
    ]
  end

And recompile the project dependencies by running:

mix deps.get

We can now add a Rust native module to our Elixir project which we will call NIF. We create it through calling this command and answering the appropriate configuration questions in the terminal:

mix rustler.new

A native folder has been added to our project directory. It contains a Cargo project called nif. The project configuration being now over, let us move on to the coding.

Rust code

In this example, we will implement a web interface for a Black 76 option pricer we already used in other tutorials. Let's create a black76.rs file in the src folder of our Cargo project and paste the following code:

use statrs::distribution::{Normal, ContinuousCDF};

pub enum CallPut {
    Call,
    Put
}

pub struct Black76 {
    cp: CallPut,
    s: f64,
    k: f64,
    v: f64,
    t: f64,
    r: f64
}

impl Black76 {

    pub fn new(callput: CallPut, underlying: f64, strike: f64, timetomaturity: f64, volatility: f64, riskfreerate: f64) -> Black76 {
        Black76{cp: callput, s: underlying, k: strike, t: timetomaturity, v: volatility, r: riskfreerate}
    }

    fn d1(&self) -> f64 {
        ((self.s/self.k).ln() + 0.5 * self.v.powf(2.0) * self.t) / (self.v * self.t.powf(0.5))
    }

    fn d2(&self) -> f64 {
        self.d1() - self.v * self.t.powf(0.5)
    }

    pub fn price(&self) -> f64 {
        let stdnorm = Normal::new(0.0, 1.0).unwrap();
        match self.cp {
            CallPut::Call => (-self.r * self.t).exp() * ( self.s * stdnorm.cdf(self.d1()) - self.k * stdnorm.cdf(self.d2()) ),
            CallPut::Put => (-self.r * self.t).exp() * ( self.k * stdnorm.cdf(-self.d2()) - self.s * stdnorm.cdf(-self.d1()) )
        }
    }

    pub fn delta(&self) -> f64 {
        let stdnorm = Normal::new(0.0, 1.0).unwrap();
        match self.cp {
            CallPut::Call =>  (-self.r*self.t).exp() * stdnorm.cdf(self.d1()),
            CallPut::Put => -(-self.r*self.t).exp() * stdnorm.cdf(-self.d1())
        }
    }
}

This code uses some code from the statrs crate which has to be added as dependency to the Cargo.toml file.

[dependencies]
rustler = "0.22.0"
statrs = "0.14"

Then in the lib.rs, we will define the functions to be exported to Elixir:

mod black76;

use black76::{CallPut, Black76};
use rustler::NifResult;

#[rustler::nif]
fn price(cp: &str, s: f64, k: f64, t: f64, vol: f64, r: f64) -> NifResult<f64> {
    let callput = match cp {
        "Call" => CallPut::Call,
        "Put" => CallPut::Put,
        _ => panic!("Callput not recognised")
    };

    Ok( Black76::new(callput, s, k, t, vol, r).price() )
}

#[rustler::nif]
fn delta(cp: &str, s: f64, k: f64, t: f64, vol: f64, r: f64) -> NifResult<f64> {
    let callput = match cp {
        "Call" => CallPut::Call,
        "Put" => CallPut::Put,
        _ => panic!("Callput not recognised")
    };

    Ok( Black76::new(callput, s, k, t, vol, r).delta() )
}

rustler::init!("Elixir.NIF", [
    price,
    delta
]);

The attribute macro rustler::nif declares the function as exportable to Elixir. The NifResult wrapper allows us to pass down Rust errors to the OTP. Finally, the rustler::init! macro registers the function to be exported in the Elixir.NIF module we will create. All the Rust code has now been writtren, so we can move on to the Elixir code.

Elixir code

In the lib folder, let's create a nix.ex file where we are going to implement the NIF module. All we need to do is to declare the signatures of the functions we implemented in our Rust crate:

defmodule NIF do
  use Rustler, otp_app: :rustphoenix, crate: "nif"

  def price(_cp, _s, _k, _t, _vol, _r), do: :erlang.nif_error(:nif_not_loaded)
  def delta(_cp, _s, _k, _t, _vol, _r), do: :erlang.nif_error(:nif_not_loaded)

end

The website we are building has a single page where we can see the set of inputs as an HTML form and the calculation result as an HTML table. The page is loaded from a GET request originally, and at each form submission, a POST request is called returning the answer of the calculation. In the /lib/rustphoenix_web/controllers/page_controller.ex file, a default PageController module has been created for us. We modify the index function and add a new one called calculate which will handle the calculations:

defmodule RustphoenixWeb.PageController do
  use RustphoenixWeb, :controller

  def index(conn, _params) do
    render(conn, "index.html",
      callput: "Call",
      underlying: 100.0,
      strike: 100.0,
      timetoexpiry: 1.0,
      volatility: 0.3,
      riskfreerate: 0.05,
      price: 0.0,
      delta: 0.0,
      csrf_token: Plug.CSRFProtection.get_csrf_token()
    )
  end

  def calculate(conn, params) do
    cp = Map.fetch!(params, "callput")
    s = Map.fetch!(params, "underlying") |> String.to_float()
    k = Map.fetch!(params, "strike") |> String.to_float()
    t = Map.fetch!(params, "timetoexpiry") |> String.to_float()
    vol = Map.fetch!(params, "volatility") |> String.to_float()
    r = Map.fetch!(params, "riskfreerate") |> String.to_float()

    price = NIF.price(cp, s, k, t, vol, r)
    delta = NIF.delta(cp, s, k, t, vol, r)

    render(conn, "index.html",
      callput: cp,
      underlying: s,
      strike: k,
      timetoexpiry: t,
      volatility: vol,
      riskfreerate: r,
      price: Float.round(price, 2),
      delta: Float.round(delta, 4),
      csrf_token: Plug.CSRFProtection.get_csrf_token()
    )
  end

end

Both methods pass several parameters to the template that renders the index.html page. We modify the file /lib/rustphoenix_web/templates/page/index.html.eex to create the form and table, while displaying the parameters accordingly:

<form action="/" method="POST">
  <input type="hidden" name="_csrf_token" value=<%= @csrf_token %> />

  <label for="callput">CallPut</label>
  <select id="callput" name="callput">
    <option value="Call">Call</option>
    <option value="Put">Put</option>
  </select>

  <label for="underlying">Underlying</label>
  <input type="number" name="underlying" min="0.0" step="0.01" value=<%= @underlying %> />

  <label for="strike">Strike</label>
  <input type="number" name="strike" min="0.0" step="0.01" value=<%= @strike %> />

  <label for="timetoexpiry">Time-To-Expiry</label>
  <input type="number" name="timetoexpiry" min="0.0" step="0.01" value=<%= @timetoexpiry %> />

  <label for="volatility">Volatility</label>
  <input type="number" name="volatility" min="0.0" step="0.01" value=<%= @volatility %> />

  <label for="riskfreerate">Risk-free Rate</label>
  <input type="number" name="riskfreerate" min="0.0" step="0.01" value=<%= @riskfreerate %> />

  <%= submit "Calculate" %>

  <table>
    <tr>
      <td>Price</td>
      <td><%= @price %></td>
    </tr>
    <tr>
      <td>Delta</td>
      <td><%= @delta %></td>
    </tr>
  </table>

</form>

<script>
  document.getElementById("callput").selectedIndex = ["Call","Put"].indexOf( "<%= @callput %>" )
</script>

In the /lib/rustphoenix_web/router.ex file, we need to add the POST route to the site root which calls a method calculate in the PageController module:

  scope "/", RustphoenixWeb do
    pipe_through :browser

    get "/", PageController, :index
    post "/", PageController, :calculate
  end

Our website is now over and we can launch our server in Debug mode on http://localhost:4000 by calling from the project root directory:

mix phx.server

rustphoenixcapture

The Phoenix framework enables hot-code reloading so feel free to experiment by adding new features and see the results in real-time.

Conclusion

The Rust integration within the Phoenix framework is elegant and user-friendly. These tools present a great opportunity to either easily improve the performance of Elixir web applications, or embed Rust functionality in a web application using a mature and feature-rich web framework.

If you like this post, don't forget to follow me on Twitter and get notified of the next publication.