Cookie Consent by Privacy Policies Generator Vegapit
Interfacing Elm with Rust WebAssembly
Last updated: 2019-12-30T10:50:31

Wasm Pack is a flexible framework in the Rust ecosystem to compile code to WebAssembly. It allows web developers to outsource their most demanding data processing tasks to some safe and high-performance Rust code. But it also provides Rust developers with an easy access to Web technologies for creating GUIs for their libraries. Elm is a fairly new language that has revolutionised the way highly interactive web pages are built. This tutorial presents a way to use Elm in conjunction with Rust to produce powerful web applications.

Project Architecture

At its core, Elm is a functional programming language that compiles into pure Javascript (JS). It can provide in a single code file, with no external dependencies, the functionalities provided by UI frameworks like Angular.js or Vue.js. Since JS acts as an interface to WebAssembly (WASM) as well, the easiest way to interface it all together is to use a JS run-time environment for the web server. The Express Node.js web application framework is a very popular solution for this purpose. The full code for this project is available on the Vegapit Github, so please feel free to inspect, install and run. It consists of a web page that passes the values of multiple input fields to a WASM function, and instantly displays the formatted calculation results.

Exposing Rust functions via WebAssembly

To move from a standard Rust library to a WASM library, small changes need to made to the basic Cargo.toml configuration file. Here is an extract of the one used in the project:

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = true
opt-level = "s"

[dependencies]
js-sys = "0.3"
wasm-bindgen = "0.2"

The crate type needs to be set to cdylib and in the dependencies section, the wasm-bindgenand js-sys need to be imported. These crates respectively allow to declare the functions to be exported, and to create the JS variable types to return. The tags added to profile.release force the compiler to optimise the size of the final WASM file, which can be critical in some use cases.

The function of interest is defined in the lib.rs file:

use wasm_bindgen::prelude::*;
use js_sys::Map;
use crate::black76::{Black76, CallPut};

#[wasm_bindgen]
pub fn pricer_black76(callput: &str, s: f64, k: f64, t: f64, v: f64, r: f64) -> Map {
    let cp = match callput {
        "Call" => CallPut::Call,
        "Put" => CallPut::Put,
        _ => panic!("Option type unknown")
    };
    let opt = Black76::new(cp, s, k, t, v, r);
    let res = Map::new();
    res.set( &JsValue::from_str("price"), &JsValue::from_f64(opt.price()) );
    res.set( &JsValue::from_str("delta"), &JsValue::from_f64(opt.delta()) );
    res
}

The #[wasm_bindgen] annotation is applied to every function that needs to be exported to WASM. Since the pricer_black76 function generates multiple outputs, a JS Map type is used in its implementation. It has to be constructed from Rust types using the JsValue casting functions. This is pretty much all that we need to create a WASM function library.

To build the module, the following command can be run from the project root:

wasm-pack build --release --target nodejs

This creates a directory pkg in the root folder, which contains all the files needed for exporting the functions as a Node.js module.

Importing WASM functions in Node.js

The Node.js application lives in the www folder. The WASM and helper files that were created by the Rust compiler need to be registered with the npm utility in order to be accessible as a Node.js module. The package.json file needs to be manually modified for this purpose:

{
  ...
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "express-handlebars": "^3.1.0",
    "rust-elm-gui": "file:../pkg"
  }
}

The newly compiled WASM module rust-elm-gui is declared in the dependencies along with its source folder. Running npm install on this configuration file will enable to register the WASM related files as modules in Node.js. The WASM module can then be imported in the Node.js main file by writing:

const wasm = require('rust-elm-gui')

It is important to note that such module can only be accessed through server-side code. So, in order to use the function from the client side, the easiest way is to encapsulate it in a REST API function. Here is how it is defined in the code:

app.post('/api/calculate', function(req,res) {
    let obj = req.body // BodyParser converting directly to JSON
    let calc = wasm.pricer_black76(
        obj.cp,
        parseFloat(obj.s),
        parseFloat(obj.k),
        parseFloat(obj.t),
        parseFloat(obj.v),
        parseFloat(obj.r)
    )
    obj.price = calc.get("price").toFixed(4)
    obj.delta = calc.get("delta").toFixed(4)
    res.json(obj)
})

This route manages POST requests collecting a JSON object and returning its modified version. We can now focus on writing some Elm code to generate the user interface.

Elm programming

The Elm file is stored in www/elm folder. For a programmer used to object orientation, the hard part in learning Elm is to start thinking in purely functional code. The syntax of the language is quite limited however, which greatly helps in understanding it.

In our example, the user is presented with a form to fill and a button to launch the calculation. This means that we can use Elm to generate these particular elements of the HTML document, as opposed to the full page. For this purpose, we assign the main variable of our Elm program to a Browser.element record which has the following signature:

Browser.element
    { init : flags -> ( model, Cmd msg )
    , view : model -> Html msg
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    }

This record requires 4 functions to be defined. Here is a short description of what each do:

  • The init function is called when the element is loaded and basically initialises our program.
  • The view function generates the HTML elements to be displayed.
  • The update function defines the procedures for responding to user actions.
  • The subscriptions function defines any signals other than GUI generated that would need to trigger an update e.g. clock tick or Websocket message.

In order to implement these functions, we can see that 2 important types need to be defined in our Elm program. First is the Model which contains all the variables that need to be tracked throughout the execution of the program. In our case, it reads:

type alias Model = 
    { 
        cp: String
        , s: String
        , k: String
        , t: String
        , v: String
        , r: String
        , price: String
        , delta: String
    }

Second is the Msg which defines the different signals that could get generated by the user through the GUI.

type Msg
    = SetCallPut String
    | SetS String
    | SetK String
    | SetT String
    | SetV String
    | SetR String
    | Calculate
    | ReceivedCalculation (Result Http.Error Model)

For example, the variant Calculate is generated when the button is pressed, or SetK when the Strike field is modified. Here is what the implementation of the update variable looks like:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        SetCallPut cp ->
            ( { model | cp = cp, price = "", delta = "" }, Cmd.none )
        SetS s ->
            ( { model | s = s, price = "", delta = "" }, Cmd.none )
        SetK k ->
            ( { model | k = k, price = "", delta = "" }, Cmd.none )
        SetT t ->
            ( { model | t = t, price = "", delta = "" }, Cmd.none )
        SetV v ->
            ( { model | v = v, price = "", delta = "" }, Cmd.none )
        SetR r ->
            ( { model | r = r, price = "", delta = "" }, Cmd.none )
        Calculate ->
            let
                encodedModel = model
                    |> encoderModel
            in
                ( model, apirequest encodedModel )
        ReceivedCalculation res ->
            case res of
               Ok data -> ( data, Cmd.none )
               Err _ -> ( model, Cmd.none )

It is worth noting that as opposed to processing all information when the button is pressed, information is processed as the user interacts with the GUI. This allows a better control over the information displayed as seen in this video example of the final product:

Compiling Elm and HTML integration

To integrate our Elm program into an HTML page, we first need to compile it into JS. Invoking the Elm compiler allows to do this, and in our case, we run from the www folder the following command:

elm make elm/Index.elm --output=public/js/index.js

Adding the generated code into an HTML file is done through a script in the receiving HTML file:

<div class="container" id="elm"></div>
<script>
    let app = Elm.Index.init({
        node: document.getElementById('elm')
    })
</script>

If you like this post, follow me on Twitter and get notified on the next posts.