Rust 🦀 and WebAssembly 🕸

这本小书描述了如何将RustWebAssembly一起使用。

这本书是为谁写的?

本书是为那些对将Rust编译成 WebAssembly 以实现快速、可靠的网络代码感兴趣的人编写的。汇编成WebAssembly,以便在网络上获得快速可靠的代码。你应该了解一些Rust,并熟悉 JavaScript,HTML,和 CSS。你不需要成为其中任何一个方面的专家。

还不了解Rust吗? 先从 The Rust Programming Language 开始

不懂JavaScript、HTML或CSS?在MDN上了解他们的情况

如何阅读此书

你应该先阅读一起使用Rust和WebAssembly的动机,以及熟悉背景和概念

教程是为了从头到尾阅读而写的。你应该跟随:自己编写、编译和运行教程中的代码。如果你以前没有一起使用过Rust和WebAssembly,那就从教程开始吧!

参考章节 可以按任何顺序进行阅读。

💡 Tip: 你可以通过点击页面上面的搜索 🔍 图标或按下 s 键 进行搜索

参与完善本书

这本书是开放源代码的! 发现一个错别字?我们是否忽略了什么?

为什么是Rust和WebAssembly?

低水平控制与高水平的人机工程学

JavaScript网络应用程序很难达到并保持可靠的性能。JavaScript的动态类型系统和垃圾收集的暂停并没有帮助。看似很小的代码改动,如果你不小心偏离了JIT的快乐路径,就会导致性能的急剧下降。你不小心偏离了JIT的快乐路径。

Rust 为程序员提供了低级控制和可靠的性能。 它没有困扰 JavaScript 的非确定性垃圾收集暂停。 程序员可以控制间接、单态化和内存布局。

小的 .wasm 尺寸

代码大小非常重要,因为 .wasm 必须通过网络下载。 Rust 缺少运行时,支持较小的 .wasm 大小,因为没有像垃圾收集器那样包含额外的膨胀。 您只需为实际使用的功能付费(按代码大小)。

不需要 重写一切

不需要丢弃现有的代码库。 您可以首先将您对性能最敏感的 JavaScript 函数移植到 Rust,以获得直接的好处。 如果您愿意,你甚至可以到此为止。

与其程序可以很好的结合

Rust 和 WebAssembly 与现有的 JavaScript 工具集成。它支持 ECMAScript 模块,你可以继续使用你已经喜欢的工具,如 npm 和 Webpack。

您期待的便利设施

Rust 拥有开发人员所期望的现代设施,例如:

  • 强大的包管理 cargo,

  • 富有表现力(和零成本)的抽象,

  • 和一个热情的社区! 😊

背景和概念

本节提供了进入 Rust 和 WebAssembly 的必要背景。开发的必要背景。

什么是WebAssembly?

WebAssembly(wasm)是一种简单的机器模型和可执行格式,有一个广泛的规范。它被设计成可移植、紧凑,并以或接近原生速度执行。

作为一种编程语言,WebAssembly是由两种格式组成的。表示相同的结构,只是方式不同而已:

  1. .wat 文本格式(称为wat,表示 "WebAssembly Text")使用 S-expressions,与 Lisp 系列语言有一些相似之处 像 Scheme 和 Clojure。
  2. .wasm 二进制格式是较低级的,目的是让 wasm 虚拟机直接使用。它在概念上类似于 ELF 和 Mach-O

作为参考,这里有一个阶乘函数,在 wat:

(module
  (func $fac (param f64) (result f64)
    local.get 0
    f64.const 1
    f64.lt
    if (result f64)
      f64.const 1
    else
      local.get 0
      local.get 0
      f64.const 1
      f64.sub
      call $fac
      f64.mul
    end)
  (export "fac" (func $fac)))

如果您对 wasm 文件的外观感到好奇,可以将 wat2wasm demo 与上述代码一起使用。

线性内存

WebAssembly有一个非常简单的内存模型。一个wasm模块可以访问一个单一的 "线性内存",这基本上是一个字节的平面阵列。这个[内存可以按页面大小(64K)的倍数增长。它不能被缩小。

WebAssembly 只是用于 Web 吗?

尽管它目前在 JavaScript 和 Web 社区中普遍受到关注,但 wasm 对其主机环境不做任何假设。因此,推测 wasm 将成为一种 "可移植的可执行 "格式,在未来被用于各种情况下是有意义的。然而,截至今天,wasm 主要与JavaScript(JS)有关,而 JavaScrip t有很多种类(包括 Web 和Node.js上的)。

教程: 康威生命游戏

这是一个用 Rust 和 WebAssembly 实现康威生命游戏的教程。

这个教程是为谁准备的?

本教程适用于已经有基本 Rust 和 JavaScript 经验的人,并希望学习如何一起使用 Rust、WebAssembly 和 JavaScript。

你应该能够自如地阅读和编写基本的Rust、JavaScript 和 HTML。你绝对不需要是一个专家。

我将学到什么?

  • 如何设置Rust工具链以编译成WebAssembly。

  • 一个用于开发由Rust、WebAssembly、JavaScript、HTML和CSS组成的多语言程序的工作流程。

  • 如何设计 API 以最大限度地利用 Rust 和 WebAssembly 的优势,同时也是 JavaScript 的优势。

  • 如何调试由Rust编译的WebAssembly模块。

  • 如何对Rust和WebAssembly程序进行时间剖析以使其更快。

  • 如何确定 Rust 和 WebAssembly 程序的大小,使.wasm二进制文件更小,更快通过网络下载。

设置

本节描述了如何设置工具链,将 Rust 程序编译为 WebAssembly,并将其集成到 JavaScript 中。到 WebAssembly,并将其集成到 JavaScript 中。

Rust 工具链

你将需要标准的 Rust 工具链,包括 rustup, rustc, 和 cargo.

按照这些说明来安装Rust工具链。

Rust 和 WebAssembly 的经验是乘着 Rust 发布的列车到了稳定期! 这意味着我们不需要任何实验性功能标志。然而,我们确实 需要 Rust 1.30 或更新版本。

wasm-pack

wasm-pack 是您构建、测试和发布 Rust 生成的 WebAssembly 的一站式商店。

Get wasm-pack here!

cargo-generate

cargo-generate 通过利用预先存在的 git 存储库作为模板,帮助您快速启动并运行新的 Rust 项目。

使用如下命令安装 cargo-generate:

cargo install cargo-generate

npm

npm 是 JavaScript 的包管理器。 我们将使用它来安装和运行 JavaScript 打包器和开发服务器。 在教程结束时,我们将把我们编译的 .wasm 发布到 npm 仓库。

按照这些说明进行安装 npm.

如果您已经安装了 npm,请使用以下命令确保它是最新的:

npm install npm@latest -g

Hello, World!

本节将告诉你如何构建和运行你的第一个 Rust 和 WebAssembly 的程序:一个提醒 "Hello, World!" 的网页。

在开始之前,请确保你已经遵循了设置说明

克隆项目模板

该项目模板预先配置了合理的默认值,因此你可以快速构建、集成和打包你的代码用于 Web。

用这个命令克隆项目模板:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

这将提示你新项目的名称。我们将使用 "wasm-game-of-life".

wasm-game-of-life

里面有什么

进入新的 wasm-game-of-life 项目

cd wasm-game-of-life

并让我们看看它的内容。

wasm-game-of-life/
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
    ├── lib.rs
    └── utils.rs

让我们详细看一下其中的几个文件。

wasm-game-of-life/Cargo.toml

Cargo.toml 文件为 cargo、Rust 的包管理器和构建工具指定依赖项和元数据。 这个预先配置了一个 wasm-bindgen 依赖项,一些我们稍后将深入研究的可选依赖项,以及正确初始化的 crate-type 以生成 .wasm 库。

wasm-game-of-life/src/lib.rs

src/lib.rs 文件是我们正在编译为 WebAssembly 的 Rust crate 的根目录。 它使用 wasm-bindgen 与 JavaScript 交互。 它导入了 window.alert JavaScript 函数,并导出了 greet Rust 函数,它会提醒一条问候消息。


#![allow(unused)]
fn main() {
mod utils;

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm-game-of-life!");
}
}

wasm-game-of-life/src/utils.rs

src/utils.rs 模块提供了一些通用的工具来让 Rust 编译成 WebAssembly 变得更容易。 我们将在本教程后面更详细地了解其中一些实用程序,例如当我们查看 调试我们的 wasm 代码 时,但我们现在可以忽略此文件。

构建项目

我们使用 wasm-pack 来编排以下构建步骤:

  • 确保我们有 Rust 1.30 或更新版本,并且通过 rustup 安装了 wasm32-unknown-unknown 目标,
  • 通过 cargo 将我们的 Rust 源编译为 WebAssembly .wasm 二进制文件,
  • 使用 wasm-bindgen 生成 JavaScript API,以便使用我们的 Rust 生成的 WebAssembly。

要做到这一切,在项目目录内运行这个命令。

wasm-pack build

构建完成后,我们可以在 pkg 目录中找到它的工件,它应该有以下内容:

pkg/
├── package.json
├── README.md
├── wasm_game_of_life_bg.wasm
├── wasm_game_of_life.d.ts
└── wasm_game_of_life.js

README.md 文件是从主项目复制的,但其他文件是全新的。

wasm-game-of-life/pkg/wasm_game_of_life_bg.wasm

.wasm 文件是由 Rust 编译器从我们的 Rust 源代码生成的 WebAssembly 二进制文件。 它包含我们所有 Rust 函数和数据的编译到 wasm 版本。 例如,它有一个导出的“问候”功能。

wasm-game-of-life/pkg/wasm_game_of_life.js

.js 文件由 wasm-bindgen 生成,包含用于将 DOM 和 JavaScript 函数导入 Rust 并将 WebAssembly 函数的良好 API 暴露给 JavaScript 的 JavaScript 胶水。 例如,有一个 JavaScript 的 greet 函数包装了从 WebAssembly 模块导出的 greet 函数。 现在,这种粘合剂并没有做太多事情,但是当我们开始在 wasm 和 JavaScript 之间来回传递更多有趣的值时,它将帮助引导这些值跨越边界。

import * as wasm from './wasm_game_of_life_bg';

// ...

export function greet() {
    return wasm.greet();
}

wasm-game-of-life/pkg/wasm_game_of_life.d.ts

.d.ts 文件包含 JavaScript 胶水的 TypeScript 类型声明。 如果您使用的是 TypeScript,您可以检查对 WebAssembly 函数的调用类型,并且您的 IDE 可以提供自动完成和建议! 如果您不使用 TypeScript,则可以放心地忽略此文件。

export function greet(): void;

wasm-game-of-life/pkg/package.json

package.json 文件包含有关生成的 JavaScript 和 WebAssembly 包的元数据。 npm 和 JavaScript 捆绑器使用它来确定包之间的依赖关系、包名称、版本和一堆其他东西。 它帮助我们与 JavaScript 工具集成,并允许我们将我们的包发布到 npm。

{
  "name": "wasm-game-of-life",
  "collaborators": [
    "Your Name <your.email@example.com>"
  ],
  "description": null,
  "version": "0.1.0",
  "license": null,
  "repository": null,
  "files": [
    "wasm_game_of_life_bg.wasm",
    "wasm_game_of_life.d.ts"
  ],
  "main": "wasm_game_of_life.js",
  "types": "wasm_game_of_life.d.ts"
}

将其放入网页

为了获取我们的 wasm-game-of-life 包并在网页中使用它,我们使用 create-wasm-app JavaScript 项目模板

wasm-game-of-life 目录中运行此命令:

npm init wasm-app www

这是我们新的 wasm-game-of-life/www 子目录包含的内容:

wasm-game-of-life/www/
├── bootstrap.js
├── index.html
├── index.js
├── LICENSE-APACHE
├── LICENSE-MIT
├── package.json
├── README.md
└── webpack.config.js

再一次,让我们仔细看看其中的一些文件。

wasm-game-of-life/www/package.json

这个 package.json 预先配置了 webpackwebpack-dev-server 依赖,以及对 hello-wasm-pack 的依赖,它是已发布到 npm 的 wasm-pack-template 包。

wasm-game-of-life/www/webpack.config.js

此文件配置 webpack 及其本地开发服务器。 它是预先配置的,你根本不需要调整它来让 webpack 和它的本地开发服务器工作。

wasm-game-of-life/www/index.html

这是网页的根 HTML 文件。 除了加载 bootstrap.js 之外,它没有做太多事情,bootstrap.js 是一个非常薄的 index.js 包装器。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <script src="./bootstrap.js"></script>
  </body>
</html>

wasm-game-of-life/www/index.js

index.js 是我们网页的 JavaScript 的主要入口点。 它导入 hello-wasm-pack npm 包,其中包含默认的 wasm-pack-template 编译的 WebAssembly 和 JavaScript 胶水,然后调用 hello-wasm-packgreet 函数。

import * as wasm from "hello-wasm-pack";

wasm.greet();

安装依赖项

首先,通过在 wasm-game-of-life/www 子目录中运行 npm install 来确保本地开发服务器及其依赖项已安装:

npm install

这个命令只需要运行一次,就会安装webpack JavaScript bundler 和它的开发服务器。

请注意,使用 Rust 和 WebAssembly 不需要 webpack, 这里它只是我们为了方便而选择的打包器和开发服务器 Parcel 和 Rollup 还应该支持将 WebAssembly 导入为 ECMAScript 模块 你也可以使用 Rust 和 WebAssembly 没有捆绑器 如果你愿意!

