SoFunction
Updated on 2025-03-04

Improve JavaScript and Rust interoperability and gain insight into wasm-bindgen components

Preface

Recently we have seen how WebAssembly quickly compiles, speeds up JS libraries, and generates smaller binary formats. We even have advanced planning for better interoperability between the Rust and JavaScript communities and other web programming languages. As mentioned in the previous post, I want to dig deep into the details of a specific component, wasm-bindgen.

Today, the WebAssembly standard defines only four types: two integer types and two floating point types. However, for the most part, JS and Rust developers are using richer types! For example, JS developers often interact with documents related to adding or modifying HTML nodes, while Rust developers use types like Result to perform error handling, and almost all programmers use strings.

Being limited to using only the types provided by WebAssembly will be too restrictive, which is why wasm-bindgen appears.

The goal of wasm-bindgen is to provide a bridge between JS and Rust types. It allows JS to call the Rust API using strings, or Rust functions to catch JS exceptions.

wasm-bindgen smoothed out the impedance mismatch between WebAssembly and JavaScript, ensuring that JavaScript can call WebAssembly functions efficiently without boilerplate, and WebAssembly can perform the same operations on JavaScript functions.

The wasm-bindgen project is described more in its README file. To get started, let's dive into an example using wasm-bindgen and explore what it has to offer.

1、Hello World!

One of the best and most classic ways to learn new tools is to use them to output "Hello, World!". Here, we will explore an example like this - the "Hello World!" reminder box pops up on the page.

The goal here is very simple. We want to define a Rust function. Given a name, it will create a dialog box on the page with Hello, $name! In JavaScript, we can define this function as:

Code

export function greet(name) {
  alert(`Hello, ${name}!`);
}

But in this example, we will write it in Rust. There are a lot of things that we have to deal with here:

  • JavaScript will call a WebAssembly module, the module name is greetexport.
  • The Rust function takes a string as an input parameter, which is the name we want to say hello.
  • Inside Rust generates a new string, which is the name passed in.
  • Finally, Rust will call JavaScript's alert function, taking the string just created as a parameter.

Start the first step, we create a new Rust project:

Code

$ cargo new wasm-greet --lib 

This will initialize a new wasm-greet folder, and our work is done here. Next we need to use the following information to modify our (equivalent to Rust):

Code

[lib] 
crate-type = ["cdylib"] 
 
[dependencies] 
wasm-bindgen = "0.2" 

Let's ignore the content of the [lib] section first, and the next part declares the dependency on wasm-bindgen. The dependencies here include all the support packages we need to use wasm-bindgen.

Next, it's time to write some code! We replaced the automatically created src/ with the following:

Code

#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
  fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
  alert(&format!("Hello, {}!", name));
}

If you are not familiar with Rust, this may seem a bit long-winded, but don't be afraid! The wasm-bindgen project has been improving over time, and to be sure, all of this is not always necessary.

The most important thing to note is the #[wasm_bindgen] attribute, which is a comment in Rust code, which means "please handle this with wrapper if necessary". Our import of the alert function and the export of the greet function are both marked as this property. Later, we will see what is happening under the hood.

First, let's get to the point by opening it in the browser as an example! Let's compile the wasm code first:

Code

$ rustup target add wasm32-unknown-unknown --toolchain nightly # only needed once 
$ cargo +nightly build --target wasm32-unknown-unknown 

This code will generate a wasm file with the path target/wasm32-unknown-unknown/debug/wasm_greet.wasm. If we use tools such as wasm2wat to see the contents in this wasm file, it may be a bit scary.

As a result, it was found that this wasm file cannot actually be called directly by JS! In order for us to use, we need to perform one or more steps:

Code

$ cargo install wasm-bindgen-cli # only needed once 
$ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir . 

A lot of incredible things happen in this step: the wasm-bindgen CLI tool post-processes the input wasm file to make it "suitable".

Let's look at the meaning of "suitable". Now we can say with certainty that if we introduce the wasm_greet.js file we just created (created by the wasm-bindgen tool), we have obtained the greet function defined in Rust.

Ultimately what we have to do next is to use a bundler to package it and create an HTML page to run our code.

At the time of writing this article, only Webpack's 4.0 release has sufficient support for the use of WebAssembly (although Chrome caveat is currently available).

One day, more bundlers will continue to support WebAssmbly. I won't describe the details here, but you can take a look at the example configuration in the Github repository. But if we look at the content, our JS in this page looks like this:
Code

const rust = import("./wasm_greet"); 
(m => ("World!")); 

…That’s all! Now that we open our web page, we will display a good "Hello, World!" dialog box, which is Rust-driven.

