介绍

wasm-bindgen促进 wasm模块和 JavaScript 之间的高级交互.

这个项目有点像一半 polyfill 的主机绑定提案,和一半用于增强 JS和wasm 编译代码之间的高级交互 (目前主要来自Rust) . 更具体地说,这个项目允许 JS / wasm 通过 字符串,JS对象,类 等进行通信,而不是纯粹的 整数和浮点数. 运用wasm-bindgen做某事,比如: 您可以在Rust中定义JS类 或 从JS获取一个字符串 或 返回一个. 这个项目在变强 {秃的也会很快}!

目前这个工具以 Rust 为重点,但 底层基础 是与语言无关的,并且希望随着时间的推移,这个工具可以稳定使用,用于 C/C ++ 等语言!

该项目的显着特点包括:

这个项目仍然相对较新,但反馈当然总是受欢迎! 如果你对设计很好奇,加上关于这个箱子可以做什么的更多信息,请查看[desgin doc].

基本用法

让我们实现相当于"你好,世界!"的示例

注意:目前这个项目使用最新Rust,你可以通过 rustup ,并使用rustup default nightly;描述*如何通过 比目前支持更丰富的类型 进行通信. 该wasm-bindgen工具将解释此信息,且在 wasm文件 更换模块 .

之前的js_hello_world.wasm文件, 被解释为它是一个 ES6模块. 而后来wasm-bindgen生成的js_hello_world.js, 是应该具有 wasm文件 的预期接口,特别是 字符串,类 等丰富类型.

wasm-bindgen工具还会生成, 实现此模块所需的一些其他文件. 例如: js_hello_world_bg.wasm是原始的wasm文件,但是经过处理的. 这是被依赖的js_hello_world_bg.wasm文件,就像之前一样,文件就像一个ES6模块.

js_hello_world.js 调用了js_hello_world_bg.wasm

注意 js_hello_world_bg.wasmjs_hello_world.wasm 的不同

此时,您可能会将这些文件插入更大的构建系统. wasm-bindgen生成的文件像普通的 ES6模块 一样 (wasm文件就已经被使用了) . 截至撰写本文时,很遗憾没有很多工具本身可以做到这一点,但 Webpack的4.0测试版本 有本机的支持! 让我们来看看它,看看它是如何工作的.

首先创建一个index.js文件:

const js = import("./js_hello_world");

js.then(js => {
  js.greet("World!");
});

请注意,我们正在使用import(..)这里因为Webpack不支持同步从主块导入模块.

接下来我们通过创建一个JS依赖项package.json:

{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "devDependencies": {
    "webpack": "^4.0.1",
    "webpack-cli": "^2.0.10",
    "webpack-dev-server": "^3.1.0"
  }
}

和我们的 webpack 配置

// webpack.config.js
const path = require('path');

module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development"
};

我们对应的index.html:

<html>
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <script src='./index.js'></script>
  </body>
</html>

最后:

$ npm install
$ npm run serve

如果你打开本地主机: 8080在浏览器中你应该看到一个Hello, world!对话框弹出!

如果这一切都有点多,不用担心!您可以[在线执行此代码][hello-online]谢谢WebAssembly Studio或者你可以跟随GitHub查看所有必需的文件,以及设置它的脚本.

刚刚发生了什么?

唷! 想知道刚刚发生了什么, 很多很多 - 两个大魔法: #[wasm_bindgen]属性和wasm-bindgenCLI工具.

#[wasm_bindgen]属性

此属性,从中 wasm-bindgencrate 导出,是将 Rust函数 暴露给 JS 的主文件. 这是一个程序宏 (因此需要最新的 Rust工具链) ,它将在 Rust 中生成适当的补丁程序,以便从您的 类型 转换为 JS 可以与之交互的接口. 最后,在解析后, 该属性还将一些信息序列化为wasm-bindgen会丢弃的输出.

下面对属性的各个部分进行了更全面的解释,但现在使用它在 自由函数,结构,impl 块 以用于 那些结构和extern { ... }块. 现在还不支持一些 Rust 特性,如 泛型,生命周期 等.

wasm-bindgenCLI工具

下半部分,都是wasm-bindgen工具的内容. 这个工具打开了 rustc 生成的模块,并找到了 它的内容编码的#[wasm_bindgen]属性. 你可以把它想象成#[wasm_bindgen]属性,创建了输出模块的特殊辨识,给wasm-bindgen启动流程.

这个信息给了wasm-bindgen, 所有它需要知道导入的JS文件. JS文件 包裹了 底层实例化的 wasm 模块 (也就是调用WebAssembly.instantiate) 然后为其中的 类/函数 提供包装器.

我们还能做什么?

多得多! 这里介绍了您可以在此项目中使用的各种功能. 你也可以在线探索此代码:


# #![allow(unused_variables)]
#fn main() {
// src/lib.rs
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

// Strings can both be passed in and received
#[wasm_bindgen]
pub fn concat(a: &str, b: &str) -> String {
    let mut a = a.to_string();
    a.push_str(b);
    return a
}

// A struct will show up as a class on the JS side of things
#[wasm_bindgen]
pub struct Foo {
    contents: u32,
}

#[wasm_bindgen]
impl Foo {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Foo {
        Foo { contents: 0 }
    }

    // Methods can be defined with `&mut self` or `&self`, and arguments you
    // can pass to a normal free function also all work in methods.
    pub fn add(&mut self, amt: u32) -> u32 {
        self.contents += amt;
        return self.contents
    }

    // You can also take a limited set of references to other types as well.
    pub fn add_other(&mut self, bar: &Bar) {
        self.contents += bar.contents;
    }

    // Ownership can work too!
    pub fn consume_other(&mut self, bar: Bar) {
        self.contents += bar.contents;
    }
}

#[wasm_bindgen]
pub struct Bar {
    contents: u32,
    opaque: JsValue, // defined in `wasm_bindgen`, imported via prelude
}

#[wasm_bindgen(module = "./index")] // what ES6 module to import from
extern {
    fn bar_on_reset(to: &str, opaque: &JsValue);

    // We can import classes and annotate functionality on those classes as well
    type Awesome;
    #[wasm_bindgen(constructor)]
    fn new() -> Awesome;
    #[wasm_bindgen(method)]
    fn get_internal(this: &Awesome) -> u32;
}

#[wasm_bindgen]
impl Bar {
    pub fn from_str(s: &str, opaque: JsValue) -> Bar {
        let contents = s.parse().unwrap_or_else(|_| {
            Awesome::new().get_internal()
        });
        Bar { contents, opaque }
    }

    pub fn reset(&mut self, s: &str) {
        if let Ok(n) = s.parse() {
            bar_on_reset(s, &self.opaque);
            self.contents = n;
        }
    }
}
#}

此宏调用,生成的JS绑定看起来像这样 翻墙. 你可以像这样查看它们:

生成的JS绑定

/* tslint:disable */
import * as wasm from './js_hello_world_wasm'; // imports from wasm file

import {
    bar_on_reset
} from './index';

import {
    Awesome
} from './index';

let cachedEncoder = null;

function textEncoder() {
    if (cachedEncoder)
        return cachedEncoder;
    cachedEncoder = new TextEncoder('utf-8');
    return cachedEncoder;
}

let cachedUint8Memory = null;

function getUint8Memory() {
    if (cachedUint8Memory === null ||
        cachedUint8Memory.buffer !== wasm.memory.buffer)
        cachedUint8Memory = new Uint8Array(wasm.memory.buffer);
    return cachedUint8Memory;
}

function passStringToWasm(arg) {
    if (typeof(arg) !== 'string')
        throw new Error('expected a string argument');
    const buf = textEncoder().encode(arg);
    const len = buf.length;
    const ptr = wasm.__wbindgen_malloc(len);
    getUint8Memory().set(buf, ptr);
    return [ptr, len];
}

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

let cachedDecoder = null;

function textDecoder() {
    if (cachedDecoder)
        return cachedDecoder;
    cachedDecoder = new TextDecoder('utf-8');
    return cachedDecoder;
}

function getStringFromWasm(ptr, len) {
    const mem = getUint8Memory();
    const slice = mem.slice(ptr, ptr + len);
    const ret = textDecoder().decode(slice);
    return ret;
}

let stack = [];

function getObject(idx) {
    if ((idx & 1) === 1) {
        return stack[idx >> 1];
    } else {
        const val = slab[idx >> 1];

        return val.obj;

    }
}

export function __wbg_f_bar_on_reset(ptr0, len0, arg1) {
    bar_on_reset(getStringFromWasm(ptr0, len0), getObject(arg1))
}

export function __wbg_s_Awesome_new() {
    return addHeapObject(new Awesome());
}

export function __wbg_s_Awesome_get_internal(arg0) {
    return Awesome.prototype.get_internal.call(getObject(arg0));
}

export function concat(arg0, arg1) {
    const [ptr0, len0] = passStringToWasm(arg0);
    const [ptr1, len1] = passStringToWasm(arg1);
    try {
        const ret = wasm.concat(ptr0, len0, ptr1, len1);

        const ptr = wasm.__wbindgen_boxed_str_ptr(ret);
        const len = wasm.__wbindgen_boxed_str_len(ret);
        const realRet = getStringFromWasm(ptr, len);
        wasm.__wbindgen_boxed_str_free(ret);
        return realRet;

    } finally {

        wasm.__wbindgen_free(ptr0, len0);

        wasm.__wbindgen_free(ptr1, len1);

    }
}

export class Foo {
    constructor(ptr) {
        this.ptr = ptr;
    }

    free() {
        const ptr = this.ptr;
        this.ptr = 0;
        wasm.__wbg_foo_free(ptr);
    }
    static new() {
        const ret = wasm.foo_new();
        return new Foo(ret);

    }
    add(arg0) {
        const ret = wasm.foo_add(this.ptr, arg0);
        return ret;
    }
    add_other(arg0) {
        const ret = wasm.foo_add_other(this.ptr, arg0.ptr);
        return ret;
    }
    consume_other(arg0) {
        const ptr0 = arg0.ptr;
        arg0.ptr = 0;
        const ret = wasm.foo_consume_other(this.ptr, ptr0);
        return ret;
    }
}

export class Bar {
    constructor(ptr) {
        this.ptr = ptr;
    }

    free() {
        const ptr = this.ptr;
        this.ptr = 0;
        wasm.__wbg_bar_free(ptr);
    }
    static from_str(arg0, arg1) {
        const [ptr0, len0] = passStringToWasm(arg0);
        const idx1 = addHeapObject(arg1);
        try {
            const ret = wasm.bar_from_str(ptr0, len0, idx1);
            return new Bar(ret);

        } finally {

            wasm.__wbindgen_free(ptr0, len0);

        }
    }
    reset(arg0) {
        const [ptr0, len0] = passStringToWasm(arg0);
        try {
            const ret = wasm.bar_reset(this.ptr, ptr0, len0);
            return ret;
        } finally {

            wasm.__wbindgen_free(ptr0, len0);

        }
    }
}

function dropRef(idx) {
    let obj = slab[idx >> 1];
    obj.cnt -= 1;
    if (obj.cnt > 0)
        return;
    // If we hit 0 then free up our space in the slab
    slab[idx >> 1] = slab_next;
    slab_next = idx >> 1;
}

export const __wbindgen_object_drop_ref = dropRef;

export const __wbindgen_throw =
    function(ptr, len) {
        throw new Error(getStringFromWasm(ptr, len));
    };

和我们使用它,在index.js:

import { Foo, Bar, concat } from "./js_hello_world";
import { booted } from "./js_hello_world_wasm";

export function bar_on_reset(s, token) {
  console.log(token);
  console.log(`this instance of bar was reset to ${s}`);
}

function assertEq(a, b) {
  if (a !== b)
    throw new Error(`${a} != ${b}`);
  console.log(`found ${a} === ${b}`);
}

function main() {
  assertEq(concat('a', 'b'), 'ab');

  // Note that to use `new Foo()` the constructor function must be annotated
  // with `#[wasm_bindgen(constructor)]`, otherwise only `Foo.new()` can be used.
  // Additionally objects allocated corresponding to Rust structs will need to
  // be deallocated on the Rust side of things with an explicit call to `free`.
  let foo = new Foo();
  assertEq(foo.add(10), 10);
  foo.free();

  // Pass objects to one another
  let foo1 = new Foo();
  let bar = Bar.from_str("22", { opaque: 'object' });
  foo1.add_other(bar);

  // We also don't have to `free` the `bar` variable as this function is
  // transferring ownership to `foo1`
  bar.reset('34');
  foo1.consume_other(bar);

  assertEq(foo1.add(2), 22 + 34 + 2);
  foo1.free();

  alert('all passed!')
}

export class Awesome {
  constructor() {
    this.internal = 32;
  }

  get_internal() {
    return this.internal;
  }
}

booted.then(main);

闭包

#[wasm_bindgen]属性支持将一些 Rust闭包 传递给JS. 你可以做的例子是:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
extern {
    fn foo(a: &Fn()); // could also be `&mut FnMut()`
}
#}

这是一个 从 JS 导入foo函数,其中第一个参数是 a: 堆栈闭包. 您可以调用此函数带有参数a:&Fn() 在正常的 html 与 JS 中获得这个函数. 然而,当·foo函数使用时,JS函数将失效,并且使用它将引发异常.

另外, 闭包还支持 参数 和 返回值 的导出,例如:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
extern {
    type Foo;

    fn bar(a: &Fn(u32, String) -> Foo);
}
#}

