This package contains a macro that eases the use of Rust-exported wasm-bindgen types in JS settings. Specifically, it generates boilerplate that upcasts from a duck-typed JS reference to a concrete Rust type implementing that interface. The main caveat is that it assumes that cloning is cheap on the struct in question since you're going to clone to take ownership of the type on the Rust side.
wasm-bindgen makes working with Wasm code in JS environments viable, but also comes with a few
sharp edges. Some of these include:
- Consuming Rust-exported types if passed by ownership.
- Not being able to use generics in Rust-exported types.
- No references to Rust-exported types in
extern "C"interfaces. - Disallowing passing references to Rust-exported types in
Vecs.
The most common workaround for these is to pass around JsValues or js_sys::Arrays plus
custom TypeScript strings (which are not checked against the actual types used),
and parse these general types manually rather than bindgen doing the glue for you,
rather than fiddling with unchecked_into or dyn_into yourself.
It would be convenient to have some way to use Rust types on the Rust side, and
have wasm-bindgen automatically generate reasonable types on the TS side.
Add both crates to your Cargo.toml:
[dependencies]
from_js_ref = "0.2"
wasm_refgen = "0.2"use std::{rc::Rc, cell::RefCell};
use wasm_bindgen::prelude::*;
use wasm_refgen::wasm_refgen;
#[derive(Clone)]
#[wasm_bindgen(js_name = "Foo")]
pub struct WasmFoo {
map: Rc<RefCell<HashMap<String, u8>>>, // Cheap to clone
id: u32 // Cheap to clone
}
#[wasm_refgen(js_ref = JsFoo)]
#[wasm_bindgen(js_class = "Foo")]
impl WasmFoo {
// ... your normal methods
}It is worth noting that the #[wasm_refgen(...)] line MUST be placed above #[wasm_bindgen(...)].
Simple use is straightforward:
// Rust
#[wasm_bindgen(js_name = doThing)]
pub fn do_thing(foo: &JsFoo) {
let wasm_foo: WasmFoo = foo.into();
// use `wasm_foo` as normal
}// JS
const foo = new Foo();
doThing(foo)wasm-bindgen only allows generics for types that are JsCast,
which JsFoo is thanks to the glue code generated by this macro.
This is so that it can clone the data safely from JS when going over
the boundary. The JS-representation of WasmFoo is a lightweight
object with a number "pointer" to Wasm memory, so cloning it at
this step is very cheap. Whether you pass a JsFoo by reference
or by value, the cost is the same due to how wasm-bindgen handles
(what it's treating as a) JS-imported type.
When you receive a JsValue and need to convert it to your Rust type,
use FromJsRef::try_from_js_value:
use from_js_ref::FromJsRef;
pub fn process(js_value: &JsValue) -> Result<(), MyError> {
let wasm_foo = WasmFoo::try_from_js_value(js_value)
.ok_or(MyError::UnexpectedType)?;
// use `wasm_foo` as normal
Ok(())
}This performs a duck-type check under the hood: it verifies the JS object
has the expected upcast method via Reflect::has, then converts through
the generated reference type.
Warning
Do not use dyn_into::<JsFoo>() or dyn_ref::<JsFoo>(). These rely on
instanceof, which does not work with wasm_refgen-generated types. The
instanceof check targets the Rust identifier name (e.g., JsFoo) rather
than the JS class name (Foo), so it always fails at runtime. Use
FromJsRef::try_from_js_value instead.
pub fn do_many_things(js_foos: Vec<JsFoo>) {
let rust_foos: Vec<WasmFoo> = js_foos.iter().map(Into::into).collect();
// ...
}This provides both an ergonomic way to get typed Vecs on the Rust side,
but also generates Array<Foo> as the TypeScript type.
This strategy gains a small amount of runtime safety by renaming
.clone to a special method that uses your struct's name. The duck-typed
interface only works if the JS object actually implements this
uniquely named method produced by the glue code. This is not as "safe"
as static type checking, but provides a lightweight way to ensure that
the correct kind of object is passed over the boundary without relying
on direct reflection.
The try_from_js_value method leverages this same mechanism: it checks
for the presence of the upcast method via Reflect::has before attempting
the conversion. If the method is missing, it returns None.
You do not need to understand how this works under the hood to use the macro, but here's a diagram of how the pieces fit together:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ JS Foo instance โ
โ Class: Foo โ
โ Object { wbg_ptr: 12345 } โ
โ โ
โโโฌโโโโโโโโโโโโโโโโโโโโโโโฌโโโ
โ โ
โ โ
Implements โ
โ โ
โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ โ
โ โ โ
โ TS Interface: Foo โ Pointer
โ only method: โ โ
โ __wasm_refgen_to_Foo โ โ
โ โ โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ โ
JS/TS โ โ
โ โ โ โ โ โ โ โ โ โ โโโ โ โ โ โ โ โ โ โ โ โ โผ โ โ โ โ โ
Wasm โ โ
โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโ
โ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ โ โ โ โ
โ โ &JsFoo โโโโโโโโโโถ WasmFoo โ โ
โ โ Opaque Wrapper โ โ Instance #1 โ โ
โ โ โ โ โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ
Into::into
(uses `__wasm_refgen_to_Foo`)
(which is a wrapper for `clone`)
โ
โ
โผ
โโโโโโโโโโโโโโโโโโ
โ โ
โ WasmFoo โ
โ Instance #2 โ
โ โ
โโโโโโโโโโโโโโโโโโ
When converting from a raw JsValue, the flow is:
JsValue
โ
โผ
Reflect::has(value, "__wasm_refgen_toWasmFoo")
โ
โโโ false โ None
โ
โโโ true
โ
โผ
unchecked_into::<JsFoo>()
โ
โผ
from_js_ref(&JsFoo)
โ
โผ
calls .__wasm_refgen_to_wasm_foo()
โ
โผ
Some(WasmFoo)