www 中使用我们本地的 wasm-game-of-life

我们不想使用 npm 中的 hello-wasm-pack 包,而是想使用我们本地的 wasm-game-of-life 包。 这将使我们能够逐步开发我们的生命游戏程序。

打开 wasm-game-of-life/www/package.json 并在 "devDependencies" 旁边添加 "dependencies" 字段,包括一个 "wasm-game-of-life": "file :../pkg" 条目:

{
  // ...
  "dependencies": {                     // Add this three lines block!
    "wasm-game-of-life": "file:../pkg"
  },
  "devDependencies": {
    //...
  }
}

接下来,修改 wasm-game-of-life/www/index.js 以导入 wasm-game-of-life 而不是 hello-wasm-pack 包:

import * as wasm from "wasm-game-of-life";

wasm.greet();

由于我们声明了一个新的依赖项,我们需要安装它:

npm install

我们的网页现在可以在本地提供服务了!

本地服务

接下来,为开发服务器打开一个新终端。 在新终端中运行服务器让我们让它在后台运行,同时不会阻止我们运行其他命令。 在新终端中,从 wasm-game-of-life/www 目录中运行以下命令:

npm run start

将您的 Web 浏览器导航到 http://localhost:8080/,您应该会看到一条警告消息:

Screenshot of the "Hello, wasm-game-of-life!" Web page alert

任何时候进行更改并希望它们反映在 http://localhost:8080/ 上,只需在 wasm-game-of-life 目录中运行 wasm-pack build 命令.

Anytime you make changes and want them reflected on http://localhost:8080/, just re-run the wasm-pack build command within the wasm-game-of-life directory.

练习

  • 修改 wasm-game-of-life/src/lib.rs 中的 greet 函数,采用 name: &str 参数来自定义警报消息,并将你的名字从内部传递给 greet 函数 wasm-game-of-life/www/index.js。 使用 wasm-pack build 重新构建 .wasm 二进制文件,然后在 Web 浏览器中刷新 http://localhost:8080/,你应该会看到一个自定义的问候语!

    答案

    wasm-game-of-life/src/lib.rs 中的 greet 函数的新版本:

    
    #![allow(unused)]
    fn main() {
    #[wasm_bindgen]
    pub fn greet(name: &str) {
        alert(&format!("Hello, {}!", name));
    }
    }
    

    wasm-game-of-life/www/index.js 中对 greet 的新调用:

    wasm.greet("Your Name");
    

康威生命游戏规则

注:如果你已经熟悉了康威的生命游戏及其规则。请随意跳到下一节!

维基百科对康威生命游戏的规则做了一个很好的描述:

生命游戏的宇宙是一个无限的二维正交方格网格, 每个方格都处于两种可能状态中的一种,活或死,(或分别为有人居住和无人居住)。 每个单元格与其八个相邻单元格相互作用,这些单元格是水平、垂直或对角相邻的单元格。 在时间的每一步,都会发生以下转换:

  1. 任何具有少于两个活邻居的活单元格都会死亡,就像人口不足一样。
  2. 任何有两个或三个活邻居的活单元格都会传给下一代。
  3. 任何拥有三个以上活邻居的活单元格都会死亡,就像人口过多一样。
  4. 任何只有三个活邻居的死单元格都会变成活单元格,就像通过繁殖一样。

这些将自动机的行为与现实生活进行比较的规则可以浓缩为以下内容:

  1. 任何有两个或三个活邻居的活单元格都能存活。
  2. 任何具有三个活邻居的死单元格都会成为活单元格。
  3. 所有其他活单元格在下一代中死亡。同样,所有其他死单元格保持死亡。

初始模式构成了系统的种子。 第一代是通过将上述规则同时应用于种子中的每个单元格, 无论是活的还是死的; 出生和死亡同时发生, 发生这种情况的离散时刻有时称为滴答声。 每 一代都是前一代的纯函数。 这些规则不断被反复应用以创造更多的世代。

考虑以下初始宇宙:

Initial Universe

我们可以通过考虑每个单元格来计算下一代。左上角的单元格已死。规则 (4) 是唯一适用于死单元格的转换规则。然而,因为左上角的单元格没有正好三个活着的邻居,所以转换规则不适用,它在下一代中仍然是死的。第一行中的每个其他单元格也是如此。

当我们考虑第二行第三列的顶部活单元格时,事情变得有趣了。对于活单元格,前三个规则中的任何一个都可能适用。在这个单元格的情况下,它只有一个活着的邻居,因此规则(1)适用:这个单元格将在下一代死亡。同样的命运等待着底部的活单元格。

中间的活单元格有两个活的邻居:顶部和底部的活单元格。这意味着规则 (2) 适用,并且它在下一代中仍然存在。

最后一个有趣的例子是中间活单元格左侧和右侧的死单元格。三个活单元格都是这两个单元格的邻居,这意味着规则(4)适用,这些单元格将在下一代变得活跃。

把它们放在一起,我们在下一个滴答后得到这个宇宙:

Next Universe

从这些简单的、确定性的规则中,出现了奇怪而令人兴奋的行为:

Gosper's glider gunPulsarSpace ship
Gosper's glider gunPulsarLighweight space ship

练习

  • 用手计算我们的例子宇宙的下一个刻度。注意到任何熟悉的东西吗?

    答案

    它应该是例子宇宙的初始状态。

    Initial Universe

    这种模式是周期性的:它在每两个ticks之后返回到初始状态。

  • 你能找到一个稳定的初始宇宙吗?就是说,一个每一代都是一样的宇宙。

    答案

    有无限多的稳定的宇宙! 琐碎稳定的宇宙是空宇宙。一个由活单元格组成的2乘2的正方形也是一个稳定的宇宙。

实现康威的生命游戏

设计

在我们深入研究之前,我们需要考虑一些设计选择。

无限宇宙

生命游戏是在一个无限的宇宙中进行的,但我们没有无限的内存和计算能力。 解决这个相当烦人的限制通常有以下三种方式之一:

  1. 跟踪宇宙的哪个子集发生了有趣的事情,并根据需要扩展该区域。 在最坏的情况下,这种扩展是无界的,实现会越来越慢,最终会耗尽内存。

  2. 创建一个固定大小的宇宙,其中边缘的单元格比中间的单元格具有更少的邻居。 这种方法的缺点是,像滑翔机一样到达宇宙尽头的无限模式被扼杀了。

  3. 创建一个固定大小的周期性宇宙,其中边缘的单元格有环绕宇宙另一侧的邻居。 因为邻居环绕着宇宙的边缘,所以滑翔机可以永远运行。

我们将实施第三个选项。

Rust 和 JavaScript 的接口

⚡ 这是本教程中需要理解和掌握的最重要的概念之一!

JavaScript 的垃圾收集堆——其中分配了“对象”、“数组”和 DOM 节点——与 WebAssembly 的线性内存空间不同,我们的 Rust 值存在于其中。 WebAssembly 目前无法直接访问垃圾收集堆(截至 2018 年 4 月,这预计会随着 “接口类型”提案 的出现而改变)。 另一方面,JavaScript可以读写WebAssembly的线性内存空间,但只能作为标量值的ArrayBufferu8i32f64,等等)。 WebAssembly 函数也接受和返回标量值。 这些是构成所有 WebAssembly 和 JavaScript 通信的构建块。

wasm_bindgen 定义了如何跨这个边界处理复合结构的共同理解。 它涉及装箱 Rust 结构,并将指针包装在 JavaScript 类中以提高可用性,或者从 Rust 索引到 JavaScript 对象表。 wasm_bindgen 非常方便,但它并没有消除考虑我们的数据表示的需要,以及跨越这个边界传递的值和结构。 相反,将其视为实现您选择的界面设计的工具。

在设计 WebAssembly 和 JavaScript 之间的接口时,我们希望针对以下属性进行优化:

  1. 最大限度地减少进出 WebAssembly 线性内存的复制。 不必要的副本会带来不必要的开销。

  2. 最小化序列化和反序列化。 与副本类似,序列化和反序列化也会产生开销,并且通常也会产生复制。 如果我们可以将不透明的句柄传递给数据结构——而不是在一侧序列化它,将它复制到 WebAssembly 线性内存中的某个已知位置,然后在另一侧反序列化——我们通常可以减少很多开销。 wasm_bindgen 帮助我们定义和使用 JavaScript 对象或盒装 Rust 结构的不透明句柄。

在我们的生活游戏中连接 Rust 和 JavaScript

让我们首先列举一些要避免的危险。 我们不想在每个滴答声中将整个宇宙复制到 WebAssembly 线性内存中。 我们不想为宇宙中的每个单元格分配对象,也不想强加跨界调用来读取和写入每个单元格。

这让我们何去何从? 我们可以将宇宙表示为一个平面数组,它存在于 WebAssembly 线性内存中,每个单元格都有一个字节。 0 是死单元格,1 是活单元格。

以下是 4 x 4 宇宙在内存中的样子:

Screenshot of a 4 by 4 universe

为了找到宇宙中某一行和某一列的单元格的阵列索引,我们可以使用这个公式:

index(row, column, universe) = row * width(universe) + column

我们有几种方法可以将宇宙的单元格暴露给 JavaScript。首先,我们将为 "宇宙 "实现std::fmt::Display,我们可以用它来生成一个渲染为文本字符的单元格的RustString。然后这个Rust字符串从WebAssembly的线性内存中复制到JavaScript的垃圾收集堆中的一个JavaScript字符串,然后通过设置HTMLtextContent来显示。在本章的后面,我们将发展这个实现,以避免在堆之间复制宇宙的单元,并渲染到<canvas>

另一个可行的设计方案是让Rust在每次打勾后返回一个改变状态的单元格的列表,而不是将整个宇宙暴露给JavaScript。这样一来,JavaScript就不需要在渲染时遍历整个宇宙,只需要遍历相关的子集。这样做的好处是,这种基于delta的设计在实现上稍显困难。

Rust 实现

在上一章中,我们克隆了一个初始项目模板。 我们现在将修改该项目模板。

让我们首先从 wasm-game-of-life/src/lib.rs 中删除 alert 导入和 greet 函数,并将它们替换为单元格的类型定义:


#![allow(unused)]
fn main() {
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
    Dead = 0,
    Alive = 1,
}
}

重要的是我们有#[repr(u8)],这样每个单元格都表示为一个字节。 同样重要的是,Dead 变体为 0Alive 变体为 1,以便我们可以轻松地通过加法计算单元格的活邻居。

接下来,让我们定义宇宙。 宇宙有宽度和高度,以及长度为 width * height 的单元向量。


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub struct Universe {
    width: u32,
    height: u32,
    cells: Vec<Cell>,
}
}

为了访问给定行和列的单元格,我们将行和列转换为单元格向量的索引,如前所述:


#![allow(unused)]
fn main() {
impl Universe {
    fn get_index(&self, row: u32, column: u32) -> usize {
        (row * self.width + column) as usize
    }

    // ...
}
}

为了计算一个单元格的下一个状态,我们需要计算它的邻居有多少是活着的。 让我们编写一个 live_neighbor_count 方法来做到这一点!


#![allow(unused)]
fn main() {
impl Universe {
    // ...

    fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
        let mut count = 0;
        for delta_row in [self.height - 1, 0, 1].iter().cloned() {
            for delta_col in [self.width - 1, 0, 1].iter().cloned() {
                if delta_row == 0 && delta_col == 0 {
                    continue;
                }

                let neighbor_row = (row + delta_row) % self.height;
                let neighbor_col = (column + delta_col) % self.width;
                let idx = self.get_index(neighbor_row, neighbor_col);
                count += self.cells[idx] as u8;
            }
        }
        count
    }
}
}

live_neighbor_count 方法使用 deltas 和 modulo 来避免使用 if 对宇宙边缘进行特殊外壳。 当应用 -1 的增量时,我们添加 self.height - 1 并让模数做它的事情,而不是尝试减去 1rowcolumn 可以是 0,如果我们试图从它们中减去 1,就会出现一个无符号整数下溢。

现在我们拥有了从当前计算下一代所需的一切! 游戏的每条规则都直接转换为“匹配”表达式的条件。 此外,因为我们希望 JavaScript 控制滴答发生的时间,我们将把这个方法放在一个 #[wasm_bindgen] 块中,以便它暴露给 JavaScript。


#![allow(unused)]
fn main() {
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    pub fn tick(&mut self) {
        let mut next = self.cells.clone();

        for row in 0..self.height {
            for col in 0..self.width {
                let idx = self.get_index(row, col);
                let cell = self.cells[idx];
                let live_neighbors = self.live_neighbor_count(row, col);

                let next_cell = match (cell, live_neighbors) {
                    // Rule 1: Any live cell with fewer than two live neighbours
                    // dies, as if caused by underpopulation.
                    (Cell::Alive, x) if x < 2 => Cell::Dead,
                    // Rule 2: Any live cell with two or three live neighbours
                    // lives on to the next generation.
                    (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                    // Rule 3: Any live cell with more than three live
                    // neighbours dies, as if by overpopulation.
                    (Cell::Alive, x) if x > 3 => Cell::Dead,
                    // Rule 4: Any dead cell with exactly three live neighbours
                    // becomes a live cell, as if by reproduction.
                    (Cell::Dead, 3) => Cell::Alive,
                    // All other cells remain in the same state.
                    (otherwise, _) => otherwise,
                };

                next[idx] = next_cell;
            }
        }

        self.cells = next;
    }

    // ...
}
}