有时,不希望这些闭包的堆栈行为影响. 例如,您希望在 JS 中通过下一次事件循环中运行setTimeout的闭包. 为此,您希望导入的函数返回,和 JS闭包 需要有效!

要支持此用例,您可以执行以下操作:


# #![allow(unused_variables)]
#fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn baz(a: &Closure<Fn()>);
}
#}

Closure类型定义在wasm_bindgen箱子和代表"长寿"的闭包. JS闭包传递给了baz之后, baz返回仍然有效,在 Rust 中 JS闭包 的有效性 与 Closure的生命周期 有关. 一旦Closure被删除, 它将释放其内部内存, 并使相应的 JS函数 无效.

像堆栈闭包一样Closure也支持FnMut:


# #![allow(unused_variables)]
#fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    fn another(a: &Closure<FnMut() -> u32>);
}
#}

这时你不能将一个 JS闭包 传递给Rust,在有限的情况下,你只能将一个 Rust闭包 传递给JS.

功能参考

此章节将尝试说明, wasm-bindgen项目中实现的各种功能. 下面这部分不是详尽无遗的,但tests用例也能成为你一个寻找功能例子的好地方.

#[wasm_bindgen]属性可以附加到函数,结构,impls 和外部模块.

其中:

  • Impls 只能包含函数,并且该属性不能附加到 impl 块中的函数 或 外部模块中的函数.
  • 任何这些类型都不允许使用生命周期参数或类型参数.
  • 其他语言模块必须有"C"abi (或没有) 列表 .
  • 带有#[wasm_bindgen]的免费函数可能有或没有 "C"abi 的列表, 都不必要用#[no_mangle]属性.

