32 releases

0.5.7 Jan 2, 2023
0.5.4 Dec 28, 2022
0.4.3 Jul 17, 2022
0.3.0 May 13, 2021
0.0.2 Nov 21, 2014

#25 in WebAssembly

Download history 15/week @ 2024-01-04 34/week @ 2024-01-11 35/week @ 2024-01-18 17/week @ 2024-01-25 9/week @ 2024-02-01 18/week @ 2024-02-08 45/week @ 2024-02-15 80/week @ 2024-02-22 44/week @ 2024-02-29 60/week @ 2024-03-07 42/week @ 2024-03-14 56/week @ 2024-03-21 74/week @ 2024-03-28 55/week @ 2024-04-04 61/week @ 2024-04-11 51/week @ 2024-04-18

244 downloads per month
Used in 12 crates (10 directly)

MIT/Apache

21KB
282 lines

js-wasm

docs.rs docs

JavaScript and WebAssembly should be a joy to use together.

This project aims to provide a simple, easy to learn, technology-agnostic way bridge the Rust and Javascript using an extremely minimal setup with out-of-box cargo compilation tools. My hope is almost any Rust developer familiar with JavaScript could learn how to use it in a lazy afternoon.

Hello World

Let's just look at a basic example of how to put things in the console:

cargo new helloworld --lib
cd helloworld
cargo add js
vim src/lib.rs
use js::*;

#[no_mangle]
pub fn main() {
    js!("function(str){
        console.log(str)
    }")
    .invoke(&["Hello, World!".into()]);
}

Notice the basic syntax is building up a function, and then invoking it with an array of parameters. Underneath the covers, this is an array of enums called InvokeParameter, i've made little converters for various types (see below) to help the data cross the barrier. For the most part you can convert data using .into() for InvokeParameter.

vim index.html
<html>
    <head>
        <meta charset="utf-8">
        <script src="https://unpkg.com/js-wasm/js-wasm.js"></script>
        <script type="application/wasm" src="helloworld.wasm"></script>
    </head>
    <body>
        Open my console.
    </body>
</html>

This library has a fairly simple mechanism for executing your WebAssembly during page load.

vim Cargo.toml
# add these lines for WebAssembly to end of Cargo.toml

[lib]
crate-type =["cdylib"]

[profile.release]
lto = true
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/helloworld.wasm .
python3 -m http.server

# open http://localhost:8000 in browser
# right click, inspect, look at message in console

Full example is here.

How it works?

The js crate makes it really easy to instantiate a javascript function and pass it parameters. Right now this crate supports these types as parameters:

  • Undefined,
  • Float64
  • BigInt
  • String
  • Javascript Object References
  • Float32Array
  • Float64Array
  • Boolean

Below are several examples that show common operations one might want to do.

Interacting with DOM objects

Here's a more complex example that invokes functions that return references to DOM objects

Screen Shot 2022-12-18 at 9 21 54 PM
use js::*;

