Rust And WebAssembly For the masses - Sharing Classes
Classes in JavaScript
The ECMAScript 6 (ES6) introduced classes into the JavaScript. But classes are just a syntactic sugar over the prototype-based inheritance in JavaScript.
It is very important to understand, the JavaScript's inheritance model is not object-oriented.
To declare a class we use the class
keyword. Similar to functions, classes can be either declared alone or expressed as an expression (assigning this to a variable).
For non-JavaScript developers, prototypical inheritance is important to understand.
Prototypical inheritance - If an object A can point to another object B, then object A is the prototype of object B. Thus object B has the qualities of both A and B. Whenever we look up for a property in object B, we will have to look for the property in object A, if it is not present in B.
So in JavaScript (almost) everything is an Object
instance. These objects sit on top of a prototype chain. The prototypal model is more powerful. In JavaScript objects are dynamic bags of properties.
The wasm-bindgen provides an option to share the classes between JavaScript and WebAssembly. That is, we define a class in Rust and use the same class in the JavaScript.
The wasm-bindgen uses annotations.
Let us see how easy it is to share classes between JavaScript and WebAssembly (Rust).
Class from Rust to JavaScript
Write some code ✍️
Create a new project.
$ cargo new --lib class_world
Created library `class_world` package
Define the wasm-bindgen
dependency for the project, Open the cargo.toml file and add the content.
[package]
name = "class_world"
version = "0.1.0"
authors = ["Sendil Kumar <sendilkumarn@live.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.56"
Open the src/lib.rs
file and replace the contents with the following.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Point {
x: i32,
y: i32,
}
#[wasm_bindgen]
impl Point {
pub fn new(x: i32, y: i32) -> Point {
Point { x: x, y: y}
}
pub fn get_x(&self) -> i32 {
self.x
}
pub fn get_y(&self) -> i32 {
self.y
}
pub fn set_x(&mut self, x: i32) {
self.x = x;
}
pub fn set_y(&mut self, y:i32) {
self.y = y;
}
pub fn add(&mut self, p: Point) {
self.x = self.x + p.x;
self.y = self.y + p.y;
}
}
We described the class with a Struct in Rust.
The Point
Struct is implemented with getters, setters and an add function that takes in x and y coordinate and return the sum.
This is a normal Rust code with only the #[wasm_bindgen]
annotation added.
Note that both the definition of the struct and the all the implementation methods are made public explicitly using
pub
keyword.
To generate the WebAssembly module using Cargo:
$ cargo build --target=wasm32-unknown-unknown
Use the wasm-bindgen CLI
to generate the binding file for the WebAssembly module generated. If you do not have wasm-bindgen available then check out this post on how to install them.
$ wasm-bindgen target/wasm32-unknown-unknown/debug/class_world.wasm --out-dir .
This generates the binding JavaScript file, the type definition files, and the WebAssembly Module.
452B class_world.d.ts
2.0K class_world.js
456B class_world_bg.d.ts
41K class_world_bg.wasm
Inside the binding file
Let us look at the class_world.js
:
This is the binding file generated by the wasm-bindgen
. Similar to the previous case it consists of TextDecoder
, getStringFromWasm0
, getUint8Memory0
. Additionally it consists of a class Point
.
The type signature of Point
class is similar to what we have defined inside the Rust. The getters, setters and the add function. Additionally in every method we are asserting the type of the input is Point
. Since WebAssembly is strictly typed, we need to have this type check.
Additionally, the wasm-bindgen produces a static method __wrap
that creates the Point
class object and attaches a pointer to it.
It adds a free
method that in turn calls the __wbg_point_free
method inside the WebAssembly module. This method is responsible for freeing up the memory taken by the Point object or class.
Please copy over the package.json
, index.js
, and webpack-config.js
from the previous post. Then run npm install
. Modify the index.js
with the following content.
import('./class_world').then(({ Point }) => {
const p1 = Point.new(10, 10)
console.log(p1.get_x(), p1.get_y())
const p2 = Point.new(3, 3)
p1.add(p2)
console.log(p1.get_x(), p1.get_y())
})
We are importing the binding file, that will in turn import the WebAssembly module.
We call the new method in the Point class and pass it "x" and "y". We print the "x" and "y" co-ordinates. This prints 10, 10
. Similarly we create another Point (p2).
Finally, we call the add function and pass it Point p2. This prints 13, 13
.
The getter method uses the pointer and fetches the value from the shared array buffer.
get_x() {
var ret = wasm.point_get_x(this.ptr);
return ret;
}
In the setter method, we pass in the pointer and the value. Since we are just passing in a number here. There is no extra conversion needed.
/**
* @param {number} x
*/
set_x(x) {
wasm.point_set_x(this.ptr, x);
}
In case of add, we take the argument and get the pointer to the Point object and pass it to the WebAssembly module.
add(p) {
_assertClass(p, Point);
var ptr0 = p.ptr;
p.ptr = 0;
return wasm.point_add(this.ptr, ptr0);
}
The wasm-bindgen makes it easy and simple to convert a class into WebAssembly module.
Class from JavaScript to Rust
We have seen how to create a class in Rust and call it in the JavaScript world. Let us pass the class from JavaScript world to Rust.
In JavaScript, classes are objects with some methods. Rust is a strictly typed language. This means the Rust compiler needs to have concrete bindings. Without them, the compiler complains about it. We need a way to assure the compiler that during the runtime will have this API available.
The extern "C"
function block comes here to help. The extern "C"
makes a function name available in Rust. Read more about them here.
Write some code ✍️
Let us create a new project.
$ cargo new --lib class_from_js_world
Created library `class_from_js_world` package
Define the wasm-bindgen dependency for the project, Open the cargo.toml file and add the content.
[package]
name = "class_from_js_world"
version = "0.1.0"
authors = ["Sendil Kumar <sendilkumarn@live.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.56"
Please copy over the package.json
, index.js
, and webpack-config.js
from the previous example. Then run npm install
.
Open the src/lib.rs
file and replace the contents with the following.
use wasm_bindgen::prelude::*;
//1
#[wasm_bindgen(module = "./point")]
extern "C" {
//2
pub type Point;
//3
#[wasm_bindgen(constructor)]
fn new(x: i32, y: i32) -> Point;
//4
#[wasm_bindgen(method, getter)]
fn get_x(this: &Point) -> i32;
#[wasm_bindgen(method, getter)]
fn get_y(this: &Point) -> i32;
//5
#[wasm_bindgen(method, setter)]
fn set_x(this: &Point, x:i32) -> i32;
#[wasm_bindgen(method, setter)]
fn set_y(this: &Point, y:i32) -> i32;
// 6
#[wasm_bindgen(method)]
fn add(this: &Point, p: Point);
}
#[wasm_bindgen]
//7
pub fn get_precious_point() -> Point {
let p = Point::new(10, 10);
let p1 = Point::new(3, 3);
// 8
p.add(p1);
p
}
At //1, we are importing the JavaScript module. This #[wasm_bindgen]
annotation import a JavaScript file point.js
.
Note the WebAssembly module does not know where to look up for the file. By convention, this JavaScript file should be present along with
Cargo.toml
. Thewasm-bindgen-CLI
looks up in that location and links the file while generating the WebAssembly Module.
extern "C"
Then we create an extern "C"
block to define the methods that we need to use. The extern "C"
block gives the type assurances during the compilation phase.
We first declare a type signature in the block (pub type Point;
). We use this as any other type inside the Rust code.
Then we define the constructor. We pass in the constructor as an argument to the wasm_bindgen annotation. This simplifies and reduces the verbose declaration. The wasm-bindgen
will do all the necessary code generation for the constructor.
Then we define a function that takes in arguments and return with their type signatures.
These functions bind to the namespace of the Point type, and we can call Point::new(x, y);
inside the Rust function.
//4 and //5 is getters and setters respectively.
//6 is the add
method. The add
method has the #[wasm_bindgen(method)]
annotation.
//7 is where we export get_precious_point()
function using #[wasm_bindgen]
annotation.
Point class in JavaScript
Create Point.js
with the following contents:
export class Point {
constructor(x, y) {
this.x = x
this.y = y
}
get_x() {
return this.x
}
get_y() {
return this.y
}
set_x(x) {
this.x = x
}
set_y(y) {
this.y = y
}
add(p1) {
this.x += p1.x
this.y += p1.y
}
}
Finally, replace the index.js with the following:
import('./class_from_js_world').then((module) => {
console.log(module.get_precious_point())
})
Run this on the browser using npm run serve. The console will print the point object (class).
Cargo Expand
Let us see how the #[wasm_bindgen]
macro is expanding the code.
$ cargo expand --target=wasm32-unknown-unknown > expanded.rs
There are a few interesting things happening here.
-
The type
Point
is converted into aStruct
. This is similar to what we have done on the previous example. -
The struct's members are
JSValue
instead of Number (x
andy
). This is because wasm_bindgen will not know what this point class is instantiating. So it creates a JavaScript object and makes that as its member.
pub struct Point {
obj: wasm_bindgen::JsValue,
}
It also defines how to construct the Point object and how to dereference it. Useful for the WebAssembly runtime to know when to allocate and when to dereference it.
All the methods that are defined are converted into the implementation of the Point struct. As you can see there is a lot of unsafe code in the method declaration. This is because the Rust code interacts directly with the raw pointers.
fn new(x: i32, y: i32) -> Point {
#[link(wasm_import_module = "__wbindgen_placeholder__")]
extern "C" {
fn __wbg_new_3ffc5ccd013f4db7(x:<i32 as ::wasm_bindgen::convert::IntoWasmAbi>::Abi,
y:<i32 as ::wasm_bindgen::convert::IntoWasmAbi>::Abi)
-> <Point as ::wasm_bindgen::convert::FromWasmAbi>::Abi;
}
unsafe {
let _ret = {
let mut __stack = ::wasm_bindgen::convert::GlobalStack::new();
let x = <i32 as ::wasm_bindgen::convert::IntoWasmAbi>::into_abi(x, &mut __stack);
let y = <i32 as ::wasm_bindgen::convert::IntoWasmAbi>::into_abi(y, &mut __stack);
__wbg_new_3ffc5ccd013f4db7(x, y)
};
<Point as ::wasm_bindgen::convert::FromWasmAbi>::from_abi(_ret,
&mut ::wasm_bindgen::convert::GlobalStack::new())
}
}
As shown above, the code generated by #[wasm_bindgen(constructor)]
macro. It first links the code with extern "C" block. The arguments are then cast such that they are inferred in the WebAssembly runtime.
Then the unsafe block. First, space is reserved in the global stack. Then both "x" and "y" are converted into type "IntoWasmAbi".
The
IntoWasmAbi
is a trait for anything that can be converted into a type that can cross the wasm ABI directly, eg., u32 or f64.
Then the function in the JavaScript is called. The returned value is then cast into Point type using FromWasmAbi.
The
FromWasmAbi
is a trait for anything that can be recovered by-value from the WASM ABI boundary, eg., a Rust u8 can be recovered from the WASM ABI u32 type.
Check out more about IntoWasmAbi and FromWasmAbi here and here.
In the next post we will see how to access JavaScript APIs in the Rust.