所有 函数参数引用的结构都应该在宏本身中定义. 参数允许实现WasmBoundary特征, 例子是:

  • 整数-Int (u64 / i64需要BigInt支持)
  • 浮点数-Floats
  • 借用的字符串&str)
  • 拥有的字符串 (String)
  • export 结构 (Foo,注释#[wasm_bindgen])
  • export 类似 C 的枚举 (Foo,注释#[wasm_bindgen])
  • 导入 带有注释的外部模块的类型 要#[wasm_bindgen]
  • 借用 export 结构 (&Foo要么&mut Bar)
  • JsValue类型和&JsValue (不是可变引用)
  • Vectors 和 支持的整数类型的切片 和JsValue类型.

上述所有内容还可以返回,除了借用的引用 Vec<JsValue>目前不支持作为函数的参数.

字符串是实现,将数据复制 in/out Rust 堆的垫片函数. 也就是说,从 JS 传递给 Rust 的字符串 被复制到Rust堆 (使用 生成的补丁 malloc 一些空间) , 然后将被适当地释放.

拥有的值通过 框 实现. 当你返回一个Foo, 它实际上变成了Box<RefCell<Foo>>,并作为指针返回给 JS .

指针是有一个定义的 ABI ,和RefCell是为了确保 JS 中的 重导入 和 重命名 的安全性. 一般来说正常使用.,你不应该看到RefCell恐慌

JS-values-in-Rust 是 通过 索引 实现的,而索引一个生成表格是实现 JS绑定 的一部分. 此表格是通过 Rust 中指定的所有权, 以及 我们返回的绑定 进行管理. 有关这方面的更多信息,请参阅design doc.

目前, 所有这些构造在 JS方面 创建相对简单的代码, 大多数在 Rust与JS 中具有1: 1的匹配.

CLI参考

wasm-bindgen工具有许多选项可用于调整生成的JS. 默认情况下,生成的JS 使用 ES模块,并且与 Node 和 浏览器兼容 (但可能需要两个用例的捆绑器) .

可以通过以下方式了解更多wasm-bindgen --help, 但一些值得注意的选择是:

  • --nodejs: 这个标志将为输出 Node 而不是 浏览器 ,允许本机使用require生成的JS 和 内部使用require而不是 ES模块. 使用此标志时,不需要进一步后处理 (也称为捆绑器) 来使用 wasm.

  • --browser: 此标志将专门输出为 浏览器,使其与 Node 不兼容. 这基本上会使生成的JS稍微小一些,因为不需要对 Node 进行运行时检查.

  • --no-modules: wasm-bindgen的默认输出是ES模块 ,但此选项表示 不应使用 ES模块,并且应为 Web浏览器定制输出. 在这种模式下window.wasm_bindgen将是一个函数,它接受 wasm文件 的 路径 来获取和实例化. 之后,可以通过以下方式从 wasm 导出函数 - window.wasm_bindgen.foo. 注意名称wasm_bindgen可配置--no-modules-global FOO选项.

  • --no-typescript: 默认情况下*.d.ts生成,但此标志将禁用生成此TypeScript文件.

  • --debug: 在"调试模式"中生成更多 JS和wasm 以帮助捕获程序员错误,但生成的文件时不打算用于生产环境

参考

下表概述了wasm-bindgen可以在 wasm ABI 边界上 发送/接收 的所有类型.

类型 T参数 &T参数 &mut T参数 T返回值
str No Yes No Yes
char Yes No No Yes
bool Yes No No Yes
JsValue Yes Yes Yes Yes
Box<[JsValue]> Yes No No Yes
*const T Yes No No Yes
*mut T Yes No No Yes
u8 i8 u16 i16 u64 i64 isize size Yes No No Yes
u32 i32 f32 f64 Yes Yes Yes Yes
Box<[u8]> Box<[i8]> Box<[u16]> Box<[i16]> Box<[u32]> Box<[i32]> Box<[u64]> Box<[i64]> Yes No No Yes
Box<[f32]> Box<[f64]> Yes No No Yes
[u8] [i8] [u16] [i16] [u32] [i32] [u64] [i64] No Yes Yes No
&[u8] &mut[u8] &[i8] &mut[i8] &[u16] &mut[u16] &[i16] &mut[i16] &[u32] &mut[u32] &[i32] &mut[i32] &[u64] &mut[u64] &[i64] &mut[i64] No No No Yes
[f32] [f64] No Yes Yes No
&[f32] &mut[f32] &[f64] &mut[f64] No No No Yes

帮助wasm-bindgen

本节包含有关如何 启动和运行 此项目以进行开发的说明.

先决条件

  1. Rust Nightly. install Rust. 安装 Rust 后,运行

    rustup default nightly
    rustup target add wasm32-unknown-unknown
    
  1. 该项目的测试使用 Node. 确保安装了 Node>=8, 因为这是在引入 WebAssembly支持 时. [isntall node].
  1. 该项目的测试也使用yarn - Node的包管理器. 安装yarn, 运行:

    npm install -g yarn
    

    或 遵循其他特定于平台的说明这里.

    一旦yarn安装后,在顶级目录中运行它:

    yarn
    

运行测试

最后,您可以运行测试cargo:

cargo test

wasm-bindgen的设计

本节旨在: 深入探讨wasm-bindgen如何工作, 通过 Rust语言. 如果您很久前阅读这篇文章,它可能不再是最新的,但随时可以打开一个问题,我们可以尝试回答问题和/或更新这个问题!

基础: ES模块

首先要了解的事情是,wasm-bindgen 建立在ES模块的基础上的. 换句话说,这个工具采取了 自定义的文件 被视为ES模块. 这意味着你可以import一个wasm文件 ,使用export得到一个普通 JS文件 的函数等.

现在不幸的是,在撰写本文时, wasm interop 的接口并不是很丰富. Wasm模块只能调用函数 或 导出专门处理i32,i64,f32,和f64之类的参数. 这是个坏消息!

但这也是这个项目的用武之地, 通过使用 类,JS对象,Rust结构,字符串 等更丰富的类型来增强 wasm模块 的 "ABI". 但请记住,一切都基于 ES模块! 使用wasm-bindgen意味着编译器实际上正在生成一个"已经被破坏"的各种各样的wasm 文件. 例如,rustc 发出的 wasm文件 没有我们想要的接口. 相反,它需要 wasm-bindgen 处理文件,生成一个foo.jsfoo_bg.wasm文件. 该foo.js是用 JS 表示的所需接口 (类,类型,字符串等) 和foo_bg.wasm模块 简单地用作实现细节 (它是从原始版本 foo.wasm 轻微修改的文件) .

随着 WebAssembly 中的 更多功能 随着时间的推移而稳定 (如主机绑定) , JS文件 有望变得越来越小. 它不太可能永远消失,但是wasm-bindgen旨在尽可能地遵循 WebAssembly规范和建议 来优化 JS/Rust.

基础#2: 在 Rust 中不引人注目

在 rsut编程方面上, wasm-bindgen箱子的设计理想是尽可能减少对 在 Rust 编码的影响. 理想情况下#[wasm_bindgen]属性在关键位置 #注释,不然您会看不上这个项目. 该属性致力于既不发明新语法又适用于现有的习语.

例如,想要暴露普通 Rust 中的一个函数,如下所示:


# #![allow(unused_variables)]
#fn main() {
pub fn greet(name: &str) -> String {
    // ...
}
#}

#[wasm_bindgen]将它 导出到JS 所需要做的就是:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    // ...
}
#}

此外,这里的设计对 Rust 的干预最少,这样我们就可以轻松利用即将到来的优势主机绑定提案. 理想情况下,您只需升级wasm-bindgen以及您的工具链,您可以立即获得对主机绑定的原始访问权限! (虽然这仍然有点远......)

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还是堆栈索引.

将函数导出到JS

好了,现在我们已经很好地掌握了JS对象以及它们是如何工作的,让我们来看看另一个特性. wasm-bindgen: 使用比数字更丰富的类型导出功能.

导出具有更多美味类型的功能的基本思想是,实际上不会直接调用wasm导出. 而是生成的foo.js模块将为wasm模块中的所有导出函数提供填充程序.

这里最有趣的转换发生在字符串上,让我们来看看.


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
pub fn greet(a: &str) -> String {
    format!("Hello, {}!", a)
}
#}

在这里,我们要定义一个看起来像的ES模块

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

要了解发生了什么,让我们看一下生成的垫片

import * as wasm from './foo_bg';

function passStringToWasm(arg) {
  const buf = new TextEncoder('utf-8').encode(arg);
  const len = buf.length;
  const ptr = wasm.__wbindgen_malloc(len);
  let array = new Uint8Array(wasm.memory.buffer);
  array.set(buf, ptr);
  return [ptr, len];
}

function getStringFromWasm(ptr, len) {
  const mem = new Uint8Array(wasm.memory.buffer);
  const slice = mem.slice(ptr, ptr + len);
  const ret = new TextDecoder('utf-8').decode(slice);
  return ret;
}

export function greet(arg0) {
  const [ptr0, len0] = passStringToWasm(arg0);
  try {
    const ret = wasm.greet(ptr0, len0);
    const ptr = wasm.__wbindgen_boxed_str_ptr(ret);
    const len = wasm.__wbindgen_boxed_str_len(ret);
    const realRet = getStringFromWasm(ptr, len);
    wasm.__wbindgen_boxed_str_free(ret);
    return realRet;
  } finally {
    wasm.__wbindgen_free(ptr0, len0);
  }
}

哎呀,那真是太多了!如果仔细观察发生了什么,我们可以看一看:

  • 字符串通过两个参数 (指针和长度) 传递给wasm. 现在我们必须将字符串复制到wasm堆上,这意味着我们将使用它TextEncoder实际上做编码. 完成后,我们使用内部函数wasm-bindgen为字符串分配空间,然后我们将稍后将该ptr / length传递给wasm.

  • 从wasm返回字符串有点棘手,因为我们需要返回一个ptr / len对,但ism目前只支持一个返回值 (多个返回值) 正在标准化) . 为了解决这个问题,我们实际上返回一个指向ptr / len对的指针,然后使用函数来访问各个字段.

  • 一些清理工作最终会在wasm中发生. 该__wbindgen_boxed_str_freefunction用于释放返回值greet在它被解码到JS堆上之后 (使用TextDecoder) . 该__wbindgen_free然后,在函数调用完成后,用于释放我们分配的空间以传递字符串参数.

接下来让我们来看看Rust的一面. 在这里,我们将看到一个大部分缩写和/或"简化"的意义,它是由它编写的:


# #![allow(unused_variables)]
#fn main() {
pub extern fn greet(a: &str) -> String {
    format!("Hello, {}!", a)
}

#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(
    arg0_ptr: *const u8,
    arg0_len: usize,
) -> *mut String {
    let arg0 = unsafe {
        let slice = ::std::slice::from_raw_parts(arg0_ptr, arg0_len);
        ::std::str::from_utf8_unchecked(slice)
    };
    let _ret = greet(arg0);
    Box::into_raw(Box::new(_ret))
}
#}

在这里,我们可以再次看到我们的greet函数是未修改的,并有一个包装器来调用它. 这个包装器将获取ptr / len参数并将其转换为字符串切片,而返回值仅被装入一个指针,然后返回到通过__wbindgen_boxed_str_*功能.

因此,一般情况下,导出函数涉及JS和Rust中的垫片,每一侧都转换为每种语言的本机类型的ism参数. 该wasm-bindgen工具管理连接所有这些垫片#[wasm_bindgen]宏也照顾Rust垫片.

大多数论点都有一个相对清晰的方法来转换它们,如果你有任何问题请告诉我!

将结构导出到JS

到目前为止,我们已经介绍了JS对象,导入函数和导出函数. 到目前为止,这给我们提供了相当丰富的基础,这太棒了!不过,我们有时想进一步定义JSclass在Rust. 或者换句话说,我们希望使用从Rust到JS的方法公开一个对象,而不仅仅是导入/导出自由函数.