到目前为止,宇宙的状态被表示为一个单元格向量。 为了使人能够读懂这些内容,让我们实现一个基本的文本渲染器。 这个想法是将宇宙一行一行地写成文本,对于每个活着的单元格,打印 Unicode 字符(“黑色中型方块”)。 对于死单元格,我们将打印(一个“白色中等正方形”)。

通过实现 Rust 标准库中的 Display 特性,我们可以添加一种以面向用户的方式格式化结构的方法。 这也会自动给我们一个 to_string 方法。


#![allow(unused)]
fn main() {
use std::fmt;

impl fmt::Display for Universe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for line in self.cells.as_slice().chunks(self.width as usize) {
            for &cell in line {
                let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
                write!(f, "{}", symbol)?;
            }
            write!(f, "\n")?;
        }

        Ok(())
    }
}
}

最后,我们定义了一个构造函数,用一个有趣的活单元格和死单元格的模式来初始化宇宙,以及一个`render'方法:


#![allow(unused)]
fn main() {
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    // ...

    pub fn new() -> Universe {
        let width = 64;
        let height = 64;

        let cells = (0..width * height)
            .map(|i| {
                if i % 2 == 0 || i % 7 == 0 {
                    Cell::Alive
                } else {
                    Cell::Dead
                }
            })
            .collect();

        Universe {
            width,
            height,
            cells,
        }
    }

    pub fn render(&self) -> String {
        self.to_string()
    }
}
}

这样,我们的生命游戏的Rust部分就完成了。

wasm-game-of-life目录下运行wasm-pack build,将其重新编译为WebAssembly。

用JavaScript进行渲染

首先,让我们在wasm-game-of-life/www/index.html中添加一个<pre>元素,将宇宙渲染进去,就在<script>标签上方:

<body>
  <pre id="game-of-life-canvas"></pre>
  <script src="./bootstrap.js"></script>
</body>

此外,我们希望<pre>在网页的中间位置。我们可以使用CSS柔性框来完成这个任务。在wasm-game-of-life/www/index.html<head>内添加以下<style>标签:

<style>
  body {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
</style>

wasm-game-of-life/www/index.js 的顶部,让我们修复我们的导入以引入 Universe 而不是旧的 greet 函数:

import { Universe } from "wasm-game-of-life";

另外,让我们获取刚刚添加的 <pre> 元素并实例化一个新的 Universe:

const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();

JavaScript在一个requestAnimationFrame循环中运行。在每个迭代中,它将当前的宇宙画到<pre>,然后调用Universe::tick

const renderLoop = () => {
  pre.textContent = universe.render();
  universe.tick();

  requestAnimationFrame(renderLoop);
};

为了开始渲染过程,我们所要做的就是为渲染循环的第一次迭代进行初始调用:

requestAnimationFrame(renderLoop);

确保你的开发服务器仍在运行(在wasm-game-of-life/www内运行npm run start),这就是http://localhost:8080/应该有的样子:

Screenshot of the Game of Life implementation with text rendering

直接从内存向画布渲染

在Rust中生成(和分配)一个String,然后让wasm-bindgen将其转换为有效的JavaScript字符串,就会对宇宙的单元格进行不必要的复制。由于JavaScript代码已经知道了宇宙的宽度和高度,并且可以直接读取构成单元格的WebAssembly的线性内存,我们将修改render方法以返回一个指向单元格数组开始的指针。

另外,我们将改用Canvas API来代替渲染Unicode文本。我们将在本教程的其余部分使用这种设计。

wasm-game-of-life/www/index.html中,让我们把之前添加的<pre>替换成我们要渲染的<canvas>(它也应该在<body>中,在加载我们JavaScript的<script>之前):

<body>
  <canvas id="game-of-life-canvas"></canvas>
  <script src='./bootstrap.js'></script>
</body>

为了从Rust的实现中获得必要的信息,我们需要为一个宇宙的宽度、高度和指向其单元格数组的指针增加一些getter函数。所有这些也都暴露在JavaScript中。在wasm-game-of-life/src/lib.rs中增加这些内容:


#![allow(unused)]
fn main() {
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    // ...

    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
}
}

接下来,在wasm-game-of-life/www/index.js中,让我们也从wasm-game-of-life中导入Cell,并定义一些常量,我们将在渲染到画布时使用:

import { Universe, Cell } from "wasm-game-of-life";

const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";

现在,让我们重写这段JavaScript代码的其余部分,不再写入<pre>textContent,而是绘制到<canvas>:

// Construct the universe, and get its width and height.
const universe = Universe.new();
const width = universe.width();
const height = universe.height();

// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;

const ctx = canvas.getContext('2d');

const renderLoop = () => {
  universe.tick();

  drawGrid();
  drawCells();

  requestAnimationFrame(renderLoop);
};

为了画出单元格之间的网格,我们画了一组等距的水平线,和一组等距的垂直线。这些线纵横交错,形成网格。

const drawGrid = () => {
  ctx.beginPath();
  ctx.strokeStyle = GRID_COLOR;

  // Vertical lines.
  for (let i = 0; i <= width; i++) {
    ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
    ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
  }

  // Horizontal lines.
  for (let j = 0; j <= height; j++) {
    ctx.moveTo(0,                           j * (CELL_SIZE + 1) + 1);
    ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
  }

  ctx.stroke();
};

我们可以通过memory直接访问WebAssembly的线性内存,它被定义在原始wasm模块wasm_game_of_life_bg。为了绘制单元格,我们得到一个指向宇宙单元格的指针,构建一个覆盖单元格缓冲区的Uint8Array,遍历每个单元格,并根据单元格是死是活,分别绘制一个白色或黑色的矩形。通过使用指针和覆盖,我们避免了在每次打勾时将单元格复制到边界上。

// Import the WebAssembly memory at the top of the file.
import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";

// ...

const getIndex = (row, column) => {
  return row * width + column;
};

const drawCells = () => {
  const cellsPtr = universe.cells();
  const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);

  ctx.beginPath();

  for (let row = 0; row < height; row++) {
    for (let col = 0; col < width; col++) {
      const idx = getIndex(row, col);

      ctx.fillStyle = cells[idx] === Cell.Dead
        ? DEAD_COLOR
        : ALIVE_COLOR;

      ctx.fillRect(
        col * (CELL_SIZE + 1) + 1,
        row * (CELL_SIZE + 1) + 1,
        CELL_SIZE,
        CELL_SIZE
      );
    }
  }

  ctx.stroke();
};

为了开始渲染过程,我们将使用与上面相同的代码来启动渲染循环的第一次迭代:

drawGrid();
drawCells();
requestAnimationFrame(renderLoop);

注意,我们在这里调用drawGrid()drawCells(),然后再调用requestAnimationFrame()。我们这样做的原因是为了在我们进行修改之前画出宇宙的_初始_状态。如果我们只是简单地调用requestAnimationFrame(renderLoop),我们最终会出现这样的情况:第一个被绘制的帧实际上是在第一次调用universe.tick()之后,也就是这些单元格生命中的第二个 "打勾"。

It Works!

通过从 wasm-game-of-life 目录中运行以下命令来重建 WebAssembly 和绑定:

wasm-pack build

确保您的开发服务器仍在运行。 如果不是,请从 wasm-game-of-life/www 目录中重新启动它:

npm run start

如果你刷新http://localhost:8080/,你应该会看到精彩的生活展示!

Screenshot of the Game of Life implementation

顺便说一句,还有一个非常简洁的算法来实现生命游戏,称为 hashlife。 它使用积极的记忆,并且实际上可以指数更快来计算它运行的时间越长的后代! 鉴于此,您可能想知道为什么我们没有在本教程中实现 hashlife。 这超出了本文的范围,我们专注于 Rust 和 WebAssembly 的集成,但我们强烈建议您自行了解 hashlife!

练习

  • 用单个太空船初始化宇宙。

  • 不是对初始宇宙进行硬编码,而是生成一个随机的宇宙,其中每个单元格有 550 次存活或死亡的机会。

    提示:使用js-sys crate 导入Math.random JavaScript 函数.

    答案 *首先,在`wasm-game-of-life/Cargo.toml`中添加`js-sys`作为依赖项:*
    # ...
    [dependencies]
    js-sys = "0.3"
    # ...
    

    然后,使用js_sys::Math::random函数来投掷硬币:

    
    #![allow(unused)]
    fn main() {
    extern crate js_sys;
    
    // ...
    
    if js_sys::Math::random() < 0.5 {
        // Alive...
    } else {
        // Dead...
    }
    }
    
  • 用一个字节表示每个单元,使单元的迭代变得容易,但它的代价是浪费了内存。每个字节有八个比特,但是我们只需要一个比特来表示每个单元格是活的还是死的。重构数据表示,使每个单元格只使用一个比特的空间。

    答案

    在Rust中,你可以使用fixedbitset crate和它的FixedBitSet类型来表示单元,而不是Vec<Cell>:

    
    #![allow(unused)]
    fn main() {
    // Make sure you also added the dependency to Cargo.toml!
    extern crate fixedbitset;
    use fixedbitset::FixedBitSet;
    
    // ...
    
    #[wasm_bindgen]
    pub struct Universe {
        width: u32,
        height: u32,
        cells: FixedBitSet,
    }
    }
    

    Universe 构造函数可以通过以下方式进行调整:

    
    #![allow(unused)]
    fn main() {
    pub fn new() -> Universe {
        let width = 64;
        let height = 64;
    
        let size = (width * height) as usize;
        let mut cells = FixedBitSet::with_capacity(size);
    
        for i in 0..size {
            cells.set(i, i % 2 == 0 || i % 7 == 0);
        }
    
        Universe {
            width,
            height,
            cells,
        }
    }
    }
    

    要在宇宙的下一个刻度中更新单元格,我们使用FixedBitSetset方法:

    
    #![allow(unused)]
    fn main() {
    next.set(idx, match (cell, live_neighbors) {
        (true, x) if x < 2 => false,
        (true, 2) | (true, 3) => true,
        (true, x) if x > 3 => false,
        (false, 3) => true,
        (otherwise, _) => otherwise
    });
    }
    

    要将指向位开头的指针传递给 JavaScript,您可以将 FixedBitSet 转换为切片,然后将切片转换为指针:

    
    #![allow(unused)]
    fn main() {
    #[wasm_bindgen]
    impl Universe {
        // ...
    
        pub fn cells(&self) -> *const u32 {
            self.cells.as_slice().as_ptr()
        }
    }
    }
    

    在 JavaScript 中,从 Wasm 内存构造一个 Uint8Array 和之前一样,只是数组的长度不再是 width * height,而是 width * height / 8 因为我们每比特有一个单元格而不是 每字节:

    const cells = new Uint8Array(memory.buffer, cellsPtr, width * height / 8);
    

    给定一个索引和 Uint8Array,您可以使用以下函数确定是否设置了 nth 位:

    const bitIsSet = (n, arr) => {
      const byte = Math.floor(n / 8);
      const mask = 1 << (n % 8);
      return (arr[byte] & mask) === mask;
    };
    

    鉴于所有这些,新版本的 drawCells 看起来像这样:

    const drawCells = () => {
      const cellsPtr = universe.cells();
    
      // This is updated!
      const cells = new Uint8Array(memory.buffer, cellsPtr, width * height / 8);
    
      ctx.beginPath();
    
      for (let row = 0; row < height; row++) {
        for (let col = 0; col < width; col++) {
          const idx = getIndex(row, col);
    
          // This is updated!
          ctx.fillStyle = bitIsSet(idx, cells)
            ? ALIVE_COLOR
            : DEAD_COLOR;
    
          ctx.fillRect(
            col * (CELL_SIZE + 1) + 1,
            row * (CELL_SIZE + 1) + 1,
            CELL_SIZE,
            CELL_SIZE
          );
        }
      }
    
      ctx.stroke();
    };
    

实现康威的生命游戏

现在我们已经使用 JavaScript 在浏览器中实现了生命游戏渲染的 Rust 实现,让我们来谈谈测试我们的 Rust 生成的 WebAssembly 函数。

我们将测试我们的 tick 函数以确保它提供我们期望的输出。

接下来,我们将要在 wasm_game_of_life/src/lib.rs 文件中现有的 impl Universe 块中创建一些 setter 和 getter 函数。 我们将创建一个 set_width 和一个 set_height 函数,以便我们可以创建不同大小的 Universe


#![allow(unused)]
fn main() {
#[wasm_bindgen]
impl Universe { 
    // ...

    /// Set the width of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_width(&mut self, width: u32) {
        self.width = width;
        self.cells = (0..width * self.height).map(|_i| Cell::Dead).collect();
    }

    /// Set the height of the universe.
    ///
    /// Resets all cells to the dead state.
    pub fn set_height(&mut self, height: u32) {
        self.height = height;
        self.cells = (0..self.width * height).map(|_i| Cell::Dead).collect();
    }

}
}

我们将在我们的 wasm_game_of_life/src/lib.rs 文件中创建另一个不带 #[wasm_bindgen] 属性的 impl Universe 块。 有一些我们需要测试的函数我们不想暴露给我们的 JavaScript。 Rust 生成的 WebAssembly 函数不能返回借用的引用。 尝试使用属性编译 Rust 生成的 WebAssembly 并查看您得到的错误。

我们将编写 get_cells 的实现来获取 Universecells 的内容。 我们还将编写一个 set_cells 函数,以便我们可以将 Universe 的特定行和列中的 cells 设置为 Alive


#![allow(unused)]
fn main() {
impl Universe {
    /// Get the dead and alive values of the entire universe.
    pub fn get_cells(&self) -> &[Cell] {
        &self.cells
    }

    /// Set cells to be alive in a universe by passing the row and column
    /// of each cell as an array.
    pub fn set_cells(&mut self, cells: &[(u32, u32)]) {
        for (row, col) in cells.iter().cloned() {
            let idx = self.get_index(row, col);
            self.cells[idx] = Cell::Alive;
        }
    }

}
}

现在我们将在 wasm_game_of_life/tests/web.rs 文件中创建我们的测试。

在我们这样做之前,文件中已经有一个工作测试。 您可以通过在 wasm-game-of-life 目录中运行 wasm-pack test --chrome --headless 来确认 Rust 生成的 WebAssembly 测试正在运行。 你还可以使用 --firefox--safari--node 选项在这些浏览器中测试你的代码。

wasm_game_of_life/tests/web.rs 文件中,我们需要导出 wasm_game_of_life crate 和 Universe 类型。


#![allow(unused)]
fn main() {
extern crate wasm_game_of_life;
use wasm_game_of_life::Universe;
}

wasm_game_of_life/tests/web.rs 文件中,我们将要创建一些飞船建造器功能。

我们需要一个用于我们的输入宇宙飞船,我们将调用 tick 函数,我们希望在一个刻度后我们将获得预期的宇宙飞船。 我们选择了要初始化为 Alive 的单元格,以在 input_spaceship 函数中创建我们的飞船。 input_spaceship 勾选后,expected_spaceship 函数中飞船的位置是手动计算的。 您可以自己确认一下,输入飞船的单元格在一个tick后与预期的飞船相同。


#![allow(unused)]
fn main() {
#[cfg(test)]
pub fn input_spaceship() -> Universe {
    let mut universe = Universe::new();
    universe.set_width(6);
    universe.set_height(6);
    universe.set_cells(&[(1,2), (2,3), (3,1), (3,2), (3,3)]);
    universe
}

#[cfg(test)]
pub fn expected_spaceship() -> Universe {
    let mut universe = Universe::new();
    universe.set_width(6);
    universe.set_height(6);
    universe.set_cells(&[(2,1), (2,3), (3,2), (3,3), (4,2)]);
    universe
}
}

现在我们将为我们的test_tick 函数编写实现。 首先,我们创建了一个 input_spaceship()expected_spaceship() 的实例。 然后,我们在 input_universe 上调用 tick。 最后,我们使用 assert_eq! 宏调用 get_cells() 以确保 input_universeexpected_universe 具有相同的 Cell 数组值。 我们将 #[wasm_bindgen_test] 属性添加到我们的代码块中,以便我们可以测试 Rust 生成的 WebAssembly 代码并使用 wasm-pack test 来测试 WebAssembly 代码。


#![allow(unused)]
fn main() {
#[wasm_bindgen_test]
pub fn test_tick() {
    // Let's create a smaller Universe with a small spaceship to test!
    let mut input_universe = input_spaceship();

    // This is what our spaceship should look like
    // after one tick in our universe.
    let expected_universe = expected_spaceship();

    // Call `tick` and then see if the cells in the `Universe`s are the same.
    input_universe.tick();
    assert_eq!(&input_universe.get_cells(), &expected_universe.get_cells());
}
}

通过在 wasm-game-of-life 目录中运行 wasm-pack test --firefox --headless 进行测试。

调试

在我们写更多的代码之前,我们会想在我们的腰带上有一些调试工具,以备出错时使用。花点时间回顾一下参考页列出了可用来调试Rust生成的WebAssembly的工具和方法.

为 Panics 启用日志记录

如果我们的代码出现混乱,我们希望在开发者控制台中显示信息性错误消息。

我们的 wasm-pack-template 带有一个可选的、默认启用的对 console_error_panic_hook crate 的依赖,该依赖被配置在 wasm-game-of-life/src/utils.rs 中。我们需要做的就是在初始化函数或公共代码路径中安装这个钩子。我们可以在 wasm-ame-of-life/src/lib.rs 中的 Universe::new 构造函数中调用它。


#![allow(unused)]
fn main() {
pub fn new() -> Universe {
    utils::set_panic_hook();

    // ...
}
}

在我们的生活游戏中加入记录的内容

让我们通过web-sys crate 使用 console.log 函数来添加一些关于我们 Universe::tick 函数中每个单元的日志

首先,添加 web-sys 作为依赖,并在 wasm-game-of-life/Cargo.toml 中启用其"console"功能:

[dependencies]

# ...

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

为了符合人体工程学,我们将把console.log函数包装成一个println!风格的宏:


#![allow(unused)]
fn main() {
extern crate web_sys;

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}
}

现在,我们可以通过在Rust代码中插入对log的调用来开始将信息记录到控制台。例如,为了记录每个单元格的状态、活的邻居数和下一个状态,我们可以这样修改wasm-game-of-life/src/lib.rs:

diff --git a/src/lib.rs b/src/lib.rs
index f757641..a30e107 100755
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -123,6 +122,14 @@ impl Universe {
                 let cell = self.cells[idx];
                 let live_neighbors = self.live_neighbor_count(row, col);

+                log!(
+                    "cell[{}, {}] is initially {:?} and has {} live neighbors",
+                    row,
+                    col,
+                    cell,
+                    live_neighbors
+                );
+
                 let next_cell = match (cell, live_neighbors) {
                     // Rule 1: Any live cell with fewer than two live neighbours
                     // dies, as if caused by underpopulation.
@@ -140,6 +147,8 @@ impl Universe {
                     (otherwise, _) => otherwise,
                 };

+                log!("    it becomes {:?}", next_cell);
+
                 next[idx] = next_cell;
             }
         }

使用调试器在每个 Tick 之间进行停顿

浏览器的步进调试器对于检查我们的 Rust 生成的 WebAssembly 与之交互的 JavaScript 很有用。

例如,我们可以使用调试器通过将 一个 JavaScript debugger; 语句 放在我们对 universe.tick() 的调用上方来暂停 renderLoop 函数的每次迭代。

const renderLoop = () => {
  debugger;
  universe.tick();

  drawGrid();
  drawCells();

  requestAnimationFrame(renderLoop);
};

这为我们提供了一个方便的检查点,用于检查记录的消息,并将当前渲染的帧与前一帧进行比较。

Screenshot of debugging the Game of Life

练习

  • tick 函数添加日志记录,记录每个单元格的行和列,这些单元格将状态从活动状态转换为死状态,反之亦然。

  • Universe::new 方法中引入 panic!()。 在 Web 浏览器的 JavaScript 调试器中检查恐慌的回溯。 禁用调试符号,在没有 console_error_panic_hook 可选依赖项的情况下重建,并再次检查堆栈跟踪。 不是那么有用吗?

添加交互性

我们将继续探索 JavaScript 和 WebAssembly 界面,为我们的 Game of Life 实现添加一些交互功能。 我们将使用户能够通过单击来切换单元格是活的还是死的,并允许暂停游戏,这使得绘制单元格图案变得更加容易。

暂停和恢复游戏

让我们添加一个按钮来切换游戏是正在播放还是暂停。 在 wasm-game-of-life/www/index.html 中,在 <canvas> 正上方添加按钮:

<button id="play-pause"></button>

wasm-game-of-life/www/index.js JavaScript 中,我们将进行以下更改:

  • 跟踪最近一次调用requestAnimationFrame 返回的标识符,以便我们可以通过使用该标识符调用cancelAnimationFrame 来取消动画。

  • 当播放/暂停按钮被点击时,检查我们是否有排队动画帧的标识符。如果我们这样做,那么游戏当前正在播放,我们想要取消动画帧,以便不再调用 renderLoop,从而有效地暂停游戏。如果我们没有排队动画帧的标识符,那么我们当前处于暂停状态,我们想调用requestAnimationFrame 来恢复游戏。

因为 JavaScript 正在驱动 Rust 和 WebAssembly,这就是我们需要做的所有事情,我们不需要更改 Rust 源。

我们引入了animationId 变量来跟踪requestAnimationFrame 返回的标识符。当没有排队的动画帧时,我们将此变量设置为 null

let animationId = null;

// This function is the same as before, except the
// result of `requestAnimationFrame` is assigned to
// `animationId`.
const renderLoop = () => {
  drawGrid();
  drawCells();

  universe.tick();

  animationId = requestAnimationFrame(renderLoop);
};

在任何时候,我们都可以通过检查 animationId 的值来判断游戏是否暂停:

const isPaused = () => {
  return animationId === null;
};

现在,当点击播放/暂停按钮时,我们检查游戏当前是否暂停或播放,并分别恢复renderLoop动画或取消下一动画帧。 此外,我们更新按钮的文本图标以反映按钮在下一次单击时将采取的操作。

const playPauseButton = document.getElementById("play-pause");

const play = () => {
  playPauseButton.textContent = "⏸";
  renderLoop();
};

const pause = () => {
  playPauseButton.textContent = "▶";
  cancelAnimationFrame(animationId);
  animationId = null;
};

playPauseButton.addEventListener("click", event => {
  if (isPaused()) {
    play();
  } else {
    pause();
  }
});

最后,我们之前通过直接调用 requestAnimationFrame(renderLoop) 来启动游戏及其动画,但我们想用对 play 的调用替换它,以便按钮获得正确的初始文本图标。

// This used to be `requestAnimationFrame(renderLoop)`.
play();

刷新 http://localhost:8080/ 我们现在应该可以通过点击按钮暂停和恢复游戏了!

"click" 事件上切换单元格的状态

现在我们可以暂停游戏了,是时候添加通过单击单元格来变异单元格的功能了。

切换一个单元格就是将它的状态从活着变为死亡或从死亡变为活着。 向 wasm-game-of-life/src/lib.rs 中的 Cell 添加 toggle 方法:


#![allow(unused)]
fn main() {
impl Cell {
    fn toggle(&mut self) {
        *self = match *self {
            Cell::Dead => Cell::Alive,
            Cell::Alive => Cell::Dead,
        };
    }
}
}

要在给定的行和列处切换单元格的状态,我们将行和列对转换为单元格向量的索引,并在该索引处的单元格上调用 toggle 方法:


#![allow(unused)]
fn main() {
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    // ...

    pub fn toggle_cell(&mut self, row: u32, column: u32) {
        let idx = self.get_index(row, column);
        self.cells[idx].toggle();
    }
}
}

这个方法被定义在impl块中,该块被注释为#[wasm_bindgen],这样它就可以被JavaScript调用。

wasm-game-of-life/www/index.js中,我们监听<canvas>元素上的点击事件,将点击事件的页面相对坐标转换为画布相对坐标,然后转换为行和列,调用toggle_cell方法,最后重新绘制场景。

canvas.addEventListener("click", event => {
  const boundingRect = canvas.getBoundingClientRect();

  const scaleX = canvas.width / boundingRect.width;
  const scaleY = canvas.height / boundingRect.height;

  const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
  const canvasTop = (event.clientY - boundingRect.top) * scaleY;

  const row = Math.min(Math.floor(canvasTop / (CELL_SIZE + 1)), height - 1);
  const col = Math.min(Math.floor(canvasLeft / (CELL_SIZE + 1)), width - 1);

  universe.toggle_cell(row, col);

  drawGrid();
  drawCells();
});

wasm-game-of-life中用wasm-pack build重新构建,然后再次刷新http://localhost:8080/,现在我们可以通过点击单元格并切换其状态来绘制我们自己的图案。s

练习

  • 引入一个<input type="range">小部件来控制每一帧动画发生多少次。

  • 添加一个按钮,当点击时将宇宙重置到一个随机的初始状态。另一个按钮可以将宇宙重设为所有死单元。

  • Ctrl + Click 时,插入一个 glider,以目标单元为中心。在Shift + Click上,插入一个脉冲星。

时间分析

在本章中,我们将提高生命游戏实现的性能。 我们将使用时间分析来指导我们的工作。

在继续之前熟悉 用于时间分析 Rust 和 WebAssembly 代码的可用工具

使用 window.performance.now 函数创建每秒帧数计时器

当我们研究加速生命游戏的渲染时,这个 FPS 计时器将非常有用。

我们首先向 wasm-game-of-life/www/index.js 添加一个 fps 对象:

const fps = new class {
  constructor() {
    this.fps = document.getElementById("fps");
    this.frames = [];
    this.lastFrameTimeStamp = performance.now();
  }

  render() {
    // Convert the delta time since the last frame render into a measure
    // of frames per second.
    const now = performance.now();
    const delta = now - this.lastFrameTimeStamp;
    this.lastFrameTimeStamp = now;
    const fps = 1 / delta * 1000;

    // Save only the latest 100 timings.
    this.frames.push(fps);
    if (this.frames.length > 100) {
      this.frames.shift();
    }

    // Find the max, min, and mean of our 100 latest timings.
    let min = Infinity;
    let max = -Infinity;
    let sum = 0;
    for (let i = 0; i < this.frames.length; i++) {
      sum += this.frames[i];
      min = Math.min(this.frames[i], min);
      max = Math.max(this.frames[i], max);
    }
    let mean = sum / this.frames.length;

    // Render the statistics.
    this.fps.textContent = `
