Rust And WebAssembly - JavaScript API
JavaScript provides a rich API to work with Objects
, Arrays
, Maps
, Sets
, and others. If we have to use or define them in the Rust then we need to provide the necessary bindings that are needed. Handcrafting the bindings will be a mundane process. But what if we have bindings to those APIs. The common API, that is present in both Node.js
and Browser
environment.
The rustwasm team's answer to that is the js-sys
crate.
The
js-sys
crate contains raw#[wasm_bindgen]
bindings to all the global APIs guaranteed to exist in every JavaScript environment by the ECMAScript standard. - RustWASM
The js-sys
crate provide bindings to the JavaScript's standard built-in objects, including their methods and properties.
Write some code ✍️
Create a default project with cargo new command.
$ cargo new --lib jsapi
Please copy over the package.json
, index.js
, and webpack-config.js
from the previous post.
Change the contents of Cargo.toml
:
[package]
name = "jsapi"
version = "0.1.0"
authors = ["Sendil Kumar <sendilkumarn@live.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.56"
js-sys = "0.3.33"
Now open the src/lib.rs
and replace the file with the following contents.
use wasm_bindgen::prelude::*;
use js_sys::Map;
#[wasm_bindgen]
pub fn new_js_map() -> Map {
Map::new()
}
We created a JavaScript Map inside Rust. This generates an empty map. We can access the map on the JavaScript side.
Create a Map and set their values and retrieve them back using the following code:
#[wasm_bindgen]
pub fn set_get_js_map() -> JsValue {
let map = Map::new();
map.set(&"foo".into(), &"bar".into());
map.get(&"foo".into())
}
Note that the return type here is
JsValue
. This is a wrapper in the Rust specifying the JavaScript values. Also, note that we are passing the string with into trait. This returns "bar" as the output when called in the JavaScript land.
We can run through the map using for each inside the Rust code like this:
#[wasm_bindgen]
pub fn run_through_map() -> f64 {
let map = Map::new();
map.set(&1.into(), &1.into());
map.set(&2.into(), &2.into());
map.set(&3.into(), &3.into());
map.set(&4.into(), &4.into());
map.set(&5.into(), &5.into());
let mut res: f64 = 0.0;
map.for_each(&mut |value, _| {
res = res + value.as_f64().unwrap();
});
res
}
This creates a map and then loads the map with values 1, 2, 3, 4, 5. Then runs over the created map and adds the value together. This produces an output of "15" (i.e., 1 + 2 + 3 + 4 + 5).
Lastly, we replace the index.js with the following contents.
import('./jsapi').then((module) => {
let m = module.new_js_map()
m.set('Hi', 'Hi')
console.log(m) // prints Map { "Hi" -> "Hi" }
console.log(module.set_get_js_map()) // prints "bar"
console.log(module.run_through_map()) // prints 15
})
What happens here?
Let us start with the generated JavaScript binding file. The generated binding files have almost the same structure as above, but with a few more functions exported.
The heap object is used as a stack here. All the JavaScript objects that are shared or referenced with the WebAssembly modules are stored in this heap. It is also important to note that once the value is accessed it is popped out from the heap.
The takeObject
function is used to fetch the object from the heap. It first gets the object at the given index. Then it removes the object from that heap index (i.e., pops it out). Finally, it returns the value.
function takeObject(idx) {
const ret = getObject(idx)
dropObject(idx)
return ret
}
Do you know RustWASM enables you to use webAPIs too, check out here
Similarly, we can use JavaScript APIs inside the Rust. The bindings are only generated for the common JavaScript API (including Node.js and the browser). Check out here for all the supported API here.
Check out more about JavaScript APIs here
Check out more about from and into here