#[wasm_bindgen]属性可以注释astructimpl块允许:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
pub struct Foo {
    internal: i32,
}

#[wasm_bindgen]
impl Foo {
    pub fn new(val: i32) -> Foo {
        Foo { internal: val }
    }

    pub fn get(&self) -> i32 {
        self.internal
    }

    pub fn set(&mut self, val: i32) {
        self.internal = val;
    }
}
#}

这是典型的Ruststruct具有构造函数和一些方法的类型的定义. 用结构注释结构#[wasm_bindgen]意味着我们将生成必要的特征impl以将此类型转换为JS边界或从JS边界转换此类型. 注释impl这里的块意味着内部函数也将通过生成的填充程序提供给JS. 如果我们看一下生成的JS代码,我们会看到:

import * as wasm from './js_hello_world_bg';

export class Foo {
    static __construct(ptr) {
        return new Foo(ptr);
    }

    constructor(ptr) {
        this.ptr = ptr;
    }

    free() {
        const ptr = this.ptr;
        this.ptr = 0;
        wasm.__wbg_foo_free(ptr);
    }

    static new(arg0) {
        const ret = wasm.foo_new(arg0);
        return Foo.__construct(ret)
    }

    get() {
        const ret = wasm.foo_get(this.ptr);
        return ret;
    }

    set(arg0) {
        const ret = wasm.foo_set(this.ptr, arg0);
        return ret;
    }
}

那实际上并不多!我们可以在这里看到我们如何从Rust转换为JS:

  • Rust中的相关函数 (没有self) 变成staticJS中的函数.
  • Rust中的方法变成了wasm中的方法.
  • 手动内存管理也在JS中公开. 该free需要调用函数来释放Rust方面的资源.

能够使用new Foo(),你需要注释new#[wasm_bindgen(constructor)].

但是,这里需要注意的一个重要方面是free被称为JS对象的是"neutered",因为它的内部指针被清空了. 这意味着将来使用此对象应该会在Rust中引发恐慌.

这些绑定的真正诡计最终会发生在Rust中,所以让我们来看看.


# #![allow(unused_variables)]
#fn main() {
// original input to `#[wasm_bindgen]` omitted ...

#[export_name = "foo_new"]
pub extern fn __wasm_bindgen_generated_Foo_new(arg0: i32) -> u32
    let ret = Foo::new(arg0);
    Box::into_raw(Box::new(WasmRefCell::new(ret))) as u32
}

#[export_name = "foo_get"]
pub extern fn __wasm_bindgen_generated_Foo_get(me: u32) -> i32 {
    let me = me as *mut WasmRefCell<Foo>;
    wasm_bindgen::__rt::assert_not_null(me);
    let me = unsafe { &*me };
    return me.borrow().get();
}

#[export_name = "foo_set"]
pub extern fn __wasm_bindgen_generated_Foo_set(me: u32, arg1: i32) {
    let me = me as *mut WasmRefCell<Foo>;
    ::wasm_bindgen::__rt::assert_not_null(me);
    let me = unsafe { &*me };
    me.borrow_mut().set(arg1);
}

#[no_mangle]
pub unsafe extern fn __wbindgen_foo_free(me: u32) {
    let me = me as *mut WasmRefCell<Foo>;
    wasm_bindgen::__rt::assert_not_null(me);
    (*me).borrow_mut(); // ensure no active borrows
    drop(Box::from_raw(me));
}
#}

和以前一样,这是从实际输出中清除的,但它与发生的事情是一样的想法!在这里,我们可以看到每个函数的垫片以及用于解除分配实例的垫片Foo. 回想一下,今天唯一有效的主义类型是数字,所以我们需要对所有人进行调整Foo变成一个u32,目前通过Box (喜欢std::unique_ptr在C ++) . 但请注意,这里有一个额外的图层,WasmRefCell. 这种类型与RefCell并且可以大部分被掩盖.

如果你感兴趣的话,这种类型的目的是在一个混乱猖獗的世界 (JS) 中维护Rust对别名的保证. 具体是&Foo类型意味着可以有你想要的尽可能多的alaising,但至关重要&mut Foo意味着它是唯一指向数据的指针 (没有其他指针) &Foo到同一个实例存在) . 该RefCelllibstd中的type是一种在运行时动态强制执行此操作的方法 (与通常发生的编译时相反) . 烘烤WasmRefCell在这里是相同的想法,添加通常在编译时发生的别名的运行时检查. 这是目前特定于Rust的功能,实际上并不属于wasm-bindgen工具本身,它只是在Rust生成的代码 (又名#[wasm_bindgen]属性) .

自定义导入行为

#[wasm_bindgen]macro支持大量配置,用于精确控制导出的导出方式以及它们在JS中生成的内容. 本节旨在有希望成为可能性的详尽参考!

  • readonly- 当附加到pubstruct field这表明它只是来自JS,并且不会生成setter.

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    pub struct Foo {
        pub first: u32,
        #[wasm_bindgen(readonly)]
        pub second: u32,
    }
    #}

    在这里first字段将是JS可读写的,但是second领域将是一个readonly在JS中没有实现setter并且尝试设置它的字段将引发异常.

  • constructor- 当附加到Rust"构造函数"时,它将使生成的JS绑定可调用为new Foo(), 例如:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    pub struct Foo {
        contents: u32,
    }
    
    #[wasm_bindgen]
    impl Foo {
        #[wasm_bindgen(constructor)]
        pub fn new() -> Foo {
            Foo { contents: 0 }
        }
    
        pub fn get_contents(&self) -> u32 {
            self.contents
        }
    }
    #}

    这可以在JS中用作:

    import { Foo } from './my_module';
    
    const f = new Foo();
    console.log(f.get_contents());
    

从JS导入函数

现在我们已经向JS导出了一些丰富的功能,现在也是时候导入一些了!这里的目标是基本实现JSimportRust中的语句,具有花哨的类型和所有.

首先,假设我们反转上面的函数,而不是想在JS中生成问候语,但是从Rust调用它. 例如,我们可能会:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen(module = "./greet")]
extern {
    fn greet(a: &str) -> String;
}

fn other_code() {
    let greeting = greet("foo");
    // ...
}
#}

导入的基本思想与导出相同,因为我们在JS和Rust中都有垫片进行必要的翻译. 让我们首先看看JS Shim的实际应用:

import * as wasm from './foo_bg';

import { greet } from './greet';

// ...

export function __wbg_f_greet(ptr0, len0, wasmretptr) {
  const [retptr, retlen] = passStringToWasm(greet(getStringFromWasm(ptr0, len0)));
  (new Uint32Array(wasm.memory.buffer))[wasmretptr / 4] = retlen;
  return retptr;
}