Frames per Second:
         latest = ${Math.round(fps)}
avg of last 100 = ${Math.round(mean)}
min of last 100 = ${Math.round(min)}
max of last 100 = ${Math.round(max)}
`.trim();
  }
};

接下来我们在 renderLoop 的每次迭代中调用 fps render 函数:

const renderLoop = () => {
    fps.render(); //new

    universe.tick();
    drawGrid();
    drawCells();

    animationId = requestAnimationFrame(renderLoop);
};

最后,不要忘记将 fps 元素添加到 wasm-game-of-life/www/index.html,就在 <canvas> 的上方:

<div id="fps"></div>

并添加 CSS 以使其格式更好:

#fps {
  white-space: pre;
  font-family: monospace;
}

瞧! 刷新 http://localhost:8080 现在我们有了一个 FPS 计数器!

使用 console.timeconsole.timeEnd 为每个 Universe::tick 计时

为了测量每次调用 Universe::tick 需要多长时间,我们可以通过 web-sys crate 使用 console.timeconsole.timeEnd

首先,将 web-sys 作为依赖添加到 wasm-game-of-life/Cargo.toml

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

因为每个 console.time 调用都应该有一个相应的 console.timeEnd 调用,所以将它们都包装在 RAII 类型中会很方便:


#![allow(unused)]
fn main() {
extern crate web_sys;
use web_sys::console;

pub struct Timer<'a> {
    name: &'a str,
}

impl<'a> Timer<'a> {
    pub fn new(name: &'a str) -> Timer<'a> {
        console::time_with_label(name);
        Timer { name }
    }
}

impl<'a> Drop for Timer<'a> {
    fn drop(&mut self) {
        console::time_end_with_label(self.name);
    }
}
}

然后,我们可以通过将此代码段添加到方法的顶部来计算每个 Universe::tick 花费的时间:


#![allow(unused)]
fn main() {
let _timer = Timer::new("Universe::tick");
}

每次调用 Universe::tick 花费的时间现在记录在控制台中:

Screenshot of console.time logs

此外,console.timeconsole.timeEnd 对将显示在浏览器的分析器的 “时间轴” 或“ 瀑布” 视图中:

Screenshot of console.time logs

发展我们的生命游戏宇宙

⚠️ 本节使用来自 Firefox 的示例屏幕截图。 虽然所有现代浏览器都有类似的工具,但使用不同的开发人员工具可能会有细微的差别。 您提取的配置文件信息将基本相同,但您所看到的视图和不同工具的命名可能会有所不同。

如果我们让生命游戏的宇宙更大,会发生什么? 用 128 x 128 Universe 替换 64 x 64 Universe(通过修改 wasm-game-of-life/src/lib.rs 中的 Universe::new)导致 FPS 从平滑的 60 下降到不稳定的 40-ish 在我的机器上。

如果我们记录一个配置文件并查看瀑布视图,我们会看到每个动画帧花费了 20 毫秒以上。 回想一下,每秒 60 帧为渲染一帧的整个过程留下了 16 毫秒。 这不仅仅是我们的 JavaScript 和 WebAssembly,还有浏览器正在做的所有其他事情,比如绘画。

Screenshot of a waterfall view of rendering a frame

如果我们查看单个动画帧内发生的情况,我们会发现CanvasRenderingContext2D.fillStyle setter 非常昂贵!

⚠️ 在 Firefox 中,如果您看到一行简单地说“DOM” 而不是上面提到的“CanvasRenderingContext2D.fillStyle”, 则可能需要在性能开发人员工具选项中打开“显示 Gecko 平台数据”选项:

Turning on Show Gecko Platform Data

Screenshot of a flamegraph view of rendering a frame

我们可以通过查看调用树的许多帧的聚合来确认这不是异常:

Screenshot of a flamegraph view of rendering a frame

我们将近 40% 的时间都花在了这个 setter 上!

⚡ 我们可能已经预料到 tick 方法中的某些东西会成为性能瓶颈, 但事实并非如此。 始终让分析引导您的注意力,因为时间可能会花在您不期望的地方。

wasm-game-of-life/www/index.jsdrawCells 函数中,fillStyle 属性为 Universe 中的每个单元格在每个动画帧上设置一次:

for (let row = 0; row < height; row++) {
  for (let col = 0; col < width; col++) {
    const idx = getIndex(row, col);

    ctx.fillStyle = cells[idx] === DEAD
      ? DEAD_COLOR
      : ALIVE_COLOR;

    ctx.fillRect(
      col * (CELL_SIZE + 1) + 1,
      row * (CELL_SIZE + 1) + 1,
      CELL_SIZE,
      CELL_SIZE
    );
  }
}

既然我们已经发现设置fillStyle是如此昂贵,那么我们该如何做才能避免频繁地设置它?我们需要根据一个单元格是活的还是死的来改变fillStyle。如果我们设置fillStyle = ALIVE_COLOR,然后在一次绘制所有活着的单元格,然后设置fillStyle = DEAD_COLOR,在另一次绘制所有死亡的单元格,那么我们只需要设置fillStyle两次,而不是对每个单元格设置一次。

// Alive cells.
ctx.fillStyle = ALIVE_COLOR;
for (let row = 0; row < height; row++) {
  for (let col = 0; col < width; col++) {
    const idx = getIndex(row, col);
    if (cells[idx] !== Cell.Alive) {
      continue;
    }

    ctx.fillRect(
      col * (CELL_SIZE + 1) + 1,
      row * (CELL_SIZE + 1) + 1,
      CELL_SIZE,
      CELL_SIZE
    );
  }
}

// Dead cells.
ctx.fillStyle = DEAD_COLOR;
for (let row = 0; row < height; row++) {
  for (let col = 0; col < width; col++) {
    const idx = getIndex(row, col);
    if (cells[idx] !== Cell.Dead) {
      continue;
    }

    ctx.fillRect(
      col * (CELL_SIZE + 1) + 1,
      row * (CELL_SIZE + 1) + 1,
      CELL_SIZE,
      CELL_SIZE
    );
  }
}

保存这些变化并刷新http://localhost:8080/后,渲染又恢复到每秒60帧的平稳状态。

如果我们再拍一张剖面图,我们可以看到现在每个动画帧只花了大约10毫秒。

Screenshot of a waterfall view of rendering a frame after the drawCells changes

分解一个单一的框架,我们看到fillStyle的成本已经没有了,我们框架的大部分时间是在fillRect中度过的,绘制每个单元格的矩形。

Screenshot of a flamegraph view of rendering a frame after the drawCells changes

让时间跑得更快

有些人不喜欢等待,他们希望每一帧动画不是发生一次宇宙的跳动,而是发生九次跳动。我们可以修改wasm-game-of-life/www/index.js中的renderLoop函数来实现这个目的。

for (let i = 0; i < 9; i++) {
  universe.tick();
}

在我的机器上,这使我们回落到每秒只有35帧。这可不好。我们要的是奶油般的60帧!

现在我们知道时间是在 Universe::tick 中度过的,所以让我们添加一些 Timer,在 console.timeconsole.timeEnd 的调用中包住它的各个部分,看看这将会给我们带来什么。我的假设是,在每个tick上分配一个新的单元格向量并释放旧的向量是昂贵的,并且占用了我们时间预算的很大一部分。


#![allow(unused)]
fn main() {
pub fn tick(&mut self) {
    let _timer = Timer::new("Universe::tick");

    let mut next = {
        let _timer = Timer::new("allocate next cells");
        self.cells.clone()
    };

    {
        let _timer = Timer::new("new generation");
        for row in 0..self.height {
            for col in 0..self.width {
                let idx = self.get_index(row, col);
                let cell = self.cells[idx];
                let live_neighbors = self.live_neighbor_count(row, col);

                let next_cell = match (cell, live_neighbors) {
                    // Rule 1: Any live cell with fewer than two live neighbours
                    // dies, as if caused by underpopulation.
                    (Cell::Alive, x) if x < 2 => Cell::Dead,
                    // Rule 2: Any live cell with two or three live neighbours
                    // lives on to the next generation.
                    (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
                    // Rule 3: Any live cell with more than three live
                    // neighbours dies, as if by overpopulation.
                    (Cell::Alive, x) if x > 3 => Cell::Dead,
                    // Rule 4: Any dead cell with exactly three live neighbours
                    // becomes a live cell, as if by reproduction.
                    (Cell::Dead, 3) => Cell::Alive,
                    // All other cells remain in the same state.
                    (otherwise, _) => otherwise,
                };

                next[idx] = next_cell;
            }
        }
    }

    let _timer = Timer::new("free old cells");
    self.cells = next;
}
}

看一下时间,很明显我的假设是不正确的:绝大部分时间都花在了实际计算下一代的单元上。令人惊讶的是,在每次勾选时分配和释放一个载体的成本似乎可以忽略不计。又一次提醒我们要始终用剖析来指导我们的工作!

Screenshot of a Universe::tick timer results

下一节要求使用nightly编译器。之所以需要它,是因为我们要用test feature gate来做基准测试。我们要安装的另一个工具是cargo benchcmp。它是一个小工具,用于比较由cargo bench产生的微观基准。

让我们写一个本地代码#[bench],做与我们的WebAssembly相同的事情,但在这里我们可以使用更成熟的剖析工具。这里是新的wasm-game-of-life/benches/bench.rs


#![allow(unused)]
#![feature(test)]

fn main() {
extern crate test;
extern crate wasm_game_of_life;

#[bench]
fn universe_ticks(b: &mut test::Bencher) {
    let mut universe = wasm_game_of_life::Universe::new();

    b.iter(|| {
        universe.tick();
    });
}
}

我们还必须注释掉所有的#[wasm_bindgen]注释,以及Cargo.toml中的"dylib"位,否则构建本地代码会失败,并出现链接错误。

有了这些,我们就可以运行cargo bench | tee before.txt来编译和运行我们的基准测试了 | tee before.txt部分将从cargo bench中获取输出,并放入一个名为before.txt的文件。

$ cargo bench | tee before.txt
    Finished release [optimized + debuginfo] target(s) in 0.0 secs
     Running target/release/deps/wasm_game_of_life-91574dfbe2b5a124

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/release/deps/bench-8474091a05cfa2d9

running 1 test
test universe_ticks ... bench:     664,421 ns/iter (+/- 51,926)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

This also tells us where the binary lives, and we can run the benchmarks again, but this time under our operating system's profiler. In my case, I'm running Linux, so perf is the profiler I'll use:

$ perf record -g target/release/deps/bench-8474091a05cfa2d9 --bench
running 1 test
test universe_ticks ... bench:     635,061 ns/iter (+/- 38,764)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.178 MB perf.data (2349 samples) ]

perf report 加载配置文件显示,我们所有的时间都花在了 Universe::tick 上,正如预期:

Screenshot of perf report

如果你按下aperf 将注解函数中的哪些指令正在花费时间:

Screenshot of perf's instruction annotation

这告诉我们,26.67%的时间花在相邻单元格的数值相加上,23.41%的时间花在获取相邻的列索引上,另外15.42%的时间花在获取相邻的行索引上。在这前三个最昂贵的指令中,第二和第三条都是昂贵的div指令。这些div实现了Universe::live_neighbor_count中的模数索引逻辑。

回顾一下wasm-game-of-life/src/lib.rs中的live_neighbor_count定义:


#![allow(unused)]
fn main() {
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
    let mut count = 0;
    for delta_row in [self.height - 1, 0, 1].iter().cloned() {
        for delta_col in [self.width - 1, 0, 1].iter().cloned() {
            if delta_row == 0 && delta_col == 0 {
                continue;
            }

            let neighbor_row = (row + delta_row) % self.height;
            let neighbor_col = (column + delta_col) % self.width;
            let idx = self.get_index(neighbor_row, neighbor_col);
            count += self.cells[idx] as u8;
        }
    }
    count
}
}

我们使用模数的原因是为了避免在第一或最后一行或一列的边缘情况下用if分支使代码变得混乱。但是,即使在最常见的情况下,我们也要付出div指令的代价,当rowcolumn都不在宇宙的边缘,它们不需要模数包装处理。相反,如果我们使用ifs来处理边缘情况,并解开这个循环,分支应该被CPU的分支预测器很好地预测到。

让我们这样重写live_neighbor_count:


#![allow(unused)]
fn main() {
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
    let mut count = 0;

    let north = if row == 0 {
        self.height - 1
    } else {
        row - 1
    };

    let south = if row == self.height - 1 {
        0
    } else {
        row + 1
    };

    let west = if column == 0 {
        self.width - 1
    } else {
        column - 1
    };

    let east = if column == self.width - 1 {
        0
    } else {
        column + 1
    };

    let nw = self.get_index(north, west);
    count += self.cells[nw] as u8;

    let n = self.get_index(north, column);
    count += self.cells[n] as u8;

    let ne = self.get_index(north, east);
    count += self.cells[ne] as u8;

    let w = self.get_index(row, west);
    count += self.cells[w] as u8;

    let e = self.get_index(row, east);
    count += self.cells[e] as u8;

    let sw = self.get_index(south, west);
    count += self.cells[sw] as u8;

    let s = self.get_index(south, column);
    count += self.cells[s] as u8;

    let se = self.get_index(south, east);
    count += self.cells[se] as u8;

    count
}
}

现在让我们再运行一次基准测试! 这次把它输出到after.txt

$ cargo bench | tee after.txt
   Compiling wasm_game_of_life v0.1.0 (file:///home/fitzgen/wasm_game_of_life)
    Finished release [optimized + debuginfo] target(s) in 0.82 secs
     Running target/release/deps/wasm_game_of_life-91574dfbe2b5a124

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/release/deps/bench-8474091a05cfa2d9

running 1 test
test universe_ticks ... bench:      87,258 ns/iter (+/- 14,632)

test result: ok. 0 passed; 0 failed; 0 ignored; 1 measured; 0 filtered out

这看起来好了很多! 我们可以通过benchcmp工具和我们之前创建的两个文本文件看到它有多好:

$ cargo benchcmp before.txt after.txt
 name            before.txt ns/iter  after.txt ns/iter  diff ns/iter   diff %  speedup
 universe_ticks  664,421             87,258                 -577,163  -86.87%   x 7.61

哇!7.61倍的速度

WebAssembly有意与常见的硬件架构紧密映射,但我们确实需要确保这种本地代码的速度也能转化为WebAssembly的速度。

让我们用wasm-pack build重建.wasm并刷新[http://localhost:8080/](http://localhost:8080/)。在我的机器上,该页面再次以每秒60帧的速度运行,用浏览器的剖析器记录另一个剖析,发现每个动画帧大约需要10毫秒。

成功了!

Screenshot of a waterfall view of rendering a frame after replacing modulos with branches

练习

  • 在这一点上,加快Universe::tick的下一个最低的目标是取消分配和释放。实现单元格的双重缓冲,即Universe维护两个向量,不释放其中任何一个,并且不在tick中分配新的缓冲。

  • 实现 "实现生命 "一章中的另一种基于delta的设计,Rust代码将改变状态的单元格的列表返回给JavaScript。这是否使渲染到<canvas>的速度更快?你能实现这种设计而不在每次勾选时分配一个新的deltas列表吗?

  • 正如我们的分析所显示的,2D <canvas>的渲染不是特别快。用WebGL渲染器取代2D画布渲染器。WebGL版本的速度有多大?在WebGL渲染成为瓶颈之前,你能把宇宙做得多大?

缩小.wasm 大小

对于我们通过网络发送给客户端的 .wasm 二进制文件,例如我们的 Game of Life Web 应用程序,我们希望关注代码大小。 我们的 .wasm 越小,我们的页面加载速度就越快,我们的用户就越开心。

我们可以通过构建配置获得我们的 Game of Life .wasm 二进制文件有多小?

花点时间查看我们可以调整的构建配置选项以获得更小的 .wasm 代码大小。

使用默认的发布构建配置(没有调试符号),我们的 WebAssembly 二进制文件是 29,410 字节:

$ wc -c pkg/wasm_game_of_life_bg.wasm
29410 pkg/wasm_game_of_life_bg.wasm

在启用 LTO、设置 opt-level = "z" 并运行 wasm-opt -Oz 后,生成的 .wasm 二进制文件缩小到只有 17,317 个字节:

$ wc -c pkg/wasm_game_of_life_bg.wasm
17317 pkg/wasm_game_of_life_bg.wasm

如果我们用 gzip 压缩它(几乎每个 HTTP 服务器都会这样做),我们会减少到 9,045 字节!

$ gzip -9 < pkg/wasm_game_of_life_bg.wasm | wc -c
9045

练习

  • 使用 wasm-snip 工具 从我们的生命游戏的 .wasm 二进制文件中删除恐慌的基础设施功能。 它节省了多少字节?

  • 使用和不使用 wee_alloc 作为其全局分配器 构建我们的游戏生命箱。 我们克隆来启动这个项目的 rustwasm/wasm-pack-template 模板有一个“wee_alloc”货物特性,您可以通过将其添加到 wasm-game-of-life/Cargo.toml[features] 部分中的 default 键来启用:

    [features]
    default = ["wee_alloc"]
    

    使用 wee_alloc 可以减少 .wasm 二进制文件的大小吗?

  • 我们只实例化一个 Universe,因此我们可以导出操作单个 static mut 全局实例的操作,而不是提供构造函数。 如果这个全局实例也使用了前面章节中讨论过的双缓冲技术,我们可以使这些缓冲区也成为 static mut 全局变量。 这从我们的生命游戏实现中删除了所有动态分配,我们可以使它成为一个不包含分配器的 #![no_std] crate。 通过完全删除分配器依赖,从 .wasm 中删除了多少大小?

发布到 npm

现在我们有了一个有效的、快速的、小的 wasm-game-of-life 包,我们可以将它发布到 npm 以便其他 JavaScript 开发人员可以重用它,如果他们需要一个现成的游戏 生活执行。

先决条件

首先,确保您有一个 npm 帐户

其次,通过运行以下命令确保您在本地登录到您的帐户:

wasm-pack login

发布

通过在 wasm-game-of-life 目录中运行 wasm-pack,确保 wasm-game-of-life/pkg 构建是最新的:

wasm-pack build

现在花点时间查看一下 wasm-game-of-life/pkg 的内容,这就是我们下一步要发布到 npm 的内容!

准备好后,运行 wasm-pack publish 将包上传到 npm:

wasm-pack publish

这就是发布到 npm 所需的全部内容!

...除了其他人也完成了本教程,因此在 npm 上使用了 wasm-game-of-life 名称,最后一个命令可能不起作用。

打开 wasm-game-of-life/Cargo.toml 并将您的用户名添加到 name 的末尾,以独特的方式消除包的歧义:

[package]
name = "wasm-game-of-life-my-username"

然后,重建并再次发布:

wasm-pack build
wasm-pack publish

这次应该可以了!

参考

本节包含 Rust 和 WebAssembly 开发的参考资料。 它的目的不是提供叙述,而是从头到尾阅读。 相反,每个小节都应该独立存在。

你应该知道的Crates

这是您在进行 Rust 和 WebAssembly 开发时应该了解的很棒的 crate 的精选列表。

您还可以在 WebAssembly 类别中浏览所有发布到 crates.io 的 crate.

与 JavaScript 和 DOM 交互

wasm-bindgen | crates.io | repository

wasm-bindgen 促进了 Rust 和 JavaScript 之间的高级交互。 它允许将 JavaScript 内容导入 Rust 并将 Rust 内容导出到 JavaScript。

wasm-bindgen-futures | crates.io | repository

wasm-bindgen-futures 是连接 JavaScript Promises 和 Rust Futures 的桥梁。 它可以双向转换,在 Rust 中处理异步任务时很有用,并允许与 DOM 事件和 I/O 操作进行交互。

js-sys | crates.io | repository

所有 JavaScript 全局类型和方法的原始 wasm-bindgen 导入,例如 ObjectFunctioneval 等。这些 API 可以在所有标准 ECMAScript 环境中移植,而不仅仅是 Web,例如 Node.js。

web-sys | crates.io | repository

所有 Web API 的原始 wasm-bindgen 导入,例如 DOM 操作、setTimeout、Web GL、Web Audio 等。

错误报告和日志记录

console_error_panic_hook | crates.io | repository

这个 crate 允许你通过提供一个将 panic 消息转发到 console.error 的 panic 钩子来调试 wasm32-unknown-unknown 上的 panics。

console_log | crates.io | repository

此 crate 为 the log crate 提供后端,将记录的消息路由到 devtools 控制台。

动态分配

wee_alloc | crates.io | repository

Wasm-Enabled,Elfin 分配器。 当代码大小比分配性能更受关注时,一个小的(~1K 未压缩的.wasm)分配器实现。

解析和生成 .wasm 二进制文件

parity-wasm | crates.io | repository

用于序列化、反序列化和构建 .wasm 二进制文件的低级 WebAssembly 格式库。 对众所周知的自定义部分的良好支持,例如 "names" 部分和 "reloc.WHATEVER" 部分。

wasmparser | crates.io | repository

一个简单的、事件驱动的库,用于解析 WebAssembly 二进制文件。 提供每个已解析事物的字节偏移量,例如在解释 relocs 时这是必需的。

解释和编译 WebAssembly

wasmi | crates.io | repository

Parity 的可嵌入 WebAssembly 解释器。

cranelift-wasm | crates.io | repository

将 WebAssembly 编译为本机主机的机器代码。 Cranelift (né Cretonne) 代码生成器项目的一部分。

你应该知道的工具

这是在进行 Rust 和 WebAssembly 开发时你应该知道的一些很棒的工具的精选列表。

开发、构建和工作流编排

wasm-pack | repository

wasm-pack 旨在成为构建和使用 Rust 生成的 WebAssembly 的一站式商店,您希望与 JavaScript、Web 或 Node.js 进行互操作。 wasm-pack 可帮助您构建 Rust 生成的 WebAssembly 并将其发布到 npm 仓库,以便与您已经使用的工作流中的任何其他 JavaScript 包一起使用。

优化和操作 .wasm 二进制文件

wasm-opt | repository

wasm-opt 工具将 WebAssembly 读取为输入,对其运行转换、优化和/或检测传递,然后将转换后的 WebAssembly 作为输出发出。 在 LLVM 通过 rustc 生成的 .wasm 二进制文件上运行它通常会创建更小且执行速度更快的 .wasm 二进制文件。 这个工具是binaryen 项目的一部分。

wasm2js | repository

wasm2js 工具将 WebAssembly 编译成“几乎是 asm.js”。 这非常适合支持没有 WebAssembly 实现的浏览器,例如 Internet Explorer 11。这个工具是 binaryen 项目的一部分。

wasm-gc | repository

一个垃圾收集 WebAssembly 模块并删除所有不需要的导出、导入、函数等的小工具。这实际上是 WebAssembly 的一个 --gc-sections 链接器标志。

由于两个原因,您通常不需要自己使用此工具:

  1. rustc 现在有一个足够新的 lld 版本,它支持 WebAssembly 的 --gc-sections 标志。 这会自动为 LTO 构建启用。
  2. wasm-bindgen CLI 工具会自动为你运行 wasm-gc

wasm-snip | repository

wasm-snipunreachable 指令替换了 WebAssembly 函数的主体。

也许您知道某些函数在运行时永远不会被调用,但是编译器无法在编译时证明这一点? 剪吧! 然后再次运行wasm-gc,它传递调用的所有函数(在运行时也永远不会被调用)也将被删除。

这对于在非调试生产版本中强行删除 Rust 的恐慌基础架构很有用。

检查 .wasm 二进制文件

twiggy | repository

twiggy.wasm 二进制文件的代码大小分析器。 它分析二进制的调用图来回答以下问题:

  • 为什么这个函数首先包含在二进制文件中? 即 哪些导出的函数正在传递调用它?
  • 这个函数的保留大小是多少? 即 如果我删除它以及删除后成为死代码的所有函数,将节省多少空间。

使用 twiggy 使你的二进制文件变得苗条!

wasm-objdump | repository

打印有关 .wasm 二进制文件及其每个部分的低级详细信息。 还支持反汇编成 WAT 文本格式。 它就像objdump,但用于WebAssembly。 这是 WABT 项目的一部分。

wasm-nm | repository

列出在 .wasm 二进制文件中定义的导入、导出和私有函数符号。 它就像 nm 但对于 WebAssembly。

项目模板

Rust 和 WebAssembly 工作组策划和维护各种项目模板,以帮助您启动新项目并开始运行。

wasm-pack-template

此模板 用于启动 Rust 和 WebAssembly 项目,与 wasm-pack 一起使用。

使用 cargo generate 来克隆这个项目模板:

cargo install cargo-generate
cargo generate --git https://github.com/rustwasm/wasm-pack-template.git

create-wasm-app

此模板 适用于使用来自 npm 的包的 JavaScript 项目,这些包是使用 wasm-pack 从 Rust 创建的。

npm init 一起使用:

mkdir my-project
cd my-project/
npm init wasm-app

该模板通常与 wasm-pack-template 一起使用,其中 wasm-pack-template 项目通过 npm link 安装在本地,并作为 create-wasm-app 项目的依赖项引入。

rust-webpack-template

此模板 预配置了所有样板,用于将 Rust 编译为 WebAssembly,并使用 Webpack 的 rust-loader 将其直接挂接到 Webpack 构建管道中。

npm init 一起使用:

mkdir my-project
cd my-project/
npm init rust-webpack

调试 Rust 生成的 WebAssembly

本节包含调试 Rust 生成的 WebAssembly 的技巧。

使用调试符号构建

⚡ 调试时,请始终确保使用调试符号进行构建!

如果你没有启用调试符号,那么在编译的 .wasm 二进制文件中将不会出现 "name" 自定义部分,并且堆栈跟踪将具有类似 wasm-function[42] 的函数名称,而不是函数的 Rust 名称,比如 wasm_game_of_life::Universe::live_neighbor_count

当使用 "debug" 构建(又名 wasm-pack build --debugcargo build)时,默认启用调试符号。

对于 "release" 版本,默认情况下不启用调试符号。 要启用调试符号,请确保在 Cargo.toml[profile.release] 部分中设置 debug = true

[profile.release]
debug = true

使用 console API 进行日志记录

日志记录是我们拥有的最有效的工具之一,用于证明和反驳关于我们的程序为什么有错误的假设。 在 Web 上,console.log 功能 是将消息记录到浏览器的开发者工具控制台的方式 .

我们可以使用 the web-sys crate 来访问 console 日志功能:


#![allow(unused)]
fn main() {
extern crate web_sys;

web_sys::console::log_1(&"Hello, world!".into());
}

或者,console.error 函数 具有与 console.log 相同的签名,但开发人员工具 当使用 console.error 时,往往还会在记录的消息旁边捕获和显示堆栈跟踪。

References

记录 Panics

console_error_panic_hook crate 通过 console.error 将意外的恐慌记录到开发者控制台。 而不是获得神秘的、难以调试的 RuntimeError: unreachable execution 错误消息,这给你 Rust 的格式化 恐慌信息。

您需要做的就是通过在初始化函数或公共代码路径中调用 console_error_panic_hook::set_once() 来安装钩子:


#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub fn init_panic_hook() {
    console_error_panic_hook::set_once();
}
}

使用调试器

不幸的是,WebAssembly 的调试故事仍然不成熟。 在大多数 Unix 系统上,DWARF 用于对调试器提供正在运行的程序的源代码级检查所需的信息进行编码。 有一种替代格式可以对 Windows 上的类似信息进行编码。 目前,没有 WebAssembly 的等价物。 因此,调试器目前提供的实用程序有限,我们最终会逐步执行编译器发出的原始 WebAssembly 指令,而不是我们编写的 Rust 源文本。

有一个W3C WebAssembly 小组调试子章程,所以期待这个故事在未来得到改进!

尽管如此,调试器对于检查与我们的 WebAssembly 交互的 JavaScript 和检查原始 wasm 状态仍然很有用。

参考

首先避免调试 WebAssembly

如果该错误特定于与 JavaScript 或 Web API 的交互,则 使用 wasm-bindgen-test 编写测试。

如果错误涉及与 JavaScript 或 Web API 的交互,那么尝试将其复制为普通的 Rust #[test] 函数,您可以在调试时利用操作系统成熟的本机工具。 使用像 quickcheck 和它的测试用例收缩器这样的测试箱来机械地减少测试用例。 最终,如果您可以在不需要与 JavaScript 交互的较小测试用例中隔离它们,您将更容易找到和修复错误。

请注意,为了在没有编译器和链接器错误的情况下运行本机 #[test],您需要确保 "rlib" 包含在 Cargo. toml 文件。

[lib]
crate-type ["cdylib", "rlib"]

时间分析

本节介绍如何使用 Rust 和 WebAssembly 分析网页,其目标是提高吞吐量或延迟。

⚡ 始终确保在分析时使用优化的构建! wasm-pack build 将默认进行优化构建。

可用工具

window.performance.now() 计时器

performance.now() 函数 返回自网页加载以来以毫秒为单位的单调时间戳。

调用 performance.now 的开销很小,因此我们可以从中创建简单、细粒度的测量,而不会扭曲系统其余部分的性能并对我们的测量造成偏差。

我们可以用它来为各种操作计时,我们可以通过web-sys crate 访问window.performance.now():


#![allow(unused)]
fn main() {
extern crate web_sys;

fn now() -> f64 {
    web_sys::window()
        .expect("should have a Window")
        .performance()
        .expect("should have a Performance")
        .now()
}
}

开发者工具分析器

所有 Web 浏览器的内置开发人员工具都包含一个分析器。 这些分析器通过调用树和火焰图等常见类型的可视化显示哪些函数占用的时间最多。

如果您 使用调试符号构建 以便“名称”自定义部分包含在 wasm 二进制文件中,那么这些分析器应该显示 Rust 函数名称,而不是像 wasm-function[123] 这样不透明的东西。

请注意,这些分析器不会显示内联函数,并且由于 Rust 和 LLVM 非常依赖内联,因此结果可能仍然有点令人困惑。

Screenshot of profiler with Rust symbols

资源

console.timeconsole.timeEnd 函数

console.timeconsole.timeEnd 函数 允许您将命名操作的时间记录到浏览器的开发者工具控制台。 你在操作开始时调用console.time("some operation"),在操作结束时调用console.timeEnd("some operation")。 命名操作的字符串标签是可选的。

您可以直接通过 web-sys crate 使用这些函数:

这是浏览器控制台中 console.time 日志的屏幕截图:

Screenshot of console.time logs

此外,console.timeconsole.timeEnd 日志将显示在浏览器分析器的“时间轴”或“瀑布”视图中:

Screenshot of console.time logs

在本机代码中使用 #[bench]

与我们通常可以通过编写 #[test]s 而不是在 Web 上调试来利用操作系统的本机代码调试工具一样,我们可以通过编写 #[bench] 函数来利用操作系统的本机代码分析工具。

在 crate 的 benches 子目录中写入你的基准。 确保你的 crate-type 包含 "rlib",否则 bench 二进制文件将无法链接你的主库。

然而! 在投入大量精力进行本机代码分析之前,请确保您知道瓶颈在 WebAssembly 中! 使用浏览器的分析器来确认这一点,否则您可能会浪费时间优化不热门的代码。

资源

缩小.wasm 代码大小

本节将教你如何优化你的 .wasm 构建以减少代码大小,以及如何识别更改 Rust 源代码的机会,以便发出更少的 .wasm 代码。

为什么要关心代码大小?

当通过网络提供 .wasm 文件时,它越小,客户端下载它的速度就越快。更快的 .wasm 下载会导致更快的页面加载时间,从而带来更快乐的用户。

但是,重要的是要记住,尽管代码大小可能不是您感兴趣的最终指标,而是更模糊且难以衡量的指标,例如“首次交互时间”。虽然代码大小在这个衡量中扮演着重要的角色(如果你还没有所有的代码,就什么也做不了!)它不是唯一的因素。

WebAssembly 通常提供给使用 gzip 压缩的用户,因此您需要确保比较 gzip 大小的差异,以便通过网络传输时间。还要记住,WebAssembly 二进制格式非常适合 gzip 压缩,通常可以减少 50% 以上的大小。

此外,WebAssembly 的二进制格式针对非常快速的解析和处理进行了优化。现在的浏览器有“基线编译器”,它解析 WebAssembly 并以最快的速度发出已编译的代码,因为它可以通过网络传入。这意味着如果你正在使用instantiateStreaming Web 请求完成后,WebAssembly 模块可能已经准备就绪。另一方面,JavaScript 通常需要更长的时间,不仅要解析,还要跟上 JIT 编译等的速度。

最后,请记住,WebAssembly 在执行速度方面也比 JavaScript 优化得多。您需要确保测量 JavaScript 和 WebAssembly 之间的运行时比较,以将其考虑到代码大小的重要性。

如果你的 .wasm 文件比预期的大,基本上不要立即沮丧!代码大小最终可能只是端到端故事中的众多因素之一。只看代码大小的 JavaScript 和 WebAssembly 之间的比较缺少树的森林。

针对代码大小优化构建

有很多配置选项可以用来让rustc制作更小的.wasm二进制文件。在某些情况下,我们用较长的编译时间来换取较小的`.wasm'尺寸。在其他情况下,我们要用WebAssembly的运行速度来换取更小的代码大小。我们应该认识到每个选项的权衡,在用运行时间速度换取代码大小的情况下,要进行剖析和测量,以做出一个明智的决定,确定这种交易是否值得。