2. How does wasm-bindgen work

Wow, that's a huge "Hello, World!". Let's dig into more details to see what's going on in the background and how the tool works.

One of the most important aspects of wasm-bindgen is that its integration is basically based on a concept that one wasm module is just another ES module. For example, in the above we want an ES module with the following signature (in Typescript):

Code

export function greet(s: string); 

WebAssembly cannot do this locally (remember, it currently supports numbers), so we rely on wasm-bindgen to fill the gap.

In the last step above, when we run the wasm-bindgen tool, you will notice that the wasm_greet.js file appears along with the wasm_greet_bg.wasm file. The former is the actual JS interface we want, performing any necessary processing to call Rust. * _bg.wasm file contains the actual implementation and all our compiled code.

We can get what Rust code is willing to expose by introducing the ./wasm_greet module. We have seen how it integrates and can continue to see how the execution is. First is our example:

Code

const rust = import("./wasm_greet"); 
(m => ("World!")); 

We import the interface asynchronously here, waiting for the import to complete (download and compile wasm). Then call the module's greet function.

Note: The asynchronous loading used here currently requires Webpack to implement, but it will never be needed. Moreover, other packaging tools may not have this feature.

If we look at the content generated by the wasm-bindgen tool for the wasm_greet.js file, we will see a code like this:

Code

import * as wasm from './wasm_greet_bg';

// ...

export function greet(arg0) {
  const [ptr0, len0] = passStringToWasm(arg0);
  try {
    const ret = (ptr0, len0);
    return ret;
  } finally {
    wasm.__wbindgen_free(ptr0, len0);
  }
}

export function __wbg_f_alert_alert_n(ptr0, len0) {
  // ...
}

Note: Remember this is generated, unoptimized code, and it may be neither elegant nor concise! ! Create a new distribution in Rust through LTO (Link Time Optimization) and then through the JS packaging tool flow (compression), it may be a little bit simplified.

Now you can learn how to use wasm-bindgen to generate greet functions. At the bottom it still calls the wasm greet function, but it is called with a pointer and length instead of a string.

For more details about passStringToWasm, visit Lin Clark's previous post. It contains all the templates, and for us this is something we need to write in addition to the wasm-bindgen tool! Then let's look at the __wbg_f_alert_alert_alert_n function.

Going deeper, the next thing we are interested in is the greet function in WebAssmbly. To understand this, let's first look at the code that the Rust compiler can access. Note that JS wrappers like the ones generated above, you don't need to write the export symbol of greet. The #[wasm_bindgen] attribute will generate a shim, which will be translated for you, named as follows:

Code

pub fn greet(name: &str) {
  alert(&format!("Hello, {}!", name));
}

#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) {
  let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) }
  let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) };
  greet(arg0);
}

You can now see the original code, greet, which is the interesting function __wasm_bindgen_generated_greet inserted by the #[wasm_bindgen] property. This is an export function (specified with the #[export_name] and extern keywords), and the parameters are pointers/length pairs passed in by JS. In the function it converts this pointer/length into a &str (a string in Rust), and then passes it to the greet function we define.

From another perspective, the #[wasm_bindgen] attribute generates two wrappers: one is to convert the JS type to wasm in JavaScript, and the other is to receive the wasm type in Rust and convert it to the Rust type.

Now let's look at the last piece of wrappers, the alert function. The greet function in Rust uses the standard format! macro to create a new string and pass it to alert. Recall that when we declared the alert method, we declared it using #[wasm_bindgen]. Now let's take a look at what is exposed to rustc in this function:

Code

fn alert(s: &str) {
  #[wasm_import_module = "__wbindgen_placeholder__"]
  extern {
    fn __wbg_f_alert_alert_n(s_ptr: *const u8, s_len: usize);
  }
  unsafe {
    let s_ptr = s.as_ptr();
    let s_len = ();
    __wbg_f_alert_alert_n(s_ptr, s_len);
  }
}

This is not what we wrote, but we can see how it became like this. The alert function is actually a simplified wrapper, which takes Rust's &str and then converts it to a wasm type (number). It calls the more interesting function __wbg_f_alert_alert_alert_n that we have seen above, but the strange thing about it is the #[wasm_import_module] attribute.

All imported functions in WebAssembly have a module in which they exist, and since wasm-bindgen is built on top of the ES module, this will also be translated into ES module import!

Currently the __wbindgen_placeholder__ module does not actually exist, but it means that the import will be rewritten by the wasm-bindgen tool to import from the JS file we generated.

Finally, for the last part of doubt, we get the JS file we generated, which contains:

Code

export function __wbg_f_alert_alert_n(ptr0, len0) {
  let arg0 = getStringFromWasm(ptr0, len0);
  alert(arg0)
}

