DEV Community

Cover image for WASM in Rust without NodeJS
Alain Viguier
Alain Viguier

Posted on

WASM in Rust without NodeJS

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
Enter fullscreen mode Exit fullscreen mode

Then re-install all the toolchain:

$ curl https://sh.rustup.rs -sSf | sh
Enter fullscreen mode Exit fullscreen mode

and verify it's correctly installed:

$ rustc --version
rustc 1.34.2 (6c2484dc3 2019-05-13)
Enter fullscreen mode Exit fullscreen mode

Install additional utilities:

$ rustup component add rustfmt
Enter fullscreen mode Exit fullscreen mode
$ rustup component add rls rust-analysis rust-src
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

$ cargo install wasm-gc
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or

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

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

$ wasm-gc target/wasm32-unknown-unknown/debug/wasm_sample.wasm
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Glue it with HTML and JavaScript

Create a www directory in your Rust project root structure:

$ mkdir www
$ cd www
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
    });
Enter fullscreen mode Exit fullscreen mode

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

$ cp ../target/wasm32-unknown-unknown/debug/wasm_sample.wasm .
Enter fullscreen mode Exit fullscreen mode

Run a web server

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

$ python3 -m http.server
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Then starts the web server:

$ python3 server.py
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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>()
}
Enter fullscreen mode Exit fullscreen mode

And compile:

$ cargo build --target wasm32-unknown-unknown
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

It will reverse the whole phrase in the textarea:

WASM
WASM

Hope this helps !

Photo by Markus Spiske on Unsplash

Discussion (4)

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 • Edited on

I solved it!
Many thanks!

Thread Thread
vthg2themax profile image
Vince Pike • Edited on

I had the same problem with no created .wasm file, but it was because I missed the step of adding

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

to the Cargo.toml file. Much appreciated writer! I don't know NodeJS, so when they throw that in, it makes things difficult to understand for a beginner of WebAssembly as it is. :-)