使用链接时间优化(LTO)进行编译

Cargo.toml中,在[profile.release]部分添加lto = true

[profile.release]
lto = true

这给了LLVM更多的机会来内联和修剪函数。这不仅会使.wasm更小,而且会使它在运行时更快!缺点是编译时间更长。缺点是编译时间会更长。

告诉LLVM对大小而不是速度进行优化

LLVM的优化通道被调整为提高速度,而不是默认的大小。我们可以通过修改Cargo.toml中的[profile.release]部分,将目标改为代码大小:

[profile.release]
opt-level = 's'

或者,更积极地优化尺寸,以进一步的潜在速度成本:

[profile.release]
opt-level = 'z'

请注意,令人惊讶的是,opt-level = "s" 有时会产生比 opt-level = "z" 更小的二进制文件。 经常测量!

使用 wasm-opt 工具

Binaryen 工具包是一组特定于 WebAssembly 的编译器工具。 它比 LLVM 的 WebAssembly 后端走得更远,使用它的 wasm-opt 工具对 LLVM 生成的 .wasm 二进制文件进行后处理,通常可以再节省 15-20% 的代码大小。 它通常会同时产生运行时加速!

# Optimize for size.
wasm-opt -Os -o output.wasm input.wasm

# Optimize aggressively for size.
wasm-opt -Oz -o output.wasm input.wasm

