We have already seen how quickly WebAssembly compiles , speeds up the js library and generates more compact binaries . We even have a general idea of how to establish interaction not only between the Rust and JavaScript communities, but also with communities of other languages. In the last article we mentioned the special tool wasm-bindgen and now I would like to dwell on it in more detail.
Currently, the WebAssembly specification describes only four data types: two integer and two floating point. However, most of the time JS and Rust developers use a much richer type system. For example, JS developers interact with the document object to add or modify HTML nodes, while Rust developers work with types such as Result for error handling, and almost all developers work with strings.
To be limited only to the types that WebAssembly defines would be too inconvenient, and here wasm-bindgen comes to the rescue. The main task of wasm-bindgen is to provide a bridge between systems of types Rust and JS. It allows the JS functions to call the Rust API by passing ordinary strings or the Rust functions to catch the exception from the JS. wasm-bindgen compensates for type mismatches and enables efficient and simple use of WebAssembly functions from JavaScript and back.
You can find a more detailed description of the wasm-bindgen project on our README . First, let's take a simple example of how to use wasm-bindgen, and then see how you can still use it.
Eternal classics. One of the best ways to try a new tool is to study its variation of the message “Hello world.” In this case, we’ll look at an example that does exactly that — it displays a dialog box that says "Hello World".
The goal here is simple, we want to create a Rust function that, when given a name, displays a dialog box that says Hello, ${name}!
. In JavaScript, we would describe it like this:
export function greet(name) { alert(`Hello, ${name}!`); }
However, we want to write this function on Rust. In order for this to work, we will need the following steps.
First, create a new Rust project:
cargo new wasm-greet --lib
This command will create a folder wasm-greet, in which we will work with you. The next step is to add the following information to our Cargo.toml
(analog package.json
for Rust):
[lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2"
We will skip the contents of the lib section for now, and in the dependencies
section we indicate the dependence of our project on the wasm-bindgen package . This package includes everything you need to use wasm-bindgen in our project.
And now let's add some code! Replace the contents of src/lib.rs
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, the example above may seem a bit verbose, but do not worry. The project wasm-bindgen is constantly being improved and I am sure that the need for such a detailed description will be eliminated in the future. The most important part here is the #[wasm_bindgen]
attribute. This is an abstract in Rust, which says that this function should be wrapped into another function if necessary. Both of our functions (both importing the alert
function and exporting the greet
function) have this attribute. A little later, we look under the hood and see what happens there.
But first, let's compile our wasm code and open it in a browser:
$ rustup target add wasm32-unknown-unknown --toolchain nightly # $ cargo +nightly build --target wasm32-unknown-unknown
Upon completion, we will get a wasm file that will be located target/wasm32-unknown-unknown/debug/wasm_greet.wasm
. If we use something like wasm2wat and look inside this file, its contents may seem a bit intimidating. It turns out that the wasm file is not yet ready for use from JS. For this we need one more step:
$ cargo install wasm-bindgen-cli # $ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir .
Just at this step all the magic happens. The wasm-bindgen command processes the wasm file and makes it ready for use. A little later, we will look at what it means to be “ready for use”, but for now it suffices to say that if we import the newly created wasm_greet.js
module, then there will be a greet
function, which is declared in Rust.
Now we can use the packer and create an HTML page on which our code will be executed. At the time of this writing, only Webpack 4.0 has enough WebAssembly support to work out of the box (however, at the moment there is a problem with the Chrome browser). Of course, over time, more and more packers will add support for WebAssembly. I will not go into details. You can see a sample configuration for webpack in the repository . If we look at the contents of our JS file, we will see the following:
const rust = import("./wasm_greet"); rust.then(m => m.greet("World!"));
… and that is all. Having opened our page in the browser we will see a dialog box with the words Hello, World!
which is created in Rust.
Whew, that was pretty big Hello, World!
. Let's look a little at what is going on under the hood and how this tool works.
One of the most important aspects of the wasm-bindgen is that the integration is based on the fundamental concept that the wasm module is just another type of ES module. In the example above, we just wanted to create an ES module with the following signature (TypeScript):
export function greet(s: string);
WebAssembly does not have the ability to do this (remember that currently wasm only supports numbers), so we use wasm-bindgen to fill in the blanks. In the last step of the last example, when we launched the wasm-bindgen
it created not only the wasm_greet.js
file, but also the wasm_greet_bg.wasm
. The first is our JS interface, which allows us to call the Rust code. And the *_bg.wasm
file contains the implementation and all the compiled code.
When we import the ./wasm_greet
module, we get the Rust code that we would like to call from JS, but at this stage we are not able to do it natively. Now that we have reviewed the integration process, let's look at the execution of this code.
const rust = import("./wasm_greet"); rust.then(m => m.greet("World!"));
Here we asynchronously import our interface, wait until it is ready (download and compile the wasm module), and call the greet
function.
Note that asynchronous loading is a Webpack requirement, but this may not always be possible and can be implemented differently in other packers.
If we look at the contents of the wasm_greet.js
file that was generated by wasm-bindgen
, we will see something like this:
import * as wasm from './wasm_greet_bg'; // ... export function greet(arg0) { const [ptr0, len0] = passStringToWasm(arg0); try { const ret = wasm.greet(ptr0, len0); return ret; } finally { wasm.__wbindgen_free(ptr0, len0); } } export function __wbg_f_alert_alert_n(ptr0, len0) { // ... }
Note. This code is not optimized and automatically generated and it is not always beautiful or small. In the process of optimization during linking, the release assembly in Rust and after passing through the minicator, it will be much smaller.
Here we see how wasm-bindgen generated the greet
function for us. Under the hood, it still calls the function greet
and wasm of the module, but now it is called not with a string, but with passing the pointer and the length as arguments. More information about the passStringToWasm
function can be found in the article from Lin Clark . If we had not used wasm-bindgen, we would have to write all this code on our own. We’ll go back to the __wbg_f_alert_alert_n
function a little later.
Having gone down on level below, we will find the following interesting point - the greet
function in WebAssembly. Let's take a look at the code that the Rust compiler sees. Note that, like the JS code that is generated above, you did not write with your hands the exported greet
symbol. wasm-bindgen generated everything you need on your own, namely:
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); }
Here we see our greet
function, as well as the function greet
, which is additionally generated using the #[wasm_bingen]
__wasm_bindgen_generated_greet
. This is the exported function (this is indicated by the #[export_name]
attribute and the extern
keyword), which accepts a pointer and a string length. It then converts this pair to & str (string to Rust) and passes it to our greet
function.
In other words, wasm-bindgen generates two wrappers: one in JavaScript, which converts types from JS to wasm and one to Rust, which accepts types of wasm and converts to Rust.
Well, let's look at the last set of wrappers for the alert
function. The greet
function in Rust uses the standard format macro ! to create a new line and then passes it to the alert
function. Remember, when we declared the alert
function, we used the #[wasm_bindgen]
attribute, now let's see what the Rust compiler sees:
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 = s.len(); __wbg_f_alert_alert_n(s_ptr, s_len); } }
This is not exactly what we wrote, but we can clearly see what is happening here. The alert
function is actually a thin wrapper that takes the string & str and then converts it into numbers that were understandable for wasm. Then the function __wbg_f_alert_alert_n
is __wbg_f_alert_alert_n
and there is a curious part - this is the #[wasm_import_module]
attribute #[wasm_import_module]
.
In order to import a function into WebAssembly, you need a module that contains it. And since wasm-bindgen is built on ES modules, the import of such a function from wasm will be interpreted as import from the ES module. The __wbindgen_placeholder__
module actually does not exist, this term indicates that this import should be processed by wasm-bindgen and a wrapper for JS generated.
And finally, we get our last piece of the puzzle - a generated JS file that contains:
export function __wbg_f_alert_alert_n(ptr0, len0) { let arg0 = getStringFromWasm(ptr0, len0); alert(arg0) }
As it turned out, a lot of things are happening under the hood and we have come a long way to call the JS function in the browser. But don't worry, the key aspect of the wasm-bindgen is that it’s all hidden. You can simply write Rust code with several attributes #[wasm_bindgen]
here and there. And then your JS code can use it as if it were another JavaScript module.
The project wasm-bindgen is very ambitious, covers a large area and at the moment I do not have enough time to describe everything. A good way to see it in action is to familiarize yourself with our examples , from simple Hello World !, to manipulating the DOM nodes of a tree from Rust.
In general, the main features of the wasm-bindgen are:
#[wasm_bindgen]
attributes are #[wasm_bindgen]
alert
), to intercept exceptions from JS, using the Result data type in Rust and the generalized way of simulating the preservation of values from JS in a Rust program.If you are interested in learning about additional features, follow our tracker .
Before completion, I would like to tell you a little about the future of the wasm-bindgen project as this is one of the most exciting topics.
From the very first day, the wasm-bindgen was designed with an eye to the fact that it can be used from many languages. While Rust is the only supported language so far, the tool will allow you to add C / C ++ in the future as well. The #[wasm_bindgen]
creates an additional section in the .wasm
file, which the parsit will then remove and wasm-bindgen
. This section describes which bindings need to be generated in JS and their interface. There is nothing Rust-specific in this section, so a plug-in with C / C ++ compiler can also create it, so that it will be possible to use wasm-bindgen
.
For me, this is the most exciting moment because I believe that this is what will allow tools like wasm-bindgen
to become the standard for interoperating WebAssembly and JS. I hope that the ability to do without unnecessary configuration code will be an advantage for all languages that can be compiled into WebAssembly.
At the moment, one of the drawbacks when importing JS functions with #[wasm_bindgen]
is that you need to describe all the functions yourself and make sure that no errors occur. At times, this process can be quite tedious (and be a source of error) and it requires automation.
All Web APIs are specified and described in WebIDL and it should be quite possible to generate all the bindings automatically from WebIDL . This means that you will not need to define the alert
function as we did in the example above, instead you could write something like this:
#[wasm_bindgen] pub fn greet(s: &str) { webapi::alert(&format!("Hello, {}!", s)); }
In this case, the webapi
package could be automatically generated from the descriptions of the WebIDL API and this would guarantee no errors.
We can develop this idea even further and use the impressive work of the TypeScript community and generate binders from TypeScript as well . This will automatically use any package with npm that has TypeScript support.
And last but not least, on the horizon of wasm-bindgen, super fast manipulations with DOM are the holy grail of many JavaScript frameworks. Today, all function calls for working with DOM go through costly transformations when moving from JavaScript to C ++ engines. With WebAssembly, these conversions may become optional. It is known that the WebAssembly type system ... is!
From the very first day, the generation of the wasm-bindgen
code has wasm-bindgen
designed with the aim of supporting hostings to the host . As soon as this function appears in WebAssembly, we will be able to directly use the imported functions without the wrappers that wasm-bindgen generates. Moreover, it will allow JS engines to aggressively optimize manipulations with DOM from WebAssembly, since all interfaces will be strongly typed and there will no longer be any need to validate them. And in this case, wasm-bindgen will not only make it easier to work with different types of data, but also provide the best of its kind performance when working with DOM.
I find working with WebAssembly incredibly interesting, not only because of the community, but also because of how fast it is evolving. The project wasm-bindgen has a bright future. It not only provides simple interoperability between JS and Rust, but also in the long term will open up new opportunities as WebAssembly develops.
Try wasm-bindgen , create a request for a new function , and stay in touch with Rust and WebAssembly.
Source: https://habr.com/ru/post/353230/
All Articles