fn query_selector(selector: &str) -> ExternRef {
    let query_selector = js!(r#"
        function(selector){
            return document.querySelector(selector);
        }"#);
    query_selector.invoke_and_return_object(&[selector.into()])
}

fn canvas_get_context(canvas: &ExternRef) -> ExternRef {
    let get_context = js!(r#"
        function(canvas){
            return canvas.getContext("2d");
        }"#);
    get_context.invoke_and_return_object(&[canvas.into()])
}

fn canvas_set_fill_style(ctx: &ExternRef, color: &str) {
    let set_fill_style = js!(r#"
        function(ctx, color){
            ctx.fillStyle = color;
        }"#);
    set_fill_style.invoke(&[ctx.into(), color.into()]);
}

fn canvas_fill_rect(ctx: &ExternRef, x: f64, y: f64, width: f64, height: f64) {
    let fill_rect = js!(r#"
        function(ctx, x, y, width, height){
            ctx.fillRect(x, y, width, height);
        }"#);
    fill_rect.invoke(&[ctx.into(), x.into(), y.into(), width.into(), height.into()]);
}

#[no_mangle]
pub fn main() {
    let screen = query_selector("#screen");
    let ctx = canvas_get_context(&screen);
    canvas_set_fill_style(&ctx, "red");
    canvas_fill_rect(&ctx, 10.0, 10.0, 100.0, 100.0);
    canvas_set_fill_style(&ctx, "green");
    canvas_fill_rect(&ctx, 20.0, 20.0, 100.0, 100.0);
    canvas_set_fill_style(&ctx, "blue");
    canvas_fill_rect(&ctx, 30.0, 30.0, 100.0, 100.0);
}

The invocation invoke_and_return_object returns a structure called an ExternRef that is an indirect reference to something received from JavaScript. You can pass around this reference to other JavaScript invocations that will receive the option. When the structure dropped according to Rust lifetimes, it's handle is released from the JavaScript side.

Callbacks and timers

This library is not opinionated about how to callback into Rust. There are several methods one can use. Here's a simple example.

use js::*;

fn console_log(s: &str) {
    let console_log = js!(r#"
        function(s){
            console.log(s);
        }"#);
    console_log.invoke(&[s.into()]);
}

fn random() -> f64 {
    let random = js!(r#"
        function(){
            return Math.random();
        }"#);
    random.invoke(&[])
}

#[no_mangle]
pub fn main() {
    let start_loop = js!(r#"
        function(){
            window.setInterval(()=>{
                this.module.instance.exports.run_loop();
            }, 1000)
        }"#);
    start_loop.invoke(&[]);
}

#[no_mangle]
pub fn run_loop(){
    console_log(&format!("{}", random()));
}

Notice how in the start_loop function, this actually references a context object that can be used to perform useful functions (see below) and for the importance of this demo, get ahold of the WebAssembly module so we can callback functions on it.

Getting data back into WebAssembly

Let's focus on one last example. A button that when you click it, fetches data from the public Pokemon API and put's it on the screen.

use js::*;

fn query_selector(selector: &str) -> ExternRef {
    let query_selector = js!(r#"
        function(selector){
            return document.querySelector(selector);
        }"#);
    query_selector.invoke_and_return_object(&[selector.into()])
}

fn add_click_listener(element: &ExternRef, callback: &str) {
    let add_click_listener = js!(r#"
        function(element, callback){
            element.addEventListener("click", ()=>{
                this.module.instance.exports[callback]();
            });
        }"#);
    add_click_listener.invoke(&[element.into(), callback.into()]);
}

fn element_set_inner_html(element: &ExternRef, html: &str) {
    let set_inner_html = js!(r#"
        function(element, html){
            element.innerHTML = html;
        }"#);
    set_inner_html.invoke(&[element.into(), html.into()]);
}

fn fetch(url: &str, callback: &str) {
    let fetch = js!(r#"
        function(url, callback){
            fetch(url).then((response)=>{
                return response.text();
            }).then((text)=>{
                const allocationId = this.writeUtf8ToMemory(text);
                this.module.instance.exports[callback](text);
            });
        }"#);
    fetch.invoke(&[url.into(), callback.into()]);
}

#[no_mangle]
pub fn main() {
    let button = query_selector("#fetch_button");
    add_click_listener(&button, "button_clicked");
}

#[no_mangle]
pub fn button_clicked() {
    // get pokemon data
    let url = "https://pokeapi.co/api/v2/pokemon/1/";
    fetch(url, "fetch_callback");
}

#[no_mangle]
pub fn fetch_callback(text_allocation_id: usize) {
    let text = extract_string_from_memory(text_allocation_id);
    let result = query_selector("#data_output");
    element_set_inner_html(&result, &text);
}

Notice in the fetch function handling, we have a function specifically for helping put strings inside of WebAssembly writeUtf8ToMemory. This returns back an ID that can be used to rebuild the string on WebAssembly side extract_string_from_memory.

The web crate

If you don't feel like recreating the wheel, there's an ongoing collection of commonly used functions accumulationg in web.

use web::*;

#[no_mangle]
fn main() {
    console_log("Hello world!");
    let body = query_selector("body");
    element_add_click_listener(&body, |e| {
        console_log(format!("Clicked at {}, {}", e.offset_x, e.offset_y).as_str());
    });
    element_add_mouse_move_listener(&body, |e| {
        console_log(format!("Mouse moved to {}, {}", e.offset_x, e.offset_y).as_str());
    });
    element_add_mouse_down_listener(&body, |e| {
        console_log(format!("Mouse down at {}, {}", e.offset_x, e.offset_y).as_str());
    });
    element_add_mouse_up_listener(&body, |e| {
        console_log(format!("Mouse up at {}, {}", e.offset_x, e.offset_y).as_str());
    });
    element_add_key_down_listener(&body, |e| {
        console_log(format!("Key down: {}", e.key_code).as_str());
    });
    element_add_key_up_listener(&body, |e| {
        console_log(format!("Key up: {}", e.key_code).as_str());
    });
}

Check out the documentation here

License

This project is licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in js-wasm by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~170KB