# Optimize for speed.
wasm-opt -O -o output.wasm input.wasm

# Optimize aggressively for speed.
wasm-opt -O3 -o output.wasm input.wasm

关于调试信息的注意事项

wasm 二进制文件大小的最大贡献者之一可能是调试信息和 wasm 二进制文件的“名称”部分。 然而,wasm-pack 工具默认会删除 debuginfo。 此外,除非还指定了 -g,否则 wasm-opt 会默认删除 names 部分。

这意味着,如果您按照上述步骤操作,则默认情况下,wasm 二进制文件中不应有 debuginfo 或 names 部分。 但是,如果您手动以其他方式在 wasm 二进制文件中保留此调试信息,请务必注意这一点!

尺寸分析

如果调整构建配置以优化代码大小并没有产生足够小的 .wasm 二进制文件,那么是时候进行一些分析以查看剩余代码大小的来源。

⚡ 就像我们如何让时间分析指导我们加速工作一样,我们希望让大小分析指导我们的代码大小缩减工作。 如果不这样做,您可能会浪费自己的时间!

twiggy 代码大小分析器

twiggy 是一个代码大小分析器 支持 WebAssembly 作为输入。 它分析二进制的调用图来回答以下问题:

  • 为什么这个函数首先包含在二进制文件中?

  • 这个函数的保留大小是多少? 即 如果我删除它以及删除后成为死代码的所有函数,会节省多少空间?

