wasm-bindgen/guide/src/design/js-objects-in-rust.md

239 lines
7.3 KiB
Markdown
Raw Normal View History

# 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 two module-local variables
in the generated `foo.js` file: a stack and a heap.
### Temporary JS objects on the stack
The stack in `foo.js` is, well, a stack. JS objects are pushed on the top of the
stack, and their index in the stack is the identifier that's passed to wasm. JS
objects are then only removed from the top of the stack as well. This data
structure is mainly useful for efficiently passing a JS object into wasm without
a sort of "heap allocation". The downside of this, however, is that 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.
```rust
// 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)
```ts
// foo.d.ts
export function foo(a: any);
```
and what we actually generate looks something like:
```js
// foo.js
import * as wasm from './foo_bg';
const stack = [];
function addBorrowedObject(obj) {
stack.push(obj);
return stack.length - 1;
}
export function foo(arg0) {
const idx0 = addBorrowedObject(arg0);
try {
wasm.foo(idx0);
} finally {
stack.pop();
}
}
```
Here we can see a few notable points of action:
* The wasm file was renamed to `foo_bg.wasm`, and we can see how the JS module
generated here is importing from the wasm file.
* Next we can see our `stack` module variable which is used to push/pop items
from the stack.
* Our exported function `foo`, takes an arbitrary argument, `arg0`, which is
converted to an index with the `addBorrowedObject` object function. The index
is then passed to wasm so wasm can operate with it.
* Finally, we have a `finally` which frees the stack slot as it's no longer
used, issuing a `pop` for what was pushed at the start of the function.
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:
```rust
// what the user wrote
pub fn foo(a: &JsValue) {
// ...
}
#[export_name = "foo"]
pub extern 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 original function, `foo`, is unmodified in the output
* A generated function here (with a unique name) is the one that's actually
exported from the wasm module
* Our generated function takes an integer argument (our index) and then wraps it
in a `JsValue`. There's some trickery here that's not worth going into just
yet, but we'll see in a bit what's happening under the hood.
### Long-lived JS objects in a slab
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, a slab.
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.
```rust
// 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:
```js
import * as wasm from './foo_bg'; // imports from wasm file
const slab = [];
let slab_next = 0;
function addHeapObject(obj) {
if (slab_next === slab.length)
slab.push(slab.length + 1);
const idx = slab_next;
const next = slab[idx];
slab_next = next;
slab[idx] = { obj, cnt: 1 };
return idx;
}
export function foo(arg0) {
const idx0 = addHeapObject(arg0);
wasm.foo(idx0);
}
export function __wbindgen_object_drop_ref(idx) {
let obj = slab[idx];
obj.cnt -= 1;
if (obj.cnt > 0)
return;
// If we hit 0 then free up our space in the slab
slab[idx] = slab_next;
slab_next = idx;
}
```
Unlike before we're now calling `addHeapObject` on the argument to `foo` rather
than `addBorrowedObject`. This function will use `slab` and `slab_next` as a
slab allocator to acquire a slot to store the object, placing a structure there
once it's found.
Note here that a reference count is used in addition to storing the object.
That's so we can create multiple references to the JS object in Rust without
using `Rc`, but it's overall not too important to worry about here.
Another curious aspect of this generated module is the
`__wbindgen_object_drop_ref` function. This is one that's actually imported from
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:
```rust
// what the user wrote
pub fn foo(a: JsValue) {
// ...
}
#[export_name = "foo"]
pub extern 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:
```rust
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_ref`
function 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.
### Indexing both a slab and the stack
You might be thinking at this point that this system may not work! There's
indexes into both the slab and the stack mixed up, but how do we differentiate?
It turns out that the examples above have been simplified a bit, but otherwise
the lowest bit is currently used as an indicator of whether you're a slab or a
stack index.