getStringFromWasmpassStringToWasm和我们之前看到的一样,和我一样__wbindgen_object_drop_ref远远高于我们现在从我们的模块出来的这个奇怪的出口!该__wbg_f_greet功能是由...产生的wasm-bindgen实际上是进口的foo.wasm模块.

生成的foo.js我们看到从中进口./greet模块与greetname (是Rust中的函数导入说) 然后是__wbg_f_greet功能是填充导入.

这里有一些棘手的ABI业务,所以让我们来看看生成的Rust. 像之前一样简化了实际生成的内容.


# #![allow(unused_variables)]
#fn main() {
extern fn greet(a: &str) -> String {
    extern {
        fn __wbg_f_greet(a_ptr: *const u8, a_len: usize, ret_len: *mut usize) -> *mut u8;
    }
    unsafe {
        let a_ptr = a.as_ptr();
        let a_len = a.len();
        let mut __ret_strlen = 0;
        let mut __ret_strlen_ptr = &mut __ret_strlen as *mut usize;
        let _ret = __wbg_f_greet(a_ptr, a_len, __ret_strlen_ptr);
        String::from_utf8_unchecked(
            Vec::from_raw_parts(_ret, __ret_strlen, __ret_strlen)
        )
    }
}
#}

在这里我们可以看到greet函数是生成的,但它主要只是一个垫片__wbg_f_greet我们正在打电话的功能. 参数的ptr / len对作为两个参数传递,对于返回值,我们在直接接收返回的指针时间接接收一个值 (长度) .

从JS导入一个类

就像我们开始导出后的功能一样,我们也想导入!现在我们已经出口了class对于JS,我们也希望能够在Rust中导入类以调用方法等. 由于JS类通常只是JS对象,因此这里的绑定看起来与上面描述的JS对象绑定非常相似.

像往常一样,让我们​​来看一个例子吧!


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen(module = "./bar")]
extern {
    type Bar;

    #[wasm_bindgen(constructor)]
    fn new(arg: i32) -> Bar;

    #[wasm_bindgen(js_namespace = Bar)]
    fn another_function() -> i32;

    #[wasm_bindgen(method)]
    fn get(this: &Bar) -> i32;

    #[wasm_bindgen(method)]
    fn set(this: &Bar, val: i32);

    #[wasm_bindgen(method, getter)]
    fn property(this: &Bar) -> i32;

    #[wasm_bindgen(method, setter)]
    fn set_property(this: &Bar, val: i32);
}

fn run() {
    let bar = Bar::new(Bar::another_function());
    let x = bar.get();
    bar.set(x + 3);

    bar.set_property(bar.property() + 6);
}
#}

不像我们以前的进口,这个更健谈!请记住,其中一个目标wasm-bindgen是尽可能使用原生的Rust语法,所以这主要是为了使用#[wasm_bindgen]属性来解释在Rust中写下的内容. 现在这里有一些属性注释,所以让我们一个接一个地进行:

  • #[wasm_bindgen(module = "./bar")]- 在导入之前看到这是声明所有后续功能都是导入表单的地方. 例如Bar类型将从中导入./bar模块.
  • type Bar- 这是一个JS类的声明,作为Rust中的一个新类型. 这意味着一种新型Bar生成的是"不透明的",但表示为内部包含aJsValue. 我们稍后会看到更多.
  • #[wasm_bindgen(constructor)]- 这表明绑定的名称实际上并未在JS中使用,而是转换为new Bar(). 此函数的返回值必须是裸类型,如Bar.
  • #[wasm_bindgen(js_namespace = Bar)]- 此属性指示函数声明是通过命名空间命名的BarJS中的类.
  • #[wasm_bindgen(static_method_of = SomeJsClass)]- 这个属性类似于js_namespace,但不是生成一个自由函数,而是生成一个静态方法SomeJsClass.
  • #[wasm_bindgen(method)]- 最后,此属性表示将发生方法调用. 第一个参数必须是JS结构,比如Bar,JS中的调用看起来像Bar.prototype.set.call(...).

考虑到所有这些,让我们来看看生成的JS.

import * as wasm from './foo_bg';

import { Bar } from './bar';

// other support functions omitted...

export function __wbg_s_Bar_new() {
  return addHeapObject(new Bar());
}

const another_function_shim = Bar.another_function;
export function __wbg_s_Bar_another_function() {
  return another_function_shim();
}

const get_shim = Bar.prototype.get;
export function __wbg_s_Bar_get(ptr) {
  return shim.call(getObject(ptr));
}

const set_shim = Bar.prototype.set;
export function __wbg_s_Bar_set(ptr, arg0) {
  set_shim.call(getObject(ptr), arg0)
}

const property_shim = Object.getOwnPropertyDescriptor(Bar.prototype, 'property').get;
export function __wbg_s_Bar_property(ptr) {
  return property_shim.call(getObject(ptr));
}

const set_property_shim = Object.getOwnPropertyDescriptor(Bar.prototype, 'property').set;
export function __wbg_s_Bar_set_property(ptr, arg0) {
  set_property_shim.call(getObject(ptr), arg0)
}

就像从JS导入函数一样,我们可以看到为所有相关函数生成了一堆填充程序. 该new静态功能有#[wasm_bindgen(constructor)]属性,这意味着它应该实际调用它而不是任何特定的方法new而是构造函数 (正如我们在这里看到的) . 静态功能another_function然而,被派遣为Bar.another_function.

getset函数是方法,所以他们经历Bar.prototype,否则他们的第一个参数隐含地是通过加载的JS对象本身getObject就像我们之前看到的

一些真正的肉开始出现在Rust的一面,所以让我们来看看:


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

impl Bar {
    fn new() -> Bar {
        extern {
            fn __wbg_s_Bar_new() -> u32;
        }
        unsafe {
            let ret = __wbg_s_Bar_new();
            Bar { obj: JsValue::__from_idx(ret) }
        }
    }

    fn another_function() -> i32 {
        extern {
            fn __wbg_s_Bar_another_function() -> i32;
        }
        unsafe {
            __wbg_s_Bar_another_function()
        }
    }

    fn get(&self) -> i32 {
        extern {
            fn __wbg_s_Bar_get(ptr: u32) -> i32;
        }
        unsafe {
            let ptr = self.obj.__get_idx();
            let ret = __wbg_s_Bar_get(ptr);
            return ret
        }
    }

    fn set(&self, val: i32) {
        extern {
            fn __wbg_s_Bar_set(ptr: u32, val: i32);
        }
        unsafe {
            let ptr = self.obj.__get_idx();
            __wbg_s_Bar_set(ptr, val);
        }
    }

    fn property(&self) -> i32 {
        extern {
            fn __wbg_s_Bar_property(ptr: u32) -> i32;
        }
        unsafe {
            let ptr = self.obj.__get_idx();
            let ret = __wbg_s_Bar_property(ptr);
            return ret
        }
    }