Wow! It turns out that there is quite a bit of hidden here, and we all have a relatively long chain of knowledge from the warnings in the browser in JS. Don't be afraid, though, the core of wasm-bindgen is that all these infrastructures are hidden! You just need to write Rust code in just a few random #[wasm_bindgen]. Then your JS can use Rust like another JS package or module.

What else can wasm-bindgen do

The wasm-bindgen project has great ambitions in this field, and we will not elaborate on it in detail here. An effective way to explore the functionality in wasm-bindgen is to explore the example directory, which covers the complete operation of the DOM node in Rust from what we saw earlier.

The advanced features of wasm-bindgen are as follows:

  • Introduce JS structures, functions, objects, etc. to call them in wasm. You can call JS methods in a structure or access properties, which gives people the feeling that Rust is "native", making people feel that Rust #[wasm_bindgen] annotations you have written can be connected.
  • Export Rust structures and functions to JS. Compared to working with numeric types only in JS, you can export a Rust structure and convert it into a class in JS. The structure can then be passed instead of just using shaping values. This example of smorgasboard allows you to understand the supported interoperability features.
  • Various other features such as importing from a global scope (like alert function), using a Result in Rust to get JS exceptions, and common methods in Rust programs to simulate storing JS values.

If you want to know more about the features, continue readingissue tracker。

3. What will wasm-bindgen do next?

Before we finish, I want to take a moment to describe the future vision of wasm-bindgen, because I think this is one of the most exciting aspects of today’s projects.

Not only Rust

From day 1, the wasm-bindgen CLI tool has been designed to be multi-language-supported. Although Rust is currently the only supported language, the tool can also be embedded in C or C++. The #[wasm_bindgen] property creates a custom part of the output (*.wasm) file that can be parsed by the wasm-bindgen tool and subsequently deleted.

This section describes which JS bindings to generate and what their interfaces are. There is no specific part about Rust in this description, so the C++ compiler plugin can easily create that part and handle it through the wasm-bindgen tool.

I find this aspect particularly exciting because I believe it makes tools like wasm-bindgen the standard practice for WebAssembly and JS integration. Hopefully all languages ​​compiled to WebAssembly will benefit and can be automatically recognized by bundler to avoid almost all configuration and build tools mentioned above.

Automatically bind JS ecosystem

The only downside to using #[wasm_bindgen] macro import function is that you have to write everything out and make sure there are no errors. This kind of automation technology that makes people feel monotonous (and easily mistaken) is already mature.

All web APIs are specified by WebIDL and are feasible in generate #[wasm_bindgen] annotations from WebIDL. This means that you don't need to define the alert function as before, but you just need to write the following:

Code

#[wasm_bindgen]
pub fn greet(s: &str) {
  webapi::alert(&format!("Hello, {}!", s));
}

In this example, WebIDL's description of web APIs can completely automatically generate a collection of webapis, ensuring no errors.

We can even take automation a step further, and the TypeScript organization has done complex work in this regard, refer to generate #[wasm_bindgen] from TypeScript as well. You can automatically bind any package with TypeScript on npm for free!

Faster performance than JS DOM operation

The last thing to say is also important for wasm-bindgen: super fast DOM operation – this is the ultimate goal of many JS frameworks. Nowadays, some intermediate tools are needed to call DOM functions, which are being moved from JavaScript implementation to C++ engine implementation. However, these tools are not necessary after WebAssembly arrives. WebAssembly has types.

From day one, wasm-bindgen code generation design has taken into account future host binding schemes. When this feature appears in WebAssembly, we can call the imported function directly without the wasm-bindgen intermediate tool.

In addition, it enables the JS engine to actively optimize WebAssembly's operations on DOM, making it better support for types, and no longer requires parameter verification when calling JS. At this point, wasm-bindgen not only makes it easy to operate rich types like string, but also provides first-class DOM operational performance.

knock off

I myself found using WebAssembly to be extremely exciting, not just because of its community, but also because of its rapid progression. wasm-bindgen tools have a bright future. It makes interoperability between JS and programming languages ​​like Rust a top-notch experience, and it will also offer long-term benefits as WebAssembly continues to evolve.

Try giving wasm-bindgen a chance to create a problem due to functional requirements, or continue to participate in Rust and WebAssembly!

About Alex Crichton (Author)

Alex is a member of Rust's core team and has been working in Rust since the end of 2012. He is currently helping WebAssembly Rust Working Group make Rust + Wasm the best experience. Alex also helps maintain Cargo (Rust's package manager), the Rust standard library, and the Rust release and CI infrastructure.

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.