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.


repo


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. The wasm-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 a Struct. This is similar to what we have done on the previous example.

  • The struct's members are JSValue instead of Number (x and y). 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.


Repo


🐦 Twitter // 💻 GitHub // ✍️ Blog

If you like this article, please leave a like or a comment. ❤️