loading...
Cover image for WASM in Rust without NodeJS

WASM in Rust without NodeJS

dandyvica profile image Alain Viguier ・5 min read

In this article, I'll talk about how to call Rust WASM methods from JavaScript, but without using NodeJS. Almost the examples I googled so far were only describing using Rust for WASM in NodeJS (but better explained now in the wasm-bindgen documentation). I think it's too complicated to grasp the whole idea. It's better (for me at least) to unravel the whole process without bringing too many players in the game.

As Rust is already installed on my Linux machine, I'll be describing how to install Rust and WASM from scratch.

Prepare Rust

Reinstall Rust

This is not a mandatory step but as I faced some issues when trying to install WASM, I did reinstall Rust completely:

$ rustup self uninstall

Then re-install all the toolchain:

$ curl https://sh.rustup.rs -sSf | sh

and verify it's correctly installed:

$ rustc --version
rustc 1.34.2 (6c2484dc3 2019-05-13)

Install additional utilities:

$ rustup component add rustfmt
$ rustup component add rls rust-analysis rust-src

Install WASM specific extension and utilities

Now it's time to install WASM extension to be able to compile Rust to WASM directly:

rustup target add wasm32-unknown-unknown

Then, install the wasm-gc utility which allows to strip down the resulting WASM binary from compilation:

$ cargo install wasm-gc

Install wabt

The WAT file is the ASCII representation of the binary WASM. You can convert the binary .wasm file to a .wat ASCII file with the wasm2wat command. Follow instructions at https://github.com/WebAssembly/wabt to install the wabt toolkit.

Install wasm-bindgen

$ cargo install wasm-bindgen-cli

A simple example: add 2 numbers

Prepare your project

Now it's time to create a new project:

# --lib for creating a library instead of a binary
$ cargo new --lib wasm_sample

and edit the Cargo.toml file to add [lib] tag:

[package]
name = "wasm_sample"
version = "0.1.0"
authors = ["dandyvica <dandyvica@gmail.com>"]
edition = "2018"

[dependencies]

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

to mean we want to create a dynamic library.

A simple Rust WASM function

Let's code a very simple function for the moment:

// this is a simple interface without leveraging from the Rust language bells and whistles
#[no_mangle]
pub extern fn add(x: u32, y: u32) -> u32 {
    x + y
}

Compile to WASM

You can now compile your code with:

# debug: the target is target/wasm32-unknown-unknown/debug/wasm_sample.wasm
$ cargo build --target wasm32-unknown-unknown

or

# release: the target is target/wasm32-unknown-unknown/release/wasm_sample.wasm
$ cargo build --target wasm32-unknown-unknown --release

The resulting .wasm file is about 822 KB. Strip it down using wasm-gc:

$ wasm-gc target/wasm32-unknown-unknown/debug/wasm_sample.wasm

As a result, the wasm_sample.wasm is 16 KB.

Using the wasm2wat utility, we can check whether the add function is indeed exported:

$ cd target/wasm32-unknown-unknown/debug
$ wasm2wat wasm_sample.wasm -o wasm_sample.wat
$ grep export wasm_sample.wat
  (export "add" (func $add))
  (export "__rustc_debug_gdb_scripts_section__" (global 3))

Glue it with HTML and JavaScript

Create a www directory in your Rust project root structure:

$ mkdir www
$ cd www

and create an index.html file:

<!DOCTYPE html>
<html>
  <head>
    <script src="wasm_sample.js"></script>
  <head>
  <body>
    <form onSubmit="return false">
      Enter X: <input type="number" name="X" required><br>
      Enter Y: <input type="number" name="Y" required><br><br>
      <input 
        type="submit" 
        value="X+Y=" 
        onClick="result.innerText = wasm_add(X.value,Y.value)">
      <label id="result"></label>   
    </form>       
  </body>
<html>

and create the wasm_sample.js JavaScript file:

// use this JS API to load the WASM module and start using it in a streaming mode
// i.e. without having to wait
WebAssembly.instantiateStreaming(fetch("wasm_sample.wasm"))
    .then(wasmModule => {
        // this saves the exported function from WASM module for use in JS
        wasm_add = wasmModule.instance.exports.add;
    });

