DEV Community

Sendil Kumar
Sendil Kumar

Posted on • Edited on • Originally published at sendilkumarn.com

Rust and WebAssembly for the masses - wasm-bindgen

Binding your WebAssembly and JavaScript with wasm-bindgen

WebAssembly can only send and receive number between JavaScript and WebAssembly module.

In order to pass other data (like String, Objects, Functions), we should create a binding file.

The binding file does the following:

  • It converts string or object to something that WebAssembly module understands.
  • It converts the returned value from WebAssembly module into string or object that JavaScript understands.

But converting them every time is a mundane task and error-prone. Fortunately Rust world came up with wasm-bindgen.

wasm-bindgen

Facilitating high-level interactions between wasm modules and JavaScript - wasm-bindgen

The wasm-bindgen provides a channel between JavaScript and WebAssembly to communicate something other than numbers, i.e., objects, strings, arrays, etc.,

Write some code ✍️

Let us start with hello_world with wasm-bindgen.

Create a new project with cargo.

$ cargo new --lib hello_world
Created library `hello_world` package
Enter fullscreen mode Exit fullscreen mode

This creates a new Rust project with the necessary files.

Once created open the project in your favourite editor.

Open the Cargo.toml file and add the wasm-bindgen dependency.


Check out my book on Rust and WebAssembly here


[package]
name = "hello_world"
version = "0.1.0"
authors = ["Sendil Kumar <sendilkumarn@live.com>"]
edition = "2018"

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

[dependencies]
wasm-bindgen = "0.2.56"
Enter fullscreen mode Exit fullscreen mode

Open the src/lib.rs file and replace the contents with the following:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello_world() -> String {
 "Hello World".to_string()
}
Enter fullscreen mode Exit fullscreen mode

We imported the wasm_bindgen library use wasm_bindgen::prelude::*;.

We annotated the hello_world() function with #[wasm_bindgen] tag.

The hello_world() function returns a String.

To generate the WebAssembly module run:

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

The cargo build command does not generate any JavaScript binding file. In order to generate the binding files, we need to run the wasm-bindgen CLI tool on the generated WebAssembly module.

Install wasm-bindgen CLI to generate the binding file.

Use cargo to install wasm-bindgen-CLI:

$ cargo install wasm-bindgen-cli
Enter fullscreen mode Exit fullscreen mode

Once successfully installed run the wasm-bindgen CLI on the generated WebAssembly module.

$ wasm-bindgen target/wasm32-unknown-unknown/debug/hello_world.wasm --out-dir .
Enter fullscreen mode Exit fullscreen mode

We instruct wasm-bindgen to generate the binding JavaScript for the generated WebAssembly module.

The --out-dir flag instructs the wasm-bindgen where to generate files. The files are generated in the current folder.

This generates the following files:

$ ls -lrta
76330 hello_world_bg.wasm
 1218 hello_world.js
  109 hello_world.d.ts
  190 hello_world_bg.d.ts
Enter fullscreen mode Exit fullscreen mode

The wasm-bindgen CLI takes the WebAssembly module (the output of the cargo build) as input and generate the bindings. The size of the binding JavaScript file is around 1.2 KB. The hello_world.js does all the translations (that are required) between JavaScript and the WebAssembly modules.

The wasm-bindgen CLI along with the binding file generates the type definition file hello_world.d.ts.

The type definition file for the WebAssembly Module (hello_world.d.ts).

The rewritten WebAssembly module hello_world.wasm that takes advantage of the binding file.

The JavaScript binding file is enough for us to load and run the WebAssembly Module.

If you are using TypeScript, then the type definition will be helpful.

Inside the binding file

The binding file imports the WebAssembly module.

import * as wasm from './hello_world_bg.wasm';
Enter fullscreen mode Exit fullscreen mode

Then we have the TextDecoder, to decode the String from the ArrayBuffer.

Since there are no input arguments available there is no need for TextEncoder (that is to encode the String from JavaScript into the shared memory).

The wasm-bindgen generates only the necessary functions inside the binding file. This makes the binding file as small as 1.2KB.

