JS Objects in Rust - The wasm-bindgen Guide (original) (raw)

  1. Introduction
  2. 1. Examples
    1. 1.1. Hello, World!
    2. 1.2. Using console.log
    3. 1.3. Small Wasm files
    4. 1.4. Without a Bundler
    5. 1.5. Synchronous Instantiation
    6. 1.6. Importing functions from JS
    7. 1.7. Working with char
    8. 1.8. js-sys: WebAssembly in WebAssembly
    9. 1.9. web-sys: DOM hello world
    10. 1.10. web-sys: Closures
    11. 1.11. web-sys: performance.now
    12. 1.12. web-sys: using fetch
    13. 1.13. web-sys: Weather report
    14. 1.14. web-sys: canvas hello world
    15. 1.15. web-sys: canvas Julia set
    16. 1.16. web-sys: WebAudio
    17. 1.17. web-sys: WebGL
    18. 1.18. web-sys: WebSockets
    19. 1.19. web-sys: WebRTC DataChannel
    20. 1.20. web-sys: requestAnimationFrame
    21. 1.21. web-sys: A Simple Paint Program
    22. 1.22. web-sys: Wasm in Web Worker
    23. 1.23. Parallel Raytracing
    24. 1.24. Wasm Audio Worklet
    25. 1.25. web-sys: A TODO MVC App
  3. 2. Reference
    1. 2.1. Deployment
    2. 2.2. JS snippets
    3. 2.3. Static JS Objects
    4. 2.4. Passing Rust Closures to JS
    5. 2.5. Receiving JS Closures in Rust
    6. 2.6. Promises and Futures
    7. 2.7. Iterating over JS Values
    8. 2.8. Arbitrary Data with Serde
    9. 2.9. Accessing Properties of Untyped JS Values
    10. 2.10. Working with Duck-Typed Interfaces
    11. 2.11. Command Line Interface
    12. 2.12. Optimizing for Size
    13. 2.13. Supported Rust Targets
    14. 2.14. Supported Browsers
    15. 2.15. Support for Weak References
    16. 2.16. Support for Reference Types
    17. 2.17. Supported Types
      1. 2.17.1. Imported JavaScript Types
      2. 2.17.2. Exported Rust Types
      3. 2.17.3. JsValue
      4. 2.17.4. Box<[T]> and Vec
      5. 2.17.5. *const T and *mut T
      6. 2.17.6. NonNull
      7. 2.17.7. Numbers
      8. 2.17.8. bool
      9. 2.17.9. char
      10. 2.17.10. str
      11. 2.17.11. String
      12. 2.17.12. Number Slices
      13. 2.17.13. Boxed Number Slices
      14. 2.17.14. Result<T, E>
    18. 2.18. #[wasm_bindgen] Attributes
      1. 2.18.1. On JavaScript Imports
        1. 2.18.1.1. catch
          1. 2.18.1.2. constructor
          2. 2.18.1.3. extends
          3. 2.18.1.4. getter and setter
          4. 2.18.1.5. final
          5. 2.18.1.6. indexing_getter, indexing_setter, and indexing_deleter
          6. 2.18.1.7. js_class = "Blah"
          7. 2.18.1.8. js_name
          8. 2.18.1.9. js_namespace
          9. 2.18.1.10. method
          10. 2.18.1.11. module = "blah"
          11. 2.18.1.12. raw_module = "blah"
          12. 2.18.1.13. no_deref
          13. 2.18.1.14. static_method_of = Blah
          14. 2.18.1.15. structural
          15. 2.18.1.16. typescript_type
          16. 2.18.1.17. variadic
          17. 2.18.1.18. vendor_prefix
      2. 2.18.2. On Rust Exports
        1. 2.18.2.1. constructor
          1. 2.18.2.2. js_name = Blah
          2. 2.18.2.3. js_class = Blah
          3. 2.18.2.4. readonly
          4. 2.18.2.5. skip
          5. 2.18.2.6. skip_jsdoc
          6. 2.18.2.7. start
          7. 2.18.2.8. main
          8. 2.18.2.9. typescript_custom_section
          9. 2.18.2.10. getter and setter
          10. 2.18.2.11. inspectable
          11. 2.18.2.12. skip_typescript
          12. 2.18.2.13. getter_with_clone
          13. 2.18.2.14. unchecked_return_type and unchecked_param_type
          14. 2.18.2.15. return_description and param_description
  4. 3. web-sys
    1. 3.1. Using web-sys
    2. 3.2. Cargo Features
    3. 3.3. Function Overloads
    4. 3.4. Type Translations
    5. 3.5. Inheritance
    6. 3.6. Unstable APIs
  5. 4. Testing with wasm-bindgen-test
    1. 4.1. Usage
    2. 4.2. Writing Asynchronous Tests
    3. 4.3. Testing in Headless Browsers
    4. 4.4. Continuous Integration
    5. 4.5. Coverage (Experimental)
  6. 5. Contributing to wasm-bindgen
    1. 5.1. Testing
  7. 5.2. Internal Design
    1. 5.2.1. JS Objects in Rust
      1. 5.2.2. Exporting a function to JS
      2. 5.2.3. Exporting a struct to JS
      3. 5.2.4. Importing a function from JS
      4. 5.2.5. Importing a class from JS
      5. 5.2.6. Rust Type conversions
      6. 5.2.7. Types in wasm-bindgen
  8. 5.3. js-sys
    1. 5.3.1. Testing
      1. 5.3.2. Adding More APIs
  9. 5.4. web-sys
    1. 5.4.1. Overview
      1. 5.4.2. Testing
      2. 5.4.3. Logging
      3. 5.4.4. Supporting More Web APIs
  10. 5.5. Publishing
  11. 5.6. Team

