October 21, 2017

Rust to WebAssembly, Made Easy

Compiling Rust to WebAssembly is still difficult. On macOS, there are weird bugs that require reinstalling LLVM and moving files around. On Linux, the official Emscripten binary in many cases isn’t bundled with the right LLVM, and the fix is to compile from scratch, a multi-hour process. In Travis CI, the standard “solution” is to install an Alpine Linux chroot to get the latest Emscripten. It’s a clever workaround, but shouldn’t there be a better way?

wargo is a drop in wrapper for cargo that automatically installs and configures Emscripten for you (on macOS or Linux), avoiding or autofixing all of the above bugs. It can also automatically run your tests in a real web browser using WebDriver! The source code is on GitHub. To install, use npm with Node 6 or newer:1

npm install -g wargo

Let’s compile our first WebAssembly project! With wargo, it’s pretty simple:

cargo new --bin meow
cd meow
wargo build

With just a single command, wargo downloads the appropriate Emscripten binary, checks for dependencies, configures environment variables, and runs cargo build. You can find your freshly compiled wasm files in the target/wasm32-unknown-emscripten directory.

Testing Locally

Another command, wargo test, automatically uses WebDriver to test your WebAssembly code. If you’ve got chromedriver installed (on macOS, brew install chromedriver) you can run this to start the WebDriver server:

chromedriver --port=4445 --url-base=/wd/hub

Once the server is up and running2 on port 4445, you can run

wargo test

in your project. We’ll automatically build a test binary and run it with WebDriver. This lets you write tests just like you would any other Rust project! Console output from the browser appears in your terminal:

$ wargo test
  Setup wasm checking dependencies...
  Setup wasm found emsdk installation in ~/.emsdk
  Setup wasm setting environment...
  Setup wasm running 'cargo test --target=wasm32-unknown-emscripten --no-run --message-format=json'
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
  Setup wasm running tests...
pre-main prep time: 566 ms

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Right now, we don’t have any tests, so none were run. But if we added a standard one with Rust’s #[test] macro, it would run here!

Testing in Travis

With a free-for-open-source Sauce Labs account, you can easily test your WebAssembly project in Travis CI, even with multiple browsers! Just put this in your .travis.yml:

language: rust
rust: stable

  - npm install -g wargo

  # install recent cmake, since Trusty only ships with an old version
  - (cd ~; curl -sSL https://cmake.org/files/v3.5/cmake-3.5.2-Linux-x86_64.tar.gz | tar -xz)
  - sudo mv /usr/bin/cmake /usr/bin/cmake.old
  - sudo ln -s ~/cmake-3.5.2-Linux-x86_64/bin/cmake /usr/bin/cmake

  - wargo build --verbose
  - wargo test --verbose

    username: "TODO your Sauce Labs username"
    access_key: "TODO your Sauce Labs access key"

If you want to test in a different type of browser, you can put Sauce Labs configuration JSON in the WEBDRIVER_CAPABILITIES environment variable. For instance, you could add this line under install: to test in Chrome 60 running on Windows 8:

- export WEBDRIVER_CAPABILITIES='{"browserName": "chrome", "platform": "Windows 8", "version": "60.0"}'

Using with Webpack

There is also the wargo-loader package for use with Webpack, based off rust-emscripten-loader by Matt Dziuban.

cargo new --bin meow
cd meow

We’ll create a package.json file with the autowasm dependency and some helper scripts:

echo '{
  "devDependencies": {
    "http-server": "^0.10.0",
    "wargo-loader": "^0.3.0",
    "webpack": "^3.7.1"
  "scripts": {
    "compile": "webpack --progress",
    "serve": "http-server"
}' > package.json

We’ll next create a index.js file to load in the WebAssembly, as well as a simple index.html file and a Webpack config file.

echo '
  const wasm = require("./main.rs")
  wasm.initialize({noExitRuntime: true}).then(module => {
    // you can call module.cwrap here to get function wrappers for Rust functions
' > src/index.js

echo '<!DOCTYPE html> <script src="build/bundle.js"></script>' > index.html
curl -L https://git.io/vdNTe > webpack.config.js

With all the files in place, we just need to install the dependencies, run webpack with the compile script, and start the server.

npm install
npm run compile
npm run serve

If you open localhost:8080, you’ll just see a blank screen. But open up the JavaScript console, and you should see your “Hello, World!” from Rust!

Please Help Bugtest!

There will be bugs. It works on all the computers I’ve tested, and in Travis, but I can only test so many environments. If you’re on macOS or Linux and can’t run wargo build on a freshly created cargo new --bin meow, that’s a bug, and I’d love a bug report if you have the time! If we can solve your bug, we can automate it away so nobody has to deal with it again.

  1. The original version was written in Rust, but the multiple-minute compile times got a bit annoying, especially in CI. And if you’re writing WebAssembly, you probably have Node installed already. 

  2. If you’re running WebDriver on a different port or host, you can customize where wargo looks with the WEBDRIVER_HOST and WEBDRIVER_PORT environment variables.