Building and augmenting libraries by calling Rust from JavaScript

Explore how to use WebAssembly (Wasm) to embed Rust inside JavaScript.
171 readers like this.
JavaScript in Vim

Alex Sanchez. CC BY-SA 4.0.

In Why should you use Rust in WebAssembly?, I looked at why you might want to write WebAssembly (Wasm), and why you might choose Rust as the language to do it in. Now I'll share what that looks like by exploring ways to embed Rust inside JavaScript.

This is something that separates Rust from Go, C#, and other languages with large runtimes that can compile to Wasm. Rust has a minimal runtime (basically just an allocator), making it easy to use Rust from JavaScript libraries. C and C++ have similar stories, but what sets Rust apart is its tooling, which we'll take a look at now.

The basics

If you've never used Rust before, you'll first want to get that set up. It's pretty easy. First download Rustup, which is a way to control versions of Rust and different toolchains for cross-compilation. This will give you access to Cargo, which is the Rust build tool and package manager.

Now we have a decision to make. We can easily write Rust code that runs in the browser through WebAssembly, but if we want to do anything other than make people's CPU fans spin, we'll probably at some point want to interact with the Document Object Model (DOM) or use some JavaScript API. In other words: we need JavaScript interop (aka the JavaScript interoperability API).

The problem and the solutions

WebAssembly is an extremely simple machine language. If we want to be able to communicate with JavaScript, Wasm gives us only four data types to do it with: 32- and 64-bit floats and integers. Wasm doesn't have a concept of strings, arrays, objects, or any other rich data type. Basically, we can only pass around pointers between Rust and JavaScript. Needless to say, this is less than ideal.

The good news is there are two libraries that facilitate communication between Rust-based Wasm and JavaScript: wasm-bindgen and stdweb. The bad news, however, is these two libraries are unfortunately incompatible with each other. wasm-bindgen is lower-level than stdweb and attempts to provide full control over how JavaScript and Rust interact. In fact, there is even talk of rewriting stdweb using wasm-bindgen, which would get rid of the issue of incompatibility.

Because wasm-bindgen is the lighter-weight option (and the option officially worked on by the official Rust WebAssembly working group), we'll focus at that.

wasm-bindgen and wasm-pack

We're going to create a function that takes a string from JavaScript, makes it uppercase and prepends "HELLO, " to it, and returns it back to JavaScript. We'll call this function excited_greeting!

First, let's create our Rust library that will house this fabulous function:

$ cargo new my-wasm-library --lib
$ cd my-wasm-library

Now we'll want to replace the contents of src/lib.rs with our exciting logic. I think it's best to write the code out instead of copy/pasting.

// Include the `wasm_bindgen` attribute into the current namespace. 
use wasm_bindgen::prelude::wasm_bindgen;

// This attribute makes calling Rust from JavaScript possible.
// It generates code that can convert the basic types wasm understands 
// (integers and floats) into more complex types like strings and 
// vice versa. If you're interested in how this works, check this out:
// https://blog.ryanlevick.com/posts/wasm-bindgen-interop/
#[wasm_bindgen]
// This is pretty plain Rust code. If you've written Rust before this
// should look extremely familiar. If not, why wait?! Check this out:
// https://doc.rust-lang.org/book/
pub fn excited_greeting(original: &str) -> String {
  format!("HELLO, {}", original.to_uppercase())
}

Second, we'll have to make two changes to our Cargo.toml configuration file:

  • Add wasm_bindgen as a dependency.
  • Configure the type of library binary to be a cdylib or dynamic system library. In this case, our system is wasm, and setting this option is how we produce .wasm binary files.
[package]
name = "my-wasm-library"
version = "0.1.0"
authors = ["$YOUR_INFO"]
edition = "2018"

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

[dependencies]
wasm-bindgen = "0.2.33"

Now let's build! If we just use cargo build, we'll get a .wasm binary, but in order to make it easy to call our Rust code from JavaScript, we'd like to have some JavaScript code that converts rich JavaScript types like strings and objects to pointers and passes these pointers to the Wasm module on our behalf. Doing this manually is tedious and prone to bugs.

Luckily, in addition to being a library, wasm-bindgen also has the ability to create this "glue" JavaScript for us. This means in our code we can interact with our Wasm module using normal JavaScript types, and the generated code from wasm-bindgen will do the dirty work of converting these rich types into the pointer types that Wasm actually understands.

We can use the awesome wasm-pack to build our Wasm binary, invoke the wasm-bindgen CLI tool, and package all of our JavaScript (and any optional generated TypeScript types) into one nice and neat package. Let's do that now!

First we'll need to install wasm-pack:

$ cargo install wasm-pack

By default, wasm-bindgen produces ES6 modules. We'll use our code from a simple script tag, so we just want it to produce a plain old JavaScript object that gives us access to our Wasm functions. To do this, we'll pass it the --target no-modules option.

$ wasm-pack build --target no-modules

We now have a pkg directory in our project. If we look at the contents, we'll see the following:

  • package.json: useful if we want to package this up as an NPM module
  • my_wasm_library_bg.wasm: our actual Wasm code
  • my_wasm_library.js: the JavaScript "glue" code
  • Some TypeScript definition files

Now we can create an index.html file that will make use of our JavaScript and Wasm:

<html>
<head>
  <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
</head>
<body>
  <!-- Include our glue code --> 
  <script src='./pkg/my_wasm_library.js'></script>
  <!-- Include our glue code --> 
  <script>
    window.addEventListener('load', async () => {
      // Load the wasm file
      await wasm_bindgen('./pkg/my_wasm_library_bg.wasm');
      // Once it's loaded the `wasm_bindgen` object is populated 
      // with the functions defined in our Rust code
      const greeting = wasm_bindgen.excited_greeting("Ryan")
      console.log(greeting)
    });
  </script>
</body>
</html>

You may be tempted to open the HTML file in your browser, but unfortunately, this is not possible. For security reasons, Wasm files have to be served from the same domain as the HTML file. You'll need an HTTP server. If you have a favorite static HTTP server that can serve files from your filesystem, feel free to use that. I like to use basic-http-server, which you can install and run like so:

$ cargo install basic-http-server
$ basic-http-server

Now open the index.html file through the web server by going to http://localhost:4000/index.html and check your JavaScript console. You should see a very exciting greeting there!

If you have any questions, please let me know. Next time, we'll take a look at how we can use various browser and JavaScript APIs from within our Rust code.

User profile image.
Ryan is a programmer working at Microsoft in Berlin as a developer advocate. In his day job he gets to program in many different programming languages but always enjoys it most when he gets to use Rust. Besides writing Rust, Ryan loves exploring spaces in Berlin and other cities, cooking/eating and dancing with reckless abandon.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.