The `wasm-bindgen` Guide

Polyfill for "JS objects in wasm"

One of the main goals of wasm-bindgen is to allow working with and passing around JS objects in wasm, but that's not allowed today! While indeed true, that's where the polyfill comes in.

The question here is how we shoehorn JS objects into a u32 for Wasm to use. The current strategy for this approach is to maintain a module-local variable in the generated foo.js file: a heap.

Temporary JS objects on the "stack"

The first slots in the heap in foo.js are considered a stack. This stack, like typical program execution stacks, grows down. JS objects are pushed on the bottom of the stack, and their index in the stack is the identifier that's passed to wasm. A stack pointer is maintained to figure out where the next item is pushed.

JS objects are then only removed from the bottom of the stack as well. Removal is simply storing null then incrementing a counter. Because of the "stack-y" nature of this scheme it only works for when Wasm doesn't hold onto a JS object (aka it only gets a "reference" in Rust parlance).

Let's take a look at an example.


# #![allow(unused_variables)]
#fn main() {
// foo.rs
#[wasm_bindgen]
pub fn foo(a: &JsValue) {
    // ...
}
#}

Here we're using the special JsValue type from the wasm-bindgen library itself. Our exported function, foo, takes a reference to an object. This notably means that it can't persist the object past the lifetime of this function call.

Now what we actually want to generate is a JS module that looks like (in TypeScript parlance)

// foo.d.ts
export function foo(a: any);

and what we actually generate looks something like:

// foo.js
import * as wasm from './foo_bg';

const heap = new Array(32);
heap.push(undefined, null, true, false);
let stack_pointer = 32;

function addBorrowedObject(obj) {
  stack_pointer -= 1;
  heap[stack_pointer] = obj;
  return stack_pointer;
}

export function foo(arg0) {
  const idx0 = addBorrowedObject(arg0);
  try {
    wasm.foo(idx0);
  } finally {
    heap[stack_pointer++] = undefined;
  }
}

Here we can see a few notable points of action:

It's also helpful to dig into the Rust side of things to see what's going on there! Let's take a look at the code that #[wasm_bindgen] generates in Rust:


