While working on the Wasm bindings for Subduction, I was dissatisfied with how brittle custom_typescript sections are.1 I doubt that I’m the first one to come up with this workaround hack, but I found myself using the same pattern in many places. Since the pattern depended on consistent internal naming, I extracted it out into a small crate.

Motivation

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.

Example

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.

Collections

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.

Some Lightweight Runtime Safety

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 will 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.

Under The Hood

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   β”‚
                       β”‚                β”‚
                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Footnotes

  1. at least at time of writing ↩