    fn set_property(&self, val: i32) {
        extern {
            fn __wbg_s_Bar_set_property(ptr: u32, val: i32);
        }
        unsafe {
            let ptr = self.obj.__get_idx();
            __wbg_s_Bar_set_property(ptr, val);
        }
    }
}

impl WasmBoundary for Bar {
    // ...
}

impl ToRefWasmBoundary for Bar {
    // ...
}
#}

在Rust,我们看到了一种新型,Bar,是为此类的导入生成的. 方式Bar内部包含一个JsValue作为一个例子Bar用于表示存储在模块的堆栈/板中的JS对象. 然后,这与我们在开始时看到JS对象的工作方式大致相同.

打电话时Bar::new我们将得到一个包含在其中的索引Bar (这本身就是一个u32在内存中被剥离) . 然后,每个函数都将索引作为第一个参数传递,否则将转发Rust中的所有内容.

自定义导入行为

#[wasm_bindgen]macro支持大量配置,用于精确控制导入导入的方式以及它们在JS中映射的内容. 本节旨在有希望成为可能性的详尽参考!

  • moduleversion- 我们见过module到目前为止表明我们可以从哪里导入物品version也允许:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen(module = "moment", version = "^2.0.0")]
    extern {
        type Moment;
        fn moment() -> Moment;
        #[wasm_bindgen(method)]
        fn format(this: &Moment) -> String;
    }
    #}

    modulekey用于配置从中导入每个项目的模块. 该versionkey不会影响生成的wasm本身,而是对像这样的工具的信息指令[WASM包-]. 像wasm-pack这样的工具会生成一个package.json为了你和version列在这里的时候module也是一个NPM包,将对应于写下来的内容package.json.

    换句话说就是用法module作为NPM包的名称和version因为版本要求允许您在Rust中内联,它依赖于NPM生态系统并从这些包中导入功能. 当捆绑一个像工具[WASM包-]一切都将自动与捆绑商联系,你应该好好去!

    请注意version需要如果module不是从一开始./. 如果module以. . 开始./那么提供版本是错误的.

  • catch- 此属性允许捕获JS异常. 这可以附加到任何导入的函数,函数必须返回一个Result哪里Err有效载荷是一个JsValue,像这样:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        #[wasm_bindgen(catch)]
        fn foo() -> Result<(), JsValue>;
    }
    #}

    如果导入的函数抛出异常则Err将返回,但引发的异常,以及其他方式Ok与函数的结果一起返回.

    默认wasm-bindgen当wasm调用最终抛出异常的JS函数时,将不执行任何操作. 现在,wasm规范不支持堆栈展开,因此不支持Rust代码不会执行析构函数. 不幸的是,这可能导致Rust中的内存泄漏,但是只要wasm实现捕获异常,我们一定会添加支持!

  • constructor- 这用于表示被绑定的函数应该实际转换为anewJS中的构造函数. 最后一个参数必须是从JS导入的类型,它将在JS中使用:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(constructor)]
        fn new() -> Foo;
    }
    #}

    这将附上new功能Foo类型 (暗示constructor并且在JS中调用此函数时它将等效于new Foo().

  • method- 这是通过方法等向导入的对象添加方法或以其他方式访问对象的属性的门户. 这应该是为了做相同的表达式foo.bar()在JS中.

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(method)]
        fn work(this: &Foo);
    }
    #}

    a的第一个参数method注释必须是附加方法所附类型的借用引用 (不可变,共享) . 在这种情况下,我们可以像这样调用这个方法foo.work()在JS (其中foo有类型Foo) .

    在JS中,这个调用将对应于访问Foo.prototype.work然后在调用import时调用它. 注意method默认情况下意味着经历prototype获取函数指针.

  • js_namespace- 此属性指示通过特定名称空间访问JS类型. 比如说WebAssembly.ModuleAPI都是通过WebAssembly命名空间. 该js_namespace可以应用于任何导入,并且只要生成的JS尝试引用名称 (如类或函数名称) ,就可以通过此命名空间访问它.

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        #[wasm_bindgen(js_namespace = console)]
        fn log(s: &str);
    }
    #}

    这是如何绑定的示例console.log(x)在Rust. 该log函数将在Rust模块中可用,并将作为调用console.log在JS中.

  • gettersetter- 这两个属性可以结合使用method表明这是一个getter或setter方法. 一个getter默认情况下,-tagged函数访问与getter函数同名的JS属性. 一个setter的功能名称目前需要以"set"开头_"它访问的属性是"set之后的后缀_".

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(method, getter)]
        fn property(this: &Foo) -> u32;
        #[wasm_bindgen(method, setter)]
        fn set_property(this: &Foo, val: u32);
    }
    #}

    我们在这里导入Foo键入并定义访问每个对象的能力property属性. 这里的第一个函数是一个getter,将在Rust中可用foo.property(),后者是可以访问的setterfoo.set_property(2). 请注意,这两个函数都有this他们被标记的论点method.

    最后,您还可以将参数传递给gettersetter用于配置访问哪个属性的属性. 显式指定属性时,方法名称没有限制. 例如,以下内容相当于上述内容:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(method, getter = property)]
        fn assorted_method_name(this: &Foo) -> u32;
        #[wasm_bindgen(method, setter = "property")]
        fn some_other_method_name(this: &Foo, val: u32);
    }
    #}

    JS中的属性可以通过访问Object.getOwnPropertyDescriptor. 请注意,这通常仅适用于类类定义的属性,这些属性不仅仅是任何旧对象的附加属性. 要访问对象上的任何旧属性,我们可以使用...

  • structural- 这是一面旗帜method注释表明应该以结构方式访问被访问的方法 (或具有getter / setter的属性) . 例如,方法是通过访问prototype和属性直接在对象上访问而不是通过Object.getOwnPropertyDescriptor.

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(method, structural)]
        fn bar(this: &Foo);
        #[wasm_bindgen(method, getter, structural)]
        fn baz(this: &Foo) -> u32;
    }
    #}

    这里的类型,Foo,不需要存在于JS中 (它没有被引用) . 相反,wasm-bindgen将生成将访问传入的JS值的填充程序bar财产到或baz财产 (取决于职能) .

  • js_name = foo- 这可以用于绑定JS中的不同函数,而不是Rust中定义的标识符. 例如,您还可以在JS中为多态函数定义多个签名:

    
    # #![allow(unused_variables)]
    #fn main() {
    #[wasm_bindgen]
    extern {
        type Foo;
        #[wasm_bindgen(js_namespace = console, js_name = log)]
        fn log_string(s: &str);
        #[wasm_bindgen(js_namespace = console, js_name = log)]
        fn log_u32(n: u32);
        #[wasm_bindgen(js_namespace = console, js_name = log)]
        fn log_many(a: u32, b: JsValue);
    }
    #}

    所有这些功能都会调用console.log在Rust中,但每个标识符在Rust中只有一个签名.