$ twiggy top -n 20 pkg/wasm_game_of_life_bg.wasm
 Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────────────
          9158 ┊    19.65% ┊ "function names" subsection
          3251 ┊     6.98% ┊ dlmalloc::dlmalloc::Dlmalloc::malloc::h632d10c184fef6e8
          2510 ┊     5.39% ┊ <str as core::fmt::Debug>::fmt::he0d87479d1c208ea
          1737 ┊     3.73% ┊ data[0]
          1574 ┊     3.38% ┊ data[3]
          1524 ┊     3.27% ┊ core::fmt::Formatter::pad::h6825605b326ea2c5
          1413 ┊     3.03% ┊ std::panicking::rust_panic_with_hook::h1d3660f2e339513d
          1200 ┊     2.57% ┊ core::fmt::Formatter::pad_integral::h06996c5859a57ced
          1131 ┊     2.43% ┊ core::str::slice_error_fail::h6da90c14857ae01b
          1051 ┊     2.26% ┊ core::fmt::write::h03ff8c7a2f3a9605
           931 ┊     2.00% ┊ data[4]
           864 ┊     1.85% ┊ dlmalloc::dlmalloc::Dlmalloc::free::h27b781e3b06bdb05
           841 ┊     1.80% ┊ <char as core::fmt::Debug>::fmt::h07742d9f4a8c56f2
           813 ┊     1.74% ┊ __rust_realloc
           708 ┊     1.52% ┊ core::slice::memchr::memchr::h6243a1b2885fdb85
           678 ┊     1.45% ┊ <core::fmt::builders::PadAdapter<'a> as core::fmt::Write>::write_str::h96b72fb7457d3062
           631 ┊     1.35% ┊ universe_tick
           631 ┊     1.35% ┊ dlmalloc::dlmalloc::Dlmalloc::dispose_chunk::hae6c5c8634e575b8
           514 ┊     1.10% ┊ std::panicking::default_hook::{{closure}}::hfae0c204085471d5
           503 ┊     1.08% ┊ <&'a T as core::fmt::Debug>::fmt::hba207e4f7abaece6

手动检查 LLVM-IR

在 LLVM 生成 WebAssembly 之前,LLVM-IR 是编译器工具链中的最终中间表示。 因此,它与最终发出的 WebAssembly 非常相似。 更多的 LLVM-IR 通常意味着更多的 .wasm 大小,如果一个函数占用 LLVM-IR 的 25%,那么它通常会占用 .wasm 的 25%。 虽然这些数字仅适用于一般情况,但 LLVM-IR 具有在 .wasm 中不存在的关键信息(因为 WebAssembly 缺乏像 DWARF 这样的调试格式):哪些子例程被内联到给定的函数中。

您可以使用以下 cargo 命令生成 LLVM-IR:

cargo rustc --release -- --emit llvm-ir

然后,您可以使用 findcargotarget 目录中定位包含 LLVM-IR 的 .ll 文件:

find target/release -type f -name '*.ll'

参考

更具侵入性的工具和技术

调整构建配置以获得更小的 .wasm 二进制文件是非常容易的。然而,当你需要走更多的路时,你要准备使用更多的侵入性技术,比如重写源代码以避免臃肿。下面是一系列 "动手 "的技术,你可以应用这些技术来获得更小的代码大小。

避免字符串格式化

format!, to_string, 等等...会带来大量的代码膨胀。如果可能的话,只在调试模式下进行字符串格式化,而在发布模式下使用静态字符串。

避免 Panicking

这绝对是说起来容易做起来难,但是像 twiggy 和手动检查 LLVM-IR 这样的工具可以帮助你找出哪些函数是恐慌的。

Panics 并不总是以panic!() 宏调用的形式出现。 它们隐含地来自许多结构,例如:

  • 索引切片在越界索引时发生 panics:my_slice[i]

  • 如果除数为零,除法会 panic:dividend / divisor

  • 解开OptionResultopt.unwrap()res.unwrap()

前两个可以翻译成第三个。 索引可以替换为容易出错的 my_slice.get(i) 操作。 除法可以用checked_div 调用代替。 现在我们只有一个案例要处理。

在不惊慌的情况下展开 OptionResult 有两种方式:安全和不安全。

安全的方法是在遇到 NoneError 时使用 abort 而不是 panicking:


#![allow(unused)]
fn main() {
#[inline]
pub fn unwrap_abort<T>(o: Option<T>) -> T {
    use std::process;
    match o {
        Some(t) => t,
        None => process::abort(),
    }
}
}

最终,无论如何,恐慌都会转化为 wasm32-unknown-unknown 中的中止,所以这会给你相同的行为,但没有代码膨胀。