Modern browsers have built-in TextDecoder and TextEncoder support. The wasm-bindgen checks and uses them if they are available else it loads it using polyfill.

const lTextDecoder = typeof TextDecoder === 'undefined' ? require('util').TextDecoder : TextDecoder;
let cachedTextDecoder = new lTextDecoder('utf-8');
Enter fullscreen mode Exit fullscreen mode

The shared memory between JavaScript and the WebAssembly module need not be initialised every time. We initialise it once and use it across.

We have the following two methods to load the memory once and use it.

let cachegetInt32Memory0 = null;
function getInt32Memory0() {
    if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
        cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
    }
    return cachegetInt32Memory0;
}
Enter fullscreen mode Exit fullscreen mode
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
    if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
        cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
    }
    return cachegetUint8Memory0;
}
Enter fullscreen mode Exit fullscreen mode

The Rust code returns String to the JavaScript land. The String is passed via the shared memory.

The Shared memory is nothing but an ArrayBuffer. So we can need only the pointer to the offset (location where it is stored) and the length of the String to retrieve the String. Both the index of the location and the length are just numbers. They are passed from the WebAssembly land to JavaScript without any problem.

The following function is used for retrieving the String from the WebAssembly module:

function getStringFromWasm0(ptr, len) {
    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
Enter fullscreen mode Exit fullscreen mode
  • ptr is an index where the offset of the location.
  • len is the length of the String.

Finally, we have the hello_world function.

/**
* @returns {string}
*/
export function hello_world() {
    try {
        wasm.hello_world(8);
        var r0 = getInt32Memory0()[8 / 4 + 0];
        var r1 = getInt32Memory0()[8 / 4 + 1];
        return getStringFromWasm0(r0, r1);
    } finally {
        wasm.__wbindgen_free(r0, r1);
    }
}
Enter fullscreen mode Exit fullscreen mode

The hello_world function is exported. We get the pointer and length from the shared memory buffer. Then pass the two numbers (r0, r1) to the getStringFromWasm function.

The getStringFromWasm function returns the String from the shared Array Buffer with ptr and len.

Once we received the output, we clear the allocated memory using wasm.__wbindgen_free(r0, r1).


cargo-expand

To understand what happens on the Rust side, let us use the cargo-expand command to expand the macro and see how the code is generated.

Note: Check here for how to install cargo expand. It is not mandatory for the course of this book. But they will help you understand what wasm-bindgen actually generates.

Open your terminal, go to the project's base directory and run cargo expand --target=wasm32-unknown-unknown > expanded.rs.

The above command generates expanded.rs.

The simple #[wasm_bindgen] annotation changes / adds the verbose part of exposing the function. All the necessary metadata that is required for the compiler to convert to WebAssembly module.

Note: Check out more about the internals of the #[wasm_bindgen] command here

The expanded.rs has the hello_world function.

pub fn hello_world() -> String {
    "Hello World".to_string()
}
Enter fullscreen mode Exit fullscreen mode

The __wasm_bindgen_generated_hello_world function is an auto generated.

#[allow(non_snake_case)]
#[export_name = "hello_world"]
#[allow(clippy::all)]
pub extern "C" fn __wasm_bindgen_generated_hello_world(
) -> <String as wasm_bindgen::convert::ReturnWasmAbi>::Abi {
    let _ret = { hello_world() };
    <String as wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
}
Enter fullscreen mode Exit fullscreen mode

The #[export_name = "hello_world"] exports the function with the name hello_world.

The function returns <String as wasm_bindgen::convert::ReturnWasmAbi>::Abi. We will see more about this type in the later posts. But if you want to understand what happens here read this post.

The function returns the String in the format the binding JavaScript file (ptr and len).


Run it 🏃‍♂️

Instead of running them using local web server we can load and run the generated files we can use bundlers like Webpack or Parcel.

We will see more in detail about how these bundlers help in the later chapters.

For now let's see how to run and load the generated files:

Note the following setup is common and we will refer it as the "default" webpack setup in the future examples.

Create a webpack.config.js to configure Webpack how to handle files.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {     
    entry: './index.js',
    output: {         
        path: path.resolve(__dirname, 'dist'),         
        filename: 'bundle.js',     
    },
    plugins: [         
        new HtmlWebpackPlugin(),         
    ],
    mode: 'development'
};
Enter fullscreen mode Exit fullscreen mode

This is a standard webpack configuration file with an HTMLWebpackPlugin. This plugin helps us to generate a default index.html rather than we create one.

Let us add a package.json file to bundle the dependencies for running the Webpack and scripts to run.

{
    "scripts": {
        "build": "webpack",
        "serve": "webpack-dev-server"
    },
    "devDependencies": {
        "html-webpack-plugin": "^3.2.0",
        "webpack": "^4.41.5",
        "webpack-cli": "^3.3.10",
        "webpack-dev-server": "^3.10.1"
    }
}
Enter fullscreen mode Exit fullscreen mode

Create an index.js file to load the binding JavaScript that in turn loads the WebAssembly module generated.

import("./hello_world").then(module => {
    console.log(module.hello_world());
});
Enter fullscreen mode Exit fullscreen mode

Now head over to the terminal and then install the npm dependencies using.

$ npm install
Enter fullscreen mode Exit fullscreen mode

Run the webpack-dev-server using

$ npm run serve
Enter fullscreen mode Exit fullscreen mode

Go to the URL on which webpack-dev-server serves (defaults to http://localhost:8080) and open the developer console in the browser to see "Hello World" printed.


wasm-bindgen options

Let us take a look at the various options wasm-bindgen supports.

--out-dir - generates the file in a particular directory.

--out-name - set a custom filename.

wasm-bindgen has the following flags:

--debug

The --debug option includes extra debug information in the generated WebAssembly module. This will increase the size of the WebAssembly module. But it is useful in development.

--keep-debug

WebAssembly modules may or may not have custom sections (We will them in the later blogs). This custom section can be used to hold the debugging information. They will be helpful while debugging the application (like in-browser dev tools). This increases the size of the WebAssembly module. This is useful in development.

--no-demangle

This flag tells the wasm-bindgen not to demangle the Rust symbol names. Demangle helps the end-user to use the same name that they have defined in the Rust file.

--remove-name-section

This will remove the debugging name section of the file. We will see more about various sections in the WebAssembly module later. This will decrease the size of the WebAssembly module.

--remove-producers-section

WebAssembly modules can have a producer section. This section holds the information about how the file is produced or who produced the file.

By default, producer sections are added in the generated WebAssembly module. With this flag, we can remove it.
It saves a few more bytes.

The wasm-bindgen provide options to generate the binding file for both Node.js and the browser environment. Let us see those flags.

--nodejs - Generates output that only works for Node.js. No ESModules.
--browser - Generates output that only works for browser With ESModules.
--no-modules- Generates output that only works for browser. No ESModules. Suitable for browsers that don't support ESModules yet.

The type definition files (*.d.ts) can be switched off by using --no-typescript flag.


If you have enjoyed the post, then you might like my book on Rust and WebAssembly. Check them out here



👇 Repo 👇

GitHub logo sendilkumarn / rustwasm-introduction

Rust and WebAssembly for masses - Introduction


Interested to explore more...

To know more about the custom section. Check out here

Check out more about webpack here

Check out this awesome blog post to know more about the ECMAScript modules.


You can follow me on Twitter.

If you like this article, please leave a like or a comment. ❤️


Top comments (3)

Collapse
 
beagleknight profile image
David Morcillo

Hi there! I just started with these blog post series and it has been great so far, congratulations!

I only detected an issue regarding the webpack configuration. The config file should be named webpack.config.js so webpack read it automatically. If it doesn't use the default name you need to provide the --config flag. (source: webpack.js.org/configuration/)

Cheers!

Collapse
 
sendilkumarn profile image
Sendil Kumar

Thanks I updated the post.

Collapse
 
palik profile image
Алексей Пастухов • Edited

Thank you for the taking the time and effort for writing this excellent post series!

For the case later posts don't mention wasm-pack, I'll do it here. It seems to me wasm-pack simplifies development process and do more code optimization.

Indeed wasm-pack got it's own blog post Build, test, pack and publish WASM modules with wasm-pack.