Rust类型转换

以前,当值输入Rust时,我们已经看到大多数类型转换的删节版本. 在这里,我们将深入探讨如何实现这一点. 转换值有两类特征,将值从Rust转换为JS的特性和反过来的特征.

从Rust到JS

首先让我们来看看从Rust到JS:


# #![allow(unused_variables)]
#fn main() {
pub trait IntoWasmAbi: WasmDescribe {
    type Abi: WasmAbi;
    fn into_abi(self, extra: &mut Stack) -> Self::Abi;
}
#}

就是这样!这实际上是目前将Rust值转换为JS值所需的唯一特征. 这里有几点:

  • 我们会去的WasmDescribe在本节后面
  • 相关类型Abi是实际生成的作为wasm导出的参数. 界限WasmAbi仅适用于类似的类型u32f64那些可以放在边界上并无损传输的.
  • 最后我们有了into_abi功能,返回Abi实际传递给JS的关联类型. 还有这个Stack然而,参数. 并非所有Rust值都可以32位通信Stack参数允许传输更多数据,稍后解释.

此特征适用于所有可转换为JS的类型,并且在codegen期间无条件使用. 例如,你经常会看到IntoWasmAbi for Foo但也IntoWasmAbi for &'a Foo.

IntoWasmAbi特征在两个地方使用. 首先,它用于将Rust导出函数的返回值转换为JS. 其次,它用于转换导入到Rust的JS函数的Rust参数.

从JS到Rust

不幸的是,从JS到Rust的上面相反的方向有点复杂. 这里我们有三个特点:


# #![allow(unused_variables)]
#fn main() {
pub trait FromWasmAbi: WasmDescribe {
    type Abi: WasmAbi;
    unsafe fn from_abi(js: Self::Abi, extra: &mut Stack) -> Self;
}

pub trait RefFromWasmAbi: WasmDescribe {
    type Abi: WasmAbi;
    type Anchor: Deref<Target=Self>;
    unsafe fn ref_from_abi(js: Self::Abi, extra: &mut Stack) -> Self::Anchor;
}

pub trait RefMutFromWasmAbi: WasmDescribe {
    type Abi: WasmAbi;
    type Anchor: DerefMut<Target=Self>;
    unsafe fn ref_mut_from_abi(js: Self::Abi, extra: &mut Stack) -> Self::Anchor;
}
#}

FromWasmAbi相对简单,基本上是相反的IntoWasmAbi. 它采用ABI参数 (通常与...相同) IntoWasmAbi::Abi) 然后辅助堆栈生成一个实例Self. 该特征主要用于那些类型有内部生命或参考.

这里的后两个特征大部分是相同的,用于生成引用 (共享和可变引用) . 他们看起来几乎一样FromWasmAbi除了他们回来了Anchor实现的类型Deref特质而不是Self.

Ref*例如,traits允许在引用而不是裸类型的函数中使用参数&str,&JsValue, 要么&[u8]. 该Anchor这里需要确保生命周期不会超出一个函数调用并保持匿名.

From*traistic系列用于将Rust导出函数中的Rust参数转换为JS. 它们还用于导入Rust的JS函数的返回值.

全球堆栈

上面提到的并非所有Rust类型都适合32位. 虽然我们可以沟通f64我们不一定能够使用所有的比特. 类似的&str需要传达两个项目,一个指针和一个长度 (64位) . 其他类型如&Closure<Fn()>有更多的信息要传输.

因此,我们需要一种通过函数签名传递更多数据的方法. 虽然我们可以添加更多的参数,但在闭包世界中这有点难以做到,因为代码生成并不像程序宏那样动态. 因此,"全局堆栈"用于传输函数调用的额外数据.

全局堆栈是wasm模块中固定大小的静态分配. 这个堆栈是从JS到Rust或Rust ot JS的任何一个函数调用的临时临时空间. Rust和生成的JS填充程序都有指向此全局堆栈的指针,并将从中读取/写入信息.

每当我们想通过时使用这个方案&str从JS到Rust,我们可以将指针作为实际的ABI参数传递,然后将长度放在全局堆栈的下一个位置.

Stack上面转换特征的参数如下所示:


# #![allow(unused_variables)]
#fn main() {
pub trait Stack {
    fn push(&mut self, bits: u32);
    fn pop(&mut self) -> u32;
}
#}

这里使用特征来促进测试,但通常调用最终不会在运行时被虚拟调度.

将类型传达给wasm-bindgen

在将Rust / JS类型相互转换时谈论的最后一个方面是如何实际传达这些信息. 该#[wasm_bindgen]宏正在运行Rust代码的语法 (未解析) 结构,然后负责生成信息wasm-bindgenCLI工具稍后会读取.

为实现这一目标,采取了一种略微不同寻常的方法. 有关Rust代码结构的静态信息通过JSON (当前) 序列化到wasm可执行文件的自定义部分. 其他信息,例如类型实际上是什么,遗憾的是直到后来在编译器中才知道由于关联类型投影和typedef之类的事情. 事实证明,我们想要传达"丰富"类型FnMut(String, Foo, &JsValue)到了wasm-bindgenCLI,处理这一切非常棘手!

要解决这个问题了#[wasm_bindgen]宏生成可执行功能其中"描述了进口或出口的类型签名". 这些可执行函数是什么的WasmDescribe特质是关于:


# #![allow(unused_variables)]
#fn main() {
pub trait WasmDescribe {
    fn describe();
}
#}

虽然看似简单,但这个特性实际上非常重要. 当你写,这样的导出:


# #![allow(unused_variables)]
#fn main() {
#[wasm_bindgen]
fn greet(a: &str) {
    // ...
}
#}

除了我们上面谈到的垫片,JS生成宏生成类似的东西:

#[no_mangle]
pub extern fn __wbindgen_describe_greet() {
    <Fn(&str)>::describe();
}

或者换句话说,它会生成调用describe功能. 这样做了__wbindgen_describe_greetshim是导入/导出的类型布局的编程描述. 然后执行这些操作wasm-bindgen跑!这些执行依赖于调用的导入__wbindgen_describe通过一个u32到主机,当多次调用时给出一个Vec<u32>有效. 这个Vec<u32>然后可以被重新分解成一个enum Descriptor它完全描述了一种类型.

总而言之,这有点迂回,但根本不会对生成的代码或运行时产生任何影响. 所有这些描述符函数都从发出的wasm文件中删除.

团队

wasm-bindgen跟着rustwasm这里描述了组织的治理:

  • 所有拉取请求 (包括由团队成员提出的请求) 必须至少由另一个团队成员批准.

  • 关于设计,架构,突破性变化,权衡等的更大,更细微的决策是由 全体团队 做出的.

成员

alexcrichton fitzgen spastorino ohanar jonathan-s
sendilkumarn belfz