Hello Wasm Js

Here's how to create sync and async function in Rust which return String, JsValue, JsError and call it from HTML, Javascript in test via headless browser.

Fun facts

  • 🚧 println! not working with wasm-bindgen-test context because --nocapture won't passthrough.

    💡 We have to use console_log! in wasm context.

  • 🚧 println!("{js_error:#?}")and console_log!("{js_error:#?}") not working because JsError didn't implement Debug.

    💡 We have to convert JsError to JsValue as workaround.

  • 🚧 #[tokio::test] will break wasm-bindgen-test.

    💡 We need #[cfg(not(target_arch = "wasm32"))] above Rust context when needed.

Structure

Cargo.toml

[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["katopz <[email protected]>"]
edition = "2018"

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

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.83"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }

# use for `async fn`.
wasm-bindgen-futures = { version = "0.4.33", default-features = false }
serde-wasm-bindgen = "0.6.1"

[dev-dependencies]
wasm-bindgen-test = "0.3.33"

# for conversion
serde-wasm-bindgen = "0.6.1"

[profile.release]
opt-level = "z"  # Optimize for size.
strip = true  # Automatically strip symbols from the binary.
lto = true # Enable Link Time Optimization (LTO)
codegen-units = 1 # Reduce Parallel Code Generation Units to Increase Optimization
panic = "abort" # Reduce panic code

utils.rs

#![allow(unused)]
fn main() {
pub fn set_panic_hook() {
    // When the `console_error_panic_hook` feature is enabled, we can call the
    // `set_panic_hook` function at least once during initialization, and then
    // we will get better error messages if our code ever panics.
    //
    // For more details see
    // https://github.com/rustwasm/console_error_panic_hook#readme
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}
}

lib.rs

#![allow(unused)]
fn main() {
mod utils;

use utils::set_panic_hook;
use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
pub fn greet(something: &str) -> String {
    // Hook when panic (optional)
    set_panic_hook();

    // Return String
    format!("Hello {something}")
}

#[wasm_bindgen]
pub async fn async_greet(something: &str) -> Result<String, JsError> {
    // Hook when panic (optional)
    set_panic_hook();

    // Return Result String
    Ok(format!("Hello {something}"))
}

#[wasm_bindgen]
pub async fn async_greet_js_value(something: &str) -> Result<JsValue, JsError> {
    // Hook when panic (optional)
    set_panic_hook();

    // Return Result String
    Ok(JsValue::from_str(format!("Hello {something}").as_str()))
}

#[wasm_bindgen]
pub fn greet_js_error() -> Result<JsValue, JsError> {
    let js_error = JsError::new("hello error!");

    // `wasm_bindgen::JsError` doesn't implement `std::fmt::Debug`
    // the trait `std::fmt::Debug` is not implemented for `wasm_bindgen::JsError`
    // 😱 uncomment below 👇 will get above error 👆
    // println!("{js_error:#?}");

    Err(js_error)
}
}

tests/web.rs

#![allow(unused)]
fn main() {
//! Test suite for the Web and headless browsers.

#![cfg(target_arch = "wasm32")]

extern crate wasm_bindgen_test;

use wasm_bindgen::*;
use wasm_bindgen_test::*;

// Running Tests in Headless Browsers
wasm_bindgen_test_configure!(run_in_browser);

// Import function to test.
use hello_wasm::*;
use serde_wasm_bindgen::from_value;

#[wasm_bindgen_test]
fn test_greet() {
    // Call greet and get an output.
    let output_string = greet("world");

    // Validate.
    assert_eq!("Hello world".to_string(), output_string);
}

#[wasm_bindgen_test]
async fn test_async_greet() {
    // Call greet and get an output.
    let output_string = async_greet("world").await.ok().unwrap();
    console_log!("{output_string}");

    // Validate.
    assert_eq!("Hello world".to_string(), output_string);
}

#[wasm_bindgen_test]
async fn test_async_greet_js_value() {
    // Call greet and get an output.
    let output_value = async_greet_js_value("world").await.ok().unwrap();

    // Validate.
    let output_string = from_value::<String>(output_value).ok().unwrap();
    assert_eq!("Hello world".to_string(), output_string);
}

#[wasm_bindgen_test]
async fn test_async_greet_js_error() {
    // Call greet and get an error.
    let js_error = greet_js_error();

    // `wasm_bindgen::JsError` cannot be formatted using `{:?}` because it doesn't implement `Debug`
    // 😱 uncomment below 👇 will get above error 👆
    // console_log!("{js_error:?}");

    // Convert JsError to JsValue.
    let js_value = JsValue::from(js_error.err());

    // And now we can Debug.
    console_log!("{js_value:?}");

    // Validate.
    assert!(format!("{js_value:?}").contains("hello error!"));
}
}

tests/index.html

<script type="module">
  import init, { greet, async_greet, async_greet_js_value } from './hello_wasm.js'

  init().then(async () => {
    const p1 = document.createElement('p')
    p1.innerText = greet('world')
    document.body.appendChild(p1)

    let text = await async_greet('world')
    const p2 = document.createElement('p')
    p2.innerText = text
    document.body.appendChild(p2)

    let text_js_value = await async_greet_js_value('world')
    const p3 = document.createElement('p')
    p3.innerText = text_js_value
    document.body.appendChild(p3)
  })
</script>

To test

wasm-pack test --headless --firefox
cp ./tests/index.html ./pkg
npx live-server ./pkg