或者,unreachable crateOptionResult 提供了一个不安全的 unchecked_unwrap 扩展方法,它告诉 Rust 编译器假设OptionSome ResultOk。 如果该假设不成立,会发生什么情况是未定义的行为。 当您 110% 知道假设成立时,您真的只想使用这种不安全的方法,而编译器不够聪明,无法看到它。 即使你走这条路,你应该有一个仍然进行检查的调试构建配置,并且只在发布构建中使用未经检查的操作。

避免分配或切换到 wee_alloc

Rust 的 WebAssembly 的默认分配器是 dlmalloc 到 Rust 的端口。 它的重量约为 10 KB。 如果您可以完全避免动态分配,那么您应该能够摆脱这十 KB。

完全避免动态分配可能非常困难。 但是从热代码路径中删除分配通常要容易得多(并且通常也有助于使这些热代码路径更快)。 在这些情况下,wee_alloc 替换默认的全局分配器 应该为您节省这十 KB 中的大部分(但不是全部)。 wee_alloc 是一个分配器,专为您需要某种类型的分配器但不需要特别快的分配器的情况而设计,并且很乐意用分配速度换取较小的代码大小。

使用特征对象而不是泛型类型参数

当您创建使用类型参数的泛型函数时,如下所示:


#![allow(unused)]
fn main() {
fn whatever<T: MyTrait>(t: T) { ... }
}

然后 rustc 和 LLVM 将为使用该函数的每个 T 类型创建该函数的新副本。 这为基于每个副本所使用的特定“T”提供了许多编译器优化机会,但这些副本在代码大小方面加起来很快。

如果你使用 trait 对象而不是类型参数,像这样:


#![allow(unused)]
fn main() {
fn whatever(t: Box<MyTrait>) { ... }
// or
fn whatever(t: &MyTrait) { ... }
// etc...
}

然后使用通过虚拟调用的动态调度,并且在 .wasm 中只发出该函数的一个版本。 缺点是失去了编译器优化机会以及间接、动态调度的函数调用的额外成本。

使用 wasm-snip 工具

wasm-snipunreachable指令替换WebAssembly函数的主体。这是一个相当重的、钝的锤子,用于那些看起来像钉子的函数,如果你足够努力地眯着眼睛。

也许你知道某些函数在运行时不会被调用,但编译器在编译时无法证明这一点?把它剪掉吧! 之后,再次运行 wasm-opt 并加上 --dce 标志,所有被剪掉的函数所调用的函数(这些函数在运行时也不可能被调用)也会被删除。

这个工具对于删除恐慌的基础结构特别有用,因为无论如何,恐慌最终都会转化为陷阱。

JavaScript 互操作

导入和导出 JS 函数

从 Rust 方面

在 JS 主机中使用 wasm 时,从 Rust 端导入和导出函数很简单:它的工作方式与 C 非常相似。

WebAssembly 模块声明了一系列导入,每个导入都有一个 module name 和一个 import nameextern { ... } 块的模块名称可以使用 #[link(wasm_import_module)] 指定,目前它默认为“env”。

出口只有一个名称。 除了任何 extern 函数,WebAssembly 实例的默认线性内存被导出为“内存”。


#![allow(unused)]
fn main() {
// import a JS function called `foo` from the module `mod`
#[link(wasm_import_module = "mod")]
extern { fn foo(); }

// export a Rust function called `bar`
#[no_mangle]
pub extern fn bar() { /* ... */ }
}

由于 wasm 的值类型有限,这些函数必须仅对原始数字类型进行操作。

从JS端

在 JS 中,wasm 二进制文件变成了 ES6 模块。 它必须使用线性内存实例化,并具有一组匹配预期导入的 JS 函数。 实例化的详细信息可在 MDN 上找到。

生成的 ES6 模块将包含从 Rust 导出的所有函数,现在可以作为 JS 函数使用。

[这里][hello world] 是整个设置的一个非常简单的示例。

超越数字

在 JS 中使用 wasm 时,wasm 模块的内存和 JS 内存之间存在明显的分裂:

  • 每个 wasm 模块都有一个线性内存(在本文档的顶部描述),在实例化期间进行初始化。 JS 代码可以自由读写这块内存

  • 相比之下,wasm 代码不能直接访问 JS 对象。

因此,复杂的互操作以两种主要方式发生:

  • 将二进制数据复制到或复制到 wasm 内存。 例如,这是向 Rust 端提供拥有的 String 的一种方式。

  • 建立一个显式的 JS 对象“堆”,然后给定“地址”。 这允许 wasm 代码间接引用 JS 对象(使用整数),并通过调用导入的 JS 函数对这些对象进行操作。

幸运的是,这个互操作的故事非常适合通过一个通用的“bindgen”风格的框架来处理:wasm-bindgen。 该框架可以编写自动映射到惯用 JS 函数的惯用 Rust 函数签名。

自定义部分

自定义部分允许将命名的任意数据嵌入到 wasm 模块中。 段数据在编译时设置并直接从 wasm 模块读取,不能在运行时修改。

在 Rust 中,自定义节是使用 #[link_section] 属性公开的静态数组([T; size]):


#![allow(unused)]
fn main() {
#[link_section = "hello"]
pub static SECTION: [u8; 24] = *b"This is a custom section";
}

这会在 wasm 文件中添加一个名为 hello 的自定义部分,rust 变量名称 SECTION 是任意的,更改它不会改变行为。 这里的内容是文本字节,但可以是任何任意数据。

自定义部分可以在 JS 端使用 WebAssembly.Module.customSections 函数读取,它接受一个 wasm 模块和部分名称作为参数,并返回一个 ArrayBuffer 数组。 可以使用相同的名称指定多个部分,在这种情况下,它们都将出现在此数组中。

WebAssembly.compileStreaming(fetch("sections.wasm"))
.then(mod => {
  const sections = WebAssembly.Module.customSections(mod, "hello");

  const decoder = new TextDecoder();
  const text = decoder.decode(sections[0]);

  console.log(text); // -> "This is a custom section"
});

哪些板块能与WebAssembly一起使用现成的产品?

最简单的方法是列出目前不能与WebAssembly一起使用的东西;避免这些东西的crack往往可以移植到WebAssembly上,并且通常Just Work。一个好的经验法则是,如果一个板块支持嵌入式和 #![no_std] 的使用,它可能也支持WebAssembly。

一个板块可能会做的事情,不会与WebAssembly一起工作。

C和系统库的依赖性

wasm中没有系统库,所以任何试图与系统库绑定的crate都不会工作。

使用C库也可能无法工作,因为wasm没有一个稳定的ABI用于跨语言通信,而且wasm的跨语言链接非常不稳定。每个人都希望这最终能起作用,特别是由于clang现在已经默认发送他们的wasm32目标,但故事还没有完全结束。

文件I/O

WebAssembly没有访问文件系统的权限,所以假设文件系统存在的crates — 没有针对WASM的解决方法 — 将不工作。

生成线程

计划将线程添加到WebAssembly,但它还没有发货。试图在 wasm32-unknown-unknown 目标上的线程上生成,会引起恐慌,从而触发wasm陷阱。

那么,哪些通用工具箱倾向于与WebAssembly一起工作?

算法和数据结构

提供特定算法数据结构实现的板块,例如A*图搜索或splay树,往往能与WebAssembly很好地配合。

#![no_std]

不依赖于标准库的 crates 往往与 WebAssembly 配合得很好。

解析器

解析器 — 只要他们只接受输入而不执行自己的 I/O — 倾向于与 WebAssembly 配合使用。

文本处理

以文本形式表达时处理人类语言复杂性的Crates 倾向于与 WebAssembly 配合使用。

Rust 模式

针对特定于 Rust 编程的特定情况的共享解决方案 倾向于与 WebAssembly 配合使用。

如何为通用Crate添加 WebAssembly 支持

本节适用于想要支持 WebAssembly 的通用 crate 作者。

也许你的 Crate 已经支持 WebAssembly!

查看有关 什么样的东西可以使 WebAssembly 的通用 crate 不可移植 的信息。 如果您的 crate 没有这些东西,它可能已经支持 WebAssembly!

您始终可以通过为 WebAssembly 目标运行 cargo build 来检查:

cargo build --target wasm32-unknown-unknown

如果该命令失败,那么您的 crate 现在不支持 WebAssembly。 如果它没有失败,那么你的箱子 * 可能 * 支持 WebAssembly。 您可以通过 为 wasm 添加测试并在 CI 中运行这些测试来 100% 确定它会(并继续这样做!)

添加对 WebAssembly 的支持

避免直接执行 I/O

在 Web 上,I/O 始终是异步的,并且没有文件系统。 将 I/O 排除在您的库之外,让用户执行 I/O,然后将输入切片传递给您的库。

例如,重构这个:


#![allow(unused)]
fn main() {
use std::fs;
use std::path::Path;

pub fn parse_thing(path: &Path) -> Result<MyThing, MyError> {
    let contents = fs::read(path)?;
    // ...
}
}

进入这个:


#![allow(unused)]
fn main() {
pub fn parse_thing(contents: &[u8]) -> Result<MyThing, MyError> {
    // ...
}
}

添加 wasm-bindgen 作为依赖

如果您需要与外部世界交互(即您不能让图书馆消费者为您驱动该交互),那么您需要添加 wasm-bindgen(以及 js-sysweb-sys,如果 您需要它们)作为编译针对 WebAssembly 时的依赖项:

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = "0.3"

避免同步 I/O

如果您必须在库中执行 I/O,则它不能是同步的。 Web 上只有异步 I/O。 使用 futures 和 [wasm-bindgen-futures 包]((https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen_futures/) 来管理异步 I/O。 如果您的库函数在某个未来类型 F 上是通用的,那么该未来可以通过 Web 上的 fetch 或通过操作系统提供的非阻塞 I/O 来实现。


#![allow(unused)]
fn main() {
pub fn do_stuff<F>(future: F) -> impl Future<Item = MyOtherThing>
where
    F: Future<Item = MyThing>,
{
    // ...
}
}

您还可以为 WebAssembly 和 Web 以及本机目标定义一个特征并实现它:


#![allow(unused)]
fn main() {
trait ReadMyThing {
    type F: Future<Item = MyThing>;
    fn read(&self) -> Self::F;
}

#[cfg(target_arch = "wasm32")]
struct WebReadMyThing {
    // ...
}

#[cfg(target_arch = "wasm32")]
impl ReadMyThing for WebReadMyThing {
    // ...
}

#[cfg(not(target_arch = "wasm32"))]
struct NativeReadMyThing {
    // ...
}

#[cfg(not(target_arch = "wasm32"))]
impl ReadMyThing for NativeReadMyThing {
    // ...
}
}

避免产生线程

Wasm 尚不支持线程(但 实验工作正在进行),因此尝试在 wasm 会恐慌。

您可以使用#[cfg(..)]s 来启用线程和非线程代码路径,具体取决于目标是否为 WebAssembly:


#![allow(unused)]
#![cfg(target_arch = "wasm32")]
fn main() {
fn do_work() {
    // Do work with only this thread...
}

#![cfg(not(target_arch = "wasm32"))]
fn do_work() {
    use std::thread;

    // Spread work to helper threads....
    thread::spawn(|| {
        // ...
    });
}
}

另一种选择是从您的库中提取线程,并允许用户“自带线程”,类似于提取文件 I/O 并允许用户自带 I/O。 这具有与想要拥有自己的自定义线程池的应用程序配合良好的副作用。

维护对 WebAssembly 的持续支持

在 CI 中构建 wasm32-unknown-unknown

通过让您的 CI 脚本运行以下命令,确保在针对 WebAssembly 时编译不会失败:

rustup target add wasm32-unknown-unknown
cargo check --target wasm32-unknown-unknown

例如,您可以将其添加到 Travis CI 的 .travis.yml 配置中:


matrix:
  include:
    - language: rust
      rust: stable
      name: "check wasm32 support"
      install: rustup target add wasm32-unknown-unknown
      script: cargo check --target wasm32-unknown-unknown

在 Node.js 和无头浏览器中进行测试

您可以使用 wasm-bindgen-testwasm-pack test 子命令在 Node.js 或无头浏览器中运行 wasmntests。 您甚至可以将这些测试集成到您的 CI 中。

在此处了解有关测试 wasm 的更多信息。

将 Rust 和 WebAssembly 部署到生产环境

⚡ 部署使用 Rust 和 WebAssembly 构建的 Web 应用程序几乎与部署任何其他 Web 应用程序相同!

要在客户端部署使用 Rust 生成的 WebAssembly 的 Web 应用程序,请将构建的 Web 应用程序的文件复制到您的生产服务器的文件系统,并配置您的 HTTP 服务器以使其可访问。

确保您的 HTTP 服务器使用 application/wasm MIME 类型

为了最快的页面加载,您需要使用 WebAssembly.instantiateStreaming 函数 通过网络传输管道化 wasm 编译和实例化(或确保您的捆绑器能够使用该函数)。 但是,instantiateStreaming 要求 HTTP 响应设置了 application/wasm MIME 类型,否则会抛出错误。

更多资源

  • 生产中 Webpack 的最佳实践。 许多 Rust 和 WebAssembly 项目使用 Webpack 来捆绑其 Rust 生成的 WebAssembly、JavaScript、CSS 和 HTML。 本指南提供了在部署到生产环境时充分利用 Webpack 的技巧。
  • Apache 文档。 Apache 是一种用于生产的流行 HTTP 服务器。
  • NGINX 文档。 NGINX 是一种用于生产的流行 HTTP 服务器。