final

final 属性与 structural 属性相反。它配置 wasm-bindgen 如何生成 JS 导入来调用导入的函数。值得注意的是,由 final 导入的函数在导入后永远不会更改,而默认导入的函数(或使用 structural)则受运行时查找规则的约束,例如遍历对象的原型链。请注意,final 不适合访问 JS 对象的数据描述符属性;要实现此目的,请使用 structural 属性。

final 属性旨在纯粹与性能相关。理想情况下,它没有用户可见的效果,并且 structural 导入(默认)最终应能够透明地切换到 final

最终的性能方面是,使用 组件模型提案,那么 wasm-bindgen 将需要生成比今天少得多的 JS 函数垫片来导入。例如,考虑今天这个导入


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
extern "C" {
    type Foo;
    #[wasm_bindgen(method)]
    fn bar(this: &Foo, argument: &str) -> JsValue;
}
#}

没有 final 属性,生成的 JS 如下所示

// without `final`
export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
    let varg1 = getStringFromWasm(arg1, arg2);
    return addHeapObject(getObject(arg0).bar(varg1));
}

我们可以在这里看到,需要这个 JS 函数垫片,但它都是相对独立的。然而,它确实以鸭子类型的方式执行 bar 方法,因为它从不验证 getObject(arg0) 是否为 Foo 类型以实际调用 Foo.prototype.bar 方法。

如果我们改为这样写


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
extern "C" {
    type Foo;
    #[wasm_bindgen(method, final)] // note the change here
    fn bar(this: &Foo, argument: &str) -> JsValue;
}
#}

它会生成这个 JS 粘合代码(大致)

const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
    let varg1 = getStringFromWasm(arg1, arg2);
    return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
}

这里的区别非常微妙,但我们可以看到被调用的函数是如何从生成的垫片中提升出来的,并且始终绑定到 Foo.prototype.bar。然后,它使用 Function.call 方法以 getObject(arg0) 作为接收者来调用该函数。

但是等等,即使使用 final,这里仍然有一个 JS 函数垫片!这是真的,这仅仅是未来 WebAssembly 提案尚未实现的事实。但是,语义与未来的 组件模型提案相匹配,因为被调用的方法只确定一次,并且它位于原型链上,而不是在调用函数时在运行时解析。

与未来提案的交互

如果您好奇想了解我们的 JS 函数垫片将如何完全消除,让我们看一下生成的绑定。我们从这里开始

const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
    let varg1 = getStringFromWasm(arg1, arg2);
    return addHeapObject(__wbg_bar_target.call(getObject(arg0), varg1));
}

...一旦实现了 引用类型提案,我们就不会需要其中一些麻烦的函数。这将把我们生成的 JS 垫片转换为如下所示

const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, arg1, arg2) {
    let varg1 = getStringFromWasm(arg1, arg2);
    return __wbg_bar_target.call(arg0, varg1);
}

越来越好!接下来我们需要组件模型提案。请注意,该提案目前正在进行一些更改,因此很难链接到参考文档,但可以说它至少会为我们提供两个不同的功能。

首先,组件模型承诺提供“参数转换”的概念。这里的 arg1arg2 值实际上是指向 utf-8 编码字符串的指针和长度,借助组件模型,我们将能够注释此导入应采用这两个参数并将它们转换为 JS 字符串(也就是说,*主机*应该这样做,WebAssembly 引擎)。使用该功能,我们可以进一步将其精简为

const __wbg_bar_target = Foo.prototype.bar;

export function __wbg_bar_a81456386e6b526f(arg0, varg1) {
    return __wbg_bar_target.call(arg0, varg1);
}

最后,组件模型提案的第二个承诺是,我们可以标记一个函数调用以指示第一个参数是函数调用的 this 绑定。今天,所有调用的导入函数的 this 值都是 undefined,并且此标志(使用组件模型配置)将指示此处的第一个参数实际上是 this

考虑到这一点,我们可以将其进一步转换为

export const __wbg_bar_a81456386e6b526f = Foo.prototype.bar;

瞧!有了 引用类型组件模型,我们现在完全不需要 JS 函数垫片来调用导入的函数。此外,未来关于 ES 模块系统的 Wasm 提案也可能意味着我们甚至不需要这里的 export const ...

还值得指出的是,随着所有这些 Wasm 提案的实现,导入 bar 函数(又名结构化)的默认方式会生成一个类似于以下的 JS 函数垫片:

export function __wbg_bar_a81456386e6b526f(varg1) {
    return this.bar(varg1);
}

这里,这个导入仍然会受到运行时原型链查找等的影响。