Polyfill for"JS objects in wasm"

其中一个主要目标是wasm-bindgen是允许在wasm中使用和传递JS对象,但今天不允许这样做!虽然确实如此,但这就是polyfill的用武之地.

这里的问题是我们如何将JS对象鞋拔成一个u32对于使用ism. 此方法的当前策略是在生成的中维护两个模块局部变量foo.jsfile: 堆栈和堆.

堆栈上的临时JS对象

堆栈中foo.js是,堆栈. JS对象被推送到堆栈的顶部,它们在堆栈中的索引是传递给wasm的标识符. 然后,JS对象也只从堆栈顶部移除. 这种数据结构主要用于有效地将JS对象传递给wasm而不需要"堆分配". 然而,它的缺点是它只适用于当wasm不能保留JS对象时 (也就是说它只能用Rust的说法获得"引用") .

我们来看一个例子.


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

我们在这里使用特殊的JsValue从中输入wasm-bindgen图书馆本身. 我们的出口功能,foo拿一个参考到一个对象. 这显然意味着它不能将对象持久化超过此函数调用的生命周期.

现在我们实际想要生成的是一个看起来像的JS模块 (在Typescript用语中)

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

我们实际生成的东西看起来像:

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

let 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();
  }
}

在这里,我们可以看到一些值得注意的行动点:

  • wasm文件已重命名为foo_bg.wasm,我们可以看到这里生成的JS模块是如何从wasm文件导入的.
  • 接下来我们可以看到我们stack模块变量,用于从堆栈中推送/弹出项目.
  • 我们的出口功能foo,任意争论,arg0,转换为索引addBorrowedObject对象功能. 然后将索引传递给wasm,因此ism可以使用它.
  • 最后,我们有一个finally它释放堆栈槽,因为它不再使用,发出一个pop对于在函数开始时推送的内容.

挖掘Rust的一面也很有帮助,看看那里发生了什么!我们来看看那些代码#[wasm_bindgen]在Rust生成:


# #![allow(unused_variables)]
#fn main() {
// 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);
}
#}

和JS一样,这里值得注意的要点是:

  • 原来的功能,foo,在输出中未经修改
  • 这里生成的函数 (具有唯一名称) 是实际从wasm模块导出的函数
  • 我们生成的函数接受一个整数参数 (我们的索引) ,然后将其包装在一个JsValue. 这里有一些不值得进入的诡计,但我们会稍微看一下发生在幕后的事情.

板中长寿的JS对象

当JS对象仅在Rust中临时使用时,上述策略很有用,例如仅在一次函数调用期间. 但有时,对象可能具有动态生命周期,或者需要存储在Rust的堆上. 为了解决这个问题,JS对象的后半部分是一个平板.

传递给wasm的JS对象不是引用,假定在wasm模块内部具有动态生命周期. 因此,堆栈的严格推送/弹出将不起作用,我们需要更多的JS对象永久存储. 为了应对这种情况,我们建立了自己的"板块分配器".

一张图片 (或代码) 值得一千个字,所以让我们展示一个例子会发生什么.


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

请注意&在前面缺少JsValue我们之前有过,而在Rust的说法中,这意味着它取得了JS值的所有权. 导出的ES模块接口与以前相同,但所有权机制略有不同. 让我们看一下生成的JS板块:

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

let 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;
}

不像以前我们现在打电话addHeapObject关于这个论点foo而不是addBorrowedObject. 这个功能会用到slabslab_next作为slab分配器获取存储对象的槽,一旦找到它就放置一个结构.

注意,除了存储对象之外,还使用引用计数. 这样我们就可以在不使用的情况下在Rust中创建对JS对象的多个引用Rc,但总的来说,担心这一点并不重要.

这个生成的模块的另一个奇怪的方面是__wbindgen_object_drop_ref功能. 这是一个实际上是从wasm导入而不是在这个模块中使用的!此函数用于表示a的生命周期结束JsValue在Rust中,或者换句话说,当它超出范围时. 否则,虽然这个功能很大程度上只是一个普通的"无板"实现.

最后,让我们再看一下Rust生成的内容:


# #![allow(unused_variables)]
#fn main() {
// 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);
}
#}

啊,看起来更熟悉!这里没有太多有趣的事情发生,所以让我们继续......

解剖学JsValue

目前JsValue在Rust中,struct实际上非常简单,它是:


# #![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);
        }
    }
}
#}

或者换句话说,它是一个新的类型包装器u32,我们从ism传递的索引. 这里的析构函数就是这里的__wbindgen_object_drop_ref函数被调用以放弃我们对JS对象的引用计数,从而释放我们的插槽slab我们在上面看到了.

如果你还记得,当我们采取&JsValue上面我们生成了一个包装器ManuallyDrop围绕本地绑定,这是因为我们想避免在对象来自堆栈时调用此析构函数.

索引板和堆栈

您可能在想这个系统可能不起作用!平板和堆栈的索引混合在一起,但我们如何区分?事实证明,上面的例子已经简化了一些,但是最低位当前用作指示你是slab还是堆栈索引.