Deploying and Using WebAssembly Under Deno on the Server Side Using Travis CI

montana

Montana Mendy

Posted on January 7, 2021

Deploying and Using WebAssembly Under Deno on the Server Side Using Travis CI

Header

Containers are a game changer in software development. They provide the operational isolation found in virtual machines without the overhead. Whereas it can take a virtual machine minutes to spin up, you can have a container up and running in seconds, or even in some cases milliseconds.

Getting started

Containers have become central to modern distributed application architecture, particularly now that container orchestration technologies such as Kubernetes and Docker Swarm have become commonplace in the enterprise. However, as popular as container technology is for implementing isolated processes for use in a distributed application, there is an alternative. That alternative is WebAssembly.

WebAssembly is a compiled binary that operates inside a runtime unit called a WebAssembly VM, or virtual machine. WebAssembly has been around for a while on the client side, and all the major web browsers support it. But you can also run it on the server side. All you need is to have a programming framework that supports server-side WebAssembly VM.

Deno — an emerging language from Ryan Stahl, the creator of Node.js — supports WebAssembly. Deno uses the V8 JavaScript runtime found in Chrome on the server side, as does Node.js. The WebAssembly VM is built right into V8, so Deno supports WebAssembly.

In this tutorial, I am going to show how to use Travis CI to build a WebAssembly binary written in Rust, and then run a demonstration application I wrote in Deno that uses the WebAssembly as an internal component.

This tutorial is high-level, and you don’t need to know the details of Rust or Deno to get benefit from reading it. The hope is that the tutorial will spur your interest to delve further into the technologies.

Understanding the demonstration application

The demonstration project that accompanies this article is a Deno application that uses a WebAssembly binary. The Deno code uses logic in the WebAssembly to do a calculation. The calculation that the WebAssembly binary provides is encapsulated into a function named cube(). The function cubes a number that is provided as a parameter. For example, when the number 3 is passed to the function, as cube(3), the function returns 27.

As shown in figure 1 below, the WebAssembly (WASM) binary is written in the programming language Rust. Rust is a compiled language. A programmer writes some code in text and stores it in a file named lib.rs. The code is passed to the Rust WASM compiler. The result is a WebAssembly binary that’s used by the Deno code.

Fig1

Figure 1: Deno uses a WebAssembly binary programmed in Rust.

That’s the high-level view. Let’s take a look at some of the detail involved in creating the WebAssembly binary in Rust and then consuming the binary in Deno.

Examining the WebAssembly code

The work required to create a WebAssembly binary in Rust is consolidated using Rust’s crate packaging technology.

The Rust programming community provides a packaging ecosystem similar to npm for Node.js and to Java’s Maven. Packages under Rust are called crates. The standard file that describes a crate is named Cargo.toml. It’s similar to the way Node.js uses package.json to describe a Node.js package. (I’ll explain the Cargo.toml file for the demonstration Rust binary in a moment.)

The way you install Rust packages from an external Rust package repository is to use the command cargo install .

Creating a WebAssembly in Rust can be a detail-laden undertaking. To simplify the process, we’re going to use an external package called wasm-pack. wasm-pack will execute against the Cargo.toml file that defines the WebAssembly binary we’re going to create. (Keep in mind that the standalone WebAssembly binary is considered a package.)

Listing 1 below shows the Cargo.toml file that describes the Rust project.

[package]
name = "cube_it"
version = "0.9.0"
edition = "2018"
authors = ["Bob Reselman <reselbob@gmail.com>"]
description = "A small project to demonstrate how to run to create a WebAssembly binary"
license = "MIT/Apache-2.0"
repository = "https://github.com/reselbob/deno-wasm"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
wasm-bindgen = "0.2"

[lib]
crate-type = ["cdylib", "rlib"]
Enter fullscreen mode Exit fullscreen mode

Listing 1: The Cargo.toml file

Lines 1 through 12 above provide typical metadata about the project. Notice in line 1 that the arbitrary name of the package is cube_it. This makes sense because the WebAssembly binary that this package definition creates has a function that will cube a number.

Lines 13 and 14 indicate that the Rust project will use an additional package, wasm-bindgen. The purpose of wasm-bindgen is to provide interoperability between the Rust binary and JavaScript code. Deno programming is done in TypeScript. However, at runtime TypeScript transpiles into JavaScript. Using wasm-bindgen allows the Rust code to access JavaScript functions such as logging and the JavaScript DOM model, so wasm-bindgen is defined in Cargo.toml.

Lines 16 and 17 assign the library cdylib to crate-type. This provides interoperability with C code. In addition, crate-type is assigned the value rlib to indicate that a Rust library will be produced.

Listing 2 below shows the Rust code for the function cube(x: u32). The function will reside in the file lib.rs.

#[no_mangle]
pub extern "C" fn cube(x: u32) -> u32 {
    x * x * x
}
Enter fullscreen mode Exit fullscreen mode

Listing 2: The Rust code in lib.rs that publishes the cube function

The logic in the function cube(x: u32) will be consumed by the Deno code.

Once the Rust package is declared in Cargo.toml and the cube() function is defined in lib.rs, we’ll need to create the Deno code to consume the WebAssembly binary that’s created.

