Access JavaScript API with Rust
Sendil Kumar
Posted on January 4, 2020
JavaScript is everywhere. It enabled billions of people to develop and deliver projects. JavaScript is easy to get started. It is for a reason, JavaScript is one of the highly used programming language.
It is true that there are weird corners in the language. But believe me every programming language out there has those corners. Check this talk here.
Javascript is a dynamically typed language. It is one of its main advantage as well as disadvantage. Dynamic typing makes JavaScript API easy to write and understand. That is contextually simpler for people. But during compilation, the compiler has to do the hard work. This often leads to runtime exception and unpredictable performance.
Being a widely used language, JavaScript comes with a lot of bells and whistles. These features provides an elegant API (not talking about smooshmap). JavaScript provides a rich API to work with Objects
, Arrays
, Maps
, Sets
, and others.
WebAssembly provides strict typing and predictable performance. The performance is much faster than compared with JavaScript. Refer ๐
Increase Rust and WebAssembly performance ๐๐๐
Sendil Kumar ใป Jul 2 '19 ใป 6 min read
But WebAssembly is not always faster, there are scenarios where JavaScript is faster than the WebAssembly module. For example, to access a DOM JavaScript is much faster than compared with WebAssembly module. The boundary crossing has an impact. During those times it is great to use JavaScript to have higher performance.
Check out my book on Rust and WebAssembly here
JavaScript and WebAssembly needs to work closely in an application. The JavaScript engine needs to provide seamless integration between the JavaScript and WebAssembly. Check out here about how Firefox made calls between JavaScript and WebAssembly faster.
For a seamless integration between JavaScript and WebAssembly, it is important that both should understand each other. The JavaScript should provide the necessary context to enable languages like Rust too interoperate. But it is a tedious process to write the necessary bindings between JavaScript and Rust. Handcrafting the bindings is a mundane process.
But what if we have bindings to those APIs, 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"
We added js-sys = "0.3.33"
as a dependency. 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 imported the wasm_bindgen
library. Then imported map
from the js_sys
crate. The js_sys
crate provides all the necessary JavaScript API information.
In the function new_js_map
we return a Map of type js_sys::Map
. The js_sys crate is responsible for defining the type information for Rust and providing all the APIs. To create a new Map we simply call Map::new()
.
That is it, we created a JavaScript Map inside Rust. Now we can access this map inside the Rust and pass it to the JavaScript world as a JavaScript Map.
Note that In Rust, return keyword is optional. The last line without a semicolon is considered a return statement.
Then we create a function to create a Map, set values into the map and retrieve it.
#[wasm_bindgen]
pub fn set_get_js_map() -> JsValue {
let map = Map::new();
map.set(&"foo".into(), &"bar".into());
map.get(&"foo".into())
}
We created a function set_get_js_map
, it is annotated with #[wasm_bindgen]
. It returns JSValue
. This is a wrapper used by Rust for specifying the JavaScript values. The JSValue
type is defined in the js_sys
crate.
JsValue is a representation of an object owned by JS. A JsValue doesn't actually live in Rust right now but actually in a table owned by the
wasm-bindgen
generated JS glue code. Eventually the ownership will transfer into wasm directly and this will likely become more efficient, but for now it may be slightly slow.
We are creating a new map using the Rust syntax. We set the value into the map. Instead of simply accepting String type, the map.set
or map.get
accepts a pointer to the JsValue
. Rust provides value to value converter into
function, that converts the value from Rust's str type into the JsValue type.
Finally we are getting the value from the map using map.get
function call. This returns "bar" as the output as a JavaScript Value (JsValue
).
We can run through the map using foreach
inside the Rust code like below:
#[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
});
To run the above code, first compile the Rust into WebAssembly module by using:
cargo build --target="wasm32-unknown-unknown"
Then run
wasm-bindgen target/wasm32-unknown-unknown/debug/jsapi.wasm --out-dir .
to generate the JavaScript bindings for the WebAssembly module.
Finally install the dependencies using npm install
and run npm run serve
. Now spin up the browser and open the developer console to see the expected results.
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;
}
If you have enjoyed the post, then you might like my book on Rust and WebAssembly. Check them out here
Do you know RustWASM enables you to use webAPIs too, check out
Create Dev's offline page with Rust and WebAssembly ๐ฆ๐กโจ
Sendil Kumar ใป Jul 3 '19 ใป 5 min read
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
You can follow me on Twitter.
If you like this article, please leave a like or a comment. โค๏ธ
Picture Courtesy: JS Gif - https://www.jstips.co/en/about/
Posted on January 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.