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
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.