Running WebAssembly under Deno

Listing 3 below shows the contents of the file main.ts. This file contains the Deno code that consumes the WebAssembly binary that contains the function cube().

const wasmCode = await      Deno.readFile("./rust_for_deno/target/wasm32-unknown-unknown/release/cube_it.wasm");
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const {cube} = wasmInstance.exports;

console.log(cube(1));
console.log(cube(2));
console.log(cube(3));
console.log(cube(4));
Enter fullscreen mode Exit fullscreen mode

Listing 3: The Deno code that consumes the WebAssembly binary

Lines 1 through 5 do the work of loading the WebAssembly into the V8 virtual machine and making the cube() function accessible to the Deno code. Lines 7 through 10 execute the cube() function four times, each time with a different parameter value, and then write the output of each call to the console.

Now that we have both the Rust WebAssembly and Deno projects coded and ready to go, we need to create a travis.yml file that will build the WebAssembly binary, as well as install the Deno runtime environment and run the Deno code.

Working with the .travis.yml file

One of the nice features of Travis CI is that it will execute a run script in response to an event generated from a webhook installed in a GitHub repository that is bound to Travis CI. Under Travis CI, the run script has a standard name, .travis.yml.

The deno-wasm demonstration project for this article stored on GitHub has a webhook installed that sends an event notification to Travis CI whenever code is committed to the deno-wasm repository. Travis CI receives the event notification and clones the code into a runtime virtual machine created just for the project. Then, it runs the instructions in the expected .travis.yml file.

Listing 4 below shows the contents of the .travis.yml file.

language: rust
os: ubuntu
branches:
    only:
    - master
before_install:
    - pwd
    - rustc --version
    - sudo apt-get update
    - sudo apt-get -y install libssl-dev pkg-config 
    - rustup target add wasm32-unknown-unknown
    - cargo install wasm-pack
    - export PATH="$HOME/.cargo/bin:$PATH"
    - cd ./rust_for_deno
    - wasm-pack build
    - cd ..
    - curl -fsSL https://deno.land/x/install/install.sh | sh
    - ls -l $HOME/.deno
    - export DENO_INSTALL="$HOME/.deno"
    - export PATH="$DENO_INSTALL/bin:$PATH"
    - deno run https://deno.land/std/examples/welcome.ts

script: 
    - deno run --allow-read main.ts
Enter fullscreen mode Exit fullscreen mode

Listing 4: The travis.yml file for building and running the deployment on Travis CI

The first thing to notice about the .travis.yml file in listing 4 above is that Travis CI supports the Rust programming language right out of the box. All you need to do is set the language definition, as shown at line 1.

The work of creating the WebAssembly binary and installing Deno is done in the before_install section, starting at line 5.

Lines 7 and 8 are convenience instructions that report the present working directory (pwd) and the current version of the Rust compiler installed (rustc --version). This is done just to make sure that the .travis.yml script is talking to the Travis CI runtime in a predictable manner.

Lines 9 and 10 install some added packages that Ubuntu needs in order to execute the wasm-pack instruction that will come.

Lines 11 through 15 do the work of installing the wasm-pack Rust package and creating the WebAssembly binary using wasm-pack.

Deno is not installed in the Travis CI runtime environment by default. Thus, lines 17 through 20 do the work of installing it and configuring the DENO_INSTALL environment variable. Also, the PATH environment variable is updated to accommodate the Deno binaries.

Line 21 runs a Deno Hello World program downloaded from the internet, to verify that Deno is up and running.

As mentioned earlier, the code that uses the cube() function in the WebAssembly binary is stored in the file main.ts. main.ts is called within the script section of the .travis.yml file at line 24 above.

The output of the call to the main.ts file is shown below in figure 2.

Fig2

Figure 2: The output from the demonstration application running under Travis CI

So there you have it. We’ve just used the Travis CI runtime environment to create a WebAssembly binary and run it in a Deno program. Granted, this is just a small demonstration program, but it does reveal the critical aspects of using WebAssembly to provide services to interested consumers in an isolated manner.

Putting it all together

It’s interesting that Solomon Hykes, the co-founder of Docker, said this about WebAssembly:

"“If WASM+WASI [WebAssembly and its system interface] existed in 2008, we wouldn’t have needed to create Docker."

Server-side WebAssembly has significant potential. Its elegance and power make it an attractive technology for the fast execution of complex algorithms on the server side. Consequently, there are a growing number of projects that are publishing tools for WebAssembly development. You can already create WASM binaries in Go, Rust, C# and C/C++. In addition, there are initiatives to bring support for WebAssembly to a wider array of languages. For example, AssemblyScript is intended to allow developers to create WebAssembly binaries from JavaScript code.

The opportunities at hand are significant. Of course, in order to make WebAssembly a viable addition to your programming toolbox, you will need to take the time to master the details of the technology. But, as current trends reveal, WebAssembly is becoming a mainstay on the server side, and realizing its value will be an investment that will yield dramatic returns.

WebAssembly is here. The time to take advantage of the opportunities at hand is now.

💖 💪 🙅 🚩
montana
Montana Mendy

Posted on January 7, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related