# #![allow(unused_variables)]
#fn main() {
// what the user wrote
pub fn foo(a: &JsValue) {
    // ...
}

#[export_name = "foo"]
pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) {
    let arg0 = unsafe {
        ManuallyDrop::new(JsValue::__from_idx(arg0))
    };
    let arg0 = &*arg0;
    foo(arg0);
}
#}

And as with the JS, the notable points here are:

The above strategy is useful when JS objects are only temporarily used in Rust, for example only during one function call. Sometimes, though, objects may have a dynamic lifetime or otherwise need to be stored on Rust's heap. To cope with this there's a second half of management of JS objects, naturally corresponding to the other side of the JS heap array.

JS Objects passed to Wasm that are not references are assumed to have a dynamic lifetime inside of the Wasm module. As a result the strict push/pop of the stack won't work and we need more permanent storage for the JS objects. To cope with this we build our own "slab allocator" of sorts.

A picture (or code) is worth a thousand words so let's show what happens with an example.


# #![allow(unused_variables)]
#fn main() {
// foo.rs
#[wasm_bindgen]
pub fn foo(a: JsValue) {
    // ...
}
#}

Note that the & is missing in front of the JsValue we had before, and in Rust parlance this means it's taking ownership of the JS value. The exported ES module interface is the same as before, but the ownership mechanics are slightly different. Let's see the generated JS's slab in action:

import * as wasm from './foo_bg'; // imports from Wasm file

const heap = new Array(32);
heap.push(undefined, null, true, false);
let heap_next = 36;

function addHeapObject(obj) {
  if (heap_next === heap.length)
    heap.push(heap.length + 1);
  const idx = heap_next;
  heap_next = heap[idx];
  heap[idx] = obj;
  return idx;
}

export function foo(arg0) {
  const idx0 = addHeapObject(arg0);
  wasm.foo(idx0);
}

export function __wbindgen_object_drop_ref(idx) {
  heap[idx ] = heap_next;
  heap_next = idx;
}

Unlike before we're now calling addHeapObject on the argument to foo rather than addBorrowedObject. This function will use heap and heap_next as a slab allocator to acquire a slot to store the object, placing a structure there once it's found. Note that this is going on the right-half of the array, unlike the stack which resides on the left half. This discipline mirrors the stack/heap in normal programs, roughly.

Another curious aspect of this generated module is the__wbindgen_object_drop_ref function. This is one that's actually imported to wasm rather than used in this module! This function is used to signal the end of the lifetime of a JsValue in Rust, or in other words when it goes out of scope. Otherwise though this function is largely just a general "slab free" implementation.

And finally, let's take a look at the Rust generated again too:


# #![allow(unused_variables)]
#fn main() {
// what the user wrote
pub fn foo(a: JsValue) {
    // ...
}

#[export_name = "foo"]
pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) {
    let arg0 = unsafe {
        JsValue::__from_idx(arg0)
    };
    foo(arg0);
}
#}

Ah that looks much more familiar! Not much interesting is happening here, so let's move on to...

Anatomy of JsValue

Currently the JsValue struct is actually quite simple in Rust, it's:


# #![allow(unused_variables)]
#fn main() {
pub struct JsValue {
    idx: u32,
}

// "private" constructors

impl Drop for JsValue {
    fn drop(&mut self) {
        unsafe {
            __wbindgen_object_drop_ref(self.idx);
        }
    }
}
#}

Or in other words it's a newtype wrapper around a u32, the index that we're passed from wasm. The destructor here is where the __wbindgen_object_drop_reffunction is called to relinquish our reference count of the JS object, freeing up our slot in the slab that we saw above.

If you'll recall as well, when we took &JsValue above we generated a wrapper of ManuallyDrop around the local binding, and that's because we wanted to avoid invoking this destructor when the object comes from the stack.

Working with heap in reality

The above explanations are pretty close to what happens today, but in reality there's a few differences especially around handling constant values likeundefined, null, etc. Be sure to check out the actual generated JS and the generation code for the full details!