DEV Community

Sendil Kumar
Sendil Kumar

Posted on • Edited on • Originally published at sendilkumarn.com

Sharing Classes between Rust and JavaScript

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.


Check out my book on Rust and WebAssembly here


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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
     }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
 }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

The wasm-bindgen makes it easy and simple to convert a class into WebAssembly module.


👇 Repo 👇

GitHub logo sendilkumarn / rustwasm-rust-to-js

Sharing classes between Rust and JavaScript


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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, replace the index.js with the following:

import("./class_from_js_world").then(module => {
    console.log(module.get_precious_point());
});
Enter fullscreen mode Exit fullscreen mode

To generate the WebAssembly module using Cargo:

$ cargo build --target=wasm32-unknown-unknown
Enter fullscreen mode Exit fullscreen mode

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_from_js_world.wasm --out-dir .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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())
 }
}
Enter fullscreen mode Exit fullscreen mode

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.

If you have enjoyed the post, then you might like my book on Rust and WebAssembly. Check them out here



👇 Repo 👇

GitHub logo sendilkumarn / rustwasm-js-to-rust

Sharing classes between JavaScript and Rust


In the next post we will see how to access JavaScript APIs in the Rust.


You can follow me on Twitter.

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


Top comments (5)

Collapse
 
peshoicov profile image
peshoicov

Thanks for the tutorial :)
However, for the second example you missed the two steps with the building process - cargo build ..... and wasm-bindgen ........
(Also - on wasm-bindgen command it complained with my package.json I copied form previous example. So I created a new package.json with only "dependencies" in it, ran the wasm-bindgen command and after that I copied the actual package.json from the previous example. That's how it worked.)
...
But what was most frustrating to me was that I got errors when I tried to compile - it didn't want to load the point.js file. It started working for me when I modified it like this:
#[wasm_bindgen(module = "/point.js")]
Absolute path & adding extension.
(Probably it's because of the wasm-bindgen version - 0.2.58 on my machine.
... I don't know.)
Anyway, thanks for the tutorial once again ... it's very useful ;)

Collapse
 
sendilkumarn profile image
Sendil Kumar

Yeah I think there is a bug in the wasm-bindgen. Currently this should be manually amended in the generated JS file.

Collapse
 
siragi profile image
siragi • Edited

same here:
relative module paths aren't supported yet
this worked:

#[wasm_bindgen(module = "point.js")]

Collapse
 
alexm77 profile image
Alex Mateescu

Finally someone who doesn't just post the add(a, b) example and concludes WASM is easy. Thanks a lot for this three part post.

Collapse
 
sendilkumarn profile image
Sendil Kumar

Thanks Will definitely add some more :)