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 withwasm-bindgen-test
context because--nocapture
won't passthrough.💡 We have to use
console_log!
inwasm
context. -
🚧
println!("{js_error:#?}")
andconsole_log!("{js_error:#?}")
not working becauseJsError
didn't implementDebug
.💡 We have to convert
JsError
toJsValue
as workaround. -
🚧
#[tokio::test]
will breakwasm-bindgen-test
.💡 We need
#[cfg(not(target_arch = "wasm32"))]
aboveRust
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