Also, copy the WASM module to your www directory to rule out directory hassles:

$ cp ../target/wasm32-unknown-unknown/debug/wasm_sample.wasm .

Run a web server

On Linux, a simple web server is available through Python by:

$ python3 -m http.server

but this implementation doesn't include the application/wasm mime type needed to import the WASM module.

You can find an example on GitHub gist to handle WASM mime types. Save the following into a server.py file:

import http.server
import socketserver

PORT = 8000

Handler = http.server.SimpleHTTPRequestHandler
Handler.extensions_map.update({
    '.wasm': 'application/wasm',
})

socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("", PORT), Handler) as httpd:
    httpd.allow_reuse_address = True
    print("serving at port", PORT)
    httpd.serve_forever()

Then starts the web server:

$ python3 server.py

Then, start your favorite browser supporting WASM et head to http://localhost:8000

WASM add

Using the wasm-bindgen crate

The previous example exposed a Rust function, but without leveraging from Rust power, like exchanging strings or structures between JS and Rust. In order to achieve that, you'll need to import the wasm-bindgen crate in your Cargo.toml.

As stated in https://rustwasm.github.io/wasm-bindgen/:

this project allows JS/wasm to communicate with strings, JS objects, > classes, etc, as opposed to purely integers and floats.

Now we're going to pass a string from JS to Rust back and forth.

Add wasm-bindgen crate

Add the dependency to wasm-bindgen in the Cargo.toml file:

[dependencies]
wasm-bindgen = "0.2.45"

Modify the lib.rs source file

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

// Reverse a string coming from JS 
#[wasm_bindgen]
pub fn reverse(s: String) -> String {
    s.chars().rev().collect::<String>()
}

And compile:

$ cargo build --target wasm32-unknown-unknown

Bind with Rust

Next step is to run wasm-bindgen to make use of high-level interactions between WASM modules and JavaScript:

$ cd target/wasm32-unknown-unknown/debug
$ wasm-bindgen --target web --no-typescript --out-dir . wasm_sample.wasm

This will create 2 WASM files: wasm_sample.wasm and wasm_sample_bg.wasm. This is the last one we're gonna use.

Strip down the WASM binary

$ wasm-gc wasm_sample_bg.wasm

and copy the WASM binary (wasm_sample_bg.wasm) and the generated JS module (wasm_sample.js) into the www directory

Edit the index.html file

Finally, edit the index.html file as follows:

<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <!-- Note the usage of `type=module` here as this is an ES6 module -->
    <script type="module">
      import { reverse, default as init } from './wasm_sample.js';

      async function run() {
        await init('./wasm_sample_bg.wasm');

        // make the function available to the browser
        window.reverse = reverse;
      }

      run();
    </script>

    <form onSubmit="return false">
        <textarea rows="40" cols="50" name="lipsum">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.         
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
        </textarea><br> 
        <input 
            type="submit" 
            value="Reverse string" 
            onClick="lipsum.value = reverse(lipsum.value)">
    </form> 
  </body>

</html>

It will reverse the whole phrase in the textarea:

WASM
WASM

Hope this helps !

Photo by Markus Spiske on Unsplash

Discussion

pic
Editor guide
Collapse
davidhur profile image
DavidHur

Hi,
Sorry I'm not familiar with it.

I tried to following this article, when I convert wasm to wat as following code,

[no_mangle]

pub extern fn add(x: u32, y: u32) -> u32 {
x + y

}

But, in my case, have no 'add' function in wat file.

grep export command output as following;

(export "memory" (memory 0))
(export "heap_base" (global 0))
(export "
data_end" (global 1))

(export "rustc_debug_gdb_scripts_section" (global 2))

Am I missed something?

My OS version and rust version as below;
Linux debian 4.9.0-9-amd64 #1 SMP Debian 4.9.168-1+deb9u2 (2019-05-13) x86_64 GNU/Linux
rustup 1.18.3 (435397f48 2019-05-22)
cargo 1.35.0 (6f3e9c367 2019-04-04)

Collapse
dandyvica profile image
Alain Viguier Author

Hi,

Difficult to spot but did you follow the article step by step ?

Collapse
davidhur profile image
DavidHur

I solved it!
Many thanks!