WebAssembly-的艺术-全-

WebAssembly 的艺术(全)

原文:zh.annas-archive.org/md5/e873dfdb0c217b9fb5ba4370ddaa3e97

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎来到WebAssembly 的艺术。本书教你如何在虚拟机级别阅读、编写和理解 WebAssembly。它将帮助你了解 WebAssembly 如何与 JavaScript、网页浏览器以及嵌入环境进行交互。到最后,你将理解 WebAssembly 是什么,它的理想使用场景,以及如何编写接近本地速度的 WebAssembly 代码。

谁应该阅读本书

本书适合那些希望理解何时以及为何使用 WebAssembly 的网页开发者。如果你真心想掌握 WebAssembly,你需要深入学习它。关于 WebAssembly 工具链,已有多本书籍进行讨论。本书并不专注于为 WebAssembly 编写 C/C++、Rust 或其他语言的代码;相反,它探索了 WebAssembly 的机制和能力。

本书适合那些想要了解 WebAssembly 是什么,它能做什么以及如何最佳使用它的用户。WebAssembly 可以比 JavaScript 表现得更好,且可以创建更小的下载和内存占用。但开发高性能的 WebAssembly 应用程序不仅仅是用 C++/Rust 或 AssemblyScript 等语言编写应用程序并将其编译为 WebAssembly。要构建一个执行速度是其 JavaScript 等效程序两到三倍的应用程序,你需要深入了解 WebAssembly 的工作原理。

读者应具备基本的网页技术知识,如 JavaScript、HTML 和 CSS,但不需要是这些技术的专家。在目前的 WebAssembly 形式下,如果不了解网页及其工作原理,使用 WebAssembly 并不容易。我不会解释网页的基础知识,但我也不假设读者对网页如何运作有太多了解。

为什么用户对 WebAssembly 感兴趣

在第一次 WebAssembly 峰会上,Ashley Williams(@ag_dubs)展示了她在 Twitter 上发起的调查结果,询问 WebAssembly 用户为什么对这项技术感兴趣。以下是结果:

  • 多语言,40.1 百分

  • 更小更快的代码,36.8 百分

  • 沙盒化(安全),17.3 百分

然后,她询问那些对 WebAssembly 支持多种语言感兴趣的用户,为什么会这样:

  • JavaScript 无法满足我的需求,43.5 百分

  • 重用现有库,40.8 百分

  • 预先存在的应用分发(distribution),8.1 百分

对于那些认为 JavaScript 无法满足其需求的用户,她询问了原因:

  • 性能差或不一致,42 百分

  • 生态系统无法满足我的需求,17.4 百分

  • 我不喜欢或理解它,31.3 百分

你可以在 YouTube 上观看她的演讲,“Why the #wasmsummit Website Isn’t Written in Wasm”,网址:www.youtube.com/watch?v=J5Rs9oG3FdI

尽管这些调查并非科学性调查,但它们仍然提供了相当有启发性的见解。首先,如果你将第一和第三次调查中那些有兴趣使用 WebAssembly 提升应用性能的用户结合起来,总数超过了 55%。毫无疑问,使用 WebAssembly 提升代码性能是可能的。但要真正利用 WebAssembly 并非魔法;你只需要知道自己在做什么。到本书结束时,你将掌握足够的 WebAssembly 知识,以显著提升你 web 应用的性能。

为什么世界需要 WebAssembly

我从 1990 年代中期开始开发 web 应用程序。最初,网页不过是带有图片的文档。随着 Java 和 JavaScript 的出现,这一情况发生了变化。那时,JavaScript 是一种玩具语言,只能为网页上的按钮添加鼠标悬停效果。Java 才是真正的技术,而 Java 虚拟机(JVM)则是令人兴奋的技术。但是,Java 从未在网页平台上发挥出其全部潜力。Java 需要插件,而插件技术最终因其安全性问题和恶意软件威胁而过时。

不幸的是,Java 是一项专有技术,这阻止了它直接集成到网页浏览器中。然而,WebAssembly 不同,因为它不是由单一技术公司单方面创建的。WebAssembly 起初是由许多硬件和软件供应商(如 Google、Mozilla、Microsoft 和 Apple)合作推出的。它在每个现代浏览器中都可以直接使用,无需插件。你可以使用它通过 Node.js 编写硬件独立的软件。由于它不是专有的,任何硬件或软件平台都可以使用它,无需支付版权费或获得许可。它实现了 1990 年代的梦想——一个二进制文件统治一切

本书内容

本书将带领你了解 WebAssembly 如何在低层次上工作,通过介绍 WebAssembly 文本格式来实现。我们将讨论许多低层次的主题,并花一些时间展示 WebAssembly 如何在 Node.js 和基于 web 的应用程序中与 JavaScript 协同工作。本书的阅读顺序是有意设计的,概念之间相互构建。书中还将有指向代码示例的引用,这些示例可以在 wasmbook.com 找到。

第一章:WebAssembly 简介

  1. 我们将详细探讨 WebAssembly 是什么,它不是什麽,以及什么时候最好使用它。你将接触到 WebAssembly 文本(WAT),它让你理解 WebAssembly 如何在最低层次上运作。我们还将设置你将用来跟随本书示例的环境。

第二章:WebAssembly 文本基础

  1. 我们将介绍 WAT 的基础知识,以及它如何与部署到 WebAssembly 的高级语言相关。你将编写你的第一个 WAT 程序,并讨论一些基础概念,如变量使用和控制流。

第三章:函数和表

  1. 我们将讨论如何在 WebAssembly 模块中创建函数并从 JavaScript 调用它们。你将构建一个检查素数的程序来说明这些概念。我们还将探讨从表格中调用函数以及性能影响。

第四章:低级位操作

  1. 你将学习可以用来提升 WebAssembly 模块性能的低级概念,例如数字系统、位掩码和 2 的补码。

第五章:WebAssembly 中的字符串

  1. WebAssembly 并没有内建的字符串数据类型,因此在本章中,你将学习字符串如何在 WebAssembly 中表示,以及如何操作它们。

第六章:线性内存

  1. 你将了解线性内存以及 WebAssembly 模块如何使用它与 JavaScript 或其他嵌入环境共享大型数据集。我们开始创建一个物体碰撞程序,让物体随机移动并检测碰撞,之后我们将在整本书中使用它。

第七章:Web 应用程序

  1. 你将学习如何使用 HTML、CSS、JavaScript 和 WebAssembly 创建一个简单的 Web 应用程序。

第八章:与 Canvas 一起工作

  1. 我们将讨论如何使用 HTML canvas 和 WebAssembly 创建极速的 Web 动画。我们使用 canvas 来优化我们的物体碰撞应用程序。

第九章:优化性能

  1. 你将学习 WebAssembly 如何在计算密集型任务中表现出色,例如碰撞检测。你将花一些时间使用 Chrome 和 Firefox 的性能分析工具以及其他优化工具来提升应用程序的性能。

第十章:调试 WebAssembly

  1. 我们将介绍调试基础知识,例如使用警告和堆栈跟踪记录到控制台。你还将学习如何使用 Chrome 和 Firefox 中的调试工具逐步调试 WebAssembly 代码。

第十一章:AssemblyScript

  1. 我们将讨论如何使用 WAT 来理解高级语言,通过使用它来评估 AssemblyScript,这是一种旨在高效部署到 WebAssembly 的高级语言。

第一章:WebAssembly 简介

本章中,你将获得 WebAssembly 的背景知识,并探索开始使用 WebAssembly 及其文本表示 WebAssembly Text (WAT)所需的工具。我们将讨论 WebAssembly 的好处,包括提升性能、旧版库集成、可移植性、安全性,以及它作为 JavaScript 替代品的应用。我们将考虑 JavaScript 与 WebAssembly 的关系,并阐明 WebAssembly 是什么以及不是什么。你将学习 WAT 内联和 S 表达式语法。我们将介绍嵌入环境的概念,并讨论如何在网页浏览器、Node.js 和 WebAssembly 系统接口(WASI)中嵌入 WebAssembly。

然后,我们将讨论使用 Visual Studio Code 作为 WAT 开发环境的好处。你将学习 Node.js 的基础知识,以及如何将其用作 WebAssembly 的嵌入环境。我们将展示如何使用 npm 安装 wat-wasm 工具,它为你提供了从 WAT 构建 WebAssembly 应用程序所需的一切。此外,我们还将编写第一个 WebAssembly 应用程序,并使用 Node.js 作为嵌入环境执行它。

什么是 WebAssembly?

WebAssembly 是一项技术,将在未来几年大幅提升网页应用程序的性能。由于 WebAssembly 是新技术,并且需要一定的解释,许多人对它及其使用方法存在误解。本书将教你 WebAssembly 是什么,以及如何使用它来构建高性能的网页应用程序。

WebAssembly是一个虚拟的指令集架构(ISA),用于栈机器。通常,ISA 是一种为特定机器设计的二进制格式。然而,WebAssembly 是为运行在虚拟机器上而设计的,这意味着它并不是为物理硬件设计的。虚拟机器使得 WebAssembly 能够在多种计算机硬件和数字设备上运行。WebAssembly 的 ISA 旨在紧凑、可移植且安全,具有小巧的二进制文件,以减少作为网页应用程序一部分进行部署时的下载时间。它容易将字节码移植到多种计算机硬件,并提供一个安全的平台来通过网络部署代码。

所有主要浏览器厂商都已经采用了 WebAssembly。根据 Mozilla 基金会的说法,WebAssembly 代码的执行速度比等效的 JavaScript 代码快 10%到 800%。一个 eBay 的 WebAssembly 项目比原始的 JavaScript 版本执行速度快了 50 倍。在本书后续内容中,我们将构建一个碰撞检测程序,用于衡量性能。我们运行它时,性能基准测试发现我们的 WebAssembly 碰撞检测代码在 Chrome 中运行速度比 JavaScript 快了四倍多,在 Firefox 中则比 JavaScript 快了两倍多。

WebAssembly 提供了自引入即时编译(JIT)JavaScript 编译器以来,网页性能上最显著的提升。现代浏览器的 JavaScript 引擎能比 JavaScript 快一个数量级地解析和下载 WebAssembly 二进制格式。WebAssembly 作为一个二进制目标,而不是像 JavaScript 这样的编程语言,允许开发者选择最适合其应用需求的编程语言。最近流行的说法“JavaScript 是网页的汇编语言”或许变得时髦,但 JavaScript 格式是一个糟糕的编译目标。JavaScript 不仅效率不如像 WebAssembly 这样的二进制格式,而且任何 JavaScript 目标代码还必须处理 JavaScript 语言的细节。

WebAssembly 在两个领域提供了巨大的网页应用性能提升。其中之一是启动速度。目前,最紧凑的 JavaScript 格式是最小化的 JavaScript,它提高了应用程序的下载大小,但必须解析、解释、JIT 编译并优化 JavaScript 代码。这些步骤在 WebAssembly 二进制格式中是多余的,WebAssembly 也更紧凑。WebAssembly 仍然需要解析,但它的速度更快,因为它是字节码格式而非文本格式。Web 引擎仍然会对 WebAssembly 进行优化,但速度要快得多,因为这种语言设计得更为简洁。

WebAssembly 提供的另一个显著的性能提升体现在吞吐量上。WebAssembly 使得浏览器引擎更容易进行优化。JavaScript 是一种高度动态且灵活的编程语言,这对 JavaScript 开发者来说非常有帮助,但却会带来代码优化的噩梦。WebAssembly 不做任何网页特定的假设(尽管它的名字如此),并且可以在浏览器之外使用。

最终,WebAssembly 可能能够做 JavaScript 能做的一切。不幸的是,目前的版本——其 MVP(最小可行产品)版本 1.0——做不到。在 MVP 版本中,WebAssembly 可以很好地完成某些任务。它并不是为了成为 JavaScript 或像 Angular、React 或 Vue 这样的框架的直接替代品。如果你现在想使用 WebAssembly,你应该有一个需要非常高性能的特定计算密集型项目。在线游戏、WebVR、3D 数学和加密是人们目前使用 WebAssembly 的有效方式。

使用 WebAssembly 的理由

在我们更深入地了解 WebAssembly 之前,先来看看你可能对使用它感兴趣的几个理由。这些解释还应该能帮助你理解 WebAssembly 是什么,以及为什么和如何使用它。

更好的性能

JavaScript 要求软件工程师做出会影响 JavaScript 引擎设计的选择。例如,你可以使用 JIT 优化编译器来优化 JavaScript 引擎的峰值性能,这可以使代码执行更快,但需要更多的启动时间。或者,你可以使用解释器,它可以立即开始执行代码,但无法达到 JIT 优化编译器的峰值性能。大多数 JavaScript 引擎设计师在他们的 Web 浏览器中使用的解决方案是同时实现这两者,但这需要更大的内存占用。你做的每一个决策都是一个权衡。

WebAssembly 允许更快的启动时间和更高的峰值性能,同时避免了过多的内存膨胀。不幸的是,你不能仅仅将 JavaScript 重新编写为 AssemblyScript、Rust 或 C++,然后期待它会发生这种情况,而不做一些额外的工作。WebAssembly 不是魔法,单纯地将 JavaScript 移植到另一种语言并编译它,而不了解 WebAssembly 在更低层次上是如何工作的,可能会导致一些令人失望的结果。编写 C++ 代码并使用优化标志将其编译为 WebAssembly 通常会比 JavaScript 稍微快一些。偶尔,程序员会抱怨他们整整一天都在用 C++ 重写应用程序,结果它只比原来快了 10%。如果是这种情况,可能这些应用程序并不会从转向 WebAssembly 中受益,它们的 C++ 最终会被编译成大部分的 JavaScript。花时间学习 WebAssembly,而不是 C++,让你的 Web 应用程序运行得飞快。

集成遗留库

有两个流行的库用于将现有库移植到 WebAssembly,它们分别是 Rust 的 wasm-pack 和 C/C++ 的 Emscripten。使用 WebAssembly 非常适合当你有现有的 C/C++ 或 Rust 编写的代码,想要将其提供给 Web 应用程序,或是想将整个现有的桌面应用程序移植到 Web 上。如果你选择这条路径,Emscripten 工具链特别适合将现有的 C++ 桌面应用程序移植到 Web 上并使用 WebAssembly。 如果这是你的选择,你可能希望你的应用程序在性能上尽可能接近原生应用的速度,只要应用程序不是资源消耗过大的话,这应该是可行的。然而,你也可能有一个需要性能调优的应用程序,使其在 Web 上的表现接近桌面版的效果。在本书结束时,你将能够评估你的工具链从现有代码生成的 WebAssembly 模块。

可移植性和安全性

我们将便携性和安全性特性合并到一个部分,因为它们通常是一起出现的。WebAssembly 起初是为了在浏览器中运行的技术,但它正在迅速扩展,成为一个可以在任何地方运行的沙盒环境。从服务器端的 WASI 代码到 WebAssembly 在嵌入式系统和物联网(IoT)中的应用,WebAssembly 工作组正在创建一个高度安全的运行时环境,防止恶意行为者破坏你的代码。我推荐你收听 Lin Clark 在第一次 WebAssembly Summit 上关于 WebAssembly 安全性和包重用的精彩演讲(www.youtube.com/watch?v=IBZFJzGnBoU/)。

尽管 WebAssembly 工作组专注于安全性,但没有任何系统是完全安全的。学习从低层次理解 WebAssembly 将帮助你为未来的安全风险做好准备。

JavaScript 怀疑论者

有些人简单地不喜欢 JavaScript,希望 JavaScript 不是主流的 Web 编程语言。不幸的是,WebAssembly 并没有能力推翻 JavaScript。今天,JavaScript 和 WebAssembly 必须共存,并且能够良好配合,如 图 1-1 所示。

但是对于世界上的 JavaScript 怀疑论者来说,还是有好消息的:WebAssembly 工具链提供了许多选项,让你能够在不写 JavaScript 的情况下编写 Web 应用程序。例如,Emscripten 允许你用 C/C++ 编写 Web 应用程序,几乎不需要,甚至完全不需要 JavaScript。你还可以使用 Rust 和 wasm-pack 编写完整的 Web 应用程序。这些工具链不仅生成 WebAssembly,还为你的应用程序生成大量的 JavaScript 配套代码。原因是,目前 WebAssembly 的能力有限,工具链用 JavaScript 代码填补这些空白。像 Emscripten 这样的成熟工具链的优点就是,它们为你做了这些。如果你正在使用这些工具链开发,了解你的代码何时会转化为 WebAssembly,何时会是 JavaScript,是很有帮助的。这本书将帮助你了解什么时候会发生这种情况。

f01001

图 1-1:JavaScript 和 WebAssembly 可以和谐共存。

WebAssembly 与 JavaScript 的关系

澄清 WebAssembly 如何与 JavaScript 一起使用并进行比较非常重要。WebAssembly 并不是 JavaScript 的直接替代品;相反,WebAssembly:

  • 下载、编译和执行速度更快

  • 允许你使用除了 JavaScript 之外的其他语言编写 Web 应用程序

  • 当正确使用时,可以为你的应用程序提供接近本地的速度

  • 在适当使用时, JavaScript 一起工作,可以提高你的 Web 应用程序的性能

  • 它不是一种汇编语言,尽管它有一个与之相关的伪汇编语言(WAT)

  • 不仅仅适用于 Web,还可以从非浏览器 JavaScript 引擎(如 Node.js)执行,或者使用实现了 WASI 的运行时执行

  • 目前还没有一种适用于所有情况的解决方案来创建 Web 应用程序

WebAssembly 是所有主要浏览器厂商合作的结果,旨在创建一个新的平台,用于通过互联网分发应用程序。JavaScript 语言源于 1990 年代末期对 web 浏览器的需求,发展成今天成熟的脚本语言。尽管 JavaScript 已经成为一种相对快速的语言,web 开发者们注意到它有时表现得不稳定。WebAssembly 是解决 JavaScript 性能问题的一个方案。

虽然 WebAssembly 无法完成 JavaScript 所能做的所有事情,但它能够在执行某些操作时,比 JavaScript 更快且消耗更少的内存。本书中,我们将对比 JavaScript 代码与对应的 WebAssembly。我们将反复进行基准测试和性能分析进行比较。在本书的结尾,你将能够判断什么时候应该使用 WebAssembly,什么时候继续使用 JavaScript 更为合适。

为什么要学习 WAT?

许多 WebAssembly 书籍和教程专注于特定的工具链,例如上述的用于 Rust 的 wasm-pack 或用于 C/C++ 的 Emscripten。其他语言的工具链,如 AssemblyScript(TypeScript 的一个子集)和 Go,目前正在开发中。这些工具链是程序员转向 WebAssembly 的一个主要原因,越来越多的 WebAssembly 语言工具链不断涌现。未来,web 开发者将能够根据项目需求而非语言可用性来选择开发语言。

在这些语言中,有一个因素是通用的,那就是理解 WebAssembly 在最低层级的工作原理。深入理解 WAT 可以让你明白为什么代码的执行速度可能没有你预期的快。它有助于你理解 WebAssembly 如何与其嵌入环境交互。在 WAT 中编写模块是尽可能接近底层(低级)工作的最佳方式。了解 WAT 可以帮助你创建最高性能的 web 应用,并允许你反汇编和评估任何为 WebAssembly 平台编写的 web 应用程序。它有助于你评估潜在的安全风险。此外,它使你能够编写尽可能接近本地速度的代码,而无需编写本地代码。

那么,什么是 WAT?WAT 就像是 WebAssembly 虚拟机的汇编语言。让我们从实际的角度来看看这意味着什么。用 Rust 或 C++ 等语言编写 WebAssembly 程序时,使用的工具链会编译出一个 WebAssembly 二进制文件,还会生成 JavaScript 配合代码和嵌入 WebAssembly 模块的 HTML。WebAssembly 文件与机器代码非常相似,因为它包含了各个部分、操作码和数据,所有这些都存储为一系列二进制数字。当你拥有一个机器代码的可执行文件时,可以将该文件反汇编成该机器的汇编语言,这是一种最低级的编程语言。汇编语言将二进制中的数字操作码替换为助记码,旨在便于人类读取。WAT 就充当了 WebAssembly 的汇编语言。

WAT 编码风格

有两种主要的 WAT 编码风格可以选择。一种风格是线性指令列表风格。这种编码风格要求开发者在心理上跟踪栈中的项。大多数 WAT 指令会将项推入栈中、从栈中弹出项,或两者兼有。如果选择使用线性指令风格,那么在调用指令之前,指令的参数必须放置在隐式栈中。另一种编码风格叫做S-表达式。S-表达式是一种树状结构的编码方式,参数以一种类似于 JavaScript 函数调用的方式传入树中。如果你在可视化栈和项的推入与弹出时遇到困难,S-表达式语法可能更适合你。你也可以根据隐式栈使用两种风格:对于较不复杂的指令使用线性指令风格,对于参数数量较多且难以跟踪时使用 S-表达式。

使用线性指令列表风格的示例

考虑在清单 1-1 中展示的简单加法函数,它使用的是 JavaScript。

function main() {
    let a_val = 1;
    let b_val = 2;
    let c_val = a_val + b_val;
}

清单 1-1:JavaScript 代码添加 a_valb_val 变量

执行这些代码行后,c_val 变量的值现在是 3,这是将 a_valb_val 相加的结果。要在 WAT 中完成相同的任务,你需要编写相当多的代码行。清单 1-2 展示了使用 WAT 编写的相同程序。

(module
  1 (global $a_val (mut i32) (i32.const 1))
  2 (global $b_val (mut i32) (i32.const 2))
    (global $c_val (mut i32) (i32.const 0))
    (func $main (export "main")
        global.get $a_val
        global.get $b_val

        i32.add
        global.set $c_val
    )
)

清单 1-2:WebAssembly 将 $a_val 加到 $b_val

示例 1-2 包含更多的代码行,因为 WAT 必须比 JavaScript 更加明确。JavaScript 在代码运行之前无法知道前两个示例中的类型是浮点数据、整数、字符串,还是它们的混合。而 WebAssembly 是提前编译成字节码的,并且必须在编译时意识到它使用的类型。JavaScript 必须在 JIT 编译器将其转换为字节码之前进行解析和标记化。一旦优化编译器开始处理这些字节码,编译器就必须观察变量是否始终为整数。如果是这样,JIT 编译器就可以创建一个假设这些变量为整数的字节码。

然而,JavaScript 永远无法确定在期望整数时是否会得到字符串数据或浮点数据;因此,随时都必须准备好丢弃优化后的代码并重新开始。WAT 代码可能更难编写和理解,但它更容易被网页浏览器执行。WebAssembly 将大量工作从浏览器转移到工具链编译器或开发人员身上。不必做那么多工作,使得浏览器更加高兴,应用程序运行得更快。

堆栈机器

如前所述,WebAssembly 是一个虚拟堆栈机器。让我们来探讨一下这意味着什么。可以将堆栈想象成一堆碗碟。这个比喻中的每个碗碟就是一块数据。当你将一个碗碟放到堆栈上时,你将它放在已经存在的碗碟上面。当你从堆栈中取出一个碗碟时,你不是从底部取出,而是从顶部取出。因此,你最后放到堆栈上的碗碟是第一个取出的碗碟。在计算机科学中,这叫做后进先出LIFO)。将数据添加到堆栈中叫做推入push),从堆栈中取出数据叫做弹出pop)。使用堆栈机器时,几乎所有指令都会与堆栈进行某种交互,要么通过推入操作将数据添加到堆栈顶部,要么通过弹出操作从堆栈顶部移除数据。图 1-2 显示了堆栈交互的示意图。

f01002

图 1-2:堆栈机器从堆栈中弹出值并将值推入堆栈。

如前所见,在示例 1-2 中,$main 函数的前两行先将 $a_val 推入堆栈 1 的顶部,然后再将 $b_val 推入其上面 2。结果是堆栈中有两个值。堆栈底部的值是 $a_val 中的值,因为它是第一个添加的,堆栈顶部的值是 $b_val 中的值,因为它是最后添加的。

重要的是要区分堆栈机器的 ISA(指令集架构),比如 WebAssembly,和寄存器机器的 ISA,比如 x86、ARM、MIPS、PowerPC 或过去 30 年里任何其他流行的硬件架构。寄存器机器必须将数据从内存移动到 CPU 寄存器中,以便执行数学运算。WebAssembly 是一个虚拟堆栈机器,必须运行在寄存器机器上。当我们编写 WAT 格式的代码时,你将近距离观察到这种交互。

栈机器通过将数据推入栈中和从栈中弹出数据来执行计算。硬件栈机器是一种稀有的计算机类型。像 WebAssembly 这样的虚拟栈机器则更为常见;例如 Java 的 JVM、Adobe Flash 播放器的 AVM2、以太坊的 EVM 和 CPython 字节码解释器。虚拟栈机器的优势在于它们能创建更小的字节码,这对于任何需要通过互联网下载或流式传输的字节码来说非常有用。

栈机器不对嵌入环境中可用的一般寄存器数量做出任何假设。这使得硬件能够选择何时使用哪些寄存器。如果你不清楚栈机器是如何工作的,WAT 代码可能会有些混乱,那么让我们再次看看$main函数的前两行代码,特别是在考虑栈的情况下(列表 1-3)。

global.get $a_val ;; push $a_val onto the stack
global.get $b_val ;; push $b_val onto the stack

列表 1-3:获取$a_val$b_val,然后将它们推入栈中

第一行获取了$a_val的值,我们将其定义为全局值,第二行获取了全局变量$b_val。这两个项最终被推入栈中,等待处理。

函数i32.add从栈中取出两个 32 位整数变量,将它们相加,然后将结果重新推入栈顶。当这两个值被放入栈中时,我们可以调用i32.add。如果你运行一个弹出栈中更多值的函数,而栈中没有足够的值,转换 WAT 为 WebAssembly 二进制的工具将不允许这样做,并会抛出编译错误。我们在$main函数的最后一行使用栈中的值来设置$c_val变量。这个值是i32.add函数调用的结果。

使用 S 表达式的示例

S 表达式是一种用于编程语言中的嵌套树结构编码风格,例如 Lisp。在列表 1-3 中,我们使用了线性指令列表风格来编写 WAT。线性指令风格在每个调用语句和表达式调用时,隐式地使用栈。对于有一定汇编语言经验的人来说,这种方法可能会感觉比较熟悉。但如果你是从高层语言(如 JavaScript)转向 WebAssembly 的话,S 表达式语法可能会更容易理解。S 表达式以嵌套结构组织你对 WAT 语句和表达式的调用。线性风格要求你在编写代码时,脑海中将项推入栈中并弹出。而 S 表达式更像是 JavaScript 函数调用,而不是线性风格。

在列表 1-2 中,我们通过栈将c_val设置为a_val + b_val。在列表 1-4 中的代码是列表 1-2 中将这些值相加的代码片段:

1 global.get $a_val ;; push $a_val onto the stack
global.get $b_val ;; push $b_val onto the stack

2 i32.add           ;; pop two values, add and place result on stack
global.set $c_val ;; pop a value off the stack and set $c_val

列表 1-4:在 WebAssembly 中添加并设置$c_val

我们将两个 32 位整数变量推送到堆栈中,这些变量是通过 global.get 1 从全局变量中获取的。然后我们通过调用 i32.add 弹出这两个值。将这两个值相加后,i32.add 2 函数将结果值推回堆栈。这就是堆栈机器的工作方式。每个指令要么将值推送到堆栈中,要么从堆栈中弹出值,或者两者兼有。

列表 1-5 显示了使用替代的 S-Expression 语法的相同函数。

(module
  (global $a_val (mut i32) (i32.const 1))
  (global $b_val (mut i32) (i32.const 2))
  (global $c_val (mut i32) (i32.const 0))
  (func $main (export "main")
  1 (global.set $c_val
      (i32.add (global.get $a_val) (global.get $b_val))
    )
  )
)

列表 1-5:WebAssembly 模块,用于相加两个值

不要让括号弄混你:它们的作用和许多语言中的{}字符相同,用来创建代码块。当编写 WAT 函数时,我们将函数包含在括号中。当你将匹配的右括号放在与左括号相同的缩进位置时,它看起来类似于你在像 JavaScript 这样的语言中缩进 {} 字符。例如,看看 global.set 1 调用前的 ( 的缩进,并用眼睛对齐它下面的闭合 )

这段代码看起来更像是一种传统的编程语言,而不是 列表 1-2,因为它似乎将参数传递给函数,而不是通过堆栈推送和弹出值。明确来说,这段代码会编译成相同的二进制文件。如果你以 S-Expressions 风格编写代码,你仍然是在堆栈上推送和弹出项。这种写 WAT 的风格只是 语法糖(使代码更易读的语法)。当你熟悉将 WebAssembly 文件反汇编成 WAT 时,你会发现反汇编工具(例如 wasm2wat)并没有提供 S-Expression 语法。

嵌入环境

如前所述,WebAssembly 并不直接运行在硬件上。你必须将 WebAssembly 二进制嵌入到一个主机环境中,这个环境控制着 WebAssembly 模块的加载和初始化。在本书中,我们使用 JavaScript 引擎,如 Node.js 和 Web 浏览器作为嵌入环境。其他环境包括 WASI,如 wasmtime(稍后定义)。但是即便我们讨论 WASI,我们在本书中不会使用它,因为它仍然非常新且在开发中。实现堆栈机器的是嵌入环境。由于现代硬件通常是寄存器机器,嵌入环境通过硬件寄存器来管理堆栈。

浏览器

你很有可能对 WebAssembly 感兴趣,因为你希望它能提升你网页应用程序的性能。所有现代浏览器的 JavaScript 引擎都实现了 WebAssembly。目前,Chrome 和 Firefox 拥有最好的 WebAssembly 调试工具,因此我们建议选择其中一款浏览器进行开发。你的 WAT 应用程序在 Microsoft Edge 中也应该能正常运行,但 Internet Explorer 不再增加新功能。遗憾的是,Internet Explorer 不支持 WebAssembly,并且永远不会支持。

当你为网页浏览器编写 WAT 时,理解哪些部分可以用 WAT 编写,哪些必须用 JavaScript 编写是至关重要的。也可能会有情况,WebAssembly 带来的性能提升不一定值得额外的开发时间。如果你理解 WAT 和 WebAssembly,你将能够做出这些决策。在使用 WebAssembly 时,你必须频繁地在性能和开发时间之间做出权衡,或者在 CPU 周期和内存之间做出牺牲,反之亦然。性能优化就是关于做出选择。

WASI

WASI 是 WebAssembly 应用程序的运行时规范,是 WebAssembly 与操作系统交互的标准。它允许 WebAssembly 使用文件系统、进行系统调用、处理输入输出。Mozilla 基金会创建了一个名为wasmtime的 WebAssembly 运行时,实施了 WASI 标准。通过 WASI,WebAssembly 能够做所有本地应用程序可以做的事情,但以安全、平台独立的方式进行。它的性能与本地应用程序相似。

Node.js 还可以使用--experimental-wasi-unstable-preview1标志运行 WASI 实验预览版。你可以用它运行与操作系统交互的 WebAssembly 应用程序,而不依赖网页浏览器。Windows、macOS、Linux 或任何其他操作系统都可以实现 WASI 运行时,因为它的设计目标是使 WebAssembly 具有可移植性、安全性,并最终实现普遍适用。

Visual Studio Code

Visual Studio Code (VS Code) 是一个开源集成开发环境(IDE),也是我用来编写本书示例的工具。VS Code 可在 Windows、macOS 和 Linux 上使用,下载地址为code.visualstudio.com/download。我们使用由 Dmitriy Tsvettsikh 编写的 WebAssembly 扩展,下载链接为marketplace.visualstudio.com/items?itemName=dtsvet.vscode-wasm。该扩展为 WAT 格式提供了代码高亮,并且包含几个其他有用的菜单项。例如,如果你有一个 WebAssembly 文件,你可以通过右键点击文件并选择Show WebAssembly菜单选项,将其反汇编为 WAT 格式。如果你想查看不是自己编写的 WebAssembly 代码,或者是通过工具链编译的代码,这非常有用。该扩展还可以将你的 WAT 文件编译为 WebAssembly 二进制文件。你可以右键点击.wat文件并选择Save as WebAssembly binary file,然后会弹出保存文件提示,允许你指定保存 WebAssembly 文件的文件名。

图 1-3 显示了扩展的截图。

f01003

图 1-3:为 VS Code 安装 WebAssembly 扩展

Node.js

Node.js 是一个出色的工具,用于测试 WebAssembly 模块与现有 JavaScript 模块的性能,并且是本书中我们在许多示例中使用的 JavaScript 运行时环境。Node.js 自带 npm (Node 包管理器),你可以使用它轻松安装代码包。WebAssembly 是编写 Node.js 原生模块的一个很好的替代方案,因为原生模块会将你锁定在特定的硬件上。如果你想为通用用途创建一个 npm 模块,编写 WebAssembly 可以让你获得原生模块的性能,同时保留 JavaScript 模块的可移植性和安全性。我们将使用 Node.js 执行本书中编写的许多应用程序。

Node.js 是我们执行 WebAssembly 的首选开发工具,无论是通过 JavaScript 还是通过 Web 服务器。在第七章中,我们将使用 Node.js 从 JavaScript 执行 WebAssembly 模块,并且将编写一个简单的 Web 服务器来提供 WebAssembly Web 应用程序。

Node.js 自带 npm,这使得安装一些用于开发 WebAssembly 的工具变得容易。在本节中,我们将展示如何使用 npm 安装 wat-wasm 模块,它是一个用于编译、优化和反汇编 WebAssembly 的工具。我们还将展示如何使用 Node.js 编写一个简单的 WebAssembly 应用程序。许多读者可能已经熟悉 Node.js,但如果没有,网上有大量的 Node.js 文档可供参考,如果你想了解更多的内容,而不仅仅是这里简短的介绍和设置部分。

安装 Node.js

必须安装 Node.js 才能完成本书中的代码示例。幸运的是,安装过程并不复杂。如果你使用的是 Windows 或 macOS,可以在 nodejs.org/en/download/ 下载适用于这两个操作系统的安装程序。

对于 Ubuntu Linux,你可以使用以下 apt 命令安装 Node:

sudo apt install nodejs

安装好 Node 后,在命令提示符(在任何平台上)中运行以下命令,以确保一切安装正常:

node -v

如果一切安装成功,你应该会看到安装的 Node.js 版本作为输出。当我们在 Windows 机器上运行命令 node -v 时,它输出如下内容:

v12.14.0

这意味着我们正在运行版本 12.14.0。

安装 wat-wasm

有许多工具可用于将 WAT 代码转换为 WebAssembly 二进制文件。事实上,在写这本书的时候,我使用了其中许多工具。最终,我在 WABT.jsBinaryen.js 的基础上编写了 wat-wasm,以减少我想展示的功能所需的包的数量。要安装 wat-wasm,请执行以下 npm 命令:

npm install -g wat-wasm

-g 标志会将 wat-wasm 安装为全局模块。在本书中,我们将使用像 wat2wasm 这样的命令行工具。为了使这些工具不仅限于当前项目,你需要将它们全局安装。一旦安装了 wat-wasm,可以通过在命令行中运行 wat2wasm 命令来确认它是否可以正常运行:

wat2wasm

然后你应该能在控制台中看到 wat-wasm 的使用日志。这将展示你在本书后续内容中将要学习的各种标志。

你可以通过创建最简单的 WAT 模块来测试 wat2wasm,如 列表 1-6 所示。创建一个名为 file.wat 的新文件,并将以下代码输入该文件:

(module)

列表 1-6:最简单的 WebAssembly 模块

安装 wat-wasm 后,你可以使用 列表 1-7 中的命令将 file.wat 文件编译为 file.wasm,即 WebAssembly 二进制文件:

wat2wasm file.wat

列表 1-7:使用 wat2wasm 汇编 file.wat 文件

在本书中,我们将始终使用 Node.js 来运行 WebAssembly 命令行应用,并将 WebAssembly 网络应用提供给浏览器打开。在下一节中,我们将编写第一个 WebAssembly 应用,并通过 Node.js 执行。

我们的第一个 Node.js WebAssembly 应用

本书从使用 Node.js 作为嵌入环境开始,而不是使用网页浏览器,以便简化代码示例中的 HTML 和 CSS 部分。稍后,在掌握基础知识后,我们将探索如何使用浏览器作为嵌入环境。

我们在 Node.js 应用中的 WAT 代码将与浏览器中的表现一样。Node.js 内部的 WebAssembly 引擎与 Chrome 内部的 WebAssembly 引擎相同,应用程序中的 WebAssembly 部分完全不关心它所运行的环境。

让我们从创建一个简单的 WAT 文件开始,并使用 wat2wasm 进行编译。创建一个名为 AddInt.wat 的文件,并将 列表 1-8 中的 WAT 代码添加到其中。

AddInt.wat

(module
    (func (export "AddInt")
    (param $value_1 i32) (param $value_2 i32)
    (result i32)
 local.get $value_1
        local.get $value_2
        i32.add
    )
)

列表 1-8:带有两个整数相加函数的 WebAssembly 模块

到目前为止,你应该能理解这段代码。花点时间仔细查看,直到你对其逻辑感到熟悉。这是一个简单的 WebAssembly 模块,包含一个 AddInt 函数,我们将其导出到嵌入环境中。现在,使用 wat2wasmAddInt.wat 编译为 AddInt.wasm,如 列表 1-9 所示。

wat2wasm AddInt.wat

列表 1-9:将 AddInt.wat 编译为 AddInt.wasm

现在我们已经准备好编写第一个 Node.js 应用的 JavaScript 部分。

从 Node.js 调用 WebAssembly 模块

我们可以使用 JavaScript 从 Node.js 调用 WebAssembly 模块。创建一个名为 AddInt.js 的文件,并将 列表 1-10 中的 JavaScript 代码添加到其中。

AddInt.js

1 const fs = require ('fs');
const bytes = fs.readFileSync (__dirname + '/AddInt.wasm');
2 const value_1 = parseInt (process.argv[2]);
const value_2 = parseInt (process.argv[3]);

3 (async () => {
4 const obj = await WebAssembly.instantiate (
                                new Uint8Array (bytes));
5 let add_value = obj.instance.exports.AddInt( value_1, value_2 );
6 console.log(`${value_1} + ${value_2} = ${add_value}`);
})();

列表 1-10:从异步 IIFE 调用 AddInt WebAssembly 函数

Node.js 可以直接从应用程序运行所在的硬盘读取 WebAssembly 文件,使用的是名为fs的内建模块,该模块可以从本地存储中读取文件。我们使用 Node.js 的require函数加载这个模块。我们使用fs模块通过readFileSync函数读取AddInt.wasm文件。我们还通过process.argv 2 数组从命令行接收两个参数。argv数组包含了从命令行传递给 Node.js 的所有参数。我们从命令行运行该函数;process.argv[0]将包含命令nodeprocess.argv[1]将包含 JavaScript 文件AddInt.js的名称。当我们运行程序时,会在命令行传入两个数字,这会设置process.argv[2]process.argv[3]

我们使用一个异步立即调用函数表达式(IIFE)来实例化 WebAssembly 模块、调用 WebAssembly 函数并将结果输出到控制台。对于不熟悉 IIFE 语法的人来说,它是一种使 JavaScript 能够在执行其余代码之前等待 Promise 的方式。当你执行像实例化 WebAssembly 模块这样的任务时,它需要时间,而你不希望在等待该过程完成时阻塞浏览器或 Node.js。(async () => {})(); 3 语法告诉 JavaScript 引擎将会有一个 Promise 对象,所以可以在等待结果的过程中做些其他事情。在 IIFE 内部,我们调用WebAssembly.instantiate 4,传入之前通过readFileSync函数读取的 WebAssembly 文件的bytes。实例化模块后,我们调用从 WAT 代码中导出的AddInt 5 函数。然后,我们调用console.log 6 语句来输出我们正在加的值和结果。

现在我们已经有了 WebAssembly 模块和 JavaScript 文件,我们可以通过命令行调用 Node.js 来运行应用程序,正如 Listing 1-11 所示。

node AddInt.js 7 9

Listing 1-11: 使用 Node.js 运行AddInt.js

运行该命令会输出以下结果:

7 + 9 = 16

两个整数的加法在 WebAssembly 中执行。在继续之前,我们将简要展示如何使用.then语法作为异步 IIFE 的替代方法。

.then 语法

另一种广泛使用的等待 Promise 返回的语法是.then语法。我们更倾向于在 Listing 1-10 中使用 IIFE 语法,但两种语法都是完全可以接受的。

创建一个名为AddIntThen.js的文件,并将 Listing 1-12 中的代码添加到其中,替换掉 Listing 1-10 中的异步 IIFE 语法,改为使用.then代码。

AddIntThen.js

const fs = require ('fs');
const bytes = fs.readFileSync (__dirname + '/AddInt.wasm');
const value_1 = parseInt (process.argv[2]);
const value_2 = parseInt (process.argv[3]);

1 WebAssembly.instantiate (new Uint8Array (bytes))
2 .then (obj => {
    let add_value = obj.instance.exports.AddInt(value_1, value_2);
    console.log(`${value_1} + ${value_2} = ${add_value}`);
  });

Listing 1-12: 使用.then语法调用 WebAssembly 函数

这里的主要区别在于WebAssembly.instantiate函数,后跟.then,并包含一个箭头函数回调,将一个对象obj作为参数传入。

现在就是时候了

现在是学习 WAT 的好时机。在撰写本文时,WebAssembly 1.0 的当前版本具有相对较小的指令集,WebAssembly 二进制文件中共有 172 个不同的操作码,尽管你不需要记住所有这些操作码。WebAssembly 支持四种不同的数据类型:i32i64f32f64,而且许多操作码是每种类型的重复命令(例如,i32.addi64.add)。如果你去除重复的操作码,你只需要了解大约 50 个不同的助记符就能掌握整个语言。随着时间的推移,WebAssembly 支持的操作码数量会增加。从 WebAssembly 的早期阶段开始学习,你将获得一定的优势。未来,记住每一个操作码将变得困难甚至不可能。

如前所述,在 WAT 中编写模块是尽可能在 Web 浏览器中贴近底层操作的最佳方式。今天 JavaScript 在浏览器中的实现方式可能会因为各种因素导致性能不一致。WebAssembly 可以消除这些不一致,而 WAT 可以帮助你优化代码,使其尽可能快速。

你可以使用像 Emscripten 这样的工具链,只需对 WebAssembly 平台有最基本的了解。然而,这样使用工具链可能会导致你的应用程序性能提升有限,并误导你得出 WebAssembly 不值得投入的结论。你会错的。如果你想从你的 Web 应用程序中获得最高性能,你必须尽可能多地了解 WebAssembly。你需要知道它能做什么,不能做什么。你必须理解它擅长什么,以及你应该在 JavaScript 中做什么。获得这些知识的最佳方式是编写 WAT 代码。最终,你可能不会用 WAT 编写应用程序,但了解这种语言有助于你理解 WebAssembly 和 Web 浏览器。

第二章:WebAssembly 文本基础

在本章中,我们将深入探讨 WAT 代码的基础知识。我们将在本书中大部分时间编写 WAT 代码,这是你可以为 WebAssembly 部署编写的最低级别的编程(尽管对于有经验的汇编程序员来说,这可能显得相当高级)。

本章涵盖了很多内容。我们将从展示 WebAssembly 中的两种注释样式开始。接下来,我们将编写传统的 hello world 应用程序。我们不从 hello world 开始,因为在 WAT 中处理字符串比你想象的要复杂。

然后我们将讨论如何使用导入对象从 JavaScript 导入数据到 WebAssembly 模块。我们将研究命名和未命名的全局与局部变量,以及 WebAssembly 支持的数据类型。我们还会讨论 S-表达式语法,以及wat2wasm编译器如何在编译代码时解包这些 S-表达式。你将深入了解条件逻辑,包括if/else语句和分支表,并且学习如何结合条件逻辑使用循环和块。

到本章结束时,你应该能够编写简单的 WebAssembly 应用程序,并通过命令行使用 Node.js 执行它们。

编写最简单的模块

每个 WAT 应用程序都必须是一个模块,因此我们首先会看看模块语法。我们在一个块中声明模块,就像示例 2-1 中展示的那样。

(module
  ;; This is where the module code goes.
)

示例 2-1:单行 WAT 注释

我们通过module关键字声明一个模块,括号内的内容是模块的一部分。要添加注释,我们使用两个分号;;,之后的内容为注释。WAT 也有块注释语法;你可以用(;打开块注释,用;)关闭块注释,如示例 2-2 所示。

(module
 (;
 This is a module with a block comment.
 Like the /* and */ comments in JavaScript
 you can have as many lines as you like inside
 between the opening and closing parenthesis
 ;)
)

示例 2-2:多行 WAT 注释

由于此模块不做任何事情,我们不会费力去编译它。相反,我们将继续编写我们的 hello world 应用程序。

WebAssembly 中的 Hello World

WAT 没有原生的字符串支持,因此处理字符串需要直接操作内存作为字符数据的数组。这些内存数据必须转换成 JavaScript 代码中的字符串,因为从 JavaScript 中操作字符串要简单得多。

在 WAT 中处理字符串时,你需要声明一个存储在 WebAssembly 线性内存中的字符数据数组。线性内存是我们将在第六章详细讨论的主题,但现在你只需知道线性内存类似于本地应用程序中的内存堆,或者 JavaScript 中的一个巨大的类型化数组。

你还需要从 WebAssembly 调用一个导入的 JavaScript 函数来处理 I/O 操作。与本地应用程序通常由操作系统处理 I/O 不同,在 WebAssembly 模块中,I/O 必须由嵌入环境来处理,无论这个环境是网页浏览器、操作系统还是运行时。

创建我们的 WAT 模块

在这一部分,我们将创建一个简单的 WebAssembly 模块,在线性内存中创建一个hello world!字符串,并调用 JavaScript 将该字符串写入控制台。创建一个新的 WAT 文件并命名为helloworld.wat。打开该文件并添加列表 2-3 中的 WAT 代码。

helloworld.wat

(module
  (import "env" "print_string" (func $print_string( param i32 )))
)

列表 2-3:导入一个函数

这段代码告诉 WebAssembly 预计从我们嵌入的环境中导入对象env,并且在该对象中我们期望得到函数print_string。当我们稍后编写 JavaScript 代码时,我们将创建这个env对象,并将print_string函数传递给 WebAssembly 模块,当我们实例化它时。

我们还设置了签名,要求一个i32类型的参数,表示我们字符串的长度。我们将此函数命名为$print_string,以便可以从我们的 WAT 代码中访问它。

接下来,我们将添加对内存缓冲区的导入。在列表 2-4 中添加加粗的行。

helloworld.wat

(module
  (import "env" "print_string" (func $print_string( param i32 )))
  **(import "env" "buffer" (memory 1))**
)

列表 2-4:导入一个函数和内存缓冲区

这个新的import告诉我们的 WebAssembly 模块,我们将从env对象导入一个内存缓冲区,并且该缓冲区将被称为buffer(memory 1)语句表示缓冲区将是一个线性内存页面:页面是你可以一次分配给线性内存的最小内存块。在 WebAssembly 中,一个页面是 64KB,这对这个模块来说足够了,所以我们只需要一个页面。接下来,在列表 2-5 中,我们将添加一些全局变量到helloworld.wat中。

helloworld.wat

(module
  (import "env" "print_string" (func $print_string( param i32 )))
  (import "env" "buffer" (memory 1))
1 (global $start_string (import "env" "start_string") i32) 
2 (global $string_len i32 (i32.const **12))**
)

列表 2-5:添加全局变量

第一个global 1 变量是一个从我们的 JavaScript 导入对象导入的数字;它映射到 JavaScript 中名为env的变量(我们还未创建)。该值将是我们字符串的起始内存位置,可以是线性内存页面中任何位置,最大为 65,535。当然,你不希望选择接近线性内存末尾的值,因为这会限制你能够存储的字符串长度。如果传入的值是0,你可以使用整个 64KB 来存储字符串。如果你传入的值是65,532,你只能使用最后四个字节来存储字符数据。如果你尝试写入一个超过已分配内存位置的值,你将会在 JavaScript 控制台中遇到内存错误。第二个全局变量$string_len 2 是一个常量,表示我们将定义的字符串的长度,我们将其设置为12

在列表 2-6 中,我们使用数据表达式在线性内存中定义了我们的字符串。

helloworld.wat

(module
  (import "env" "print_string" (func $print_string( param i32 )))
  (import "env" "buffer" (memory 1))
  (global $start_string (import "env" "start_string") i32)
  (global $string_len i32 (i32.const 12))
  **(data (global.get $start_string) "hello world!")**
)

列表 2-6:添加数据字符串

我们首先传递模块将要写入数据的内存位置。数据存储在模块将从 JavaScript 导入的$start_string全局变量中。第二个参数是数据字符串,我们将其定义为字符串"hello world!"

现在,我们可以定义我们的 "helloworld" 函数并将其添加到模块中,如 列表 2-7 所示。

helloworld.wat

(module
  (import "env" "print_string" (func $print_string (param i32)))
  (import "env" "buffer" (memory 1))
  (global $start_string (import "env" "start_string") i32)
  (global $string_len i32 (i32.const 12))
  (data (global.get $start_string) "hello world!")	
1 (func (export "helloworld")
  2 (call $print_string (global.get $string_len))
  )
)

列表 2-7:向 WebAssembly 模块添加 "helloworld" 函数

我们将函数定义并导出为 "helloworld",以便在 JavaScript 1 中使用。这个函数唯一的功能就是调用导入的 $print_string 2 函数,并将我们定义为全局的字符串长度传递给它。现在,我们可以像下面这样编译 WebAssembly 模块:

wat2wasm helloworld.wat

运行 wat2wasm 会生成一个 helloworld.wasm 模块。为了执行这个 WebAssembly 模块,我们需要创建一个 JavaScript 文件来运行它。

创建 JavaScript 文件

现在,我们将创建 helloworld.js 来运行我们的 WebAssembly 模块。创建并打开 JavaScript 文件,在文本编辑器中添加 Node.js 文件常量和三个变量,如 列表 2-8 所示。

helloworld.js

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/helloworld.wasm');

1 let hello_world = null; // function will be set later
2 let start_string_index = 100; // linear memory location of string
3 let memory = new WebAssembly.Memory ({ initial: 1 }); // linear memory
...

列表 2-8:声明 JavaScript 变量

hello_world 1 变量最终会指向 WebAssembly 模块导出的 helloworld 函数,因此我们暂时将其设置为 nullstart_string_index 2 变量是我们字符串在线性内存数组中的起始位置。我们将其设置为 100,以避免接近 64KB 的限制。我们随意选择了地址 100。你可以选择任何地址,只要你使用的内存不超过 64KB 限制。

最后的变量保存了 WebAssembly.Memory 3 对象。传入的数字表示你希望分配的页面数。我们通过传入 {initial: 1} 作为唯一的参数来初始化它,表示分配一个页面。你最多可以通过这种方式分配两吉字节的内存,但如果设置的值过高,可能会导致错误,因为浏览器可能无法找到足够的连续内存来满足请求。

列表 2-9 展示了我们需要声明的下一个变量 importObject,它将在我们实例化 WebAssembly 模块时传入。

helloworld.js

...
let importObject = {
1 env: {
  2 buffer: memory,
  3 start_string: start_string_index,
  4 print_string: function (str_len) {
      const bytes = new Uint8Array (memory.buffer,
        start_string_index, str_len);
      const log_string = new TextDecoder('utf8').decode(bytes);
      console.log (log_string);
    }
 }
};
...

列表 2-9:在 JavaScript 中声明 importObject

importObject 内部,我们添加了一个名为 env 1 的对象,它是 环境(environment)的缩写,尽管你可以根据自己的喜好命名这个对象,只要它与 WebAssembly 导入声明中的名称匹配即可。这些是将传递给 WebAssembly 模块的值,当它被实例化时。如果你希望 WebAssembly 模块能够访问嵌入环境中的任何函数或值,可以将它们传递到这里。env 对象包含内存缓冲区 2 和我们字符串在 buffer 中的起始位置 3。env 中的第三个属性 4 包含我们的 JavaScript 函数 print_string,该函数将在 WebAssembly 模块中被调用,如 列表 2-9 所示。这个函数从我们的内存缓冲区中获取字符串的长度,并结合我们的起始字符串索引来创建一个字符串对象。然后,应用程序将在命令行上显示该字符串对象。

此外,我们添加了一个 IIFE,它异步加载我们的 WebAssembly 模块,然后调用helloworld函数,如示例 2-10 所示。

helloworld.js

...
( async () => {   
  let obj = await
1 WebAssembly.instantiate(new Uint8Array (bytes), importObject);
2 ({helloworld: hello_world} = obj.instance.exports);
3 hello_world();
})();

示例 2-10:在异步 IIFE 中实例化 WebAssembly 模块

async模块的第一行等待WebAssembly.instantiate函数调用,但与示例 1-1 中的简单加法示例不同,我们将之前声明的importObject传递给该函数。然后,我们使用解构语法从obj.instance.exports中提取helloworld函数,将hello_world变量设置为obj.instance.exports中的函数 2。

我们 IIFE 的最后一行调用了hello_world 3 函数。我们将箭头函数括在圆括号中,然后在函数声明的末尾添加函数调用圆括号,这会导致该函数立即执行。

Once you have the JavaScript and WebAssembly files, run the following call to `node` from the command line: ``` node helloworld.js ``` You should see the following output on the command line: ``` hello world! ``` We’ve built the ubiquitous hello world application! Now that you have the hello world application under your belt, we’ll explore variables and how they work in WAT. ## WAT Variables WAT treats variables a little differently than other programming languages, so it’s worth providing you with some details here. However, the browser manages local or global WAT variables in the same way it manages JavaScript variables. WAT has four global and local variable types: i32 (32-bit integer), i64 (64-bit integer), f32 (32-bit floating-point), and f64 (64-bit floating-point). Strings and other more sophisticated data structures need to be managed directly in linear memory. We’ll cover linear memory and the use of more complicated data structures in WAT in Chapter 6\. For now, let’s look at each variable type. ### Global Variables and Type Conversion As you might expect, you can access globals in WAT from any function, and we generally use globals as constants. *Mutable globals* can be modified after they’re set and are usually frowned upon because they can introduce side effects in functions that use them. You can import global variables from JavaScript, allowing the JavaScript portion of your application to set constant values inside your module. When importing global variables, keep in mind that, at the time of this writing, standard JavaScript number variables don’t support 64-bit integer values. Numbers in JavaScript are 64-bit floating-point variables. A 64-bit floating-point variable can represent every value in a 32-bit integer, so JavaScript has no trouble making this conversion. However, you cannot represent all possible 64-bit integer values with a 64-bit floating-point value. Unfortunately, this means that you can work with 64-bit integers in WebAssembly, but if you want to send 64-bit values to JavaScript, it requires additional effort, which is beyond the scope of this book. Another detail you must know about data types in WebAssembly and JavaScript is that JavaScript treats all numbers as 64-bit floating-point numbers. When you call a JavaScript function from WebAssembly, the JavaScript engine will perform an implicit conversion to a 64-bit float, no matter what data type you pass. However, WebAssembly will define the imported function as having a specific data type requirement. Even if you pass the same function into the WebAssembly module three times, you’ll need to specify a type that the parameter passed from WebAssembly. Let’s create a module named *globals.wat* that imports three numbers from JavaScript. The WAT file in Listing 2-11 declares global variables for a 32-bit integer, a 32-bit floating-point, and a 64-bit floating-point numeric value. **globals.wat** ``` (module 1 (global $import_integer_32 (import "env" "import_i32") i32) (global $import_float_32 (import "env" "import_f32") f32) (global $import_float_64 (import "env" "import_f64") f64) 2 (import "js" "log_i32" (func $log_i32 (param i32))) (import "js" "log_f32" (func $log_f32 (param f32))) (import "js" "log_f64" (func $log_f64 (param f64))) (func (export "globaltest") 3 (call $log_i32 (global.get $import_integer_32)) 4 (call $log_f32 (global.get $import_float_32)) 5 (call $log_f64 (global.get $import_float_64)) ) ) ``` Listing 2-11: Importing alternative versions of the JavaScript function We first declare the globals, including their types and import location 1. We’re also importing a `log` function from JavaScript. WebAssembly requires us to specify data types, so we import three functions, each with different types for the parameter: a 32-bit integer, a 32-bit float, and a 64-bit float 2. The variable passed into `$log_f64` is `(global.get` `$import_float_64)`, which tells WebAssembly that the variable we’re pushing onto the stack is global. If you wanted to push a local variable called `$x` onto the stack, you would need to execute the expression `(local.get` `$x)`. We’ll cover local variables later in this chapter. In JavaScript, all of these functions take a dynamic variable. The JavaScript functions will be almost identical. In the function `globaltest`, we call the 32-bit integer version of the `log` function (`$log_i32`) 3, followed by the 32-bit float (`$log_f32`) 4 and the 64-bit float (`log_f64)` 5. These functions will log three different messages to demonstrate the perils of moving between the native 64-bit floating-point values in JavaScript and the data types supported by WebAssembly. Before we look at the output, we need to create a JavaScript file to run our WebAssembly module. We’ll start by declaring a `global_test` variable followed by a `log_message` function that will be called for each of our data types, as shown in Listing 2-12. **globals.js** ``` const fs = require('fs'); const bytes = fs.readFileSync('./globals.wasm'); let global_test = null; let importObject = { js: { log_i32: (value) => { console.log ("i32: ", value) }, log_f32: (value) => { console.log ("f32: ", value) }, log_f64: (value) => { console.log ("f64: ", value) }, }, env: { import_i32: 5_000_000_000, // _ is ignored in numbers in JS and WAT import_f32: 123.0123456789, import_f64: 123.0123456789, } }; ... ``` Listing 2-12: Setting `importObject` functions and values In Listing 2-12, there are three different JavaScript functions passed to the WebAssembly module using `importObject`: `log_i32`, `log_f32`, and `log_f64`. Each of these functions is a wrapper around the `console.log` function. The functions pass a string as a prefix to the value from the WebAssembly module. These functions take in only a single parameter called `value`. JavaScript doesn’t assign a type to the parameter in the same way WebAssembly does, so the same function could have been used three times. The only reason we didn’t use the same function three times is because we wanted to change the string that prefixed the values to keep the output clear. We chose the values in Listing 2-12 to demonstrate the limitations of each data type. We set the global variable `import_int32` to a value of `5,000,000,000`, which we pass into WebAssembly as a 32-bit integer. That value is larger than can be held by a 32-bit integer. We set the global variable `import_f32` to `123.0123456789`, which has a higher level of precision than is supported by the 32-bit floating-point variable set in our WebAssembly module. The final global variable set in the `importObject` is `import_f64`, which, unlike the previous two variables, is large enough to hold the value passed into it. The code in Listing 2-13 instantiates our WebAssembly module and executes the `globaltest` function. **globals.js** ``` ... ( async () => { let obj = await WebAssembly.instantiate(new Uint8Array (bytes), importObject); ({globaltest: global_test} = obj.instance.exports); global_test(); })(); ``` Listing 2-13: Instantiating the WebAssembly module in the asynchronous IIFE Now that we have all our code in the JavaScript and WAT files, we can compile the WAT file into *globals.wasm* using the following `wat2wasm` call: ``` wat2wasm globals.wat ``` After compiling *globals.wasm*, we run our application using the following `node` command: ``` node globals.js ``` When you run this JavaScript file using `node`, you should see the output in Listing 2-14 logged to the console. ``` i32: 705032704 f32: 123.01234436035156 f64: 123.0123456789 ``` Listing 2-14: Output logged to the console from *globals.js* We passed in a value of `5,000,000,000` using our `importObject`, but our output shows a value of `705,032,704`. The reason is that a 32-bit unsigned integer has a maximum value of 4,294,967,295\. If you add `1` to that number, the 32-bit integer wraps back around to a value of `0`. So if you take the 5,000,000,000 number we passed in and subtract 4,294,967,296, the result is 705,032,704\. The lesson is, if you’re dealing with numbers larger than a few billion, you might not be able to work with 32-bit integers. Unfortunately, as mentioned earlier, you can’t pass 64-bit integers to JavaScript from WebAssembly. If you want to pass 64-bit integers to JavaScript from WebAssembly, you’ll need to convert them to 64-bit floats or pass them as two 32-bit integers. We passed a value of `123.0123456789` to our WebAssembly module, but because the 32-bit floating-point number has such limited precision, the best it can do is approximate that number, and it doesn’t do a great job of it. A 32-bit floating-point number in JavaScript and WebAssembly uses 23 bits to represent the number and multiplies it by two raised to an 8-bit exponent value. All floating-point numbers are approximations, but 64-bit floating-point numbers do a much better job of those approximations. The performance differences you’ll see using 32-bit versus 64-bit floating-point numbers vary with your hardware. If you want to use 32-bit floating-point numbers to improve the performance of your application, it’s a good idea to know the target hardware. Some mobile devices might see a larger performance boost using 32-bit floating-point numbers. The final message shows the 64-bit floating-point value returned to JavaScript as `f64: 123.0123456789`. As you can see, this is the first number that remains unmodified from what we passed into the WebAssembly module. That by no means indicates that you should always use 64-bit floating-point numbers. Addition, subtraction, and multiplication typically perform three to five times faster with integers. Dividing by powers of two is also several times faster. However, division by anything but a power of two can be faster with floating-point numbers. We’ll explore these data types in more detail in Chapter 4\. Now that you have a better understanding of globals and types, let’s examine local variables. ### Local Variables In WebAssembly, the values stored in local variables and parameters are pushed onto the stack with the `local.get` expression. In Chapter 1, we wrote a small function that performed the addition of two parameters passed into the function that looked like Listing 2-15. **AddInt.wat** ``` (module (func (export "AddInt") (param $value_1 i32) (param $value_2 i32) (result i32) local.get $value_1 local.get $value_2 i32.add ) ) ``` Listing 2-15: WebAssembly module with a 32-bit integer add Let’s make a few modifications to the code. To demonstrate how we can use local variables, we’ll square the value of the sum that `AddInt` returned. Create a new file named *SumSquared.wat* and add the code in Listing 2-16. The changes are called out with numbers. **SumSquared.wat** ``` (module (func (export 1"SumSquared") (param $value_1 i32) (param $value_2 i32) (result i32) 2 (local $sum i32) 3 (i32.add (local.get $value_1) (local.get $value_2)) 4 local.set $sum 5 **(**i32.mul (6local.get $sum) (local.get $sum)) ) ) ``` Listing 2-16: Bit integer parameter and local variable definition First, we change the name in the export to `SumSquared` 1. We add a local variable called `$sum` 2 that we’ll use to store the result of the call to `i32.add` 3. We change `i32.add` to use the S-Expression syntax. Immediately after that, we call `local.set` `$sum` to pop the value off the stack and set the new local variable `$sum` 4. Then we call `i32.mul` 5 using the S-Expression syntax, passing in the value of `$sum` for both parameters. This is done through a call to `local.get` 6. To test this function, create a new JavaScript file named *SumSquared.js* and add the code in Listing 2-17. ``` const fs = require('fs'); const bytes = fs.readFileSync(__dirname + '/SumSquared.wasm'); const val1 = parseInt(process.argv[2]); const val2 = parseInt(process.argv[3]); (async () => { const obj = await WebAssembly.instantiate(new Uint8Array (bytes)); let sum_sq = obj.instance.exports.SumSquared(val1, val2); console.log ( `(${val1} + ${val2}) * (${val1} + ${val2}) = ${sum_sq}` ); })(); ``` Listing 2-17: JavaScript that executes the *SumSquared.js* WebAssembly module Once you’ve created your *SumSquared.js* function, you can run it the same way you ran the *AddInt.js* file earlier, making sure to pass in two extra parameters that represent the values you want to sum and then square. The following command will add 2 and 3, and then square the result: ``` node SumSquared.js 2 3 ``` The output of that run looks like this: ``` (2 + 3) * (2 + 3) = 25 ``` You should now understand how to set a local variable from a value on the stack and how to add a value to the stack from a global variable. Next, let’s explore how to unpack the S-Expression syntax. ### Unpacking S-Expressions So far we’ve been mixing the use of S-Expressions with the linear WAT syntax. However, the browser debugger doesn’t keep your S-Expressions intact when you’re debugging; instead, it unpacks them. Because you’ll want to use your knowledge of WAT to decompile and debug WebAssembly, you’ll need to understand the unpacking process. We’ll walk through the process the `wat2wasm` compiler uses to unpack a short piece of WAT code. The unpacking process evaluates the expressions inside out first and then in order. It initially dives into each S-Expression looking for subexpressions. If subexpressions exist, it evaluates the subexpressions first. If two expressions are at the same depth, it evaluates them in order. Let’s look at Listing 2-18. ``` 1 (i32.mul ;; executes 7th (last) 2 (i32.add ;; executes 3rd 3 (i32.const 3) ;; executes 1st 4 (i32.const 2) ;; executes 2nd ) 5 (i32.sub ;; executes 6th 6 (i32.const 9) ;; executes 4th 7 (i32.const 7) ;; executes 5th ) ) ``` Listing 2-18: Using the S-Expression syntax First, we need to go inside our `i32.mul` expression 1 to see if any subexpressions exist. We find two subexpressions, an `i32.add` expression 2 and an `i32.sub` expression 5. We look at the first of these two expressions and go inside `i32.add` 2, evaluating `(i32.const` `3)` 3, which pushes a 32-bit integer `3` onto our stack. Because nothing is left to evaluate inside that statement, we move on to evaluate `(i32.const` `2)` 4, which pushes a 32-bit integer `2` onto the stack. Then the S-Expression executes `i32.add` 2. The first three lines executed in the S-Expression are shown in Listing 2-19. ``` i32.const 3 i32.const 2 i32.add ``` Listing 2-19: Code from `i32.add` after it’s unpacked Now that `i32.add` is executed, the next piece to get unpacked is `i32.sub`. Similarly, the code first goes inside the S-Expression and executes the `(i32.const` `9)` expression 6 followed by the `(i32.const` `7)` expression 7. Once those two constants are pushed onto the stack, the code executes `i32.sub`. The unpacked subexpression looks like Listing 2-20. ``` i32.const 9 i32.const 7 i32.sub ``` Listing 2-20: Code from `i32.sub` after the S-Expression is unpacked After the `i32.add` and `i32.sub` S-Expressions have been executed, the unpacked version executes the `i32.mul` command. The fully unpacked version of the S-Expression is shown in Listing 2-21. ``` i32.const 3 ;; Stack = [3] i32.const 2 ;; Stack = [2, 3] i32.add ;; 2 & 3 popped from stack, added sum of 5 pushed onto stack [5] i32.const 9 ;; Stack = [9,5] i32.const 7 ;; Stack = [7,9,5] i32.sub ;; 7 & 9 popped off stack . 9-7=2 pushed on stack [2,5] i32.mul ;; 2,5 popped off stack, 2x5=10 is pushed on the stack [10] ``` Listing 2-21: Example of using the WAT stack How the stack machine works might seem a little daunting at first, but it will feel more natural once you get accustomed to it. We recommend using S-Expressions until you’re comfortable with the stack machine. The S-Expression syntax is an excellent way to ease your way into WAT if you’re only familiar with higher-level languages. ### Indexed Variables WAT doesn’t require you to name your variables and functions. Instead, you can use index numbers to reference functions and variables that you haven’t yet named. From time to time, you might see WAT code that uses these indexed variables and functions. Sometimes this code comes from disassembly, although we’ve also seen people write code that looks like this occasionally. Code that calls `local.get` followed by a number is retrieving a local variable based on the order it appears in the WebAssembly code. For example, we could have written our *AddInt.wat* file in Listing 2-21 like the code in Listing 2-22. ``` (module (func (export "AddInt") 1 (param i32 i32) (result i32) 2 local.get 0 3 local.get 1 i32.add ) ) ``` Listing 2-22: Using variables As you can see, we don’t name the parameters in the `param` 1 expression. A convenient part of this code style is that you can declare multiple parameters in a single expression by adding more types. When we call `local.get`, we need to pass in a zero indexed number to retrieve the proper parameter. The first call to `local.get` 2 retrieves the first parameter by passing in `0`. The second call to `local.get` 3 retrieves the second parameter by passing in `1`. You can also use this syntax for functions and global variables. I find this syntax difficult to read, so I won’t use it in this book. However, I felt it was necessary to introduce because some debuggers use it. ### Converting Between Types JavaScript developers don’t need to deal with converting between different numeric types. All numbers in JavaScript are 64-bit floating-point numbers. That simplifies coding for developers but comes at a performance cost. When you’re working with WebAssembly, you need to be more familiar with your numeric data. If you need to perform numeric operations between two variables with different data types, you’ll need to do some conversion. Table 2-1 provides the conversion functions you can use in WAT to convert between the different numeric data types. Table 2-1: Numeric Type Conversion Functions | **Function** | **Action** | | --- | --- | | i32.trunc_s/f64 i32.trunc_u/f64 | Convert a 64-bit float to a 32-bit integer | | i32.trunc_s/f32 i32.trunc_u/f32 i32.reinterpret/f32 | Convert a 32-bit float to a 32-bit integer | | i32.wrap/i64 | Convert a 64-bit integer to a 32-bit integer | | i64.trunc_s/f64 i64.trunc_u/f64 i64.reinterpret/f64 | Convert a 64-bit float to a 64-bit integer | | i64.extend_s/i32 i64.extend_u/i32 | Convert a 32-bit integer to a 64-bit integer | | i64.trunc_s/f32 i64.trunc_u/f32 | Convert a 32-bit float to a 64-bit integer | | f32.demote/f64 | Convert a 64-bit float to a 32-bit float | | f32.convert_s/i32 f32.convert_u/i32 f32.reinterpret/i32 | Convert a 32-bit integer to a 32-bit float | | f32.convert_s/i64 f32.convert_u/i64 | Convert a 64-bit integer to a 32-bit float | | f64.promote/f32 | Convert a 32-bit float to a 64-bit float | | f64.convert_s/i32 f64.convert_u/i32 | Convert a 32-bit integer to a 64-bit float | | f64.convert_s/i64 f64.convert_u/i64 f64.reinterpret/i64 | Convert a 64-bit integer to a 64-bit float | I omitted quite a bit of information from this table to stay focused. The `_u` and `_s` suffixes on expressions, such as `convert`, `trunc`, and `extend`, let WebAssembly know whether the integers you’re working with are unsigned (cannot be negative) or signed (can be negative), respectively. A `trunc` expression truncates the fractional portion of a floating-point number when it converts it to an integer. Floating-point numbers can be promoted from an `f32` to an `f64` or demoted from an `f64` to an `f32`. Integers are simply converted to floating-point numbers. The `wrap` command puts the lower 32 bits of a 64-bit integer into an `i32`. The `reinterpret` command keeps the bits of an integer or floating-point value the same when it reinterprets them as a different data type. ## if/else Conditional Logic One way that WAT differs from an assembly language is that it contains some higher-level control flow statements, such as `if` and `else`. WebAssembly doesn’t have a boolean type; instead, it uses `i32` values to represent booleans. An `if` statement requires an `i32` to be on the top of the stack to evaluate control flow. The `if` statement evaluates any non-zero value as true and zero as false. The syntax for an `if`/`else` statement using S-Expressions looks like Listing 2-23. ``` ;; This code is for demonstration and not part of a larger app (if (local.get $bool_i32) (then ;; do something if $bool_i32 is not 0 ;; nop is a "no operation" opcode. nop ;; I use it to stand in for code that would actually do something. ) (else ;; do something if $bool_i32 is 0 nop ) ) ``` Listing 2-23: The `if`/`else` syntax using S-Expressions Let’s also look at what the unpacked version of the `if`/`else` statements look like. Unpacking an `if`/`else` statement might look a little different than you would expect. There is no `(then``)` expression in the unpacked version. Listing 2-24 shows how the code in Listing 2-23 would look after it’s unpacked. ``` ;; This code is for demonstration and not part of a larger app local.get $bool_i32 if ;; do something if $bool_i32 is not 0 nop else ;; do something if $bool_i32 is 0 nop end ``` Listing 2-24: The `if`/`else` statement using the linear syntax The `then` S-Expression is pure syntactic sugar and doesn’t exist in the unpacked version of our code. The unpacked version requires an `end` statement that doesn’t exist in the S-Expression syntax. When you’re writing high-level programs, you use boolean logic with your `if`/`else` statements. In JavaScript, you might have an `if` statement that looks something like this: ``` if( x > y && y < 6 ) ``` To replicate this in WebAssembly, you would need to use expressions that conditionally return 32-bit integer values. Listing 2-25 shows how we would do the logic from the JavaScript `if` example with `x` and `y` as 32-bit integers. ``` ;; This code is for demonstration and not part of a larger app (if (i32.and (i32.gt_s (local.get $x) (local.get $y) ) ;; signed greater than (i32.lt_s (local.get $y) (i32.const 6) ) ;; signed less than ) (then ;; x is greater than y and y is less than 6 nop ) ) ``` Listing 2-25: An `if` expression with an `i32.and` using S-Expression syntax It looks a bit complicated in comparison. The `i32.and` expression performs a bitwise AND operation on 32-bit integers. It ends up working out because `i32.gt_s` and `i32.lt_s` both return `1` if true and `0` if false. In WebAssembly, you must keep in mind that you’re using bitwise AND/OR operations; if you use an `i32.and` on a value of `2` and a value of `1`, it will result in `0` because of the way the binary AND works. You might want a logical AND instead of a binary AND, but `i32.and` is a binary AND. If you’re unfamiliar with binary AND/OR operations, we discuss them in more detail in Chapter 4\. In some ways, complicated `if` expressions look better when they’re unpacked. Listing 2-26 shows the code in Listing 2-25 without the sugar. ``` ;; This code is for demonstration and not part of a larger app local.get $x local.get $y i32.gt_s ;; pushes 1 on the stack if $x > $y local.get $y i32.const 6 i32.lt_s ;; pushes 1 on the stack if $y < 6 i32.and ;; do a bitwise and on the last two values on the stack if ;; x is greater than y and y is less than 6 nop end ``` Listing 2-26: An `if` statement with `i32.and` using stack syntax Listing 2-27 shows there are similar expressions you can use if `$x` and `$y` are 64-bit or 32-bit floating-point numbers. ``` ;; This code is for demonstration and not part of a larger app (if (i32.and 1 (f32.gt (local.get $x) (local.get $y) ) 2 (f32.lt (local.get $y) (f32.const 6) ) ) (then ;; x is greater than y and y is less than 6 nop ) ) ``` Listing 2-27: Using `f32` comparisons but `i32.and` results Notice that we changed `i32.gt_s` and `i32.lt_s` to `f32.gt` 1 and `f32.lt` 2, respectively. Many integer operations must specify whether they support negative numbers using the `_s` suffix. You don’t have to do that for floating-point numbers, because all floating-point numbers are signed and have a dedicated sign bit. There are a total of 40 comparison expressions in WebAssembly. Table 2-2 shows expressions that are useful in conjunction with the `if`/`else` expressions. Unless otherwise stated, these functions pop two values off the stack, compare them, and push `1` on the stack if true and `0` on the stack if false. Table 2-2: Functions to Use with `if/else` | **Function** | **Action** | | --- | --- | | i32.eq i64.eq f32.eq f64.eq | Test for equality | | i32.ne i64.ne f32.ne f64.ne | Not equal | | i32.lt_s i32.lt_u i64.lt_s i64.lt_u f32.lt f64.lt | Less than test. The `_s` suffix indicates signed comparison; `_u` indicates unsigned. | | i32.le_s i32.le_u i64.le_s i64.le_u f32.le f64.le | Less than or equal test. The `_s` suffix indicates signed comparison; `_u` indicates unsigned. | | i32.gt_s i32.gt_u f32.gt f64.gt i64.gt_s i64.gt_u | Greater than test. The `_s` suffix indicates signed comparison; `_u` indicates unsigned. | | i32.ge_s i32.ge_u i64.ge_s i64.ge_u f32.ge f64.ge | Greater than or equal test. The `_s` suffix indicates signed comparison; `_u` indicates unsigned. | | i32.and i64.and | Bitwise AND | | i32.or i64.or | Bitwise OR | | i32.xor i64.xor | Bitwise exclusive OR | | i32.eqz i64.eqz | Test a floating-point number to see if it has a zero value | ## Loops and Blocks The branching expressions in WAT are different than branching statements you might find in an assembly language. The differences prevent the spaghetti code that comes about as the result of jumps to arbitrary locations. If you want your code to jump backward, you must put your code inside a loop. If you want your code to jump forward, you must put it inside a block. For the kind of functionality you would see in a high-level programming language, you must use the loop and block statements together. Let’s explore these structures with some throwaway code examples that won’t be a part of a larger app. ### The block Statement First, we’ll look at the `block` expression. The block and loop statements in WAT work a bit like `goto` statements in assembly or some low-level programming languages. However, the code can only jump to the end of a `block` if it’s inside that `block`. That prevents the code from arbitrarily branching to a `block` label from anywhere within your program. If the code jumps to the end of a `block`, the code that performs that jump must exist inside that `block`. Listing 2-28 shows an example. ``` ;; This code is for demonstration and not part of a larger app 1 (block $jump_to_end 2 br $jump_to_end ;; code below the branch does not execute. br jumps to the end of the block 3 nop ) ;; This is where the br statement jumps to 4 nop ``` Listing 2-28: Declaring a `block` in WAT The `br` 2 statement is a branch statement that instructs the program to jump to a different location in the code. You might expect `br` to jump back to the beginning of the block where the label is defined 1. But that isn’t what happens. If you use a `br` statement within a block to jump to the block’s label, it exits that block and begins to execute the code immediately outside the block 4. That means that the code directly below the `br` statement 3 never executes. As mentioned earlier, this code isn’t meant to be used, we only wanted to demonstrate how the `block` and `br` statements work. The way we use the `br` statement here isn’t useful. Because the `br` statement always branches to the end of the labeled block, you want it to branch conditionally. The `br_if` conditional branch in Listing 2-29 is used to branch given a condition, unlike the code in Listing 2-28. ``` ;; This code is for demonstration and not part of a larger app (block $jump_to_end 1 local.get $should_I_branch 2 br_if $jump_to_end ;; code below the branch will execute if $should_I_branch is 0 3 nop ) 4 nop ``` Listing 2-29: Branching to the end of the block with `br_if` The new version of the code pushes a 32-bit integer value `$should_I_branch` onto the stack 1. The `br_if` statement pops the top value off the stack 2, and if that value isn’t `0`, branches to the end of the `$jump_to_end` block 4. If `$should_I_branch` is `0`, the code in the block below the `br_if` statement 3 executes. ## The loop Expression The `block` expression always jumps to the end of the `block` on a branch. If you need to jump to the beginning of a block of code, use the `loop` statement. Listing 2-30 shows how a WAT `loop` statement works. You would be mistaken if you think this code executes in an infinite loop. ``` ;; This code is for demonstration and not part of a larger app (loop $not_gonna_loop ;; this code will only execute once 1 nop ) ;; because there is no branch in our loop, it exits the loop block at the end 2 nop ``` Listing 2-30: A `loop` expression that doesn’t loop In fact, a `loop` expression in WAT doesn’t loop on its own; it needs a branch statement located inside the loop to branch back to the beginning of the `loop` expression. A `loop` block will execute the code inside it 1 just like a `block` expression and, without a branch, exits at the end of the block 2. If for any reason you want to create an infinite loop, you need to execute a `br` statement at the end of your `loop`, as shown in Listing 2-31. ``` ;; This code is for demonstration and not part of a larger app (loop $infinite_loop ;; this code will execute in an infinite loop nop 1 br $infinite_loop ) ;; this code will never execute because the loop above is infinite 2 nop ``` Listing 2-31: Branching in an infinite loop The `br` statement 1 always branches back to the top of the `$infinite_loop` block with every iteration. The code below the `loop` 2 never executes. ### Using block and loop Together To make your loop able to break and continue, you need to use the `loop` and the `block` expressions together. Let’s put together a little WebAssembly module and JavaScript app that finds factorials. The program will run a loop until we have the factorial value of the number passed into the function. That will allow us to test the `continue` and `break` functionality of our `loop` expression. Our simple loop will calculate the factorial value for each number up to some parameter value `$n` that we’ll pass in from JavaScript. Then the value of `$n` factorial will be returned to JavaScript. Create a new file named *loop.wat* and add the code in Listing 2-32. **loop.wat** ``` (module 1 (import "env" "log" (func $log (param i32 i32))) (func $loop_test (export "loop_test") (param $n i32) (result i32) (local $i i32) (local $factorial i32) (local.set $factorial (i32.const 1)) 2 (loop $continue (block $break ;; $continue loop and $break block 3 (local.set $i ;; $i++ (i32.add (local.get $i) (i32.const 1)) ) 4 ;; value of $i factorial (local.set $factorial ;; $factorial = $i * $factorial (i32.mul (local.get $i) (local.get $factorial)) ) ;; call $log passing parameters $i, $factorial 5 (call $log (local.get $i) (local.get $factorial)) 6 (br_if $break (i32.eq (local.get $i) (local.get $n)));;if $i==$n break from loop 7 br $continue ;; branch to top of loop )) 8 local.get $factorial ;; return $factorial to calling JavaScript ) ) ``` Listing 2-32: Branching forward and backward with a `loop` and a `block` The first expression in this module is an import of the `$log` function 1. In a moment, we’ll write this function in JavaScript and call it on every pass through our loop to log the value of `$i` factorial for each pass. We labeled the loop `$continue` 2 and the block `$break` 2 because branching to `$continue` will continue to execute the loop and branching to `$break` will break out of the loop. We could have done this without using the `$break` block, but we want to demonstrate how the loop can work in conjunction with a block. This allows your code to work like a `break` and a `continue` statement in a high-level programming language. The `loop` increments `$i` 3 and then calculates a new `$factorial` value by multiplying `$i` by the old `$factorial` value 4. It then makes a call to log with `$i` and `$factorial` 5. We use a `br_if` to break out of the `loop` if `$i` == `$n` 6. If we don’t break out of `loop`, we branch back to the top of `loop` 7. When the `loop` exits, we push the value of `$factorial` onto the stack 8 so we can return that value to the calling JavaScript. Once you have your WAT file, compile it into a WebAssembly file using the following command: ``` wat2wasm loop.wat ``` Now we’ll create a JavaScript file to execute the WebAssembly. Create a *loop.js* file and enter the code in Listing 2-33. **loop.js** ``` const fs = require('fs'); const bytes = fs.readFileSync(__dirname + '/loop.wasm'); 1 const n = parseInt(process.argv[2] || "1"); // we will loop n times let loop_test = null; let importObject = { env: { 2 log: function(n, factorial) { // log n factorial to output tag console.log(`${n}! = ${factorial}`); } } }; ( async() => { 3 let obj = await WebAssembly.instantiate( new Uint8Array(bytes), importObject ); 4 loop_test = obj.instance.exports.loop_test; 5 const factorial = loop_test(n); // call our loop test 6 console.log(`result ${n}! = ${factorial}`); 7 if (n > 12) { console.log(` =============================================================== Factorials greater than 12 are too large for a 32-bit integer. =============================================================== `) } })(); ``` Listing 2-33: Calling the `loop_test` from JavaScript The `log` 2 function, which our WAT code will call, logs a string to the console with the values of `n` 1 and `n` factorial passed from the WAT `loop`. When we instantiate the module 3, we pass a value of `n` to the `loop_test` 4 function. The `loop_test` function finds the factorial as a result 5. We then use a `console.log` 6 call to display the value of `n` and `n` factorial. We have a check at the end to make sure the number we enter isn’t greater than a value of `12` 7, because signed 32-bit integers only support numbers up to about 2 billion. Run *loop.js* using `node` by executing the following on the command line: ``` node loop.js 10 ``` Listing 2-34 shows the output you should see on the command line. ``` 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 result 10! = 3628800 ``` Listing 2-34: Output from *loop.js* Now that you know how loops work in WAT, let’s look at branch tables. ### Branching with br_table Another way to use the `block` expression in WAT is in conjunction with a `br_table` *expression,* which allows you to implement a kind of `switch` statement. It’s meant to provide the kind of jump table performance you get with a `switch` statement when there are a large number of branches. The `br_table` expression takes a list of blocks and an index into that list of blocks. It then breaks out of whichever block your index points to. The awkward thing about using a branch table is that the code can only break out of a block it’s inside. That means you must declare all of your blocks ahead of time. Listing 2-35 shows what the WAT code looks like to build a `br_table`. ``` ;; This code is for demonstration and not part of a larger app 1 (block $block_0 (block $block_1 (block $block_2 (block $block_3 (block $block_4 (block $block_5 2 (br_table $block_0 $block_1 $block_2 $block_3 $block_4 $block_5 (local.get $val) ) 3 ) ;; block 5 i32.const 55 return ) ;; block 4 i32.const 44 return ) ;; block 3 i32.const 33 return ) ;; block 2 i32.const 22 return ) ;; block 1 i32.const 11 return ) ;; block 0 i32.const 0 return ``` Listing 2-35: Using the `br_table` syntax from within WAT We define all the `block` expressions before the `br_table` expression 1. So when the `br_table` expression is called 2, it’s not always completely clear where in the code it will jump. This is why we added the comments 3 in the code indicating which `block` was ending. The `br_table` provides some performance improvement over the use of `if` expressions when you have a large number of branches. In our testing, using the `br_table` expression wasn’t worthwhile until there were about a dozen branches. Of course this will depend on the embedding environment and hardware it runs on. Even at this number of branches, the `br_table` was still slower on Chrome than `if` statements. Firefox with about a dozen branches was noticeably faster with the `br_table` expression. ## Summary In this chapter, we covered many of the WAT programming basics. After learning to create and execute a WebAssembly module in Chapter 1, you moved on to creating the traditional hello world application in this chapter. Creating a hello world application is a bit more advanced in WAT than in most programming languages. After completing a few initial programs, we began looking at some of the basic features of WAT and how they differ from a traditionally high-level language like JavaScript. We explored variables and constants and how they can be pushed onto the stack using WAT commands. We discussed the S-Expression syntax and how to unpack it. We also briefly mentioned indexed local variables and functions, and introduced you to that syntax. You learned the basic branching and looping structures, and how to use them within the WAT syntax. In the next chapter, we’ll explore functions and function tables in WAT, and how they interact with JavaScript and other WAT modules.

第三章:函数与表格

在本章中,我们将探讨 WebAssembly 中的函数:我们应该如何以及何时从 JavaScript 或其他 WebAssembly 模块导入函数,如何将 WebAssembly 函数导出到嵌入环境,并从 JavaScript 调用这些函数。你将了解 WebAssembly 中的表格以及如何调用表格中定义的函数。我们还将研究调用在 WebAssembly 模块内外定义的函数对性能的影响。

WebAssembly 模块通过导入和导出函数与嵌入环境进行交互。为了让 WebAssembly 使用函数,我们必须从嵌入环境导入函数,并导出函数供网页调用。我们还可以编写 WebAssembly 模块内的函数,通过 export statement 导出到嵌入环境。否则,函数默认仅在模块内使用。

Function calls will always result in some lost computing cycles. But it’s necessary to know that a WebAssembly module will lose *more* cycles when calling an imported JavaScript function than when calling a function defined inside your WebAssembly module. ## When to Call Functions from WAT Every function we’ve defined up to this point includes the `(export)` expression to export the function so JavaScript can call it. However, not every function should use `export`. Every call to a WebAssembly function from JavaScript incurs an overhead cost, so you generally wouldn’t export WAT functions that do only small tasks. Small functions that don’t use many computing cycles might be better kept in the JavaScript to reduce overhead. Make sure your WAT code does as much as possible before returning to JavaScript; smaller functions shouldn’t use `export`. The WAT functions most suited for exporting are those that loop over and process a lot of data. We recommend using many WAT functions in the early versions of your code to aid in the debugging process. Stepping through your WAT code in a debugger is easier to follow when your code is broken into many small functions. Chapter 10 covers debugging WebAssembly code in detail. As you tune your code for performance, you might decide to remove some of these functions by placing their code inline wherever the function had been called. Any internal function that’s called thousands of times from your exported WebAssembly function is a good candidate for moving inline. Chapter 9 covers performance tuning in detail. ## Writing an is_prime Function We’ve already seen examples of exporting functions to JavaScript in previous chapters, but those functions were terrible candidates for WebAssembly performance improvement because they didn’t do a whole lot. Here we’ll write an intentionally slow algorithm to determine whether an input is a prime number. This function is a good candidate for creation in WebAssembly and improved performance over JavaScript because it involves a significant number of calculations. Create an *is_prime.js* file and an *is_prime.wat* file to start creating this app. ### Passing Parameters Let’s start with the basics. First, we create a module, and then create a function inside that module that can be exported as `is_prime`. This function takes a single 32-bit integer parameter and returns a 32-bit integer that is `1` if the number passed in is prime and `0` if it’s not. Place the code in Listing 3-1 in the *is_prime.wat* file. ``` (module (func (1export "is_prime") (2param $n i32) (3result i32) 4 i32.const 0 ;; remove later ) ) ``` Listing 3-1: WebAssembly `is_prime` function stub We export this function to JavaScript as `is_prime` 1. JavaScript passes in a single parameter `param` `$n` as an `i32` 2. When complete, the function returns a 32-bit integer to JavaScript 3. To compile, this function expects to find a 32-bit integer on the stack when the function completes, so we add the line `i32.const` `0` 4. Without this line, the compiler will throw an error when we run `wat2wasm` because it’s expecting to return a number when the function completes, and it needs that number to be on the stack when the function ends. This function is set for export alone and is not labelled for internal use. The `(func``)` expression begins with an `export` expression, not a `$is_prime` label. If we wanted to call this function from within the WebAssembly module or from the JavaScript, we would label it as shown in Listing 3-2. ``` (func 1$is_prime (export "is_prime") (param $n i32) (result i32) ``` Listing 3-2: Exporting the function ### Creating Internal Functions Let’s add a little more code to the *is_prime.wat* file. No prime numbers are even except for the number 2, so we’ll write a function that checks whether the input number is even by looking at the last bit in the integer. All odd numbers have a 1 in the integer’s lowest order bit. Listing 3-3 shows the code for the `$even_check` function that we’ll add to the *is_prime.wat* file. **is_prime.wat (part 1 of 4)** ``` (module ;; add the $even_check function to the top of the module (func $even_check (1param $n i32) (result i32) local.get $n i32.const 2 2 i32.rem_u ;; if you take the remainder of a division by 2 i32.const 0 ;; even numbers will have a remainder 0 3 i32.eq ;; $n % 2 == 0 ) ... ``` Listing 3-3: Defining the `$even_check` function The `$even_check` function takes a single parameter `$n` 1 and will return a value of `1` if `$n` is even and `0` if `$n` is odd. We use the remainder operation `i32.rem_u` 2, which divides `$n` by 2 and finds the remainder. An even number will have a remainder of 0, so we compare the remainder returned from `i32.rem_u` with 0 using an `i32.eq` 3 expression. Now let’s create another simple function to handle the exception case if the number being passed in is 2, which is the only even prime number. It takes a single parameter and returns a `1` (true) if the number passed in is 2 or `0` (false) if it isn’t. We’ll call this function `$eq_2`, as shown in Listing 3-4. **is_prime.wat (part 2 of 4)** ``` ... ;; add the $eq_2 function after $even_check (func $eq_2 (param $n i32) (result i32) local.get $n i32.const 2 1 i32.eq ;; returns 1 if $n == 2 ) ... ``` Listing 3-4: The `$eq_2` function checks whether the value passed in is 2. We use `i32.eq` 1 to determine whether `$n` has a value of `2`, return `1` if it does, and return `0` if it doesn’t. Writing the `$eq_2` function is overkill, but because we’re demonstrating how calling functions work, another example couldn’t hurt. In Listing 3-5, we add a `$multiple_check` function that checks whether the first parameter `$n` is a multiple of the second parameter `$m`. If the input number has multiples, it means it’s divisible and therefore cannot be prime. **is_prime.wat (part 3 of 4)** ``` ... ;; add $multiple_check after $eq_2 (func $multiple_check (param $n i32) (param $m i32) (result i32) 1 local.get $n 2 local.get $m 3 i32.rem_u ;; get the remainder of $n / $m i32.const 0 ;; I want to know if the remainder is 0 4 i32.eq ;; that will tell us if $n is a multiple of $m ) ... ``` Listing 3-5: Defining a `$multiple_check` function that checks whether `$n` is a multiple of `$m` The `$multiple_check` function takes in two parameters, an integer `$n` 1 and a second integer `$m` 2, and checks whether `$n` is a multiple of `$m`. To do this, we get the remainder of `$n / $m` using `i32.rem_u` ``3 and then check whether that remainder is `0` using `i32.eq` 4. If the remainder is `0`, the `$multiple_check` function returns `1`. If the remainder is anything else, the `$multiple_check` returns `0`.`` ````` ### Adding the is_prime Function Now that we have all the internal functions defined, let’s change the definition of the `is_prime` exported function so it returns `1` if the number passed in is prime and `0` if it’s not. Listing 3-6 shows the new version of the `is_prime` function. **is_prime.wat (part 4 of 4)** ``` ... ;; add the is_prime exported function after $multiple_check (func (export "is_prime") (param $n i32) (result i32) 1 (local $i i32) 2 (if (i32.eq (local.get $n) (i32.const 1)) ;; 1 is not prime (then i32.const 0 return )) (if (call $eq_2 (local.get $n)) ;; check to see if $n is 2 (then i32.const 1 ;; 2 is prime return ) ) (block $not_prime (call $even_check (local.get $n)) br_if $not_prime ;; even numbers are not prime (except 2) (local.set $i (i32.const 1)) (loop $prime_test_loop (local.tee $i (i32.add (local.get $i) (i32.const 2) ) ) ;; $i += 2 local.get $n ;; stack = [$n, $i] i32.ge_u ;; $i >= $n if ;; if $i >= $n, $n is prime i32.const 1 return end (call $multiple_check (local.get $n) (local.get $i)) br_if $not_prime ;; if $n is a multiple of $i this is not prime br $prime_test_loop ;; branch back to top of loop ) ;; end of $prime_test_loop loop ) ;; end of $not_prime block i32.const 0 ;; return false ) ) ;; end of module ``` Listing 3-6: The `$is_prime` function definition Before we added the code in Listing 3-6, the `is_prime` function didn’t actually test for prime numbers. Previously, it always returned `0` (to be interpreted as false). Now that we’ve coded the `is_prime` function, it will return `1` if the number passed in is prime and `0` if it’s not. At the beginning of this function, we create a local variable `$i` 1, which we use later as a loop counter. We check whether `$n` is `1` and `return 0` 2 if it is because the number one isn’t a prime number. We then eliminate half of the numbers by checking whether the number is 2, or even. If the number is 2, it’s prime; if it’s even but not 2, it’s not prime. We divide the number by every odd number from 3 to `$n-1`. If `$n` is evenly divisible by any of those numbers, it’s not prime. If it’s not evenly divisible by any of those numbers, it’s prime. The `$is_prime` function is rather large, so we’ll review it a piece at a time. Listing 3-7 is the portion of the code that tests whether the number is 2. ``` ... ;; the beginning of the $is_prime function in listing 3-6 (if 1(call $eq_2 (local.get $n)) ;; check to see if $n is 2 2 (then i32.const 1 ;; 2 is prime return ) ) ... ``` Listing 3-7: The `$eq_2` number check from `$is_prime` in Listing 3-6 The `if` statement calls the `$eq_2` 1 function defined earlier and passes it `$n`. If the value of `$n` is `2`, this function returns `1`; if not, it returns `0`. The `then` expression 2 runs if the value returned by the call is `1`, indicating that the number is prime. Then we begin a block of code called `$not_prime`. If at any time the number is determined not to be a prime number, we exit this block, causing the function to exit with a return value of `0`, denoting the input isn’t prime. Listing 3-8 shows the beginning of that `block`. ``` ... ;; code from the $is_prime function in listing 3-6 1 (block $not_prime 2 (call $even_check (local.get $n)) 3 br_if $not_prime ;; even numbers are not prime (except 2) ... ``` Listing 3-8: If the number is even, jump to `$not_prime`; from `$is_prime` in Listing 3-6 This block first calls `$even_check` 2 to see whether the number is even. Because we verified earlier that this number isn’t 2, any other even number wouldn’t be a prime. If `$even_check` returns `1`, the code leaves the `$not_prime` 1 block using the `br_if` 3 statement. Next, Listing 3-9 begins the loop that checks the numbers that `$n` might be divisible by. ``` ... ;; code from the $is_prime function in listing 3-6 1 (local.set $i (i32.const 1)) 2 (loop $prime_test_loop 3 (local.tee $i (4i32.add (local.get $i) (i32.const 2) ) ) ;; $i += 2 5 local.get $n ;; stack = [$n, $i] 6 i32.ge_u ;; $i >= $n 7 if ;; if $i >= $n, $n is prime i32.const 1 return end ... ``` Listing 3-9: Prime number test loop; from `$is_prime` in Listing 3-6 Right before the loop, we set the value of `$i` to `1` 1 because we’re looping over odd values when we increment through the loop. We call the loop `$prime_test_loop` 2 so when we branch to `$prime_test_loop` 2, it jumps back to that label. We use the `local.tee` 3 command in combination with an `i32.add` 4 command to increment the value of `$i` by `2` (because we’re only testing odd numbers) and leave the value calculated by `i32.add` on the stack when `$i` is set. The `local.tee` command is like the `local.set` command in that it sets the value of the variable you pass to it to the value on top of the stack. The difference is that `local.set` pops that value off the top of the stack, whereas `local.tee` leaves the value on it. We want to keep the new value for `$i` on the stack to compare its value with `$n`, which we push onto the stack in the next line using a `local.get` 5 expression. The `i32.ge_u` 6 expression pulls the last two values off the stack and checks whether the value we had in `$i` is greater than or equal to the value in `$n` (because a number can’t be divisible by a number greater than it) and assumes these integers are unsigned (because negative numbers can’t be prime). If this evaluates to true, the expression pushes `1` onto the stack, and if false, pushes `0` onto the stack. The `if` 7 statement that follows pulls one value off the stack and then executes the code between the `if` and `end` statements if the value pulled off the stack isn’t `0`. The upshot is, if `$i` is greater than or equal to `$n`, the number is prime. That means we only execute the code between the `if` and `end` statements if we incremented `$i` until its value is greater than or equal to `$n` without having ever found a number that evenly divides `$n`. Listing 3-10 shows the code that checks whether `$i` evenly divides `$n`, which would mean `$n` isn’t prime. ``` ... ;; code from the $is_prime function in listing 3-6 1 (call $multiple_check (local.get $n) (local.get $i)) 2 br_if $not_prime ;; if $n is a multiple of $i this is not prime 3 br $prime_test_loop ;; branch back to top of loop ) ;; end of $prime_test_loop loop ) ;; end of $not_prime block 4 i32.const 0 ;; return false ) ``` Listing 3-10: Call `$multiple_check` inside the prime test loop; from `$is_prime` in Listing 3-6 The `call` 1 expression at the beginning calls `$multiple_check`, passing it `$n` and `$i`. That returns `1` if `$n` is evenly divisible by `$i` and `0` if it’s not. If the value returned is `1`, the `br_if` 2 statement jumps to the end of the `$not_prime` block, causing the `is_prime` function to return `0` 4 (false). If `$multiple_check` returns `0`, we branch back to the top of the `loop` 3. We do this to continue testing numbers until `$i` is greater than `$n`, or we find a `$i` where `$n` is evenly divisible by `$i`. With all of our WAT code written, we can use `wat2wasm` to compile our WebAssembly module: ``` wat2wasm is_prime.wat ``` ### The JavaScript Once you’ve compiled the WebAssembly module, create a JavaScript file named *is_prime.js* and add the code in Listing 3-11 to load and call the WebAssembly `is_prime` function. **is_prime.js** ``` const fs = require('fs'); const bytes = fs.readFileSync(__dirname + '/is_prime.wasm'); const value = parseInt(process.argv[2]); (async () => { const obj = await WebAssembly.instantiate(new Uint8Array(bytes)); if(1!!obj.instance.exports.is_prime(value)) { 2 console.log(` ${value} is prime! `); } else { 3 console.log(` ${value} is NOT prime `); } })(); ``` Listing 3-11: The *is_prime.js* file calls the `is_prime` WebAssembly function. When the `is_prime` function is called, passing in the value taken from a command line argument, the `!!` 1 operator coerces the value from an integer into a true or false boolean value. If the value returned is `true`, we log out a message stating that the value is prime 2. If the value returned is `false`, the message indicates that the value isn’t prime 3. Now we can run the JavaScript function using `node` like this to check if 7 is a prime number: ``` node is_prime.js 7 ``` Here’s the output you should see. ``` 7 is prime! ``` We’ve created several functions in our WebAssembly module that we use to check whether or not a number is prime. You should now be familiar with the basics of creating functions and calling those functions from within a WAT module. ## Declaring an Imported Function In this section, we’ll look at declaring functions as imports in more detail. We’ll need to import the `"print_string"` function from JavaScript, as shown in Listing 3-13. ``` (import "env" "print_string" (func $print_string( param i32 ))) ``` Listing 3-13: Declaring an imported function `print_string` The `import` statement in Listing 3-13 tells the module to import an object called `print_string` passed inside an object called `env`. These names must correspond with the names in the JavaScript code. Within the JavaScript, you can call the object anything you like, but once you name it in the JavaScript, you must use the same name when you import it into WebAssembly. Listing 3-14 shows what the import object looked like in the *helloworld.js* file. **helloworld.js** ``` let importObject = { 1 env: { 2 buffer: memory, 3 start_string: start_string_index, 4 print_string: function(str_len) { const bytes = new Uint8Array( memory.buffer, start_string_index, str_len ); const log_string = new TextDecoder('utf8').decode(bytes); console.log(log_string); } } }; ``` Listing 3-14: Defining the `importObject` In the `"hello world"` app, we use a memory `buffer` 2 to set string data. We also let the WebAssembly module know the location of the string data (`start_string`) 3 inside the memory buffer. We put both objects inside an object called `env` to separate it from the JavaScript function. We then create the `print_string` function 4 inside the `env` 1 object as the JavaScript callback that prints a string from linear memory. At the time of this writing, there isn’t a standardized convention for naming objects inside the ``importObject. We’ve chosen the `env` object to represent objects related to the embedded environment and for the function callbacks, but you can organize the `importObject` in any way you like.`` ````### JavaScript Numbers When you create a JavaScript callback function, the only data type that function can receive is a JavaScript number. If you look at the `print_string` function we created in the `"hello world"` application, it passed a single variable. That variable is `str_len`, and it’s the byte length of the string displayed in the console. Unfortunately, only numbers can be passed as parameters to JavaScript functions. Chapter 5 explains in detail what to do to pass other kinds of data back to JavaScript. ### Passing Data Types WebAssembly can pass three of the four main data types back to functions imported from JavaScript: they include 32-bit integers, 32-bit floating-point numbers, and 64-bit floating-point numbers. At the time of this writing, you can’t pass 64-bit integers to a JavaScript function. The BigInt WebAssembly proposal will change this when it’s implemented. Until then, you must choose the data type you want to convert it to and perform the conversion inside the WebAssembly module. If you pass a 32-bit integer or floating-point number to JavaScript, JavaScript converts it to a 64-bit float, which is the native JavaScript number type. ### Objects in WAT WAT doesn’t support object-oriented programming (OOP). Creating classes and objects using WAT could potentially be accomplished using a combination of data structures, the function table, and indirect function execution, but that is beyond the scope of this book. In Chapter 6, we’ll explore how to create more sophisticated data structures within linear memory that will allow you to group the data together into linear memory in a way that is similar to using a *struct* in C/C++. Unfortunately, implementing OOP features, such as object methods, class inheritance, and polymorphism, are beyond what we can accomplish in this book. ## Performance Implications of External Function Calls In this section, we’ll explore the performance implications of calling imported and exported functions. When you call a JavaScript function in WAT, you lose some cycles to overhead. This number isn’t extremely large, but if you execute an external JavaScript function in a loop that iterates 4,000,000 times, it can add up. To get an idea of the kind of performance hit an application suffers from calls to JavaScript versus internal WebAssembly function calls, we created a WebAssembly test module. It executes a simple increment 4,000,000 times in an external JavaScript function and 4,000,000 times in a WebAssembly function in the same module. Because the code does very little, most of the difference in execution time can be attributed to the overhead of calling an external JavaScript function. Calling an internal WebAssembly function executed four to eight times faster on Chrome than internal WebAssembly calls and two to two and a half times faster in Firefox than internal calls. In our tests, Chrome performed worse than Firefox when crossing the JavaScript/WebAssembly boundary. Let’s walk through this performance test. You first need to create a new WAT file. Create an empty file and name it *func_perform.wat*. Then open the file and create a module with one import and one global variable, as shown in Listing 3-15. **func_perform.wat (part 1 of 4)** ``` (module ;; external call to a JavaScript function 1 (import "js" "external_call" (func $external_call (result i32))) 2 (global $i (mut i32) (i32.const 0)) ;; global for internal function ... ``` Listing 3-15: Importing a JavaScript `external_call` function The `import` expression 1 will import a function we’ll define in the JavaScript. That function will return a value to the WebAssembly module. The `global` expression 2 creates a mutable global variable with an initial value of `0`. In general, it’s considered bad practice to use mutable global variables, but this code is only intended as a means to test the difference in performance between a WebAssembly and JavaScript function call. After we define the global variable, we define the WebAssembly function we’ll be calling 4,000,000 times, as shown in Listing 3-16. **func_perform.wat (part 2 of 4)** ``` ... 1 (func $internal_call (result i32) ;; returns an i32 to calling function global.get $i i32.const 1 i32.add 2 global.set $i ;; The first 4 lines of code in the function increments $i 3 global.get $i ;; $i is then returned to the calling function ) ... ``` Listing 3-16: Internal call to the WebAssembly function The function `$internal_call` 1 returns a 32-bit value to the calling function, which will be the incremented value of the `$i` global variable. All this function does is increment the value of `$i` 2 and then push it back on to the stack 3 to return it to the calling function. Next, we need to create a function that JavaScript can call with an `export` expression. This function needs to call the `$internal_call` function 4,000,000 times. Listing 3-17 shows the code for that external function. **func_perform.wat (part 3 of 4)** ``` ... 1 (func (export "wasm_call") ;; function "wasm_call" exported for JavaScript 2 (loop $again ;; $again loop 3 call $internal_call ;; call $internal_call WASM function i32.const 4_000_000 4 i32.le_u ;; is the value in $i <= 4,000,000? 5 br_if $again ;; if so repeat the loop ) ) ... ``` Listing 3-17: Four million calls to an internal function This function 1 is exported to be called from the JavaScript code. It has a simple loop labeled `$again` 2 that will call the `$internal_call` function 3 4,000,000 times. The loop does this by comparing the value returned by `$internal_call` (the value in the global `$i`) to `4_000_000`. If the value of `$i` is less than 4,000,000 (`i32.le_u`) 4, it branches back to the beginning of the `$again` loop 5. Now that we have a function that will test the internal call to the WebAssembly function, in Listing 3-18 we create an almost identical function that will make a call to an external JavaScript function. **func_perform.wat (part 4 of 4)** ``` ... 1 (func (export "js_call") (loop $again 2 (call $external_call) ;; calls the imported $external_call function i32.const 4_000_000 i32.le_u ;; is the value returned by $external_call <= 4,000,000? br_if $again ;; if so, branch to the beginning of the loop ) ) ) ;; end of module 3 ``` Listing 3-18: Four million calls to an external JavaScript function There are only two differences between this function and the one in Listing 3-19. The first is the name of the function we export, `"js_call"` 1, which calls the imported `$external_call` 2 function. The second is that we put a closing parenthesis at the end to close the `module` expression 3. Once you’ve finished creating *func_perform.wat*, compile it into a WebAssembly module using the following command: ``` wat2wasm func_perform.wat ``` Now compile this module with `wat2wasm`. Then create an empty JavaScript file named *func_perform.js.* This JavaScript will load and call our WebAssembly module. Add the JavaScript in Listing 3-19. **func_perform.js (part 1 of 2)** ``` const fs = require('fs'); const bytes = fs.readFileSync(__dirname + '/func_perform.wasm'); 1 let i = 0; let importObject = { js: { 2 external_call: function () { // The imported JavaScript function i++; 3 return i; // increment i variable and return it } } }; ... ``` Listing 3-19: The `external_call` function defined in the JavaScript `importObject` We declare the variable `i` 1 and initialize its value to `0`. Inside `importObject` we create a function `"external_call"` 2 which we imported into the WebAssembly earlier. The only thing this function does is increment and then return the value in `i` 3. Next, we need to instantiate the *func_perform.wasm* module and execute `wasm_call` and `js_call`, as shown in Listing 3-20. **func_perform.js (part 2 of 2)** ``` ... (async () => { 1 const obj = await WebAssembly.instantiate(new Uint8Array(bytes), importObject); // destructure wasm_call and js_call from obj.instance.exports 2 ({ wasm_call, js_call } = obj.instance.exports); let start = Date.now(); 3 wasm_call(); // call wasm_call from WebAssembly module let time = Date.now() - start; 4 console.log('wasm_call time=' + time); // execution time in ms start = Date.now(); 5 js_call(); // call js_call from WebAssembly module time = Date.now() - start; 6 console.log('js_call time=' + time); // execution time in milliseconds })(); ``` Listing 3-20: Asynchronous IIFE definition inside JavaScript Like the other apps in this book so far, we must instantiate the *func_perform.wasm* module 1 in the JavaScript. We use the JavaScript destructuring syntax to create the `wasm_call` and `js_call` functions 2. This destructuring is an ECMAScript 2015 syntax that’s convenient for pulling multiple variables out of an object. Alternatively, you could set `wasm_call` and `js_call` variables to the values in `obj.instance.exports`. After retrieving the functions from the WebAssembly module, we call `wasm_call` 3 and log the time it took to execute in milliseconds 4 to the console. Next, we call the `js_call` function 5 and log 6 the time it took to execute. Run the JavaScript using node like so: ``` node func_perform.js ``` You should see something like this output logged to the console. ``` wasm_call time=7 js_call time=32 ``` It took 7 milliseconds to call the WebAssembly function 4,000,000 times and 32 milliseconds for JavaScript. This might seem like a large difference, but a 25-millisecond difference spread over 4,000,000 calls is actually pretty small. If this function is only being called once per frame, the difference is trivial. However, if you have a loop that executes hundreds or thousands of times per frame render, it might be worth considering arranging your code differently for performance reasons. In the next section, we’ll look at function tables and their performance. ## Function Tables JavaScript can set variables to functions, allowing an application to dynamically swap functions at runtime. WebAssembly doesn’t have this feature, but it does have *tables*, which at the time of this writing can only hold functions. For that reason, we’ll refer to them as *function tables*, although there are plans to support other types in the future. Function tables allow WebAssembly to dynamically swap functions at runtime, which allows compilers to support features such as function pointers and OOP virtual functions. For example, C/C++ programs use WebAssembly tables to implement function pointers. Currently, tables only support the `anyfunc` type (`anyfunc` is a generic WebAssembly function type), but in the future they might support JavaScript objects and DOM elements as well. Unlike import objects, JavaScript and WebAssembly can dynamically change tables at runtime. There is a performance cost to calling a function from a table rather than through an import because a function table entry must be called indirectly. Let’s compare the performance of functions called through a table and those called directly. ### Creating a Function Table in WAT In this section, we’ll create and export a simple function table in WAT. We’ll build a module with four functions; two of which are imported from JavaScript, and two are defined inside the WebAssembly module. These functions will be very simple because the goal is to compare the performance of table function execution to a direct import. Create a new file named *table_export.wat* and enter the code in Listing 3-21. **table_export.wat** ``` (module ;; javascript increment function 1 (import "js" "increment" (func $js_increment (result i32))) ;; javascript decrement function 2 (import "js" "decrement" (func $js_decrement (result i32))) 3 (table $tbl (export "tbl") 4 anyfunc) ;; exported table with 4 functions (global $i (mut i32) (i32.const 0)) 4 (func $increment (export "increment") (result i32) 5 (global.set $i (i32.add (global.get $i) (i32.const 1))) ;; $i++ global.get $i ) 6 (func $decrement (export "decrement") (result i32) 7 (global.set $i (i32.sub (global.get $i) (i32.const 1))) ;; $i-- global.get $i ) ;; populate the table 8 (elem (i32.const 0) $js_increment $js_decrement $increment $decrement) ) ``` Listing 3-21: Exporting functions in a table Two imports are in this module: a JavaScript `increment` function 1 and a JavaScript `decrement` function 2. However, you cannot add a JavaScript function to a function table from within JavaScript. There is a `WebAssembly.Table` function set that allows you to set functions in a table, only with a function defined in a WebAssembly module. We can work around this restriction by importing the JavaScript function into a WebAssembly module and adding it to the table there. The `table` 3 expression creates the table and names it `$tbl`, which we can reference within the WAT code. We export `$tb1` and tell `table` the expression that there are four objects in this table of type `anyfunc` (currently the only type of table object supported). We then create two WebAssembly functions: the `$increment` function 4 sets the value of the global `$i` to `$i`+1 5 and the `$decrement` function 6 sets the value of `$i` to `$i`-1 7. The last thing we do in this module is set the values in the table using the `elem` expression 8. As its first parameter, the `elem` expression takes the index of the first element we set. We set all four elements, so we use `(i32.const` `0)` because the first parameter is the starting index we want to update. We then follow that parameter with the four function variables we want in the table. Alternatively, if we only wanted to set the first two items in the table, we wouldn’t need to pass in all four function names and would do this: ``` (elem (i32.const 0) $js_increment $js_decrement) ;; set first 2 table func ``` To set the second two items, we would change the value of the first parameter to let the expression know we’re starting with the third item in the table, as shown here: ``` (elem (i32.const 2) $increment $decrement) ;; set table items 3 and 4 ``` The first parameter of the `elem` statement in Listing 3-21 is an index of `0`, similar to an array. When you’ve finished adding this code, compile *table_export.wat* into *table_export.wasm* using the `wat2wasm` command. #### Sharing a Table Between Modules Now that we have a *table_export.wasm*, let’s create a second WebAssembly file that shares the same table. Create a new WAT file named *table_test.wat*. We’ll begin the new WebAssembly module with a series of `import` statements and a `type` definition expression, as shown in Listing 3-22. **table_test.wat (part 1 of 3)** ``` (module 1 (import "js" "tbl" (table $tbl 4 anyfunc)) ;; import increment function 2 (import "js" "increment" (func $increment (result i32))) ;; import decrement function 3 (import "js" "decrement" (func $decrement (result i32))) ;; import wasm_increment function 4 (import "js" "wasm_increment" (func $wasm_increment (result i32))) ;; import wasm_decrement function 5 (import "js" "wasm_decrement" (func $wasm_decrement (result i32))) ;; table function type definitions all i32 and take no parameters 6 (type $returns_i32 (func (result i32))) ... ``` Listing 3-22: Function imports in the WebAssembly module The first `import` statement 1 imports the table, with four `anyfunc`functions, in the *table_export.wat* file. Next, we import the JavaScript functions `increment` 2 and `decrement` 3, which we’ll use to compare the performance of imported JavaScript functions against JavaScript functions defined in tables. We then import the `wasm_increment` 4 and `wasm_decrement` 5 functions defined in the *table_export.wat* file in the table. With this we can test the performance of a `call_indirect` to a table element with a call to the same function imported directly with an `import` statement. The last expression is a `type` expression 6 which defines the signature of the functions in the table. I have to provide this `$returns_i32` type as a static parameter to `call_indirect`. We can expect that `call_indirect` will be slower than `call` because the type of the indirectly called function must be dynamically checked to match this provided type. Now, let’s use the code in Listing 3-23 to define four global variables that we’ll use to index into the function table. **table_test.wat (part 2 of 3)** ``` ... 1 (global $inc_ptr i32 (i32.const 0)) ;; JS increment function table index 2 (global $dec_ptr i32 (i32.const 1)) ;; JS decrement function table index 3 (global $wasm_inc_ptr i32 (i32.const 2)) ;; WASM increment function index 4 (global $wasm_dec_ptr i32 (i32.const 3)) ;; WASM decrement function index ... ``` Listing 3-23: Global variable function table indexes These four global variables are indexes into the function table and give us an easier way to keep track of which index corresponds to which function. The `$inc_ptr` 1 and `$dec_ptr` 2 variables point to the JavaScript `increment` and `decrement` functions, and `$wasm_inc_ptr` 3 and `$wasm_dec_ptr` 4 point to the WebAssembly versions of the `increment` and `decrement` functions in the table. #### Defining the Test Functions With the imports and globals defined, we’ll define the four test functions. All of these functions will do the same task using different methods: they’ll call an `increment` function 4,000,000 times from a `loop` and then call a `decrement` function 4,000,000 times. One function will call the functions indirectly from the imported table; one will call them directly from an `import`; one will call a WebAssembly version from a table; and the last will call the `increment` and `decrement` functions from a direct `import`. In the end, we should be able to compare the performance of calling JavaScript functions through a table or directly, as well as compare the performance of calling a WebAssembly module function directly or through a table. Listing 3-24 shows the four function definitions. **table_test.wat (part 3 of 3)** ``` ;; Test performance of an indirect table call of JavaScript functions 1 (func (export "js_table_test") (loop $inc_cycle ;; indirect call to JavaScript increment function (call_indirect (type $returns_i32) (global.get $inc_ptr)) i32.const 4_000_000 i32.le_u ;; is the value returned by call to $inc_ptr <= 4,000,000? br_if $inc_cycle ;; if so, loop ) (loop $dec_cycle ;; indirect call to JavaScript decrement function (call_indirect (type $returns_i32) (global.get $dec_ptr)) i32.const 4_000_000 i32.le_u ;; is the value returned by call to $dec_ptr <= 4,000,000? br_if $dec_cycle ;; if so, loop ) ) ;; Test performance of direct call to JavaScript functions 2 (func (export "js_import_test") (loop $inc_cycle call $increment ;; direct call to JavaScript increment function i32.const 4_000_000 i32.le_u ;; is the value returned by call to $increment<=4,000,000? br_if $inc_cycle ;; if so, loop ) (loop $dec_cycle call $decrement ;; direct call to JavaScript decrement function i32.const 4_000_000 i32.le_u ;; is the value returned by call to $decrement<=4,000,000? br_if $dec_cycle ;; if so, loop ) ) ;; Test performance of an indirect table call to WASM functions 3 (func (export "wasm_table_test") (loop $inc_cycle ;; indirect call to WASM increment function (call_indirect (type $returns_i32) (global.get $wasm_inc_ptr)) i32.const 4_000_000 i32.le_u ;; is the value returned by call to $wasm_inc_ptr<=4,000,000? br_if $inc_cycle ;; if so, loop ) (loop $dec_cycle ;; indirect call to WASM decrement function (call_indirect (type $returns_i32) (global.get $wasm_dec_ptr)) i32.const 4_000_000 i32.le_u ;; is the value returned by call to $wasm_dec_ptr<=4,000,000? br_if $dec_cycle ;; if so, loop ) ) ;; Test performance of direct call to WASM functions 4 (func (export "wasm_import_test") (loop $inc_cycle call $wasm_increment ;; direct call to WASM increment function i32.const 4_000_000 i32.le_u br_if $inc_cycle ) (loop $dec_cycle call $wasm_decrement ;; direct call to WASM decrement function i32.const 4_000_000 i32.le_u br_if $dec_cycle ) ) ) ``` Listing 3-24: Performance testing The first function we define for `export` is `js_table_test` 1. This function runs 8,000,000 indirect calls to simple JavaScript functions. The function following `js_table_test` is `js_import_test` 2. The `js_import_test` function calls the same functions `js_table_test` does. However, it does it directly from an `import`. This allows us to compare the performance of 8,000,000 runs of the same function with and without the use of a table. The other two functions, `wasm_table_test` 3 and `wasm_import_test` 4, are the same as the `js` versions of those functions but call WebAssembly module functions instead of JavaScript functions. Once you’ve created your *table_test.wasm* file, create a new JavaScript file named *table.js* and add the code in Listing 3-25. **table.js (part 1 of 4)** ``` const fs = require('fs'); const export_bytes = fs.readFileSync(__dirname+'/table_export.wasm'); const test_bytes = fs.readFileSync(__dirname + '/table_test.wasm'); let i = 0; let increment = () => { i++; return i; } let decrement = () => { i--; return i; } ... ``` Listing 3-25: JavaScript `increment` and `decrement` functions We’ll test these functions by calling them directly using an `import` and indirectly using a table. #### Creating the WebAssembly importObject in JavaScript Now we need an `importObject` that we’ll use for both of the WebAssembly modules. This object defines all of the functions, values, and tables we want to pass from the JavaScript into a WebAssembly module. In Listing 3-27, we’ll also use it to pass a table from one WebAssembly module to another. We’ll initially set the `tbl`, `wasm_decrement`, and `wasm_increment` objects to `null` because they’re not yet being used by the *table_export.wasm* module. But they’ll be needed when we load the *table_test.wasm* module. Listing 3-26 shows the `importObject`. **table.js (part 2 of 4)** ``` ... const importObject = { js: { 1 tbl: null, // tbl is initially null and is set for the second WASM module 2 increment: increment, // JavaScript increment function decrement: decrement, // JavaScript decrement function 3 wasm_increment: null, // Initially null, set to function by second module wasm_decrement: null // Initially null, set to function by second module } }; ... ``` Listing 3-26: JavaScript `importObject` At this point, the `tbl` 1 value we pass in through the `importObject` is `null`, because it’s created in the *table_export.wasm* module, and we’ll need to initialize that value with the table exported from *table_export.wasm*. The `increment` and `decrement` 2 functions were defined earlier in the JavaScript. We haven’t defined the `wasm_increment` and `wasm_decrement` 3 functions yet because we’ll need to put them into the `import` function after they’re created in *table_export.wasm*. #### Instantiating the WebAssembly Modules Now let’s look at how to instantiate the two WebAssembly modules in Listing 3-27. **table.js (part 3 of 4)** ``` ... 1 (async () => { // instantiate the module that uses a function table 2 let table_exp_obj = await WebAssembly.instantiate( new Uint8Array(export_bytes), importObject); // set the tbl variable to the exported table 3 importObject.js.tbl = table_exp_obj.instance.exports.tbl; 4 importObject.js.wasm_increment = table_exp_obj.instance.exports.increment; 5 importObject.js.wasm_decrement = table_exp_obj.instance.exports.decrement; 6 let obj = await WebAssembly.instantiate( new Uint8Array(test_bytes), importObject); ... ``` Listing 3-27: Instantiating a WebAssembly module in asynchronous IIFE As we’ve done previously, we use an asynchronous IIFE 1 to instantiate the WebAssembly modules. However, now we instantiate two WebAssembly modules instead of just a single module to demonstrate how we can share functions and function tables between WebAssembly modules. When we instantiate the *table_export.wasm* module, we put the WebAssembly object into a variable called `table_exp_obj` 2. In the past we’ve put all the WebAssembly module objects into a variable called `obj`, but because we’re using more than one module in this app, we need more specific names. We use the `table_exp_obj` to set the `tbl` variable 3, defined earlier in the `importObject`, to the function table created in the *table_export.wasm* module. Next, we set the `wasm_increment` 4 and `wasm_decrement` 5 variables in the `importObject`. Then we instantiate the *table_test.wasm* module 6. The last block of code in this section, in Listing 3-28, performs the test and measures the time in milliseconds each test took to run. **table.js (part 4 of 4)** ``` ... // use destructuring syntax to create JS functions from exports 1 ({ js_table_test, js_import_test, wasm_table_test, wasm_import_test } = obj.instance.exports); i = 0; // i variable must be reinitialized to 0 let start = Date.now(); // get starting timestamp 2 js_table_test(); // run function that tests JS table calls let time = Date.now() - start; // find out how much time it took to run console.log('js_table_test time=' + time); i = 0; // i must be reinitialized to 0 start = Date.now(); // get starting timestamp 3 js_import_test(); // run function that tests JS direct import calls time = Date.now() - start; console.log('js_import_test time=' + time); i = 0; // i must be reinitialized to 0 start = Date.now(); // get starting timestamp 4 wasm_table_test(); // run function that tests WASM table calls time = Date.now() - start; // find out how much time it took to run console.log('wasm_table_test time=' + time); i = 0; // i must be reinitialized to 0 start = Date.now(); // get starting timestamp 5 wasm_import_test(); // run function that tests WASM direct import calls time = Date.now() - start; // find out how much time it took to run console.log('wasm_import_test time=' + time); })(); ``` Listing 3-28: Calling the WebAssembly functions and recording the execution time We use the JavaScript destructuring syntax to create four variables 1, `{js_table_test, js_import_test, wasm_table_test, wasm_import_test}`, from the `obj.instance.exports` object’s attributes. This is just a handy way to create all four of these function variables at once and set them to the functions of the same name exported by the WebAssembly module. Then we run each of the functions one by one using `node`, like this: ``` node table.js ``` Here are the four lines you should see displayed to the console that show the `js_table_test` 2, `js_import_test` 3, `wasm_table_test` 4, and `wasm_import_test` 5 runtime: ``` js_table_test time=67 js_import_test time=60 wasm_table_test time=25 wasm_import_test time=20 ``` The line displaying the `js_import_test` 3 runtime shows that for this run, calling the JavaScript through an `import` executed about 10 percent faster than calling the same function using a table. Although this might not seem like a tremendous difference, it depends on your application’s needs. We then see the time in milliseconds it takes to call similar WebAssembly versions of the `increment` and `decrement` functions. This difference is a bit more significant: the table call takes about 25 percent longer than the direct `import`. In this section, you learned how to share functions and function tables between different WebAssembly modules. You also learned the difference between calling a function directly from an `import` and calling one indirectly through a table, and the performance implications of calling a function using a table. Understanding how functions are called, imported, exported, and used in function tables are fundamental features of the language you’ll need to understand before you master WebAssembly development. ## Summary In this chapter, we examined calling WebAssembly functions from JavaScript and JavaScript functions from WebAssembly. We covered what the performance implications of each type of call are. We also created an app that tests for prime numbers to demonstrate the kind of function that it makes sense to create in a WebAssembly module. We looked at passing parameters to functions defined in WebAssembly and how to create functions in WebAssembly that won’t be available to JavaScript. We looked into what it takes to create functions that manipulate strings within WebAssembly and how to access those strings from JavaScript. We dove further into data types in WebAssembly and how they translate into data types in JavaScript. We reviewed tables and how to use them to indirectly call WebAssembly and JavaScript functions, including WebAssembly functions created in a second module. We also spent time investigating the performance implications of using tables and functions created in a second WebAssembly module. In the next chapter, we’ll explore using WAT for low-level programming.```` `````

第四章:低级位操作

本章讨论了一些用于 WebAssembly 应用的位操作技巧,我们将在后续章节的项目中应用这些技巧,以提高应用的性能。对于不熟悉低级编程的读者来说,这个话题可能会有一定挑战,因此如果你对处理二进制数据不感兴趣,可以继续阅读下一章,并根据需要随时参考本章内容。

在探索位操作技巧之前,我们将先介绍一些基本概念,包括三种不同的数字进制——十进制、十六进制和二进制;整数和浮点数运算的细节;以及二的补码、大小端字节序。此外,我们还将研究高位和低位、位掩码、位移和位旋转等内容。

WebAssembly 使你能够在 Web 浏览器中尽可能接近硬件层面。如果你希望编写执行速度极快的 WebAssembly,理解如何在比特级别操作数据非常有用。位操作对于理解 WebAssembly 处理的数据类型、它们的表现以及限制也至关重要。WAT 可以以类似汇编语言的方式在比特级别操作数据。低级编程是一个复杂的主题,如果你之前没有接触过本章中的一些概念,可能无法立即掌握它们。好消息是,你并不需要了解所有这些低级概念才能使用 WebAssembly,但理解低级 WebAssembly 有助于你为 Web 编写快速、高性能的代码。很多时候,编译器优化的代码会生成执行位操作的代码,因此了解这些位操作如何工作,对于将 WebAssembly 二进制代码反汇编为 WAT 代码时也非常重要。

二进制、十进制和十六进制

十六进制系统是计算机编程中常用的数字系统,它使用基数 16,而不是你从两岁开始就使用的十进制数字系统。计算机程序员使用十六进制(hex),因为计算机本地使用二进制,十六进制比十进制更简洁地转换为二进制。你不会学到如何手动将十进制转换为十六进制,因为我假设你要么已经知道如何做,要么有一个计算器可以做到这一点;大多数计算器应用程序都提供程序员模式,可以为你完成这个转换,正如图 4-1 中所示。

f04001

图 4-1:Microsoft Windows 计算器应用的程序员模式

我还提供了一个简单的十进制到十六进制的转换工具,wasmbook.com/hex,以及一个用 WAT 编写的在线计算器,wasmbook.com/calculator.html。请记住,如果你想将数字数据嵌入到 WAT 字符串中,需要使用两位数的十六进制数字,而不是十进制数字。

整数与浮点运算

在讨论如何对类型进行位操作之前,我们需要先讨论 WebAssembly 支持的类型的细节。WebAssembly 的两种主要数据类型是整数和浮点数。这些类型在声明时会分配给所有的局部和全局变量,并且在函数声明中应使用这些类型作为参数。整数表示整数,并且可以表示负数和正数。然而,使用负整数比使用负浮点数稍微复杂一些。

你可以将整数和浮点数存储在变量中,也可以存储在线性内存中。如果你熟悉 JavaScript,线性内存就像一个类型化的无符号整数数组。我们将在第六章中进一步讨论线性内存。

在这一节中,我们将探讨整数和浮点变量是如何工作的,不同类型的浮点和整数变量,以及如何对它们进行一些基本的二进制操作。首先,我们简要讨论一下 WebAssembly 支持的四种主要数据类型。有两种整数数据类型(i32i64)以及两种浮点数据类型(f32f64)。

整数

对于大多数数学运算,整数运算通常比浮点运算更快。WebAssembly 当前支持 32 位和 64 位整数,但不幸的是,不能像其他数据类型一样轻松地与 JavaScript 共享 64 位整数。

i32(32 位整数)

i32 数据类型速度快,占用空间小,并且可以在 WebAssembly 和 JavaScript 之间轻松传递。它可以表示无符号数据范围为 0 到 4,294,967,295,表示有符号整数的范围为 -2,147,483,648 到 2,147,483,647。如果你处理的数字值不会超过十亿,并且不需要小数,使用 i32 是一个不错的选择。处理有符号或无符号值更多的是与对数据进行的操作有关,而不是变量中的数据本身。i32 数据类型使用 2 补码表示负数。

2 补码

2 补码是一种广泛使用的技术,用于以二进制格式表示负数。计算机只以二进制工作,因此内存中只有 1 和 0。二进制可以相对容易地转换为十进制,但如何仅用 1 和 0 表示负数却不太显而易见。需要注意的是,浮点数有专门的符号位,因此不使用 2 补码。

为了理解 2 的补码如何工作,让我们借用一个比喻来说明。假设你有一个按键计数器,每次按下一个按钮时,它都会将一个数字刻度拨动一位 (图 4-2)。这个计数器只有一个十进制数字显示,因此它从 0 开始,计数到 9,但当你按下第十次时,它会回到 0。从计数器的角度来看,增加 10 等同于增加 0,因为你的计数器最终回到与起始位置相同的位置。顺便说一下,按下按钮 9 次在功能上等同于减去 1,除了 0 之外的所有数字都是如此。

f04002

图 4-2:当加 1 时,9 会回到 0。

将高位数作为负数使用仅在你知道你的数字范围不包括所选择的负数时才有用。如果你声明 9 等于 –1,但你需要用计数器数到 9,那么这个数字系统就无法工作。溢出系统是有符号 8 位整数支持从 –128 到 127 的数字的原因。数字 255(八个 1)的无符号 8 位编码与 –1 的有符号 8 位表示相同,因为将 255 加到任何数字上会导致比特像里程表一样滚动并减去 1。最大的数字变成负数是因为它们会导致比特溢出。执行 2 的补码转换的代码使用二进制异或(XOR)操作翻转所有位,然后将数字 1 加到结果上,从而得到该数字的负数。你将在本章稍后看到执行 XOR 和位翻转的代码。

i64(64 位整数)

i64 数据类型可以表示无符号整数在 0 和 18,446,744,073,709,551,615 之间的正整数,以及有符号整数在 –9,223,372,036,854,775,808 和 9,223,372,036,854,775,807 之间的整数。WAT 中的 i64 数据类型在声明变量时没有指定它是有符号还是无符号的。相反,WAT 必须根据用户是否希望将数字视为有符号或无符号来选择执行的操作。并非所有操作都需要你做出这个选择:例如,i64.addi64.subi64.mul 无论整数是否有符号,都可以正常工作。与之对比的是除法函数,如 i64.div_si64.div_u。除法操作必须区分数字是有符号还是无符号,因此必须在不同版本的操作符后附加 _s_u 后缀。

如前所述,i64 数据类型的一个问题是你无法直接在 WebAssembly 和 JavaScript 之间来回传递 64 位整数。JavaScript 只使用 64 位浮点数,但 64 位浮点数可以容纳 32 位整数和 32 位浮点数。最关键的问题是,将 64 位整数传递到 JavaScript 部分的应用中可能会很麻烦。

浮点数

浮点数在二进制中包含三部分:符号位,后跟表示指数的一系列位,然后是表示有效数字的位(有时称为尾数有效数字)。请记住,二进制中没有小数点;计算机科学家必须发明一种系统来表示二进制数中的小数点。符号位表示数字是正数还是负数。指数表示小数点需要移动多少位(左移或右移),有效数字则是浮点数的数字部分。让我们看看如何通过使用指数来构造十进制浮点数:如果你拿一个像 345 的数字,并将它乘以 10 的 2 次方,它会在数字的末尾添加两个零,如图 4-3 所示。这实际上是将小数点向右移动了两位。

f04003

图 4-3:使用正指数将小数点向右移动。

仅使用十进制数字,你会说指数是 2,后跟 3 个小数位,因此 2345 就是数字 34,500,或 345 × 10²,如图 4-4 所示。

f04004

图 4-4:使用十进制指数

为了得到分数值,我们需要一个负的指数而不是正的指数,如图 4-5 所示:负指数将小数点向左移动。

f04005

图 4-5:使用负指数将小数点向左移动。

指数为负 2 时,345 变成了 3.45,结果是一个分数值。请注意,这种系统的问题:到目前为止,我们还没有方法来表示第一个指数位的负号。我们说过可以用 2345 来表示 345 × 10²,但我们没有表示负号。我们可以选择两种方法中的一种:2 的补码和偏置指数。首先,我们可以使用像 2 的补码这样的方式,并将较高的数字值赋予负数,因此 8345 可以表示 345 × 10^(–2),因为 10 – 2 = 8。但这不是浮点数设计者选择的方法。相反,他们使用偏置指数,通过简单地从指数中减去一个特定的选定值来给出负指数。例如,如果我们决定总是从指数数字中减去 5,那么 3345 就可以表示 345 × 10^(–2),因为 3 – 5 = –2。

实数浮动点数是二进制的,而非十进制,但基本原理是相同的。在二进制浮动点数中,最高有效位是符号位,如果数值为正则为 0,若为负则为 1。符号位之后的八位表示指数。尾数表示一个介于一和二之间的分数值。最左侧的位表示 0.5,接着是 0.25、0.125、0.0625,依此类推,每次减半。因为尾数的最小值为 1,所以尾数的实际值总是比所有这些分数字节的和大 1。位的布局如图 4-6 所示。

f04006

图 4-6:32 位浮动点数位

尾数的最小值为 1 的问题引出了数字 0 的处理。将一个非零值提升到任何幂次时永远不会得到 0 的值。为了弥补这一点,指数有两个特殊值,用于表示浮动点数中的 0 和无限大。如果指数和尾数的所有位都为 0,则表示的数值为 0,尽管 0⁰为 1。值得注意的是,浮动点数可以根据符号位的值表示 0 和-0。无限大和-无限大是指数位全为 1,尾数位全为 0 的数值。如果尾数位不是 0,则浮动点数表示为 NaN(不是数字)。

次正常数

次正常数(有时称为非规范化数)是 IEEE-754 浮动点数规范中的另一种浮动点数边界情况。次正常数是一种边界情况,其中指数位全为 0。在所有指数位均为 0 的情况下,尾数值不再增加 1 到所表示的值。这使得可以表示更小的十进制数值。虽然本书中不使用次正常数,但你至少应该知道它们的存在。

f64/number

f64数据类型是双精度 64 位浮动点数。f64有 52 位表示有效数字,11 位表示指数,1 位表示符号位。该数据类型允许高精度,但在大多数硬件上,其执行速度比整数或较小的浮动点数要慢。它的一个优点是它是 JavaScript 用于所有数字的数据类型,使得它在 JavaScript 与 WebAssembly 之间转换数据时非常方便。

f32

f32数据类型比f64更小且更快,但精度要低得多,这意味着它能够使用的有效数字较少。f64数据类型大约有 16 位十进制有效数字,而f32只有大约七位。由于二进制数字表示有效数字,它与十进制数字并不完全对齐。有效十进制数字的数量只是一个近似值,但它能帮助你了解每种类型的限制。

高位和低位

本节我们将讨论位的有效性。低位是二进制数的最低有效位;有效性意味着 表示最大值。图 4-7 中,数字 128 中的 1 是最高有效位,而 8 是最低有效位。

f04007

图 4-7:最高有效位和最低有效位

代表最大数值的数字是最高有效位。在这里,1 是最高有效位,因为它处于 100 位,而没有更高位的数字。插图中的那个可怜的小 8 是最低有效位,因为它表示的是个位数。

二进制数也有高位。在计算机中,一个字节总是由八个二进制位组成;即使数字是 00000001,左边的七个二进制位仍然存在于字节中。例如,数字 37 的二进制表示是 100101,但在计算机内存中,字节中的值是 00100101。该字节中的最高有效位(高位)是最左边的 0,如图 4-8 所示。

f04008

图 4-8:高位和低位

该字节中的低位是最右侧的 1,高位是最左侧的 0。

仅仅查看变量的高位和低位可以揭示一些字节的信息。整数中的低位表示该整数是偶数(0)还是奇数(1)。带符号整数的高位决定了该数字是正数(0)还是负数(1)。

位操作

WebAssembly 提供了低级别的数据操作抽象。如果你愿意付出努力,通过在位级别进行数据操作,你可以经常提供更好的代码性能。了解这些操作如何工作也可以在你尝试提高代码性能时发挥作用,即使是在高级语言中。我们将在本书后面写的应用中使用本章中的许多操作,因此根据需要回顾这一章。这些操作非常通用,因此很难给出具体的应用实例。然而,当实际情况出现时,哪个操作适用会显而易见。

移位和旋转位

在本节中,我们将介绍位的移位和旋转操作。这些是基本的位操作,我们在本书中会时常使用。一个字节的数据由八个位组成,可以存储从 0 到 255 的十进制数值,等同于从 0 到 FF 的十六进制值。你知道四个位称为 半字节(nibble)吗?单个十六进制数字由一个半字节(半个字节)组成,十进制值为 0 到 15,十六进制值为 0 到 F。

四个位可以存储一个十六进制数字,这使得在 WAT 中处理十六进制数字相对容易,尤其是在进行移位时。移位是一种通用操作,是我们在后续章节中讨论的优化的基础。移位有点像将位从悬崖上推下来,并用 0(或者对于某些带符号的右移,用 1)替换它们。你可以按任意数量的位进行移位,且可以向左或向右移位。例如,二进制 1110 1001 在十六进制中是 E9(在十进制中是 233)。如果我们使用(i32.shr_u)表达式将该数字右移 4 位,它将返回二进制 0000 1110,或在十六进制中是 0E。图 4-9 展示了 E9 四位右移的戏剧化效果。

f04009

图 4-9:将十六进制 E9 右移四位得到 0E。

将数据向右移位可以是一种有用的技巧。每向右移一位,相当于除以 2。同样,每向左移一位,相当于乘以 2。你可以将移位与位屏蔽结合使用,以隔离二进制数据的某些部分。

在 WebAssembly 中,左移是不依赖符号的,但右移时我们使用带符号或无符号的移位操作。当你进行二进制数的右移或左移时,被移出的位通常会被用 0 替换到整数的另一侧。然而,对于负数,如果进行带符号的右移,1 会从左侧移入,以保持整数的符号。对二进制补码进行符号移位,可以保持符号(即负号)。

位旋转与移位不同,它将位反转并移到变量的另一侧。如果你将位向右旋转,最不重要的位将转移到最前面,成为最重要的位,其他所有位都将向右移位。WAT 使用rotl(向左旋转)或rotr(向右旋转)命令进行位旋转,如图 4-10 所示。

f04010

图 4-10:将位向右旋转

使用 AND 和 OR 进行位屏蔽

位屏蔽是一种方法,用于根据我们使用的掩码类型,将整数中的某些指定位设置为 1 或 0,从而隔离或覆盖它们。在编写高性能应用程序时,这非常有用。此外,需要知道 WebAssembly 没有布尔值的概念。在 WebAssembly 中,比较通常返回 1 表示真,0 表示假。当你在许多编程语言中使用布尔逻辑时,必须使用i32.andi32.or。在本节中,我们将使用i32.andi32.or进行位屏蔽,隔离或覆盖位值。

使用按位 AND 进行掩码时,整数中的 0 位在比特位的竞争中获胜。掩码中的 0 位就像胶带一样,覆盖了整数中的任何其他位,并将它们替换为 0。掩码中的 1 位则允许原始值透过掩码显示。图 4-11 展示了将二进制 1011 1110(190)与 0000 0111(7)进行 AND 掩码的效果。

如你在图 4-11 中看到的,原始值的所有位都被 AND 掩码中的 0 覆盖。当对两个不同的整数执行按位 AND 操作时,0 位胜出(参见图 4-12)。

f04011

图 4-11:使用按位 AND 进行掩码

f04012

图 4-12:按位 AND 操作,0 位胜出

当你使用 OR 掩码时,使用 i32/i64.or 操作,结果恰好相反:1 位会覆盖任何包含 1 的位。你不能像使用 AND 掩码那样隔离位。相反,你使用它将特定位设置为 1。图 4-13 展示了使用 OR 进行掩码的效果。

f04013

图 4-13:使用按位 OR 进行掩码

如你在图 4-13 中看到的,使用按位 OR 操作时,掩码中的 1 位覆盖了初始整数值中的任何位。因此,在按位 OR 操作中,1 位会覆盖 0 位(参见图 4-14)。

f04014

图 4-14:在按位 OR 中,1 位总是胜出。

XOR 位翻转

我们将要讲解的最后一个二进制操作是 XOR。与 AND 和 OR 不同,掩码的结果是明确的。XOR i32/i64.xor 操作稍有不同。如果 0 遮掩了 0,1 遮掩了 0,或 0 遮掩了 1,XOR 操作就像典型的 i32/i64.or 操作一样。奇怪的地方出现在两个 1 的情况下,这会导致 XOR 操作将结果位设置为 0。这个特性对于 位翻转 很有用,它能将每个位翻转为其相反值。有些操作要求你将整数中的每个 1 位改为 0,将每个 0 位改为 1。图 4-15 展示了如何使用 XOR 翻转一个字节中的每一位。

f04015

图 4-15:XOR 位翻转

在我们之前讨论的 2 补码中,我们提到,如果你想找到任何数的负数,你可以通过翻转每个位并加上 1 来实现。你可以通过对一个所有位都设置为 1 的整数进行 XOR 操作来翻转所有位。让我们编写一个小应用程序,使用 2 补码和位翻转将一个整数值转换为其负数。创建一个名为 twos_complement.wat 的文件,并将列表 4-1 中的代码添加进去。

twos_complement.wat

(module
  (func $twos_complement (export "twos_complement")
  (param $number i32)
  (result i32)
    local.get $number
  1 i32.const 0xffffffff  ;; all binary 1s to flip the bits
  2 i32.xor               ;; flip all the bits
    i32.const 1
 3 i32.add             ;; add one after flipping all bits for 2s complement
  )
)

列表 4-1:2 补码函数

这是一个非常简单的模块,接受一个名为$numberi32参数。我们将i32推送到栈上,接着是一个所有位都是 1 的 32 位数字。当我们调用i32.xor 2 时,原始数字的所有位都会被翻转。每个 1 变为 0,每个 0 变为 1。然后我们调用i32.add 3,将该数字加上 1,得到 2 的补码,从而得到负数。这个代码作为 2 的补码工作原理的演示非常有效;然而,如果我们只是从0中减去$number来取反,它会表现得更好。

大端与小端

你熟悉的所有数字都采用大端格式排列,这意味着最高位的数字在左侧,最低位的数字在右侧。大多数计算机硬件使用小端格式,其中最低位的数字在左侧,最高位的数字在右侧,因此 128 会写成 821。请记住,字节序指的是字节顺序,而不是数字顺序,所以我使用的小端十进制 821 示例是一个过于简化的例子,不能直接转化为二进制。小端硬件将字节按照与典型顺序相反的方式排序。数字 168,496,141 用大端十六进制表示为 0A0B0C0D。高位字节是 0A,低位字节是 0D,因为每个十六进制数字由一个半字节(nibble)表示。如果我们按照小端字节序排序字节,它们将按 0D0C0B0A 排列,如图 4-16 所示。

f04016

图 4-16:大端与小端字节序

目前大多数硬件出于性能原因使用小端字节序。WebAssembly 无论硬件如何,都会使用小端字节序。当你在 WebAssembly 中使用(data)语句初始化数据时,保持字节顺序非常重要。

总结

本章涵盖了许多低级编程概念。我们查看了低级编程中使用的不同数字进制(十进制、十六进制和二进制)。我们研究了整数和浮点算术的细节,并涉及了 2 的补码以及大端和小端字节序。我们讨论了位操作,包括高位和低位、位掩码、位移和位旋转。这些低级选项将在后续章节的应用中变得有用,通过操作位来提高性能。

在下一章,你将学习几种管理字符串作为数据结构的方法,包括空字符终止字符串和长度前缀字符串。我们还将探索复制字符串以及将数字数据转换为十进制、十六进制和二进制格式的过程。

第五章:WebAssembly 中的字符串

本章讨论如何在 WAT 中处理字符串,因为 WebAssembly 不像高级语言那样具有内建的字符串数据类型。要在 WebAssembly 中表示字符串数据,您必须将线性内存设置为 ASCII 或 Unicode 字符值。您需要知道将数据存储在何处以及字符串将使用多少字节。

本章将先介绍 ASCII 和 Unicode 字符格式,然后探讨字符串对象之间的关系,以及如何在线性内存中存储它们。您将学习如何让 JavaScript 从线性内存中获取字符串并将其输出到命令行。一旦您知道如何将字符串数据从 WebAssembly 传递到 JavaScript,我们将介绍两种流行的字符串管理方法:空终止字符串和长度前缀字符串,讨论每种技术的优缺点。您将学习如何通过逐字节复制和 64 位复制将字符串从线性内存中的一个位置复制到另一个位置。然后,您将学习如何将整数数据转换为十进制、十六进制和二进制格式的数字字符串。

ASCII 和 Unicode

在 WebAssembly 中处理字符串时,您需要知道您使用的是哪种字符集,因为不同的字符集在线性内存中的表现不同。美国信息交换标准代码(ASCII) 是一种 7 位字符编码系统,支持最多 128 个字符,其中第 8 位可能用于错误检查,或者简单地设置为 0。ASCII 字符集在仅支持英语的代码中效果良好。

Unicode 转换格式(UTF) 有 7 位、8 位、16 位和 32 位几种类型,分别称为 UTF-7、UTF-8、UTF-16 和 UTF-32。UTF-7 和 ASCII 是相同的。UTF-8 包含了 UTF-7,并通过创建灵活的长度格式来支持一些额外的拉丁字母、中东和亚洲字符,当格式的起始字节超出 ASCII 字符集时,可以通过增加字节来适应。UTF-16 也是一种灵活长度的字符集,其中大多数字符占用两个字节。由于某些编码会将字符所占字节数扩展到四个字节,UTF-16 支持超过 110 万个字符。UTF-32 是一个固定的 32 位字符集,支持超过 40 亿个字符。在本书中,我们将专门使用 ASCII/UTF-7 字符集,因为它简单易读且易于理解。

线性内存中的字符串

将字符串从 WebAssembly 传递到 JavaScript 的唯一方法是像我们在第二章的 hello world 应用中那样,在内存buffer对象中创建一个字符数据数组。然后,你可以将一个 32 位整数传递给 JavaScript,该整数表示该字符数据在内存缓冲区中的位置。这个方案的唯一问题是它没有告诉 JavaScript 数据的结束位置。C 语言通过使用空终止字节来管理这一点:值为 0 的字节(不是字符 0)告诉程序字符串在前一个字节处结束。我们将探讨三种在 WAT 和 JavaScript 之间传递字符串的方法,包括空终止,并进一步讨论如何复制字符串。

将字符串长度传递给 JavaScript

与字符串交互的最明显方式是将字符串位置和字符串长度传递给 JavaScript,这样 JavaScript 就可以从线性内存中提取字符串,并且能够知道它的结束位置。创建一个名为strings.wat的新 WAT 文件,并添加列表 5-1 中的代码。

strings.wat(第一部分,共 11 部分)

(module
;; Imported JavaScript function (below) takes position and length
1 (import "env" "str_pos_len" (func $str_pos_len (param i32 i32)))
2 (import "env" "buffer"   (memory 1))
;; 30 character string
3 (data (i32.const 256) "Know the length of this string")
;; 35 characters
4 (data (i32.const 384) "Also know the length of this string")

5 (func (export "main")
;; length of the first string is 30 characters
 6 (call $str_pos_len (i32.const 256) (i32.const 30))
;; length of the second string is 35 characters
 7 (call $str_pos_len (i32.const 384) (i32.const 35))
 )
)

列表 5-1:将字符串从 WebAssembly 传递到 JavaScript

本模块导入了我们将创建的一个 JavaScript 函数"str_pos_len" 1,该函数通过结合字符串的位置和它在线性内存中的位置,来在内存缓冲区中查找字符串。我们还需要导入我们将在 JavaScript 中声明的内存缓冲区 2。

接下来,我们在内存中定义了两个字符串:"Know the length of this string" 3 和"Also know the length of this string" 4。这两个字符串表明我们需要知道这些字符串的长度,因为它们仅仅是线性内存中的字符数组,我们需要标明它们的起始和结束位置。第一个字符串有 30 个字符,第二个字符串有 35 个字符。稍后,在"main" 5 函数中,我们调用$str_pos_len两次。第一次 6,我们传入第一个字符串在内存中的位置(i32.const 256),接着是该字符串的长度(i32.const 30)。这告诉我们稍后写的 JavaScript 从内存位置 256 开始提取 30 个字节并将其显示到控制台。第二次调用$str_pos_len 7 时,我们传入第二个字符串在内存中的位置(i32.const 384),接着是该字符串的长度(i32.const 35)。然后,JavaScript 将第二个字符串显示到控制台。使用列表 5-2 中的命令编译 WebAssembly 模块。

wat2wasm strings.wat

列表 5-2:编译strings.wat

编译 WebAssembly 模块后,创建一个名为strings.js的 JavaScript 文件,并输入列表 5-3 中的代码。

strings.js(第一部分,共 3 部分)

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/strings.wasm');

let memory = new WebAssembly.Memory( {initial: 1 });

let importObject = {
  env: {
    buffer: memory,
  1 str_pos_len: function(str_pos, str_len) {
    2 const bytes = new Uint8Array( memory.buffer,
                                    str_pos, str_len );
    3 const log_string = new TextDecoder('utf8').decode(bytes);
      4 console.log(log_string);
    }
  }
};

( async () => {
  let obj = await WebAssembly.instantiate( new Uint8Array(bytes),
                                           importObject );

  let main = obj.instance.exports.main;

  main();
})();

列表 5-3:调用 WebAssembly 字符串函数的 JavaScript 代码

importObject内部,我们定义了str_pos_len 1,它获取字符串在内存中的位置及其长度。它使用长度的位置来检索一个字节数组 2,该数组的长度是指定的。我们使用TextDecoder将字节数组转换为字符串 3。然后我们调用console.log 4 来显示字符串。当你运行 JavaScript 时,你应该能在清单 5-4 中看到这个信息。

Know the length of this string
Also know the length of this string

清单 5-4:字符串长度输出

接下来,我们将讨论空字符终止字符串,这是一种用于跟踪字符串长度的方法,C/C++等语言使用它。

空字符终止字符串

传递字符串的第二种方法是空字符终止(或零终止)字符串。空字符终止是一种由 C 编程语言使用的定义字符串长度的方法。在空字符终止的字符串中,你将值为 0 的字符作为数组中的最后一个字符。空字符终止字符串的优点是,你在使用它时不需要知道字符串的长度。缺点是,这需要在处理字符串时进行更多计算,因为你的程序需要花时间查找终止的空字节。我们来打开strings.wat文件,并添加清单 5-5 中的代码来处理我们的空字符终止字符串。首先,我们需要添加一个null_str函数的导入,这个函数我们稍后将在 JavaScript 中定义。

strings.wat(第二部分,共 11 部分)

(module
1 (import "env" "str_pos_len" (func $str_pos_len (param i32 i32)))
;; add line below
2 (import "env" "null_str" (func $null_str (param i32)))
...

清单 5-5:修改strings.wat(来自清单 5-1)以导入null_str函数

请注意,与str_pos_len 1 不同,null_str 2 函数只需要一个i32参数,因为与它一起工作的代码只需要知道字符串在线性内存中的起始位置。至于代码如何找到空字符的位置并操作它,则由代码自行决定。

接下来,在定义缓冲区的import语句与定义早期字符串的(data)表达式之间,在清单 5-6 中,我们添加了另外两个数据表达式,定义了空字符终止的字符串。

strings.wat(第三部分,共 11 部分)

...
  (import "env" "buffer" (memory 1))
;; add the two lines below
1 (data (i32.const 0) "null-terminating string\00")
2 (data (i32.const 128) "another null-terminating string\00")

  (data (i32.const 256) "Know the length of this string")
...

清单 5-6:修改strings.wat(来自清单 5-1),以添加空字符终止的字符串数据

第一个数据定义了字符串"null terminating string\00"1。请注意最后三个字符\00\字符是 WAT 中的转义字符。如果你在转义字符后面跟上两个十六进制数字,它将定义一个你指定值的数字字节。这意味着\00表示一个值为0的字节。第二个数据表达式创建了字符串"another null terminating string\00" 2,它也是空字符终止的,并以\00结尾。

在清单 5-7 中,我们在main函数的开头添加了两行代码,调用导入的$null_str JavaScript 函数,并将空字符终止字符串在线性内存中的位置传递给它。

strings.wat(第四部分,共 11 部分)

...
(func (export "main")
1 (call $null_str (i32.const 0)) ;; add this line
2 (call $null_str (i32.const 128)) ;; add this line

    (call $str_pos_len (i32.const 256) (i32.const 30))
...

清单 5-7:修改strings.wat(来自清单 5-1)以调用null_str函数

我们传入值0 1,它是我们定义字符串"null terminating string\00"的内存位置。然后,我们传入值128 2,这是我们定义字符串"another null terminating string\00"的内存位置。

一旦你在 WAT 文件中做出这些更改,打开strings.js以添加更多代码。首先,向importObject中嵌套的env对象中添加一个新函数,如列表 5-8 所示。

strings.js(第二部分,共 3 部分)

...
1 const max_mem = 65535;// add this line

  let importObject = {
  env: {
    buffer: memory,
    // add the null_str function to the importObject here
  2 null_str: function(str_pos) **{** // add this function
    3 let bytes = new Uint8Array( memory.buffer,
                                  str_pos, max_mem-str_pos );

    4 let log_string = new TextDecoder('utf8').decode(bytes);
    5 log_string = log_string.split("\0")[0];
    6 console.log(log_string);
    }, // end of function
    str_pos_len: function(str_pos, str_len) {
...

列表 5-8:从列表 5-3 开始,添加到importObject中的null_str函数,在strings.js

这段代码首先通过定义变量max_mem 1 来设置字符串的最大可能长度。为了找到以空字符终止的字符串,我们将一块线性内存(最大字符串长度)解码为一个长字符串,然后使用 JavaScript 的 split 函数来获取空字符终止的字符串。在env对象中,我们添加了一个名为null_str 2 的函数,它接受一个str_pos参数。然后,JavaScript 需要从内存缓冲区中提取字节数组,起始位置由传入函数的str_pos参数指定。在将内存缓冲区转化为字符串之前,我们不能直接在其上进行搜索。转换为字符串之前,首先需要将其转换为字节数组 3。然后,我们创建一个TextDecoder对象,将这些字节解码为一个长字符串 4。

我们使用空字节"\0" 5 将字符串拆分为一个数组。以空字节拆分会创建一个以空字节终止的字符串数组。数组中的第一个元素才是我们定义的实际字符串。我们使用 split 作为一种简单的快速方法,将字符串从线性内存中提取出来。然后我们将log_string设置为数组中的第一个字符串。我们调用 JavaScript 的console.log函数 6,并将log_string传递给它,以便将该字符串显示到控制台。因为我们使用了来自 WebAssembly 的两个不同的字符串,所以我们应该能在控制台上看到列表 5-9 中的四条消息。

null-terminating string
another null-terminating string
Know the length of this string
Also know the length of this string

列表 5-9:空字符终止字符串的输出

长度前缀字符串

存储字符串的第三种方法是将字符串的长度放在字符串数据的开头。用这种方法创建的字符串称为长度前缀字符串,它可以提高处理性能。我们当前的前缀方式将字符串限制为最大长度 255,因为一个字节的数据只能容纳 0 到 255 之间的数字。

我们先从修改当前的strings.wat文件开始,如列表 5-10 所示,添加一行新的导入语句,用于导入稍后在 JavaScript 中定义的len_prefix函数。

strings.wat(第五部分,共 11 部分)

(module
  (import "env" "str_pos_len" (func $str_pos_len (param i32 i32)))
  (import "env" "null_str" (func $null_str (param i32)))
 ;; add the line below
1(import "env" "len_prefix" (func $len_prefix (param i32)))
...

列表 5-10:修改strings.wat,从列表 5-1 开始,添加len_prefix函数导入

len_prefix 1 函数将读取字符串的第一个字节来找出长度。

接下来,我们添加两个新的字符串,它们以十六进制数表示其长度。将列表 5-11 中的代码添加到 strings.wat

strings.wat(第六部分,共 11 部分)

...
  (data (i32.const 384) "Also know the length of this string")

  ;; add the next four lines.  Two data elements and two comments
 ;; length is 22 in decimal, which is 16 in hex
1 (data (i32.const 512) "\16length-prefixed string")
 ;; length is 30 in decimal, which is 1e in hex
2 (data (i32.const 640) "\1eanother length-prefixed string")

 (func (export "main")
...

列表 5-11:修改 strings.wat,参照列表 5-1,以添加长度前缀的字符串数据

第一个字符串,"\16length-prefixed string",包含 22 个字符,因此我们用 \16 作为前缀,因为 22 的十进制数在十六进制中是 16。第二个字符串,"\1eanother length-prefixed string",包含 30 个字符,因此我们用十六进制的 \1e 作为前缀。

接下来,我们需要为刚刚创建的两个字符串所在的内存位置添加两次调用 len_prefix 函数。"main" 函数现在应该像列表 5-12 中的代码那样。

strings.wat(第七部分,共 11 部分)

...
  (func (export "main")
    (call $null_str (i32.const 0))
    (call $null_str (i32.const 128))

    (call $str_pos_len (i32.const 256) (i32.const 30))
    (call $str_pos_len (i32.const 384) (i32.const 35))

  1 (call $len_prefix (i32.const 512))    ;; add this line
  2 (call $len_prefix (i32.const 640))    ;; add this line

 )
...

列表 5-12:修改 strings.wat,参照列表 5-1,以添加对 $len_prefix 函数的调用

第一次调用 $len_prefix 1 时,将数据字符串 "\16length-prefixed string" 的内存位置 512 传入。第二次调用 2 时,将第二个长度前缀字符串 "\1eanother length-prefixed string" 的内存位置 640 传入。

在运行之前,我们需要为我们的 JavaScript importObject 添加一个新函数。打开 strings.js,并将 len_prefix 函数添加到 importObject,如列表 5-13 所示。

strings.js(第三部分,共 3 部分)

...
let importObject = {
  env: {
    buffer: memory,
    null_str: function (str_pos) {
      let bytes = new Uint8Array(memory.buffer, str_pos,
                                 max_mem - str_pos);

      let log_string = new TextDecoder('utf8').decode(bytes);
      log_string = log_string.split("\0")[0];
      console.log(log_string);
    }, // end null_str function 
    str_pos_len: function (str_pos, str_len) {
      const bytes = new Uint8Array(memory.buffer,
        str_pos, str_len);
      const log_string = new TextDecoder('utf8').decode(bytes);
      console.log(log_string);
 },
1 len_prefix: function (str_pos) {
    2 const str_len = new Uint8Array(memory.buffer, str_pos, 1)[0];
    3 const bytes = new Uint8Array(memory.buffer,
                                   str_pos + 1, str_len);
    4 const log_string = new TextDecoder('utf8').decode(bytes);
      console.log(log_string);
    }
  }
};
...

列表 5-13:将 len_prefix 函数添加到 strings.js 中的 importObject,参照列表 5-3

新的 len_prefix 1 函数接受一个字符串位置,然后从该位置取出第一个字节,作为常量 str_len 2 中的数字。它使用 str_len 中的值,从线性内存中复制适当数量的 bytes 3,以便将其解码为 log_string 4,然后记录到控制台中。

现在我们有了 WAT 和 JavaScript,可以使用 wat2wasm 编译 WAT 模块,如列表 5-14 所示。

wat2wasm strings.wat

列表 5-14:编译 strings.wat

然后我们可以使用 node 运行我们的 JavaScript 文件,如列表 5-15 所示。

node strings.js

列表 5-15:使用 node 运行 strings.js

你应该会看到列表 5-16 中的输出。

null-terminating string
another null-terminating string
Know the length of this string
Also know the length of this string
length-prefixed string
another length-prefixed string

列表 5-16:strings.js 的输出

在接下来的章节中,你将学习如何使用 WAT 复制字符串。

复制字符串

复制字符串从线性内存中的一个位置到另一个位置的最简单方法是逐字节循环,加载每个字节的数据,然后将其存储到新位置。然而,这种方法比较慢。一种更高效的方法是使用 64 位整数加载和存储,每次复制八个字节。不幸的是,并非所有字符串的长度都是八个字节的倍数。为了尽可能高效地处理所有情况,我们需要结合逐字节复制和更快速的 64 位复制方法。

字节逐一复制

我们将首先编写一个做较慢逐字节复制的函数:它接收源内存位置、目标内存位置以及要复制的字符串长度作为参数。

让我们继续向strings.wat文件中添加代码。在列表 5-17 中,我们将函数$byte_copy添加到strings.wat文件中。

strings.wat(第八部分,共 11 部分)

...
(func $byte_copy
  (param $source i32) (param $dest i32) (param $len i32)
  (local $last_source_byte i32)

1 local.get $source
  local.get $len
2 i32.add   ;; $source + $len

  local.set $last_source_byte         ;; $last_source_byte = $source + $len

  (loop $copy_loop (block $break
  3 local.get $dest    ;; push $dest on stack for use in i32.store8 call
  4 (i32.load8_u (local.get $source)) ;; load a single byte from $source
  5 i32.store8                        ;; store a single byte in $dest

  6 local.get $dest
    i32.const 1
    i32.add
  7 local.set $dest                   ;; $dest = $dest + 1

    local.get $source
    i32.const 1
    i32.add
  8 local.tee $source                 ;; $source = $source + 1

    local.get $last_source_byte
    i32.eq
    br_if $break
    br $copy_loop
  )) ;; end $copy_loop
)
...

列表 5-17:将逐字节复制字符串的慢速方法添加到strings.wat中(见列表 5-1)

这个$byte_copy函数将从$source 1 到 $source + $len 2 的内存块复制到内存位置$dest 3 到 $dest + len,每次复制一个字节。这个循环使用表达式(i32.load8_u``)$source加载一个字节 4。然后,它使用命令i32.store8将该字节存储到$dest位置 5。接着,我们在$dest变量 7 中递增目标位置 6,并且将$source 8 变量递增,使这两个变量指向内存中的下一个字节。

64 位复制

逐字节复制字符串比实际需要的慢,而 64 位整数有 8 字节长,一次复制 8 字节比逐字节复制要显著更快。我们将编写另一个函数,类似于$byte_copy,通过一次复制 8 字节来显著加快数据复制速度。不幸的是,并不是所有的字符串长度都是 8 的倍数。如果字符串的长度为 43 个字符,我们可以通过五次 8 字节的复制来复制前 40 个字节,但对于最后的 3 个字节,我们仍需要回到逐字节复制的方法。

需要注意的是,这些字节复制不会阻止越界内存访问。代码将尝试从不该读取或写入的地方复制数据。然而,如果你尝试访问线性内存之外的数据,WebAssembly 的安全模型会导致读取或写入失败,从而停止代码执行。如前所述,这些函数并非用于通用目的,而是用来演示不同的字符串复制方式。

将 64 位复制函数$byte_copy_i64添加到strings.wat文件中(见列表 5-18)。

strings.wat(第九部分,共 11 部分)

...
;; add this block of code to the `strings.wat` file
(func $byte_copy_i64
  (param $source i32) (param $dest i32) (param $len i32)
  (local $last_source_byte i32)

  local.get $source
  local.get $len
  i32.add

  local.set $last_source_byte

**(loop $copy_loop (block $break**
  1 (i64.store (local.get $dest) (i64.load (local.get $source)))

local.get $dest
  2 i32.const 8
    i32.add
    local.set $dest;; $dest = $dest + 8

local.get $source
  3 i32.const 8
    i32.add
    local.tee $source;; $source = $source + 8

    local.get $last_source_byte
    i32.ge_u
 br_if $break
    br $copy_loop
  ));; end $copy_loop
)
...

列表 5-18:将更快的复制字符串方法添加到strings.wat中(见列表 5-2)

加载和存储函数分别是(i64.load``)(i64.store``)1,它们每次加载和存储 64 位(8 字节)数据。这种方法比逐字节加载和存储单个字节快四到五倍(在 x64 架构上)。另一个显著的区别是,$dest 2 和 $source 3 每次增加8,而不是1

组合复制函数

如前所述,并非所有字符串的长度都是 8 的倍数。因此,我们将在列表 5-19 中定义一个新的改进函数,该函数使用$byte_copy_i64函数一次复制 8 字节,然后使用$byte_copy函数复制剩余的字节,后者一次复制一个字节。

strings.wat (第十部分,共 11 部分)

...
(func $string_copy
  (param $source i32) (param $dest i32) (param $len i32)
  (local $start_source_byte i32)
  (local $start_dest_byte   i32)
  (local $singles           i32)
  (local $len_less_singles  i32)

  local.get $len
1 local.set $len_less_singles  ;; value without singles

  local.get $len
  i32.const 7                  ;; 7 = 0111 in binary
2 i32.and
  local.tee $singles           ;; set $singles to last 3 bits of length

3 if                           ;; if the last 3 bits of $len is not 000
    local.get $len
    local.get $singles
    i32.sub
  4 local.tee $len_less_singles  ;; $len_less_singles = $len - $singles

    local.get $source
    i32.add
    ;; $start_source_byte=$source+$len_less_singles
  5 local.set $start_source_byte

    local.get $len_less_singles
    local.get $dest
    i32.add
  6 local.set $start_dest_byte  ;; $start_dest_byte=$dest+$len_less_singles

  7 (call $byte_copy (local.get $start_source_byte)
 (local.get $start_dest_byte)(local.get $singles))
  end

  local.get $len
8 i32.const 0xff_ff_ff_f8 ;; all bits are 1 except the last three which are 0
9 i32.and                 ;; set the last three bits of the length to 0
  local.set $len
a (call $byte_copy_i64 (local.get $source) (local.get $dest) (local.get $len))
)
...

列表 5-19:在可能的情况下每次复制八个字节,否则每次复制一个字节。

如前所述,$string_copy 函数必须结合八字节和单字节复制函数,以尽可能快速地复制字符串。$len 参数是字符串的总长度(以字节为单位)。局部变量 $len_less_singles 1 是可以用 64 位复制复制的字节数。我们通过屏蔽掉最后三位来获取这个数值。$singles 变量是剩余的三位,不在八的倍数范围内,通过执行按位 (i32.and) 表达式 2(为位掩码欢呼)在 $len7(二进制 111)之间设置。长度的最后三位表示如果我们大多数字节使用八字节复制,剩余字节的数量。举个例子,对 $len 为 190 和 7 使用 i32.and 表达式,结果如图 Figure 5-1 所示。

f05001

图 5-1:使用二进制与运算(AND)屏蔽掉除了最后三位之外的所有位

如你所见,调用 i32.and 并传入 190 和 7 的值,结果为二进制 110,即十进制 6。i32.and 表达式将 $len 参数的最后三位之外的所有位都设置为 0。

如果 $singles 的值不为零 3,代码首先复制那些无法使用 64 位复制的单独字节。if 块将 $len_less_singles 4 设置为 $len - $singles:即必须单独复制的字节数。局部变量 $start_source_byte 5 被设置为 $source+$len_less_singles,将其设置为逐字节复制的起始字节 7。

然后,变量 $start_dest_byte 6 被设置为 $dest+$len_less_singles,它设置为逐字节复制的目标位置。接下来,分支调用 $byte_copy 来复制这些剩余的单字节。

if 块之后,代码必须使用 64 位复制函数 (call $byte_copy_i64)a 复制它可以复制的字节。我们通过使用按位 (i32.and) 9 表达式,将 $len 中的长度与 32 位常量值 0xff_ff_ff_f8 8 进行运算,来确定要复制的字节数。值 0xff_ff_ff_f8 的二进制表示是除最后三位外全为 1,最后三位为 0。使用按位与运算将长度的最后三位清零,从而使长度成为八的倍数。

现在我们已经有了字符串复制函数,接下来让我们修改 main 函数来测试它。将 main 函数修改为只包含 列表 5-20 中的代码。

strings.wat (第十一部分,共 11 部分)

...
(func (export "main")
  1 (call $str_pos_len (i32.const 256) (i32.const 30))
  2 (call $str_pos_len (i32.const 384) (i32.const 35))

  3 (call $string_copy
        (i32.const 256) (i32.const 384) (i32.const 30))

  4 (call $str_pos_len (i32.const 384) (i32.const 35))
  5 (call $str_pos_len (i32.const 384) (i32.const 30))
  )

列表 5-20:strings.watmain 函数的新版本(列表 5-2)

我们删除了打印空终止字符串和长度前缀字符串到控制台的代码。我们保留了打印位于线性内存地址256的字符串 1,"Know the length of this string",以及位于内存地址384的字符串 2,"Also know the length of this string"。保留这些行会在复制之前将原始字符串值打印到控制台。

调用$string_copy 3 将 30 个字节从第一个字符串复制到第二个字符串。然后我们打印第二个字符串的位置及其原始字符串长度。这样会将"Know the length of this stringtring" 4 打印到控制台,看起来是错误的,因为它以stringtring这个词结尾。最后一个词没有以string结尾并且多出了五个字符的原因是我们需要将长度更改为我们复制的字符串的长度。如果我们复制的是空终止字符串或长度前缀字符串,这不会是问题,因为空字节或前缀会帮助我们追踪长度:但在这种情况下,我们需要知道新长度是30

当我们调用$str_pos_len,传入384作为索引和30作为长度 5 时,它会正确地将"Know the length of this string"打印到控制台。我们可以使用清单 5-21 中的命令重新编译strings.wat

wat2wasm strings.wat

清单 5-21:编译strings.wat

从命令行运行strings.js以查看清单 5-22 中的输出。

Know the length of this string
Also know the length of this string
Know the length of this stringtring
Know the length of this string

清单 5-22:在添加对$string_copy的调用后,strings.js的输出

In the next section, you’ll learn how to turn numbers into strings. ### Creating Number Strings When you’re working with strings, converting numeric data into string data is frequently required. High-level languages like JavaScript have functions that can do this for you, but in WAT you’ll need to build your own functions. Let’s look at what it takes to create strings from numbers in decimal, hexadecimal, and binary. Create a WAT file named *number_string.wat* and add the code in Listing 5-23 to the beginning of the file. **number_string.wat (part 1 of 7)** ``` (module 1 (import "env" "print_string" (func $print_string (param i32 i32))) 2 (import "env" "buffer" (memory 1)) 3 (data (i32.const 128) "0123456789ABCDEF") 4 (data (i32.const 256) " 0") 5 (global $dec_string_len i32 (i32.const 16)) ... ``` Listing 5-23: Imported objects and data in the WebAssembly module The beginning of this module imports a `$print_string` 1 function and one page of linear memory 2 from JavaScript. Next, we define a `data` 3 element with an array of characters that contains every hexadecimal character. Then we define a data element that will hold our string `data` 4, followed by the length of that `data` string 5. In the next few listings, we define three functions that create number strings in three different formats. We use the first of these functions, `$set_dec_string`, to set the `$dec_string` linear memory area. Listing 5-24 contains the code that turns an integer into a decimal string. The code can be a bit challenging to follow, so I’ll give you an overview before showing the code. At a high level, when we assemble a string from a number, we need to look at the number one digit at a time and add the character form of that digit to our string. Let’s say the number we’re looking at is 9876\. In the 1s place is the digit 6\. We can find this digit by dividing the full number by 10 and using the remainder (called a *modulo*). In WAT, that code would be the type `i32.rem_u` (`rem` for remainder). Next, you use the character form of the number 6 and add it to your string. Other digits need to move along as if on a conveyor belt (illustrated in Figure 5-2). The way you do this in code is to divide by 10\. Because this divide is on an integer, you don’t get a fractional number, and the 6 is simply thrown away. You then use the remainder to get the next digit (7) and add that to the string. You continue on until all the digits are gone. Listing 5-24 shows the source code for the `$set_dec_string` function. **number_string.wat (part 2 of 7)** ``` ... (func $set_dec_string (param $num i32) (param $string_len i32) (local $index i32) (local $digit_char i32) (local $digit_val i32) local.get $string_len 1 local.set $index ;; set $index to the string length local.get $num i32.eqz ;; is $num is equal to zero if ;; if the number is 0, I don't want all spaces local.get $index i32.const 1 i32.sub local.set $index ;; $index-- ;; store ascii '0' to memory location 256 + $index (i32.store8 offset=256 (local.get $index) (i32.const 48)) end (loop $digit_loop (block $break ;; loop converts number to a string local.get $index ;; set $index to end of string, decrement to 0 i32.eqz ;; is the $index 0? br_if $break ;; if so break out of loop local.get $num i32.const 10 2 i32.rem_u ;; decimal digit is remainder of divide by 10 3 local.set $digit_val ;; replaces call above local.get $num i32.eqz ;; check to see if the $num is now 0 if i32.const 32 ;; 32 is ascii space character local.set $digit_char ;; if $num is 0, left pad spaces else 4 (i32.load8_u offset=128 (local.get $digit_val)) local.set $digit_char ;; set $digit_char to ascii digit end local.get $index i32.const 1 i32.sub local.set $index ;; store ascii digit in 256 + $index 5 (i32.store8 offset=256 (local.get $index) (local.get $digit_char)) local.get $num i32.const 10 i32.div_u 6 local.set $num ;; remove last decimal digit, dividing by 10 br $digit_loop ;; loop )) ;; end of $block and $loop ) ``` Listing 5-24: A WebAssembly function that creates a decimal string from an integer We start the function with `$index` 1, a variable that points to the last byte in `$dec_string`. We set the values of this string from right to left, so the `$index` variable needs to be decremented every pass through the loop. Each pass through the loop, the number value set in `$dec_string` is the final base-10 digit. To get this value, we divide the `$num` 6 value by `10` and get the remainder 2 with modulo 10\. This value is stored in the local variable `$digit_val` 3 so we can later use it to set an ASCII character in the `$dec_string` data. We use `$digit_val` as an offset into the `$digit_char` string to load a character with `i32.load8_u` 4. That character is then written to an address that is `$dec_string+$index` using `i32.store8` 5. Figure 5-2 illustrates the process. ![f05002](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f05002.png) Figure 5-2: Look at digits one at a time and append characters one at a time. Now that we have the function that does most of the work in our WebAssembly module, let’s add a function to export to the JavaScript, as shown in Listing 5-25. **number_string.wat (part 3 of 7)** ``` ... (func (export "to_string") (param $num i32) 1 (call $set_dec_string (local.get $num) (global.get $dec_string_len)) 2 (call $print_string (i32.const 256) (global.get $dec_string_len)) ) ) ``` Listing 5-25: The `to_string` function The function is very simple. It calls `$set_dec_string` 1 passing in the number we want to convert to a string and the length of the string we want including the left padding. It then calls the JavaScript `print_string` 2 function passing in the location of the string we created in linear memory `(i32.const` `256)` and the length of that string. Now that we’ve completed the *number_string.wat* file, we can compile it using `wat2wasm` in Listing 5-26. ``` wat2wasm number_string.wat ``` Listing 5-26: Compiling *number_string.wat* Next, we need to write the JavaScript that will run our WebAssembly module. Create a file named *number_string.js* and add the code in Listing 5-27. **number_string.js** ``` const fs = require('fs'); const bytes = fs.readFileSync(__dirname + '/number_string.wasm'); 1 const value = parseInt(process.argv[2]); let memory = new WebAssembly.Memory({ initial: 1 }); (async () => { const obj = await WebAssembly.instantiate(new Uint8Array(bytes), { env: { buffer: memory, 2 print_string: function (str_pos, str_len) { const bytes = new Uint8Array(memory.buffer, str_pos, str_len); const log_string = new TextDecoder('utf8').decode(bytes); // log_string is left padded. 3 console.log(`>${log_string}!`); } } }); obj.instance.exports.to_string(value); })(); ``` Listing 5-27: The JavaScript that calls the WebAssembly module to convert the number to a string The JavaScript code loads the WebAssembly module and takes an additional argument 1 that we’ll convert into a number string and left pads the string up to 16 characters. The WebAssembly module will call the `print_string` 2JavaScript function, which writes the string to the console, appending a `>` character to the beginning of the string and a `!` character to the end. We place these extra characters into the `console.log` 3 output to show where the string coming from the WebAssembly module begins and ends. You can run the JavaScript using the `node` command in Listing 5-28. ``` node number_string.js 1229 ``` Listing 5-28: Use `node` to call the *number_string.js* file, passing in `1229`. The result is that the number `1229` is converted to a string, and the output in Listing 5-29 is logged to the console. ``` > 1229! ``` Listing 5-29: The number `1229` is left padded to 16 characters, beginning with `>` and ending with `!`. In the next section, we’ll use similar techniques to create a hexadecimal string. ### Setting a Hexadecimal String Converting an integer to a hexadecimal string is very similar to converting an integer to a decimal number, as we did in the preceding section. We use a bit mask to look at specific hex digits and a shift to remove the digit, similar to our decimal conveyor belt (see Figure 5-2). Recall from Chapter 4 that four bits of data is called a nibble and a hexadecimal digit corresponds to one nibble of data. At a high level, the code needs to look at the integer one nibble at a time as one hexadecimal digit. We look at the lowest order nibble, also the lowest order hex digit, and then add that digit to the string. Rather than finding the remainder, we use a mask to only look at the last digit. Instead of dividing by 10, we remove the last hex digit by shifting off four bits (one nibble). In hexadecimal, each digit represents a number from 0 to 15 instead of 0 to 9, so each digit in hex must be one of the following: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F where the value of A = 10, B = 11, C = 12, D = 13, E = 14, and F = 15\. We often use hexadecimal as an alternative to binary numbers, which can get extremely long to represent; for example, the number 233 in decimal is 11101001 in binary. We can shorten 233 in the binary form into the hexadecimal E9 because 1110 is 14 or E in hex, and 1001 is 9 in both decimal and hex. We mask E9 (binary 1110 1001) using `(i32.and``)` with a value of binary 0000 1111 (0F) to find the least significant nibble. Using `i32.and` in that way results in E9 masked into 09, as shown in Figure 5-3. ![f05003](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f05003.png) Figure 5-3: E9 byte masked to 09 with a mask of 0F (0000 1111) We use a combination of a bit shift and an AND mask to convert the integer data into a hexadecimal string. Here, we create a hexadecimal version of the `$set_dec_string` function called `$set_hex_string`. This function sets a hexadecimal string based on a number passed into it. We can make this loop simpler than the loop in `$set_dec_string` because we can use simple bit manipulation to find the offset into `$digits`. The end of the function adds the extra ASCII characters `0x` to indicate that the display string is in a hexadecimal format. Listing 5-30 shows what the `$set_hex_string` function looks like. **number_string.wat (part 4 of 7)** ``` ... ;; add this code before the $set_dec_string function (global $hex_string_len i32 (i32.const 16)) ;; hex character count (data (i32.const 384) " 0x0") ;; hex string data (func $set_hex_string (param $num i32) (param $string_len i32) (local $index i32) (local $digit_char i32) (local $digit_val i32) (local $x_pos i32) global.get $hex_string_len local.set $index ;; set the index to the number of hex characters (loop $digit_loop (block $break local.get $index i32.eqz br_if $break local.get $num i32.const 0xf ;; last 4 bits are 1 1 i32.and ;; the offset into $digits is in the last 4 bits of number 2 local.set $digit_val ;; the digit value is the last 4 bits local.get $num i32.eqz 3 if ;; if $num == 0 local.get $x_pos i32.eqz if local.get $index 4 local.set $x_pos ;; position of 'x' in the "0x" hex prefix else i32.const 32 ;; 32 is ascii space character local.set $digit_char end else ;; load character from 128 + $digit_val 5 (i32.load8_u offset=128 (local.get $digit_val)) local.set $digit_char end local.get $index i32.const 1 i32.sub 6 local.tee $index ;; $index = $index - 1 local.get $digit_char ;; store $digit_char at location 384+$index 7 i32.store8 offset=384 local.get $num i32.const 4 8 i32.shr_u ;; shifts 1 hexadecimal digit off $num local.set $num br $digit_loop )) local.get $x_pos i32.const 1 i32.sub i32.const 120 ;; ascii x 9 i32.store8 offset=384 ;; store 'x' in string local.get $x_pos i32.const 2 i32.sub i32.const 48 ;; ascii '0' a i32.store8 offset=384 ;; store "0x" at front of string ) ;; end $set_hex_string ... ``` Listing 5-30: Create a hexadecimal string from an integer. Add this before the `$set_dec_string` function. In the `$set_dec_string` function, we use a modulo 10 to find each digit, and then shift that digit off by dividing it by 10\. Instead of finding a remainder, the `$set_hex_string` function can use `i32.and` 1 to mask all but the last four bits of `$num`. The value of that nibble is a single hexadecimal digit and is used to set `$digit_val` 2. If all remaining digits are `0` 3, we set the position to put the hexadecimal string prefix of `0x` in the local variable `$x_pos` 4. Otherwise, if any remaining digits are `1` or greater, we use the value in `$digit_val` to load 5 an ASCII value for that hexadecimal digit from the `$digits` string and store it into `$digit_char`. Then we decrement `$offset` 6 and use that value to store the character in `$digit_char` into the `$hex_string` data 7. The loop then shifts off one hexadecimal digit (four bits) using `i32.shr_u` 8, which shifts bits to the right. The last task this function does is append the `0x` prefix to the string by using the value we set earlier in `$x_pos` as an offset and storing an ASCII `x` character in that position 9. It then decrements the `$x_pos` position and stores the ASCII `0` a. The process looks a bit like Figure 5-4. ![f05004](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f05004.png) Figure 5-4: Creating a hexadecimal string from an integer After adding the `$set_hex_string` function, we need to update the `to_string` function to call `$set_hex_string` and print the resulting string to the console. Update the `to_string` function to look like the code in Listing 5-31. **number_string.wat (part 5 of 7)** ``` ... (func (export "to_string") (param $num i32) (call $set_dec_string (local.get $num) (global.get $dec_string_len)) (call $print_string (i32.const 256) (global.get $dec_string_len)) 1 (call $set_hex_string (local.get $num) (global.get $hex_string_len)) 2 (call $print_string (i32.const 384) (global.get $hex_string_len)) ) ) ``` Listing 5-31: Update to the `to_string` function calling `$set_hex_string` and `$print_string` These two new statements call `$set_hex_string` 1 to set the hexadecimal string in linear memory. We then call `$print_string` 2 passing in the memory location of the hexadecimal string `(i32.const` `384)` and the length of the string. No changes to the JavaScript file are necessary. All we need to do is recompile our WAT file, as shown in Listing 5-32. ``` wat2wasm number_string.wat ``` Listing 5-32: Compiling *number_string.wat* with `wat2wasm` Once you’ve recompiled the WebAssembly module, you can run the *number_string.js* file using `node`, as shown in Listing 5-33. ``` node number_string.js 2049 ``` Listing 5-33: Running *number_string.js* passing in the value `2049` Listing 5-34 shows the output. ``` > 2049! > 0x801! ``` Listing 5-34: The second line is the output of the hexadecimal string conversion. In the next section, we’ll add a function to generate a string of binary digits from a 32-bit integer. ### Setting a Binary String The final format we’ll cover is converting an integer to a string that represents the binary data. It’s best to get an intuitive sense for binary numbers when you’re working with low-level code. Having a better conceptual grasp on the numbers can sometimes help you create improvements in your code’s performance by using bit manipulation as an alternative to decimal math. Computers work with binary, even if your code is working with decimal. Understanding what the computer is doing is often helpful when you’re trying to improve your code’s performance. We’ll create the `$set_bin_string` function, which uses a double loop to separate every 4-bit nibble with a space character to make it more readable. We’ll use `(i32.and``)` against the number `1` to see whether the last bit is a `1` or a `0`, and then shift a single bit off the number every pass through the inner loop of the function. Listing 5-35 shows what the code looks like. **number_string.wat (part 6 of 7)** ``` ... ;; add this code before the $set_hex_string function (global $bin_string_len i32 (i32.const 40)) (data (i32.const 512) " 0000 0000 0000 0000 0000 0000 0000 0000") (func $set_bin_string (param $num i32) (param $string_len i32) (local $index i32) (local $loops_remaining i32) (local $nibble_bits i32) global.get $bin_string_len local.set $index 1 i32.const 8 ;; there are 8 nibbles in 32 bits (32/4 = 8) local.set $loops_remaining ;; outer loop separates nibbles 2 (loop $bin_loop (block $outer_break ;; outer loop for spaces local.get $index i32.eqz br_if $outer_break ;; stop looping when $index is 0 i32.const 4 3 local.set $nibble_bits ;; 4 bits in each nibble 4 (loop $nibble_loop (block $nibble_break ;; inner loop for digits local.get $index i32.const 1 i32.sub local.set $index ;; decrement $index local.get $num i32.const 1 5 i32.and ;; i32.and 1 results in 1 if last bit is 1 else 0 if ;; if the last bit is a 1 local.get $index i32.const 49 ;; ascii '1' is 49 6 i32.store8 offset=512 ;; store '1' at 512 + $index else ;; else executes if last bit was 0 local.get $index i32.const 48 ;; ascii '0' is 48 7 i32.store8 offset=512 ;; store '0' at 512 + $index end local.get $num i32.const 1 8 i32.shr_u ;; $num shifted right 1 bit local.set $num ;; shift off the last bit of $num local.get $nibble_bits i32.const 1 i32.sub local.tee $nibble_bits ;; decrement $nibble_bits i32.eqz ;; $nibble_bits == 0 9 br_if $nibble_break ;; break when $nibble_bits == 0 br $nibble_loop )) ;; end $nibble_loop local.get $index i32.const 1 i32.sub local.tee $index ;; decrement $index i32.const 32 ;; ascii space a i32.store8 offset=512 ;; store ascii space at 512+$index br $bin_loop )) ;; end $bin_loop ) ... ``` Listing 5-35: Create a binary string from an integer. Add this code before the `$set_hex_string` function. The `$set_bin_string` function has two loops. The outer loop puts a space between each of the nibbles. Because we are working with 32-bit numbers in this code, there are eight nibbles 1 that we need to loop over. We label the outer loop `$bin_loop` 2, and the block we break from is called `$outer_break` 2. Before the inner loop, we need to set the local variable `$nibble_bits` to `4` 3. The code loops over four bits for each nibble in the inner loop. Inside the inner loop `$nibble_loop` 4, we place a `$nibble_block` block that can break out of the inner loop 4. Inside `$nibble_loop`, we use an `(i32.and``)` 5 expression along with an `if`/`else` statement to determine whether the last bit in the `$num` variable is `1` or `0`. If it’s `1`, we use an `i32.store8` 6 statement to store an ASCII `1` in the linear memory address `$index` + `512`. If it isn’t `1`, we store an ASCII `0` in that location 7. Next, we need to shift that bit off for the next pass through the loop. As we did in the `$set_hex_string` function, we’re using a `(i32.shr_u``)` 8 expression for this shifting, but this time we’re shifting off a single bit instead of four bits. After looping through the `$nibble` loop four times, we break 9 out of it and store an ASCII space character at the linear memory position `$offset` + `$bin_string` a. Now we can call `$set_bin_string` to print the value of our string from the `to_string` function. Update the `to_string` function with the code in Listing 5-36. **number_string.wat (part 7 of 7)** ``` ... (func (export "to_string") (param $num i32) (call $set_dec_string (local.get $num) (global.get $dec_string_len)) (call $print_string (i32.const 256) (global.get $dec_string_len)) (call $set_hex_string (local.get $num) (global.get $hex_string_len)) (call $print_string (i32.const 384) (global.get $hex_string_len)) 1 (call $set_bin_string (local.get $num) (global.get $bin_string_len)) 2 (call $print_string (i32.const 512) (global.get $bin_string_len)) ) ) ``` Listing 5-36: Adding `$set_bin_string` to the `to_string` function The first of the two `call` statements we just added (`call $set_bin_string` 1) sets the binary string using the function defined in Listing 5-35. The second `call` statement (`call $print_string` 2) prints the binary string to the console. Now let’s recompile the WebAssembly module, as shown in Listing 5-37. ``` wat2wasm number_string.wat ``` Listing 5-37: Recompiling *number_string.wat* with the binary string function We can now run the *number_string.js* file using the `node` command in Listing 5-38. ``` node number_string.js 4103 ``` Listing 5-38: Running *number_string.js* with the binary string WebAssembly module The output in Listing 5-39 will be logged to the console. ``` > 4103! > 0x1007! > 0000 0000 0000 0000 0001 0000 0000 0111! ``` Listing 5-39: The binary string logged to the console ## Summary This chapter focused on how to work with strings in WAT. You learned about the ASCII and Unicode character formats. You stored string objects in linear memory and learned how to use JavaScript to retrieve the strings and output them to the command line. We covered how to pass string data from WebAssembly to JavaScript and examined two popular methods for string management, null-terminated strings and length-prefixed strings. You copied a string from one location in linear memory to another, using a byte-by-byte copy and a 64-bit copy. Then we explored how to convert integer data into number strings in decimal, hexadecimal, and binary format. In the next chapter, we’ll focus on using linear memory in WAT.

第六章:线性内存

在本章中,我们将探讨线性内存是什么,如何使用它在 JavaScript 和 WebAssembly 代码之间共享数据,以及如何从 JavaScript 中创建它。我们还将从 WebAssembly 中更新线性内存,然后在 JavaScript 代码中使用这些更新。

在计算机游戏中,一个常见的任务是碰撞检测,即检测并适当地响应两个物体接触的情况。随着物体数量的增加,所需的计算量会呈指数增长。碰撞检测是构建 WebAssembly 的一个理想候选任务。在本章中,我们将创建一个在 JavaScript 中随机定义的圆形列表,并将该圆形数据添加到 WebAssembly 的线性内存中。然后,我们将使用这些数据来判断这些圆形是否发生碰撞。

WebAssembly 在处理大量需要大量计算的数据时表现得最好,并且可以让它运行。WebAssembly 模块比 JavaScript 更快地执行数学运算。然而,每次 JavaScript 和 WebAssembly 模块之间的交互都有一定的成本。您可以使用线性内存在 JavaScript 中加载大量数据,然后在 WebAssembly 模块内进行处理。

WebAssembly 中的线性内存

线性内存充当一个巨大的数据数组,可以在 WebAssembly 和 JavaScript 之间共享。如果你熟悉低级编程,线性内存类似于本地应用程序中的堆内存。如果你熟悉 JavaScript,可以把它看作一个巨大的ArrayBuffer对象。像 C 和 C++ 这样的语言通过在计算机的栈上分配内存来创建局部变量。栈分配的局部变量在函数执行完毕后立即从内存中释放。这一高效的过程意味着在栈上分配和释放数据就像是递增和递减栈指针一样简单。您的应用程序只需递增栈指针,瞧,您就得到了一个新的已分配变量,如图 6-1 所示。

f06001

图 6-1:栈指针

栈非常适合用于局部变量。然而,WAT 中的一个限制是,使用栈的局部变量只能是四种类型之一,且它们都属于数值类型。有时,您可能需要更复杂的数据结构,例如字符串、结构体、数组和对象。

分配命令,例如 C 中的malloc和 C++、JavaScript 中的new,将内存分配到堆上,这些语言的内存管理库必须寻找一个足够大的空闲内存区域来容纳所需的内存块。随着时间的推移,这可能导致内存碎片化,即已分配的内存块之间被未分配的内存分隔开,如图 6-2 所示。

WebAssembly 线性内存以大块叫做页面的形式分配,一旦分配后就无法释放。WebAssembly 内存的管理方式也有点像汇编语言的内存管理:一旦您为 WebAssembly 模块分配了所选数量的页面,您作为程序员必须跟踪您在使用内存时的用途以及它的位置。在接下来的几节中,我们将更仔细地研究如何通过探索内存页面来使用线性内存。

f06002

图 6-2:JavaScript 和 WebAssembly 传递的线性内存数据

页面

页面是 WebAssembly 模块可以分配的最小数据块。在本文写作时,所有 WebAssembly 页面都是 64KB 大小。在当前版本 WebAssembly 1.0 中,您无法更改该大小,尽管 WebAssembly 社区组正在进行提案,计划根据应用程序的需求使页面大小可变。本文写作时,应用程序可以分配的最大页面数为 32,767,总内存大小为 2GB。对于 Web 应用程序来说,这种最大内存分配已经足够,但对于基于服务器的应用程序则有限制。增加页面大小可能使服务器应用程序能够增加它们可以分配的最大线性内存。对于嵌入式 WebAssembly 应用程序来说,64KB 可能过大;例如,ATmega328 只有 32KB 的闪存。WebAssembly 的更新可能会在您阅读本文时消除这一限制。

您可以在 WebAssembly 模块内部或在嵌入环境中为导入创建应用程序将使用的页面数量。

在代码中创建页面

要在 WAT 中分配一个线性内存页面,使用一个简单的(memory)表达式,如列表 6-1 所示。

(memory 1)

列表 6-1:在 WAT 中声明一个内存页面

1传入内存表达式指示模块为线性内存预留一个页面。要分配当前可以为 WebAssembly 模块在运行时分配的最大内存,请使用列表 6-2 中的表达式。

(memory 32_767)

列表 6-2:声明最大内存页面数

尝试将 32_767 传入(memory)表达式会导致编译错误。使用(memory)表达式创建的内存对嵌入环境不可访问,除非您包含(export)表达式。

在嵌入环境中创建内存

创建线性内存的另一种方式是在嵌入环境内。如果嵌入环境是 JavaScript,则创建该内存的代码是new WebAssembly.Memory,如列表 6-3 所示。

const memory = new WebAssembly.Memory({initial: 1});

列表 6-3:在 JavaScript 中创建 WebAssembly Memory 对象

然后,您可以使用列表 6-4 中的(import)表达式从 WebAssembly 模块访问它。

(import "js" "mem" (memory 1))

列表 6-4:在 JavaScript 中导入分配的内存页面

使用import要求你在 JavaScript 中创建一个 Memory 对象,使用 WebAssembly.Memory 类,然后在通过 import 对象初始化 WebAssembly 模块时将其传入。创建一个名为 pointer.js 的文件,并添加 Listing 6-5 中的 JavaScript 代码,这段代码创建了一个 WebAssembly Memory 对象。

pointer.js

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/pointer.wasm');
const memory = new WebAssembly.Memory({1initial: 1, 2maximum: 4});

const importObject = {
  env: {
  3 mem: memory,
  }
};

( async () => {
 let obj = await WebAssembly.instantiate(new Uint8Array(bytes),
                                        4 importObject);
  let pointer_value = obj.instance.exports.get_ptr();
  console.log(`pointer_value=${pointer_value}`);
})();

Listing 6-5: 初始化 WebAssembly 线性内存并将其传递给 WebAssembly 模块。

在创建时,这段代码传入一个具有两个初始化值的对象。initial 1 参数是必需的,通过传入 1,我们指示 JavaScript 引擎为线性内存保留一页空间(64KB)。第二个值 maximum 2 是可选的,它让浏览器知道我们可能希望稍后增加线性内存的大小,并且我们大概率不会希望将内存增长到超过四页。你可以通过调用 memory.grow 方法来增加线性内存的大小。虽然不需要设置最大值来增加内存,但传入最大值会告诉浏览器为增长的内存预留更多空间,因为很可能会调用 grow 方法。如果你尝试将线性内存增长到超过传入的最大值,应用程序将抛出错误。创建内存对象后,我们通过 importObject 中的 env.mem 3 将其传递给 WebAssembly 模块。JavaScript 在实例化时将 importObject 传入模块 4。

指针

指针 是一个引用内存中某个位置的变量。指针在计算机科学中有多种应用,但在这个上下文中,我们将用它们来指向线性内存中的数据结构。图 6-3 显示了一个指针指向内存位置 5,该位置的值为 99。

f06003

图 6-3:指向内存中第五个字节的指针

WebAssembly 的指针行为与 C 或 C++ 中你可能熟悉的指针不同,后者可以指向局部变量或堆上的变量。Listing 6-6 中的 C 代码创建了一个名为 ptr 的指针,指向局部变量 x 的地址。

int x = 25;
int *ptr = &x;

Listing 6-6: 在 C 中设置指针值的示例

指针ptr被设置为x变量的地址,x是一个局部变量,它在栈上有一个地址。WebAssembly 没有像 C 语言中的int*整数指针类型那样的独立指针类型。WebAssembly 的线性内存是一个大型数据数组。当你在 WAT 中表示指针时,你必须将数据放入线性内存中;然后,指针就是指向该数据的i32索引。编译程序为 WebAssembly 时,Listing 6-6 中的变量x会在线性内存中接收一个地址。与 C 语言不同,WAT 不能创建指向局部或全局变量的指针。要在 WebAssembly 中实现类似 C 语言的指针功能,你可以将一个全局变量设置为线性内存中的特定位置,并使用该全局变量来设置或检索存储在 WebAssembly 线性内存中的值,如 Listing 6-7 所示。

pointer.wat

(module
1 (memory 1)
2 (global $pointer i32 (i32.const 128))
3 (func $init
  4 (i32.store
  5 (global.get $pointer)  ;; store at address $pointer
  6 (i32.const 99)         ;; value stored
    )
  )
7 (func (export "get_ptr") (result i32)
8 (i32.load (global.get $pointer)) ;; return value at location $pointer
  )
9 (start $init)
)

Listing 6-7: 在 WAT 中模拟指针

该模块创建了一页线性内存 1 和一个指向内存位置128的全局$pointer2。我们创建了一个函数$init3,该函数使用(i32.store)4 表达式将$pointer指向的内存位置的值设置为99。传递给(i32.store)5 的第一个参数是存储值的内存位置,第二个参数 6 是你想要存储的值。要从这个指针位置检索值,你可以使用i32.load8 表达式,并传入你要检索的内存位置。我们创建了一个函数"get_ptr"7 来检索这个值。然后,(start $init)9 语句将$init3 作为模块初始化函数进行调用。start语句声明一个函数作为模块的初始化函数。这个函数将在模块实例化时自动执行。

一旦你编译了pointer.wasm文件并使用node执行它,你应该会看到以下输出:

pointer_value=99

JavaScript 内存对象

现在我们对线性内存的工作原理有了一些了解,我们将创建一个 WebAssembly 内存对象,从 WebAssembly 模块内部初始化数据,然后从 JavaScript 访问这些数据。当你在处理线性内存时,很可能你希望从 WebAssembly 和嵌入环境中访问它。在这种情况下,嵌入环境是 JavaScript,因此我们将在 JavaScript 中定义线性内存,以便在 WebAssembly 模块初始化之前访问它。这个 WAT 模块类似于 Listing 6-7,但它将从 JavaScript 导入线性内存。

创建 WebAssembly 内存对象

创建一个名为store_data.wat的文件,并将 Listing 6-8 中的代码添加到该文件中。

store_data.wat

(module
1 (import "env" "mem" (memory 1))
2 (global $data_addr (import "env" "data_addr") i32)
3 (global $data_count (import "env" "data_count") i32)

4 (func $store_data (param $index i32) (param $value i32)
    (i32.store
     (i32.add
      (global.get $data_addr) ;; add $data_addr to the $index*4 (i32=4 bytes)
      (i32.mul (i32.const 4) (local.get $index)) ;; multiply $index by 4
     )
     (local.get $value) ;; value stored
    )
  )

5 (func $init
    (local $index i32)

  6 (loop $data_loop
      local.get $index

      local.get $index
      i32.const 5
      i32.mul      

    7 call $store_data ;; called with parameters $index and $index * 5

      local.get $index
      i32.const 1
      i32.add          ;; $index++

      local.tee $index
    8 global.get $data_count
      i32.lt_u
      br_if $data_loop
    )

 9 (call $store_data (i32.const 0) (i32.const 1))

  )

a (start $init)
)

Listing 6-8: 在 WebAssembly 中创建线性内存对象

Listing 6-8 中的模块从 JavaScript 嵌入环境中导入其线性内存 1,我们稍后将定义它。它从 JavaScript 导入我们将加载到全局变量$data_addr 2 中的数据地址。它还导入了$data_count 3,该变量包含在模块初始化时我们将存储的i32整数的数量。$store_data 4 函数接收一个索引和一个值,并将数据位置($data_addr + $index * 4)设置为$value(我们乘以 4 是因为i32类型占用四个字节)。通过使用$data_addr,一个导入的全局变量,允许 JavaScript 决定存储这些值的内存模块位置。

如 Listing 6-6 所示,$init 5 函数在模块初始化时执行,因为存在(start $init)语句。与之前的$init函数不同,这个函数在一个循环 6 中初始化数据。循环可以是初始化线性内存中特定部分数据的有用方法,使其设置为相同的值,或某些可能在循环中计算的值。这个循环根据模块从 JavaScript 导入的$data_count 8 全局变量设置了几个 32 位整数。当这个loop调用$store_data 7 时,它传入一个索引,表示loop已经完成的次数,以及一个值,值为$index * 5。我选择了$index * 5 的值,这样当我们显示数据时,你会看到数据值以 5 为增量递增。

loop之后,我们再调用一次$store_data 9,将数组中的第一个数据值设置为1。如果不初始化它,内存缓冲区将以所有数据初始化为0。因为loop将第一个数据值设置为0,所以当我们在 JavaScript 中查看数据时,无法清楚地看到设置的数据从何处开始。将其设置为1,使得在下一节中我们从 JavaScript 显示数据时,数据集的开始更加明显。

在完成创建store_data.wat文件后,使用wat2wasm编译它,生成store_data.wasm文件。

在控制台上使用颜色记录

在编写store_data.js部分之前,让我们简要了解一个名为colors的 Node 模块,它允许你使用自选颜色将日志行输出到控制台。在后续部分,我们将使用这个包来更方便地查看输出数据中的不同结果。要安装 colors,请使用npm命令,如 Listing 6-9 所示。

npm i colors

Listing 6-9: 使用npm安装 colors 模块

现在我们可以引入我们的应用程序来使用它,这使我们能够修改 JavaScript 中的字符串类型,添加设置颜色、加粗文本及其他一些功能的属性。创建一个名为colors.js的文件,并添加 Listing 6-10 中的代码。

colors.js

const colors = require('colors');

console.log('RED COLOR'.red.bold);  // logs bold red text
console.log('blue color'.blue); // logs blue text

Listing 6-10: 用颜色将日志输出到控制台

当你使用node运行colors.js时,如 Listing 6-11 所示,日志输出将以我们指定的颜色显示。

node colors.js

Listing 6-11: 运行color.js并将颜色日志输出到控制台。

现在你应该能看到清单 6-12 中的输出,第一行是红色,第二行是蓝色。

**RED COLOR**
blue color

清单 6-12:应用颜色模块

我们将在未来的应用程序中使用颜色模块,以改善控制台中输出的外观。在书中,红色的输出会变为黑色,但会加粗。接下来,让我们继续创建一个 store_data.js 文件。

在 store_data.js 中创建 JavaScript

现在我们需要一个 store_data.js JavaScript 文件来执行 store_data.wasm 模块。我们使用清单 6-13 来创建这个 JavaScript 文件。

store_data.js(第一部分 / 共 2 部分)

 const colors = require('colors'); // allow console logs with color
  const fs = require('fs');
  const bytes = fs.readFileSync(__dirname + '/store_data.wasm');

// allocate a 64K block of memory
1 const memory = new WebAssembly.Memory({initial: 1 });
// 32-bit data view of the memory buffer
2 const mem_i32 = new Uint32Array(memory.buffer);

3 const data_addr = 32; // the address of the first byte of our data

// The 32-bit index of the beginning of our data
4 const data_i32_index = data_addr / 4;
5 const data_count = 16; // the number of 32-bit integers to set

6 const importObject = { // The objects WASM imports from JavaScript
    env: {
      mem: memory,
 data_addr: data_addr,
      data_count: data_count
    }
  };
...

清单 6-13:一个 WebAssembly 线性内存缓冲区和带有全局导入的 importObject

我们创建了三个常量,第一个常量创建了一个新的 WebAssembly.Memory 1 对象,我们将在初始化 WebAssembly 模块时使用它。常量 mem_i32 2 提供了一个 32 位整数视图,用于查看内存缓冲区。这一点很重要,必须记住它不是缓冲区数据的副本,而是以特定的方式将缓冲区视为一个 32 位无符号整数数组。当我们从 WebAssembly 模块内部更改内存缓冲区中的值时,我们可以使用这个 mem_i32 视图查看这些值的变化。常量 data_addr 3 是我们在 WebAssembly 模块中设置的数据的字节位置。这个位置是 字节索引 m,而不是 32 位整数数组的编号。

因为一个 32 位整数占四个字节,我们需要一个起始数据索引,它是 data_addr 常量除以 4。我们将这个值设置为 data_i32_index 4。然后,我们定义了由 const data_count 5 指定的模块中设置的 32 位整数值的数量。本段代码中的最后一个 constimportObject 6。importObject 包含三个导入的数据对象,用于 WebAssembly 模块。

清单 6-14 中的 JavaScript 最后一部分使用了 IIFE 来实例化 WebAssembly 模块并将线性内存中的值输出到控制台。

store_data.js(第二部分 / 共 2 部分)

...
( async () => {
1 let obj = await WebAssembly.instantiate(new Uint8Array(bytes),
                                          importObject );

2 for( let i = 0; i < data_i32_index + data_count + 4; i++ ) {
    let data = mem_i32[i];
    if (data !== 0) {
    3 console.log(`data[${i}]=${data}`.red.bold);
    }
    else {
    4 console.log(`data[${i}]=${data}`);
    }

  }
})();

清单 6-14:在 IIFE 实例化 WebAssembly 模块后输出线性内存中的数据值

这段 JavaScript 代码的最后部分实例化了 store_data.wasm 模块,并传入了我们在清单 6-13 中创建的 importObject。初始化 WebAssembly 模块后,内存缓冲区中的数据会发生变化,因为 WAT 代码中的 $init 函数会在初始化过程中运行。然后,我们开始循环 2 遍历 mem_i32 数组,从内存缓冲区的第一个地址开始,并在数据设置后显示四个整数。如果值不为 0,则在浏览器中通过将其日志显示为红色 3 来展示 mem_i32 中的值;如果值为 0,则以默认的控制台颜色 4 显示。

使用 node 运行 store_data.js;你应该可以在控制台中看到清单 6-15 中的输出日志。

data[0]=0
data[1]=0
data[2]=0
data[3]=0
data[4]=0
data[5]=0
data[6]=0
data[7]=0
**data[8]=1**
**data[9]=5**
**data[10]=10**
**data[11]=15**
**data[12]=20**
**data[13]=25**
**data[14]=30**
**data[15]=35**
**data[16]=40**
**data[17]=45**
**data[18]=50**
**data[19]=55**
**data[20]=60**
**data[21]=65**
**data[22]=70**
**data[23]=75**
data[24]=0
data[25]=0
data[26]=0
data[27]=0

清单 6-15:数据输出

WebAssembly 模块设置的第一个数据元素是data[8],这就是红色输出开始的位置。值8data_i32_index常量中的值,是data_addr值的四分之一。在代码中设置了 16 个整数,因为我们已将const data_count设置为 16。在清单 6-15 中的数据里,所有值为0的数据元素没有在 WebAssembly 模块中被设置。你可以看到,前八个数字以及最后四个数字都是0,它们都以默认控制台颜色显示。

碰撞检测

之前,我们在 JavaScript 中创建了内存缓冲区对象,但它是从 WebAssembly 模块初始化的。这一次,我们将在 JavaScript 中初始化内存缓冲区,并使用在 JavaScript 中生成的值。我们还将创建更多有趣的数据结构来处理我们的碰撞检测数据。当修改 WebAssembly 内存缓冲区中的数据时,我们希望将数据分组到结构中,以便更易于管理。我们将在 JavaScript 中创建一组随机的圆形定义,每个定义都包含一个 x 和 y 坐标以及半径。然后,JavaScript 将这些值设置到 WebAssembly 内存缓冲区中。为了组织线性内存中的对象,你需要使用基地址、跨度和偏移量的组合。

基地址、跨度和偏移量

在 WebAssembly 模块内处理线性内存时,我们需要在低级别理解我们的数据结构。在我们的 JavaScript 中,我们将线性内存中的数据当作 JavaScript 类型化数组来处理。在 WebAssembly 模块内部,线性内存更像是一个内存堆或一个大型字节数组。当我们想要创建一个数据结构数组时,我们需要知道该数组的起始地址(基地址)、跨度(每个结构之间的字节距离)以及任何结构属性的偏移量(我们在结构中找到属性的位置)。

我们将在线性内存中处理一个有四个属性的结构:x 和 y 坐标、半径和碰撞标志。我们将跨度设置为单位跨度,如图 6-4 所示,这意味着数组中每个结构之间的距离与结构的大小匹配。

f06004

图 6-4:设置单位跨度

为了获取我们想要访问的特定数据结构的内存地址,我们将结构的索引乘以跨度并加上基地址。基地址是我们结构数组的起始地址。

作为单位跨度的替代方案,你可以给跨度添加填充。如果开发人员决定将他们的结构地址对齐到 2 的幂次方,他们可能会在结构末尾添加未使用的字节(称为填充)。例如,如果我们希望我们的结构与 16 字节地址对齐,我们可以在结构末尾添加四个字节的填充,使其跨度为 16,如图 6-5 所示。

然而,在这个例子中我们不需要填充。我们数据结构中的每个属性都有一个偏移量。例如,假设我们有两个 32 位整数属性,xy,它们分别是数据结构中的前两个属性。第一个属性 x 位于数据结构的开头,因此其偏移量为 0。由于 x 属性是 32 位整数,它占据了数据结构的前四个字节。这意味着 y 的偏移量从第五个字节开始,偏移量为 4(字节 0、1、2、3)。通过使用基本地址(我们数据结构的起始地址)、步幅和每个属性的偏移量,我们可以构建一个数据结构数组。

f06005

Figure 6-5: 填充步幅

从 JavaScript 加载数据结构

让我们开始创建一个名为 data_structures.js 的新示例应用程序的 JavaScript 文件。在这个应用中,我们将创建表示内存中圆形的结构体。稍后,我们将对这些圆形进行碰撞检测。将 Listing 6-16 中的代码添加到 data_structures.js 中。

data_structures.js(第一部分,共 3 部分)

const colors = require('colors'); // allow console logs with color
const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/data_structures.wasm');
 // allocate a 64K block of memory
1 const memory = new WebAssembly.Memory({initial: 1});

 // 32-bit view of memory buffer
  const mem_i32 = new Uint32Array(memory.buffer);

  const obj_base_addr = 0; // the address of the first byte of our data
2 const obj_count = 32;    // the number of structures
3 const obj_stride = 16;   // 16-byte stride

  // structure attribute offsets
4 const x_offset = 0;
5 const y_offset = 4;
  const radius_offset = 8;			
  const collision_offset = 12;			

  // 32-bit integer indexes
  const obj_i32_base_index = obj_base_addr / 4; // 32-bit data index
  const obj_i32_stride = obj_stride / 4;        // 32-bit stride

 // offsets in the 32-bit integer array
6 const x_offset_i32 = x_offset / 4;
  const y_offset_i32 = y_offset / 4;
 const radius_offset_i32 = radius_offset / 4;
  const collision_offset_i32 = collision_offset / 4;

7 const importObject = { // The objects WASM imports from JavaScript
    env: {
      mem: memory,
      obj_base_addr: obj_base_addr,
      obj_count: obj_count,
      obj_stride: obj_stride,
      x_offset: x_offset,			
      y_offset: y_offset,
      radius_offset: radius_offset,
      collision_offset: collision_offset,
    }
  };
...

Listing 6-16: 设置常量以定义碰撞检测程序的结构

首先,我们创建一系列 const 值,用于在 WebAssembly 内存缓冲区中创建结构体。正如 Listing 6-16 中的代码,这段代码创建了一个单一的 64KB WebAssembly 内存页面 1,并通过 32 位无符号整数视图来访问该数据。obj_base_addr 常量将数据结构的基本地址设置为 0,即页面内存的第一个字节。

我们将obj_count 2 const 设置为此代码中结构体的数量。obj_stride 3 常量保存结构体的字节数。我们将其设置为16,因为这个结构体包含四个 32 位整数,合计 16 字节。接下来的 const 声明组包含了属性的偏移量。

x_offset 4 是 x 从结构体开始位置的偏移量,即 x 值位置在每个结构体中的字节数。y_offset 5 是结构体中 y 值位置的字节数,其值为 4,因为 x 值是 32 位整数,使得 y 值位于结构体的第五个字节。然后我们设置了 radius 6 属性和 collision 7 属性的偏移量。

我们通过将字节地址除以 4 和步幅除以 4 来计算整数索引和步幅。原因是字节地址和步幅是字节数,而整数索引是 32 位整数(4 字节)。我们还需要找到整数数组中的索引,这是通过将字节索引除以 4 来计算的。importObject 7 已被修改以包括我们添加的新 const 值。

在定义了常量之后,我们将创建一系列随机大小的圆形供程序使用。如前所述,圆形由 x 和 y 坐标以及半径定义。我们将随机定义圆形的 x 和 y 坐标,值从 0 到 99,半径在 1 到 11 之间。列表 6-17 中的代码循环遍历内存对象,为每个结构设置随机值。

data_structures.js(第二部分,共 3 部分)

...
  for( let i = 0; i < obj_count; i++ ) {
  1 let index = obj_i32_stride * i + obj_i32_base_index;

 2 let x = Math.floor( Math.random() * 100 );
    let y = Math.floor( Math.random() * 100 );
    let r = Math.ceil( Math.random() * 10 );

  3 mem_i32[index + x_offset_i32] = x;
    mem_i32[index + y_offset_i32] = y;
    mem_i32[index + radius_offset_i32] = r;
  }
...

列表 6-17:使用随机的 x 和 y 坐标以及半径初始化圆形

循环为每个结构获取一个索引,用于碰撞检测圆形。xy 和半径 r 的值被设置为随机值。这些随机值随后用于基于对象索引和属性偏移设置内存值。

显示结果

接下来,我们需要实例化 data_structures.wasm 模块,它运行 $init 函数,执行我们在此数据测试中随机生成的每个圆形之间的碰撞检测。列表 6-18 显示了添加到 data_structures.js 的代码。

data_structures.js(第三部分,共 3 部分)

...
( async () => {
1 let obj = await WebAssembly.instantiate(new Uint8Array(bytes),
                                          importObject );

2 for( let i = 0; i < obj_count; i++ ) {
  3 let index = obj_i32_stride * i + obj_i32_base_index;

  4 let x = mem_i32[index+x_offset_i32].toString().padStart(2, ' ');
    let y = mem_i32[index+y_offset_i32].toString().padStart(2, ' ');
    let r = mem_i32[index+radius_offset_i32].toString()
                   .padStart(2,' ');
    let i_str = i.toString().padStart(2, '0');
    let c = !!mem_i32[index + collision_offset_i32];

    if (c) {
    5 console.log(`obj[${i_str}] x=${x} y=${y} r=${r} collision=${c}`
          .red.bold);
    }
    else {
    6 console.log(`obj[${i_str}] x=${x} y=${y} r=${r} collision=${c}`
          .green);
    }
  }
})();

列表 6-18:WebAssembly 运行后,代码循环遍历线性内存,查找圆形碰撞。

这个 IIFE 函数实例化 WebAssembly 模块 1,并循环遍历 mem_i32 数组中的对象 2。这个循环使用步幅、索引和基索引值 3 获取结构的索引。然后,我们使用计算出的索引从 mem_i32 4 数组中获取 xy、半径和碰撞值。如果发生碰撞,这些值会以红色 5 显示在控制台上,如果没有碰撞,则以绿色 6 显示。

我们现在有了加载一系列定义圆形的 JavaScript 代码,每个圆形的 x 和 y 坐标是随机选择的,值在 0 到 100 之间。每个圆形还有一个半径,值在 1 到 10 之间随机选择。WebAssembly 内存缓冲区已使用这些值进行初始化。JavaScript 将在 importObject 中设置适当的偏移量和步幅值。除了每个结构中的 xy 和半径外,还会留出四个字节来存储碰撞值。如果圆形与另一个圆形发生碰撞,值为 1,如果没有碰撞,则值为 0。WebAssembly 模块的初始化 (start) 函数计算碰撞。WebAssembly 模块初始化完成后,控制台显示此碰撞检查的结果。此时,我们还没有定义 WebAssembly 模块。接下来我们将定义它。

碰撞检测函数

在其他部分中,我们在 JavaScript 之前定义了 WAT 代码。在这一部分中,JavaScript 初始化定义圆形的值,这些值存储在结构体数组中。因此,我们将在本节中首先编写 JavaScript。当你在两个圆之间进行碰撞检测时,你会使用毕达哥拉斯定理来判断圆心之间的距离是否大于圆的半径和。本节中的 WAT 代码循环遍历我们在 WebAssembly 内存中定义的每个圆,将其与其他圆进行比较,查看它们是否发生碰撞。碰撞检测的细节不是本节的重点,因此我们不会过多深入探讨。它仅仅是演示如何将数据分离到结构体中,并使用这些数据与 WAT 代码进行计算。

WAT 代码的第一部分定义了从 JavaScript 导入的内容。Listing 6-19 显示了 WAT 模块的开头。

data_structures.wat(第一部分,共 6 部分)

(module
  (import "env" "mem" (memory 1))           
1 (global $obj_base_addr  (import "env" "obj_base_addr") i32)
2 (global $obj_count    (import "env" "obj_count") i32)
3 (global $obj_stride   (import "env" "obj_stride") i32)

 ;; attribute offset locations
4 (global $x_offset     (import "env" "x_offset") i32)
5 (global $y_offset     (import "env" "y_offset") i32)
6 (global $radius_offset  (import "env" "radius_offset") i32)
7 (global $collision_offset (import "env" "collision_offset") i32)
...

Listing 6-19:导入定义数据结构的全局变量

传递给 WebAssembly 模块的全局变量定义了线性内存的布局和其中的数据结构。$obj_base_addr 1 个全局变量是定义圆形结构体在内存中的位置。$obj_count 2 个全局变量是在线性内存中定义的圆的数量。$obj_stride 3 个全局变量是每个圆形定义之间的字节数。接下来,我们导入每个属性的值。$x_offset 4、$y_offset 5、$radius_offset 6 和 $collision_offset 7 是对象的 xy、半径和碰撞标志值的字节偏移量。必须在该模块内设置这些值。

接下来,我们将定义 $collision_check 函数。这个函数的工作原理的细节只有在你对圆形碰撞检测的工作方式感兴趣时才有价值。简而言之,它使用毕达哥拉斯定理来判断两个圆之间的距离是否小于圆的半径和。简要解释一下,假设第一个圆的半径为 R[1],第二个圆的半径为 R[2],圆之间的距离为 D,如图 6-6 所示。如果 R[1] + R[2] 小于 D,则不会发生碰撞。

f06007

图 6-6:如果 R[1] + R[2] 小于圆之间的距离,则不会发生碰撞。

如果距离小于 R[1] + R[2],则发生碰撞,如图 6-7 所示。

f06008

图 6-7:R[1] + R[2] 大于圆之间的距离。

Listing 6-20 展示了 $collision_check 函数的代码。

data_structures.wat(第二部分,共 6 部分)

...
1 (func $collision_check
    (param $x1 i32) (param $y1 i32) (param $r1 i32)
    (param $x2 i32) (param $y2 i32) (param $r2 i32)
    (result i32)

    (local $x_diff_sq i32)
    (local $y_diff_sq i32)
    (local $r_sum_sq i32)

    local.get $x1
    local.get $x2
    i32.sub
    local.tee $x_diff_sq
    local.get $x_diff_sq
    i32.mul
      2 local.set $x_diff_sq  ;; ($x1 - $x2) * ($x1 - $x2)

    local.get $y1
 local.get $y2
    i32.sub
    local.tee $y_diff_sq
    local.get $y_diff_sq
    i32.mul
      3 local.set $y_diff_sq  ;; ($y1 - $y2) * ($y1 - $y2)

    local.get $r1
    local.get $r2
    i32.add
    local.tee $r_sum_sq
    local.get $r_sum_sq
    i32.mul
      4 local.tee $r_sum_sq   ;; ($r1 + $r2) * ($r1 + $r2)

    local.get $x_diff_sq
    local.get $y_diff_sq
      5 i32.add  ;; pythagorean theorem A squared + B squared = C squared

      6 i32.gt_u ;; if distance is less than sum of the radii return true
)
...

Listing 6-20:一个 WebAssembly 碰撞检测函数

该函数接受两个圆形的xy和半径属性 1,然后如果它们重叠则返回1,否则返回0。它首先通过从$x2减去$x1来计算两个圆形之间的x距离。然后它将该值平方,并存储在$x_diff_sq 2 中;接着通过从$y2减去$y1来计算两个圆形之间的y距离。它将这个结果平方并存储在$y_diff_sq 3 中。我们要建立的是毕达哥拉斯定理 A² + B² = C² 5。在这个场景中,$x_diff_sq是 A²,$y_diff_sq是 B²。这两者的和是 C²,然后与半径平方的和 4 进行比较。如果半径²大于 C²,表示圆形重叠,函数返回1;否则,函数返回0。该函数使用i32.gt_u 6 表达式来做出这个决定。

$collision_check函数之后,我们需要一些辅助函数。$get_attr辅助函数接受一个对象基地址参数和一个属性偏移量参数,并返回该地址位置在线性内存中的值。列表 6-21 展示了该函数。

data_structures.wat(第三部分,共 6 部分)

...
1 (func $get_attr (param $obj_base i32) (param $attr_offset i32)
    (result i32)
    local.get $obj_base
    local.get $attr_offset
  2 i32.add         ;; add attribute offset to base address
  3 i32.load        ;; load the address and return it
  )
...

列表 6-21:从线性内存中检索对象属性

在函数定义 1 中,$obj_base参数是对象的基地址,而$attr_offset是我们要检索的特定属性的偏移量。该函数将这两个值相加 2,然后从该地址加载值 3,并将其作为结果返回。

下一个辅助函数是$set_collision,它将两个圆形对象的碰撞标志设置为真。列表 6-22 展示了该函数。

data_structures.wat(第四部分,共 6 部分)

...
1 (func $set_collision
    (param $obj_base_1 i32) (param $obj_base_2 i32)
    local.get $obj_base_1
    global.get $collision_offset
  2 i32.add   ;; address = $obj_base_1 + $collision_offset
    i32.const 1
  3 i32.store ;; store 1 as true in the collision attribute for this object

    local.get $obj_base_2
    global.get $collision_offset
  4 i32.add   ;; address = $obj_base_2 + $collision_offset
    i32.const 1
  5 i32.store ;; store 1 as true in the collision attribute for this object
  )
...

列表 6-22:为给定对象设置碰撞属性

该函数接受两个对象基地址参数 1,用于设置内存中这些对象的碰撞标志。它通过将$obj_base_1加到$collision_offset 2,然后将该位置在线性内存中的值设置为1 3。接着,它将$obj_base_2加到$collision_offset 4,并将该位置的值设置为1 5。

现在我们已经定义了其他函数,可以将$init函数添加到 WAT 代码中,如列表 6-23 所示。

data_structures.wat(第五部分,共 6 部分)

...
(func $init
1 (local $i i32)     ;; outer loop counter
  (local $i_obj i32) ;; address of ith object
  (local $xi i32)(local $yi i32)(local $ri i32) ;; x,y,r for object i

2 (local $j i32)     ;; inner loop counter
  (local $j_obj i32) ;; address of the jth object
  (local $xj i32)(local $yj i32)(local $rj i32) ;; x,y,r for object j

  (loop $outer_loop
  (local.set $j (i32.const 0))  ;; $j = 0

  (loop $inner_loop
    (block $inner_continue
    ;; if $i == $j continue
  3 (br_if $inner_continue (i32.eq (local.get $i) (local.get $j) ) )

 ;; $i_obj = $obj_base_addr + $i * $obj_stride
    (i32.add (global.get $obj_base_addr)
           4 (i32.mul (local.get $i) (global.get $obj_stride) ) )

 ;; load $i_obj + $x_offset and store in $xi
  5 (call $get_attr (local.tee $i_obj) (global.get $x_offset) )
    local.set $xi    

 ;; load $i_obj + $y_offset and store in $yi
    (call $get_attr (local.get $i_obj) (global.get $y_offset) )
    local.set $yi    

 ;; load $i_obj + $radius_offset and store in $ri
    (call $get_attr (local.get $i_obj) (global.get $radius_offset) )
    local.set $ri    

 ;; $j_obj = $obj_base_addr + $j * $obj_stride
  6 (i32.add (global.get $obj_base_addr)
             (i32.mul (local.get $j)(global.get $obj_stride)))

 ;; load $j_obj + $x_offset and store in $xj
    (call $get_attr (local.tee $j_obj) (global.get $x_offset) )
    local.set $xj    

 ;; load $j_obj + $y_offset and store in $yj
    (call $get_attr (local.get $j_obj) (global.get $y_offset) )
    local.set $yj    

 ;; load $j_obj + $radius_offset and store in $rj
    (call $get_attr (local.get $j_obj) (global.get $radius_offset) )
    local.set $rj    

 ;; check for collision between ith and jth objects
  7 (call $collision_check
      (local.get $xi)(local.get $yi)(local.get $ri)
      (local.get $xj)(local.get $yj)(local.get $rj))

    if ;; if there is a collision
    8 (call $set_collision (local.get $i_obj) (local.get $j_obj))
    end
  )

  9 (i32.add (local.get $j) (i32.const 1)) ;; $j++

 ;; if $j < $obj_count loop
    (br_if $inner_loop
      (i32.lt_u (local.tee $j) (global.get $obj_count)))
  )

  a (i32.add (local.get $i) (i32.const 1)) ;; $i++

 ;; if $i < $obj_count loop
  (br_if $outer_loop
 (i32.lt_u (local.tee $i) (global.get $obj_count) ) )
 )
)
...

列表 6-23:一个双重循环,检查线性内存中每个对象之间的碰撞

函数以两组局部变量开始。一组包含计数器、对象地址,以及用于外层循环 1 的xyr局部变量。第二组局部变量用于内层loop 2 中。该函数的核心是一个双重循环,比较线性内存中每个圆与其他所有圆,查找发生碰撞的圆。内层循环的开始检查$i是否与$j 3 相等。如果相等,代码跳过对该$j对象的检查,否则每个圆都会与自己发生碰撞。

下一行代码计算第i个对象 4 的线性内存地址,公式为$obj_base_addr + $i * $obj_stride。然后,它使用local.tee表达式在下一行的(call $get_attr) 5 表达式中设置$i_obj的值。对$getattr 5 的调用获取第i个对象的x值,并将其赋给$xi

接下来的四行代码以相同的方式将值加载到$yi$ri中。然后,$xj$yj$rj 6 也通过调用$get_attr来设置。这些值会传递给$collision_check 7 函数,如果$i$j两个圆相撞,它会返回1,如果不相撞,则返回0。紧随其后的if语句会执行对$set_collision 8 的call,如果发生了碰撞,该函数会将这两个对象的碰撞标志设置为1。循环结束时,$j 9 会递增,并且如果$j小于$obj_count,则会跳转回内层loop的顶部。外层loop结束时,$i会递增,如果$i小于$obj_count,则会跳转回外层loop的顶部。

我们在这个模块中调用的最后一项是(start $init) 语句,如清单 6-24 所示,它在模块初始化时执行$init函数。

data_structures.wat(第六部分,共 6 部分)

...
  (start $init)
)

清单 6-24:start表示模块初始化时会执行的函数。

现在我们已经将所有代码放入data_structures.wat文件中,可以使用wat2wasm来编译 WebAssembly 文件,如清单 6-25 所示。

wat2wasm data_structures.wat

清单 6-25:编译 data_structures.wat

一旦我们有了编译好的 data_structures.wasm 文件,就可以使用node运行 data_structures.js,如清单 6-26 所示。

node data_structures.js

清单 6-26:运行 data_structures.js

输出将类似于清单 6-27。

**obj[00] x=48 y=65 r= 4 collision=true**
**obj[01] x=46 y=71 r= 6 collision=true**
**obj[02] x=12 y=75 r= 3 collision=true**
obj[03] x=54 y=43 r= 2 collision=false
obj[04] x=16 y= 6 r= 1 collision=false
**obj[05] x= 5 y=21 r= 9 collision=true**
obj[06] x=71 y=50 r= 5 collision=false
**obj[07] x=11 y=13 r= 5 collision=true**
**obj[08] x=43 y=70 r= 7 collision=true**
obj[09] x=88 y=60 r= 9 collision=false
**obj[10] x=96 y=21 r= 9 collision=true**
**obj[11] x= 5 y=87 r= 2 collision=true**
obj[12] x=64 y=39 r= 3 collision=false
**obj[13] x=75 y=74 r= 6 collision=true**
**obj[14] x= 2 y=74 r= 8 collision=true**
**obj[15] x=12 y=85 r= 7 collision=true**
obj[16] x=60 y=27 r= 5 collision=false
**obj[17] x=43 y=67 r= 2 collision=true**
obj[18] x=38 y=53 r= 3 collision=false
obj[19] x=34 y=39 r= 5 collision=false
**obj[20] x=42 y=62 r= 2 collision=true**
obj[21] x=72 y=93 r= 7 collision=false
**obj[22] x=78 y=79 r= 8 collision=true**
obj[23] x=50 y=96 r= 7 collision=false
**obj[24] x=34 y=18 r=10 collision=true**
obj[25] x=19 y=44 r= 8 collision=false
**obj[26] x=92 y=82 r= 7 collision=true**
obj[27] x=59 y=56 r= 3 collision=false
**obj[28] x=41 y=75 r= 9 collision=true**
**obj[29] x=28 y=29 r= 6 collision=true**
**obj[30] x=32 y=10 r= 1 collision=true**
**obj[31] x=83 y=15 r= 6 collision=true**

清单 6-27:data_structures.js 的输出

在实际输出中,任何与其他圆形相撞的圆形应该用红色文本显示,而没有与其他圆形相撞的圆形应该用绿色文本显示。现在我们有一个应用程序,使用 JavaScript 将随机生成的圆形数据加载到 WebAssembly 线性内存中。然后,WebAssembly 模块中的初始化函数会遍历所有数据,并在发生圆形相撞时更新线性内存。碰撞检测是 WebAssembly 的一个很好的使用案例,因为它允许你将大量数据加载到线性内存中,并让 WebAssembly 模块以快速高效的方式进行处理。

总结

本章中,你学习了什么是 WebAssembly 线性内存以及如何从 WebAssembly 模块或 JavaScript 中创建它。接下来,我们在 WebAssembly 模块中初始化了线性数据,并从 JavaScript 访问了这些数据。然后,我们使用基址、步幅和属性偏移量在线性内存中创建了数据结构,并通过 JavaScript 使用随机数据初始化了这些数据结构。

最终的项目是一个包含圆形数据结构的数组,每个数据结构都有 x 和 y 坐标以及半径。这些数据被传递到 WebAssembly 模块中,模块使用双重循环遍历圆形数据结构,寻找重叠的圆形。如果找到两个圆形重叠,WebAssembly 模块会在这两个圆形的线性内存中设置一个碰撞标志。然后,JavaScript 会遍历所有这些圆形,显示它们的 x 和 y 坐标、半径,以及它们是否与其他圆形发生了碰撞。

到这一点,你应该已经理解如何在 WAT 和 JavaScript 中操作和设置线性内存。你还应该能够在你的应用程序中使用线性内存来创建数据结构,并在 WebAssembly 中处理大量数据,然后通过 JavaScript 显示出来。在下一章,我们将探讨如何从 WebAssembly 操作文档对象模型(DOM)。

第七章:网页应用程序

本章将帮助你理解 WebAssembly 如何通过 JavaScript 与 DOM 交互。虽然这看起来可能有些繁琐,但这是理解 WebAssembly 及其优缺点的必要之恶。如果你正在使用 WebAssembly 工具链,你需要了解该工具链将生成多少额外的 JavaScript 胶水代码。从这一点开始,大多数示例将通过网页运行,而不是通过命令行使用 node

我们将从使用 Node.js 创建一个简单的静态网页服务器开始。WebAssembly 网页应用程序不能直接从文件系统在网页浏览器中加载;相反,它们需要你运行一个网页服务器。Node.js 提供了我们创建网页服务器所需的所有工具。然后,我们将编写我们的第一个 WebAssembly 网页应用程序。

我们将编写的第二个网页应用程序重用了我们在第五章中编写的函数,这些函数从 HTML 的输入元素获取一个数字,并将其传递给 WebAssembly,WebAssembly 将这个数字转换为十进制、十六进制和二进制字符串。

本章结束时,你将了解编写一个加载并实例化 WebAssembly 模块的网页应用程序的基础知识,然后从该模块内调用函数。应用程序还将把来自这些模块的数据写入 DOM 元素。本章中的示例并不代表你通常会使用 WebAssembly 编写的应用程序类型,它们只是演示了一个网页如何加载、实例化并与 WebAssembly 模块进行交互。

DOM

现代基于网页的应用程序非常复杂,容易让人忘记,HTML 页面本质上只是一个简单的文档。网络最初是为了共享文档和信息而构思的,但很快就显现出我们需要一种标准的方法,使用像 JavaScript 或 Java 这样的语言来动态更新这些文档。DOM 被设计为一个与语言无关的接口,用于操作 HTML 和 XML 文档。由于 HTML 文档是一个树状结构,DOM 将文档表示为一个逻辑树。DOM 是 JavaScript 和其他语言修改网页应用程序中 HTML 的方式。

WebAssembly 1.0 版本没有直接操作 DOM 的手段,因此 JavaScript 必须对 HTML 文档进行所有修改。如果你使用的是一个工具链,如 Rust 或 Emscripten,DOM 的操作通常是通过 JavaScript 胶水代码来完成的。通常,WebAssembly 在网页应用程序中的部分应该专注于处理数字数据,但对于 DOM,大多数数据处理可能是字符串操作。WebAssembly 中字符串操作的性能完全取决于你用于该任务的库。因此,DOM 重的工作通常最好保留在应用程序的 JavaScript 部分。

设置一个简单的 Node 服务器

要设置一个静态网页服务器,首先为你的项目创建一个文件夹,并在 VS Code 或你选择的 IDE 中打开它。我们需要使用 npm 安装两个包。使用 清单 7-1 中的命令安装第一个包 connect

npm install connect --save-dev

清单 7-1:使用 npm 安装 connect 包。

使用 清单 7-2 中的命令安装第二个包 serve-static

npm install serve-static --save-dev

清单 7-2:使用 npm 安装 serve-static

安装完包后,创建一个名为 server.js 的文件,并输入 清单 7-3 中的代码,定义一个静态网页服务器。

server.js

var connect = require('connect');
var serveStatic = require('serve-static');
connect().use(serveStatic(__dirname + "/")).listen(8080, function(){
  console.log('localhost:8080');
});

清单 7-3:Node.js http 服务器代码

我们已经创建了一个静态服务器,它可以提供当前目录中的文件,但我们还没有任何文件可以提供。使用 VS Code 创建一个名为 index.html 的文件,并输入一些 HTML,类似于 清单 7-4 中的代码。

index.html

<html>
  <head></head>
  <body>
    <h1>OUR SERVER WORKS!</h1>
  </body>
</html>

清单 7-4:一个简单的网页

现在,你可以使用以下命令运行你的 Node.js 网络服务器:

node server.js

一个网络服务器开始在端口 8080 上运行。通过在浏览器中输入 localhost:8080 来测试;你应该会看到类似 图 7-1 的内容。

f07001

图 7-1:测试我们的简单静态服务器

现在我们已经有了一个工作的 Node.js 网络服务器,让我们创建我们的第一个 WebAssembly 网页应用。

我们的第一个 WebAssembly 网络应用

我们将从一个简单的网页应用开始,它接受两个数字输入,将它们相加,然后显示这些值。该应用的最终版本可以在 wasmbook.com/add_message.html 访问。

这个应用展示了 WebAssembly 如何与 DOM 交互。你会发现,我们并没有改变 WebAssembly 模块的工作方式,而是改变了嵌入的环境,而 WebAssembly 本身对此毫不知情。

要创建一个网页应用,我们必须运行一个网页服务器,编写一个包含与 DOM 交互的 JavaScript 的 HTML 页面,并使用 instantiateStreaming 函数加载 WebAssembly 模块(而不是像前面章节中那样使用 instantiate)。我们将定义一个将两个整数相加的 WebAssembly 模块,以及一个加载并运行该 WebAssembly 模块的 HTML 文件。在清单 1-8 中,JavaScript 使用 Node.js 加载并执行 WebAssembly 模块,调用了 AddInt 函数。在这个应用中,HTML 文件将包含该 JavaScript,并且需要一个浏览器来运行该应用。

清单 7-5 显示了具有加法功能的 WAT 模块。创建一个名为 add_message.wat 的文件,并在其中添加 清单 7-5 中的代码。

add_message.wat

(module
1 (import "env" "log_add_message"
    (func $log_add_message (param i32 i32 i32)))

2 (func (export "add_message")
    3 (param $a i32) (param $b i32)
      (local $sum i32)

      local.get $a
      local.get $b
    4 i32.add
      local.set $sum

    5 (call $log_add_message
      6 (local.get $a) (local.get $b) (local.get $sum))
  )
)

清单 7-5:add_message.wat 文件将两个数字相加并调用一个 JavaScript 日志函数。

这个 WAT 模块此时应该很熟悉了。它从 JavaScript 中导入了 log_add_message 1,并定义了一个将导出到嵌入环境中的函数 add_message 2。它还接收两个 i32 类型的参数 3。这两个参数相加 4 并存储在一个局部变量 $sum 中。接着,它调用 JavaScript 函数 log_add_message 5,传入 $a$b 参数,以及 $sum 6,这两个参数的和。

此时,你可能会想知道 WebAssembly 如何与 DOM 交互。遗憾的事实是,WebAssembly 1.0 无法直接与 DOM 交互。它必须依赖于嵌入环境(JavaScript)来执行所有交互。调用 WebAssembly 模块时与在 Node.js 和网页之间的区别,完全是在嵌入环境中。WebAssembly 模块只能调用嵌入环境中的函数。我们将在 HTML 页面中创建 JavaScript 函数。WebAssembly 模块将调用这些 JavaScript 函数,这些函数将更新 DOM。使用 wat2wasm 编译 add_message.wat

定义 HTML 头部

现在我们将创建我们的 HTML 页面。当我们以前使用 Node.js 作为嵌入环境时,可以直接使用纯 JavaScript,但对于静态网站,你需要一个 HTML 页面。网页浏览器不会像 Node.js 一样直接执行 JavaScript。网页浏览器加载 HTML 页面,其中嵌入 JavaScript 代码在 <script> 标签内。假设你对 HTML 基础有一定了解,但如果没有,这个示例应该也能很容易跟上。创建一个新的文件 add_message.html 并添加 列表 7-6 中的代码。

add_message.html(第一部分,共 3 部分)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0">
  <title>Add Message</title>
...

列表 7-6:add_message 应用的 HTML 头部主要是 HTML 基本模板。

这是 HTML 开始标签和头部信息。它仅设置一些字体配置,并将应用程序名称 Add Message 显示为标题。

JavaScript

在结束 head 元素之前,我们包含了一个 script 标签来写入 JavaScript。与我们使用 Node.js 时类似,JavaScript 代码是实例化和执行 WebAssembly 模块中函数所必需的。HTML 页面使用 script 标签来包含这段 JavaScript,如 列表 7-7 中所示。

add_message.html(第二部分,共 3 部分)

...
  <script>
  1 //const sleep = m => new Promise(r => setTimeout(r, m));
    var output = null;
    var add_message_function;

  2 var log_add_message = (a, b, sum) => {
      if (output == null) {
        console.log("page load not complete: log_add_message");
        return;
      }
 3 output.innerHTML += `${a} + ${b} = ${sum}<br>`;
    };

    let importObject = {
      env: {
      4 log_add_message: log_add_message,
      }
    };

    (async () => {
    5 // await sleep(5000);
      let obj = await
    6 WebAssembly.instantiateStreaming(fetch('add_message.wasm'),
                                        importObject);
      add_message_function = obj.instance.exports.add_message;
    7 let btn = document.getElementById("add_message_button");
      btn.style.display = "block";
    })();

  8 function onPageLoad() {
      //(async () => {
    9 //await sleep(5000);
    a output = document.getElementById("output");
      //})();
    }
  </script>
...

列表 7-7:加载 WebAssembly 模块的 JavaScript 代码位于 script 标签内。

在构建网页时,我们需要注意所有网页元素何时加载完成,以及流式加载和实例化 WebAssembly 模块所需的时间。

这个应用将消息写入段落标签 output。在 JavaScript 执行时,输出段落尚未加载,因为它位于 HTML 页面更下方。WebAssembly 模块将异步流式加载,因此你无法确定 WebAssembly 模块是在页面加载完成之前还是之后实例化的。

为了测试无论这些事件按何种顺序发生该函数是否都能正常工作,我们在开头创建了一个 sleep 1 函数来强制 JavaScript 等待。此函数在这里被注释掉。为了测试加载顺序,请取消注释此处的 sleep,以及 IIFE 内部或 onPageLoad 函数中的 sleep

我们创建了 add_message_function 变量,作为占位符,当 WebAssembly 模块实例化后,它将指向 add_message 函数。

接下来,我们定义了 log_add_message 2,其中包含一个箭头函数,用于检查 output 是否被设置为非 null 的值。output 的默认值为 null,但一旦页面加载完成,output 将被设置为具有 idoutput 的段落元素;因此,如果在页面加载完成之前函数运行,该函数将记录一条信息。log_add_message 4 函数由 WebAssembly 模块导入并调用,该模块将两个要添加的参数和它们的和传递给 log_add_message。该函数随后将这些值写入列表 7-8 中的 output 3 HTML 段落标签。

在 IIFE 中,sleep 5 函数被注释掉了,但你可以恢复它进行测试。然而,在从网页加载 WebAssembly 模块时,您需要使用 WebAssembly.instantiateStreaming 6 结合 fetch 调用来检索该模块。一旦模块实例化,add_message_button 7 元素就会从 DOM 中获取,并通过将其 style.display 属性设置为 block 使其可见。此时,用户就可以点击这个按钮来运行 WebAssembly 函数。

此外,我们定义了 onPageLoad 8 函数,该函数在 HTML body 加载完成时执行。该函数将列表 7-7 中顶部定义的 output 变量设置为具有 idoutput 的段落标签。在页面加载之前,output 变量的值为 null。如果需要 output 标签的函数在页面加载完成之前执行,它可以在使用之前检查是否为 null。这样可以防止代码在段落标签加载之前尝试使用它。我们还包含了一个可选的 sleep 9 函数,可以用来延迟设置 output 变量,这使我们能够模拟当页面加载时间比预期更长时的情况。

HTML Body

HTML 的 body 标签包含将在网页上显示的 DOM 元素。请将列表 7-8 中的代码添加到 add_message.html 文件中,放在 script 标签下方。

add_message.html(第三部分,共 3 部分)

...
</head>
1 <body onload="onPageLoad()"
      style="font-family: 'Courier New', Courier, monospace;">
2 <input type="number" id="a_val" value="0"><br><br>
3 <input type="number" id="b_val" value="0"><br><br>
4 <button id="add_message_button" type="button" style="display:none"
5 onclick="add_message_function(  
                document.getElementById('a_val').value,
                document.getElementById('b_val').value )">
    Add Values
  </button>
  <br>
6 <p id="output" style="float:left; width:200px; min-height:300px;">
  </p>
</body>
</html>

列表 7-8:HTML body 标签中的 DOM 元素

body 1 标签包含一个 onload 属性,该属性调用 JavaScript 的 onPageLoad 函数。这确保了我们的 JavaScript 中的 output 变量在 output 段落标签存在之前不会被设置。

然后我们有两个input元素,其id分别为a_val 2 和 b_val 3。这些输入框中的值在点击button 4 元素时会传递给 WebAssembly。当模块实例化后,onclick属性被设置为调用add_message_function,该函数会调用 WebAssembly 模块中的add_message函数。调用add_message函数时,会将两个输入框中的值(a_valb_val)传递给它。此外,我们还有一个idoutput 6 的段落标签,用于显示来自 WebAssembly 模块的值。

我们的完整 Web 应用程序

现在我们应该能够运行我们的 web 应用程序。如前所述,我们必须通过 web 服务器来提供网页,因此首先确保清单 7-3 中的 web 服务器正在运行,方法是使用清单 7-9 中的命令。

node server.js

清单 7-9:运行简单的 web 服务器。

如果你在清单 7-10 中遇到错误,说明该端口上已经运行着一个 web 服务器。

Error: listen EADDRINUSE: address already in use :::8080

清单 7-10:端口已被占用时的 web 服务器错误

出现此错误可能意味着你是在不同的命令行中运行server.js。确保 web 服务器正在运行后,在浏览器中打开以下 URL:localhost:8080/add_message.html

你应该会看到类似图 7-2 的屏幕。

f07002

图 7-2:add_message.html Web 应用程序

在两个数字字段中设置值,然后点击添加值查看加法结果(图 7-3)。

f07003

图 7-3:通过应用程序添加的两个加法消息

请注意,WebAssembly 模块调用了 JavaScript 函数,就像在其他章节中一样。在本章中,你无需学习 WAT 中的任何新命令。由于 WebAssembly 1.0 无法直接与 DOM 交互,我们所有的 DOM 更改都在 JavaScript 中进行。尽管这是我们第一次使用 HTML 页面,但它并未影响 WebAssembly 的工作。WebAssembly 1.0 还是相当有限的,它对于增加计算密集型应用的性能非常有用。随着更多功能的添加,后续的 WebAssembly 版本将会有所改变。但目前,在决定哪些应用最适合使用这一新技术时,你需要牢记这些限制。

十六进制和二进制字符串

我们将继续创建第二个应用程序,该应用程序使用第五章中的函数将数字数据转换为十进制、十六进制和二进制字符串,并将它们显示在网页上。可以在wasmbook.com/hex_and_binary.html查看最终的应用程序。

HTML

HTML 基本与清单 7-6 相同,但title内容不同。创建一个名为hex_and_binary.html的文件,并添加清单 7-11 中的代码。

hex_and_binary.html(第一部分,共 3 部分)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
 content="width=device-width, initial-scale=1.0">
  <title> 1 Hex and Binary</title>
...

列表 7-11:hex_and_binary.html 文件开头的模板代码

这里的 title 标签包含 HexBinary 1。接下来,在 列表 7-12 中,我们添加 script 标签和 JavaScript 代码,这些代码将实例化并调用 WebAssembly 模块。

hex_and_binary.html(第二部分,共 3 部分)

...
  <script>
 // allocate a 64K block of memory
    const memory = new WebAssembly.Memory({ initial: 1 });
    var output = null;

 // function will change when WebAssembly module is instantiated
  1 var setOutput = (number) => {
 // this message will appear if you run the function
 // before the WebAssembly module is instantiated.
    2 console.log("function not available");
      return 0;
    };

 // This function will be called from a button click and runs
 // the setOutput function in the WebAssembly module.
  3 function setNumbers(number) {
    4 if (output == null) {
 // if page has not fully loaded return
        return;
      }

 // calling WebAssembly setOutput function generates the HTML
 // string and puts it in linear memory returning its length
    5 let len = setOutput(number);

 // we know the position and length of the HTML string in
 // linear memory so we can take it out of the memory buffer
    6 let bytes = new Uint8Array(memory.buffer, 1024, len);

 // convert the bytes taken from linear memory into a
 // JavaScript string and use it to set the HTML in output
    7 output.innerHTML = new TextDecoder('utf8').decode(bytes);
    }

  8 function onPageLoad() {
 // when the page load is complete, set the output variable
 // to the element with an id of "output"
    9 output = document.getElementById("output");
      var message_num = 0;
    }

    let importObject = {
      env: {
 buffer: memory
      }
    };

    (async () => {
 // use WebAssembly.instantiateStreaming in combination with
 // fetch instead of WebAssembly.instantiate and fs.readFileSync
      let obj = await WebAssembly.instantiateStreaming(
                        fetch('hex_and_binary.wasm'),
                        importObject);
 // reset the setOutput variable to the setOutput
 // function from the WASM module
    a setOutput = obj.instance.exports.setOutput;
         let btn = document.getElementById("set_numbers_button");
         btn.style.display = "block";
    })();

  </script>
</head>
...

列表 7-12:hex_and_binary.html 文件的 JavaScript 代码

script 标签首先创建变量 setOutput 1,并将其设置为一个箭头函数,该函数将 "function not available" 2 输出到控制台。如果用户在 WebAssembly 模块加载完成之前点击 设置数字 按钮,则会显示此消息。

接下来,我们定义了 setNumbers 3 函数,当用户点击 设置数字 按钮时会调用该函数。如果页面加载尚未完成,则在按钮点击时 output 仍为 null 4,我们将从该函数返回。然后,setNumbers 函数调用 WebAssembly 模块中的 setOutput 5,该函数从传入的数字创建一个 HTML 字符串,并返回该字符串的长度,我们将使用它从线性内存中检索字符串。我们从线性内存的 buffer 6 中获取用于创建显示字符串的 bytes

然后,output 标签的 innerHTML 7 属性被设置为从这些 bytes 生成的显示字符串,该字符串使用 TextDecoder 对象显示在网页中。

我们定义了 onPageLoad 8 函数,该函数在 body 标签加载完成后执行。该函数设置了 output 9 变量,用于显示 WebAssembly 模块输出的字符串。它还实例化了 WebAssembly 模块,并将 setOutput 变量设置为 WebAssembly 模块中的 setOutput 函数,这样我们就可以从 JavaScript 中调用 setOutput

最后,我们需要 body 标签,其中包含一个 output 标签,用于显示 WebAssembly 函数调用的输出,一个数字 input 用于接收用户输入,以及一个 button,点击后将调用 setNumbers 函数。列表 7-13 显示了该代码。

hex_and_binary.html(第三部分,共 3 部分)

...
<!-- body tag calls onPageLoad when the body load is complete -->
1 <body onload="onPageLoad()"
      style="font-family: 'Courier New', Courier, monospace;">
2 <div id="output"><!-- displays output from WebAssembly -->
    <h1>0</h1>
    <h4>0x0</h4>
    <h4> 0000 0000 0000 0000 0000 0000 0000 0000</h4>
  </div>
  <br>
 <!-- user enters input to convert to hex and binary here -->
3 <input type="number" id="val" value="0"><br><br>
 <!-- when user clicks this button, the WASM function is run -->
4 <button id="set_numbers_button" type="button" style="display:none"
  5 onclick="setNumbers( document.getElementById('val').value )">
    Set Numbers
  </button>
</body>
</html>

列表 7-13:HTML 页面中的 UI 元素

onload 1 属性告诉浏览器在 body 标签加载完成时执行 onPageLoad 函数。标签 2 <div id="output"> 是 WebAssembly 模块输出显示的位置。数字 input 标签 3,<input type="number" id="val" value="0"> 是用户输入要转换为十六进制和二进制的数字的地方。button 4 在点击时使用 onclick 5 属性调用 WebAssembly 模块。现在我们已经有了 HTML 页面,可以为此应用程序创建 WAT 文件。

WAT

该应用程序中有大量的 WAT 代码,因此我们将其分为四个部分。同时,你需要从第五章复制几个函数。创建一个名为 hex_and_binary.wat 的文件,并将 列表 7-14 中的代码添加进去。

hex_and_binary.wat(第一部分,共 4 部分)

(module
  (import "env" "buffer" (memory 1))

 ;; hexadecimal digits
1 (global $digit_ptr i32 (i32.const 128))
  (data (i32.const 128) "0123456789ABCDEF")
 ;; the decimal string pointer, length and data section
2 (global $dec_string_ptr  i32 (i32.const 256))
(global $dec_string_len  i32 (i32.const 16))
  (data (i32.const 256) "               0")

 ;; the hexadecimal string pointer, length and data section
3 (global $hex_string_ptr  i32 (i32.const 384))
(global $hex_string_len  i32 (i32.const 16))
(data (i32.const 384) "             0x0")

 ;; the binary string pointer, length and data section
4 (global $bin_string_ptr  i32 (i32.const 512))
(global $bin_string_len  i32 (i32.const 40))
(data (i32.const 512) " 0000 0000 0000 0000 0000 0000 0000 0000")

 ;; the h1 open tag string pointer, length and data section
5 (global $h1_open_ptr i32  (i32.const 640))
(global $h1_open_len i32  (i32.const 4))
(data (i32.const 640) "<H1>")

 ;; the h1 close tag string pointer, length and data section
6 (global $h1_close_ptr i32  (i32.const 656))
(global $h1_close_len i32  (i32.const 5))
(data (i32.const 656) "</H1>")

 ;; the h4 open tag string pointer, length and data section
7 (global $h4_open_ptr i32  (i32.const 672))
(global $h4_open_len i32  (i32.const 4))
(data (i32.const 672) "<H4>")

 ;; the h4 close tag string pointer, length and data section
8 (global $h4_close_ptr i32  (i32.const 688))
(global $h4_close_len i32  (i32.const 5))
(data (i32.const 688) "</H4>")

 ;; the output string length and data section
9 (global $out_str_ptr i32 (i32.const 1024))
(global $out_str_len (mut i32) (i32.const 0))

...

列表 7-14:模块开头的字符串数据定义

我们定义了一系列数据段、指针和数据长度,用于从整数数据中组装十进制、十六进制和二进制字符串。$digit_ptr 1 全局变量是指向包含 16 个十六进制数字 0 到 F 的数据段的指针,该数据段定义在线性内存位置 128。这段数据将用于三种整数到字符串的转换。我们还定义了长度和指针的全局变量,以及用于十进制 2、十六进制 3 和二进制 4 字符串的数据段。我们将使用的代码大部分来自第五章的部分内容。

接下来,我们有几个表示 HTML 标签的字符串。包含 5 个开标签和 6 个闭标签的 H1 标签指针、长度和数据段,以及 7 个开标签和 8 个闭标签的 H4 标签数据。这些字符串将用于组合我们的 HTML 输出字符串,并存储在线性内存位置 1024 9,我选择这个位置是因为它未被使用。

当我们将字符串数据复制到输出字符串时,我们需要跟踪该字符串的新长度,并将该值传递给 JavaScript;因此,我们使用全局变量 $out_str_len 来跟踪输出字符串的长度。为了避免重复包含第五章中原始函数的代码,我使用省略号 (…) 和注释来指示包含复制函数代码的清单号。请从原始清单中复制并粘贴所有六个函数的代码,清单编号为 清单 7-15。

hex_and_binary.wat(第二部分,共 4 部分)

...
1 (func $set_bin_string (param $num i32) (param $string_len i32)
  ;; $set_bin_string defined in listing 5-35
...
)

2 (func $set_hex_string (param $num i32) (param $string_len i32)
  ;; $set_hex_string defined in listing 5-30
...
) ;; end $set_hex_string

3 (func $set_dec_string (param $num i32) (param $string_len i32)
  ;; $set_dec_string defined in listing 5-24
...
)

4 (func $byte_copy
  (param $source i32) (param $dest i32) (param $len i32)
  ;; $byte_copy defined in listing 5-17
...
)

5 (func $byte_copy_i64
  (param $source i32) (param $dest i32) (param $len i32)
  ;; $byte_copy_i64 defined in listing 5-18
...
)

6 (func $string_copy
  (param $source i32) (param $dest i32) (param $len i32)
  ;; $string_copy defined in listing 5-19
...
  )
...

清单 7-15:来自第五章的重用函数

首先是数字转字符串的转换函数。$set_bin_string 1 函数将数字转换为二进制字符串。它的参数包括需要转换为二进制字符串的 i32 $num 和输出字符串的长度 $string_len,该长度包括通过空格填充的半字节(见清单 5-35)。接下来是 $set_hex_string 2 函数,它将数字和长度转换为以 0x 前缀表示十六进制数字的十六进制字符串(见清单 5-30)。然后,$set_dec_string 3 函数将数字转换为十进制字符串(见清单 5-24)。

接下来是三个复制函数,分别用于按字节、每次复制八个字节和复制字符串。每个函数都接收三个参数:$source 参数是我们要复制的源字符串,$dest 参数是我们要复制到的目标字符串,$len 是字符串的长度。首先是 $byte_copy 4 函数,它每次复制一个字节(见清单 5-17)。$byte_copy_i64 5 函数每次复制八个字节(见清单 5-18)。$string_copy 6 函数使用 $byte_copy_i64 每次复制八个字节,直到剩余少于八个字节时,再使用 $byte_copy 函数一个字节一个字节地复制剩余的字节(见清单 5-17)。

有一个最终的复制命令不在 列表 7-15 中。它是 $append_out 函数,该函数始终通过将给定的源字符串复制到当前输出字符串的末尾来附加字符串。将 列表 7-16 中的代码添加到 hex_and_binary.wat

hex_and_binary.wat(第三部分,共 4 部分)

...
 ;; append the source string to the output string
1 (func $append_out (param $source i32) (param $len i32)
 2 (call $string_copy
   (local.get $source)
     (i32.add
      (global.get $out_str_ptr)
      (global.get $out_str_len)
    )
   (local.get $len)
  )

 ;; add length to the output string length
  global.get $out_str_len
  local.get $len
  i32.add
  3 global.set $out_str_len
)
...

列表 7-16:$append_out 函数将内容附加到输出字符串。

$append_out 1 函数使用 $string_copy 2 将源字符串附加到输出字符串的末尾,然后将刚刚附加的字符串的长度加到 $out_str_len 3 中,该变量表示输出字符串的长度。

本模块的最终函数是 setOutput,它创建我们用来设置 output div 标签的字符串。它已被导出,以便可以从 JavaScript 中调用。将 列表 7-17 中的代码添加到 WAT 文件的末尾。

hex_and_binary.wat(第四部分,共 4 部分)

...
(func (export "setOutput") (param $num i32) (result i32)
 ;; create a decimal string from $num value
1 (call $set_dec_string
    (local.get $num) (global.get $dec_string_len))    
 ;; create a hexadecimal string from $num value
2 (call $set_hex_string
    (local.get $num) (global.get $hex_string_len))    
 ;; create a binary string from $num value
3 (call $set_bin_string
    (local.get $num) (global.get $bin_string_len))    

    i32.const 0
  4 global.set $out_str_len ;; set $out_str_len to 0

 ;; append <h1>${decimal_string}</h1> to output string
  5 (call $append_out
      (global.get $h1_open_ptr) (global.get $h1_open_len))
    (call $append_out
 (global.get $dec_string_ptr) (global.get $dec_string_len))
    (call $append_out
      (global.get $h1_close_ptr) (global.get $h1_close_len))

 ;; append <h4>${hexadecimal_string}</h4> to output string
  6 (call $append_out
      (global.get $h4_open_ptr) (global.get $h4_open_len))
    (call $append_out
      (global.get $hex_string_ptr) (global.get $hex_string_len))
    (call $append_out
      (global.get $h4_close_ptr) (global.get $h4_close_len))

 ;; append <h4>${binary_string}</h4> to output string
  7 (call $append_out
      (global.get $h4_open_ptr) (global.get $h4_open_len))
    (call $append_out
      (global.get $bin_string_ptr) (global.get $bin_string_len))
    (call $append_out
      (global.get $h4_close_ptr) (global.get $h4_close_len))

 ;; return output string length
  8 global.get $out_str_len
  )
)

列表 7-17:setOutput 函数已导出,可供 JavaScript 调用

列表 7-17 中 set_output 函数的前三个调用分别是 $set_dec_string 1、$set_hex_string 2 和 $set_bin_string 3。这些函数将传递给 setOutput 的数字转换为十进制字符串、十六进制字符串和二进制字符串,并存储在线性内存中。一旦这些字符串被设置,全局变量 $out_str_len 4 会被重置为 0,从而重置输出字符串,使得附加到输出字符串时会覆盖当前内存中的字符串。重置值后,我们可以开始将内容附加到输出字符串。

接下来是九次调用 $append_out,分为三组。前三次调用会附加一个开闭的 H1 标签,并将十进制 5 字符串放在其中。这将创建一个 HTML 字符串,在网页中显示十进制数值。接下来的三次调用将十六进制 6 字符串放入 H4 元素中,然后将二进制 7 字符串放入另一个 H4 元素中。最后,使用对 global.get $out_str_len 8 的调用将输出字符串的长度加载到栈中,并将其返回给调用的 JavaScript。

编译并运行

WAT 模块已完成,因此使用 wat2wasm 编译你的 hex_and_binary.wasm 文件,如 列表 7-18 所示。

wat2wasm hex_and_binary.wat

列表 7-18:使用 wat2wasm 编译 hex_and_binary.wat

验证你是否在运行 server.js,并使用地址 http://localhost:8080/hex_and_binary.html 在浏览器中打开 hex_and_binary.html

图 7-4 显示了你应在屏幕上看到的类似内容。

f07004

图 7-4:将十进制转换为十六进制和二进制

输入一个数字并试试吧。例如,在 图 7-5 中,我输入了数字 1025 并点击了设置数字。

f07005

图 7-5:将 1025 转换为十六进制和二进制

这个应用程序使用了我们在第五章中创建的几个 WebAssembly 函数,将十进制数字转换为十六进制和二进制字符串。我们添加了一些额外的功能,在 WebAssembly 模块中创建了 HTML 标签,以便我们可以将 HTML 传递给 JavaScript 并在网页上显示。正如你所看到的,从 WAT 操作字符串和操作 DOM 是相当繁琐的。如果你正在使用工具链,很多繁重的工作将为你完成。这些功能中的一部分可能会编译成 JavaScript 附加代码。

总结

WebAssembly 1.0 并不直接与用户界面交互。它的最佳应用场景是数学密集型应用。在基于 WebAssembly 构建的 Web 应用程序中与 DOM 交互时,操作 DOM 主要是 JavaScript 部分的任务。从 WebAssembly 中操作字符串完全取决于实现。尽管如此,WebAssembly 仍然是许多 Web 应用程序,特别是图形应用程序(如游戏)的绝佳选择。但在当前状态下,它并不设计为直接与 DOM 一起使用。

本章开始时,我们创建了一个简单的 JavaScript Web 服务器,并使用 Node.js 运行。你不能从文件系统加载 WebAssembly Web 应用程序,而是必须通过 Web 服务器来提供页面。我们编写了我们的第一个 WebAssembly Web 应用程序,它将两个数字相加,然后将这些数字记录到名为 output 的段落标签中。

Web 应用程序和 Node.js 应用程序之间的主要区别在于嵌入环境。Node.js 命令行应用程序完全用 JavaScript 编写,而 Web 应用程序将其 JavaScript 嵌入在 HTML 页面中。Node.js 可以直接从文件系统加载 WebAssembly 模块,而 Web 应用程序使用 instantiateStreamingfetch 从 Web 服务器流式传输 WebAssembly 模块并实例化它。Node.js 应用程序将其输出记录到控制台,而 HTML 页面则更新了 DOM 元素的 innerHTML

我们编写的第二个应用程序显示了传入 WebAssembly 模块的数字的十进制、十六进制和二进制表示。这是通过组装一个包含要在应用程序中显示的 HTML 元素的字符串来完成的。该应用程序重用了第五章中为字符串操作创建的几个函数。此应用程序中的 JavaScript 将字符串写入我们网页中一个 div 标签的 innerHTML

我们编写的这两个应用程序都不是 WebAssembly 的特别理想用例。我在本章的目标是创建我们第一个 WebAssembly Web 应用程序,而不一定是要创建那些确实适合使用 WebAssembly 的 Web 应用程序。在下一章中,我们将渲染到 HTML 画布并检查该画布上大量物体之间的碰撞检测。这些任务,通常出现在 Web 游戏中,更好地代表了 WebAssembly 1.0 能够提升你的 Web 应用程序性能的功能。

第八章:使用 Canvas

在本章中,您将学习如何使用 WebAssembly 与 HTML canvas 元素配合,在 Web 应用程序中创建快速高效的动画。我们将操作 WebAssembly 线性内存中的像素数据,然后将这些像素数据直接转移到 HTML canvas 上。我们将继续使用我们的随机碰撞体对象示例(列表 6-16),通过在 JavaScript 线性内存中生成对象,然后使用 WebAssembly 来移动这些对象、检测碰撞并进行渲染。由于可能发生碰撞的数量随着对象数量的增加呈指数增长,这种图形碰撞检测是测试 WebAssembly 功能的一个极好案例。到本章结束时,我们将有一个能够每秒测试数千个不同碰撞体之间碰撞的应用程序。在这个例子中,如果没有发生碰撞,我们的对象将绘制成绿色,如果发生碰撞,则绘制成红色。

如前所述,网页浏览器最初是为展示简单的在线文档而设计的,这意味着对文档中任何元素位置的修改通常会导致整个页面被重新渲染。这对于任何需要高帧率图形效果的应用程序(如游戏)来说,都是一个性能噩梦。此后,浏览器已发展成复杂的应用托管环境,因此需要开发一种更为复杂的渲染模型:canvas。canvas 元素由苹果公司在 2004 年为其 Safari 浏览器推出,并在 2006 年作为 HTML 标准的一部分被采纳。在 canvas 元素的范围内,Web 开发人员可以渲染 2D 图像和动画,性能比之前通过操作 DOM 所能实现的要好得多。使用 canvas 与 WebAssembly 可以帮助我们以极快的速度将动画渲染到浏览器中。

渲染到 Canvas

关于 HTML canvas API 已经写了整本书,因此我们这里只触及 WebAssembly 演示所需的一些特性。与 DOM 一样,WebAssembly 不能直接与 canvas 交互。相反,我们必须将像素数据直接从线性内存渲染到 canvas 元素上。这使我们能够用最少的 JavaScript 代码编写 canvas 应用程序。在编写 WebAssembly 代码之前,我们将先编写 HTML 和 JavaScript 部分。要查看完成后的应用程序效果,可以浏览 wasmbook.com/collide.html

在 HTML 中定义 Canvas

和往常一样,我们将把 HTML 文件分成几个部分,并逐个分析。这第一部分定义了 canvas,这是网页中渲染动画的区域。创建一个名为 collide.html 的文件,并添加 列表 8-1 中的代码。

collide.html(第一部分,共 5 部分)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Collision detection</title>
</head>
<body>
1 <canvas 2id="cnvs" 3width="512" 4height="512"></canvas>
...

列表 8-1:HTML 定义 canvas

这里需要关注的元素是canvas 1 元素。我们给canvas元素赋予idcnvs 2,以便稍后使用document.getElementById来获取 canvas 元素。我们将width 3 和height 4 设置为512,选择 512 是因为 512 是 2⁹或者十六进制 0x200。这个选择使得通过二进制逻辑处理宽度和高度更加方便,这有助于在我们合理设计代码的情况下提升应用程序的性能。

在 HTML 中定义 JavaScript 常量

在我们的 JavaScript 代码开头,我们将添加常量值,用于配置 WebAssembly 模块中的一些顶层设置。这些值将在 JavaScript 和 WebAssembly 之间共享。将这些值定义在 JavaScript 中可以更简便地更新配置。我们从一些与 canvas 相关的常量开始,这些常量设置了 WebAssembly 与 HTML canvas元素之间交互的参数。我们还定义了一组常量,用于定义我们在线性内存中组织数据的基本地址、步长和偏移量,这些常量决定了我们正在渲染的对象。此外,我们必须定义一个新的ImageData对象,该对象将线性内存缓冲区的一部分划分为一个应用程序可以直接绘制到 canvas 的对象。将清单 8-2 中的代码添加到你的 HTML 文件中。

collide.html(第二部分,共 5 部分)

...
<script>
1 const cnvs_size = 512;  // square canvas where width and height = 512

2 const no_hit_color = 0xff_00_ff_00; // no hit color (green)
  const hit_color = 0xff_00_00_ff;    // hit color (red)

 // pixels count is canvas_size x canvas_size because it's a square canvas
3 const pixel_count = cnvs_size * cnvs_size; 

4 const canvas = document.getElementById("cnvs");
5 const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, 512, 512);

 // the number of bytes needed for that pixel data is the number of pixels * 4
6 const obj_start = pixel_count * 4; // 4 bytes in every pixel.
  const obj_start_32 = pixel_count;  // 32-bit offset to the starting object
  const obj_size = 4;       // how many pixels is the square object
  const obj_cnt = 3000;     // 3000 objects
  const stride_bytes = 16;  // there are 16 bytes in each stride

  const x_offset  = 0;      // x attribute is bytes 0-3
  const y_offset  = 4;      // y attribute is bytes 4-7
  const xv_offset = 8;      // x velocity attribute is bytes 8-11
  const yv_offset = 12;     // y velocity attribute is bytes 12-15

7 const memory = new WebAssembly.Memory({initial: 80});
  const mem_i8 = new Uint8Array(memory.buffer);         // 8-bit view
  const mem_i32 = new Uint32Array(memory.buffer);       // 32-bit view

8 const importObject = {
  env: {
    buffer: memory,

    cnvs_size: cnvs_size,
    no_hit_color: no_hit_color,
 hit_color: hit_color,
    obj_start: obj_start,
    obj_cnt: obj_cnt,
    obj_size: obj_size,

    x_offset: x_offset,
    y_offset: y_offset,
    xv_offset: xv_offset,
    yv_offset: yv_offset
  }
 };

 // An ImageData object can be blitted onto the canvas
  const image_data = 
  9 new ImageData( new Uint8ClampedArray(memory.buffer, 0, obj_start),
                 cnvs_size, 
                 cnvs_size );
...

清单 8-2:在 JavaScript 中配置图像数据

我们有一个名为cnvs_size 1 的常量,保存canvas元素的高度和宽度,因为它们是相同的。接下来,我们定义了两个常量,用于定义十六进制颜色值。第一个,no_hit_color 2,定义了当对象没有与其他对象发生碰撞时的颜色。第二个,hit_color,定义了当对象与其他对象发生碰撞时的颜色。这些十六进制数字的含义在“位图图像数据”第 162 页中有更详细的说明。接着我们定义了pixel_count 3,通过将canvas_size平方得到,因为我们有一个正方形的 canvas。

接下来,我们处理 Canvas API 接口,即绘图上下文,它允许 JavaScript 与canvas进行交互。处理 HTML canvas时有几种选择。我们将使用"2d" canvas 上下文,因为它相对简单。在这里,我们通过调用document.getElementById来获取 HTML canvas 中的上下文,从而创建一个canvas 4 元素常量。然后,我们在该canvas常量上调用getContext 5 函数,以创建一个包含上下文接口的常量,我们将其命名为ctx。我们将使用这个ctx对象将 WebAssembly 生成的位图渲染到canvas元素上。

在与画布相关的常量之后,紧接着是一组与线性内存对象相关的常量。这些常量以obj_start 6 常量开头,并遵循我们在第六章中讨论的基址、步长和偏移格式。obj_start中的基地址必须指示一个地址,该地址紧随我们线性内存开始处的所有像素数据之后。我们将obj_start设置为pixel_count * 4,因为每个像素占用四个字节的数据,且对象数据紧跟在这一大小的区域之后。在这个区域中,我们使用一些常量来定义步长大小和每个对象属性的偏移量。我们定义了初始大小为 80 页的线性内存 7,足以容纳我们所需的所有对象和像素数据。然后,我们创建了该数据对象的 8 位和 32 位视图。到目前为止,我们创建的所有常量必须通过importObject 8 传入 WebAssembly 模块。

最后,我们创建一个新的ImageData 9 对象,这是一个 JavaScript 接口,我们可以通过它访问画布元素中底层的像素数据。我们在清单 8-2 中创建的Memory 7 对象有一个名为buffer的属性,这是一个包含线性内存中数据的类型化数组。buffer属性是一个数据缓冲区,可以表示画布上显示的像素数据。要创建一个新的ImageData对象,必须将memory.buffer对象作为Uint8ClampedArray传递给ImageData对象,同时传入画布的宽度和高度。

创建随机对象

接下来,我们将创建随机对象,类似于我们在本书中之前所做的那样。我们继续使用随机数据,因为它使我们能够专注于 WebAssembly,而不是数据本身。然而,WebAssembly 没有随机数功能,因此在 JavaScript 中创建我们的随机对象要简单得多。对象有四个属性:x 和 y 坐标(位置),以及 x 和 y 速度(运动)。我们使用 32 位整数来表示这些属性的值。清单 8-3 展示了创建多个对象数据的循环代码,这些对象由我们之前定义的object_cnt常量表示。

collide.html(第三部分,共 5 部分)

...
1 const stride_i32 = stride_bytes/4;
2 for( let i = 0; i < obj_cnt * stride_i32; i += stride_i32 ) {

 // value less than canvas_size
  3 let temp = Math.floor(Math.random() * cnvs_size);

 // set object x attribute to random value
  4 mem_i32[obj_start_32 + i] = temp; 

 //random value less than canvas_size
  5 temp = Math.floor(Math.random()*cnvs_size);

 // set object y attribute to random value
  6 mem_i32[obj_start_32 + i + 1] = temp; 

 // random value between -2 and 2
  7 temp = (Math.round(Math.random() * 4) - 2); 

 // set x velocity to random value
  8 mem_i32[obj_start_32 + i + 2] = temp; 

 // random value between -2 and 2
  9 temp = (Math.round(Math.random() * 4) - 2);  

 // set y velocity to random value
 a mem_i32[obj_start_32 + i + 3] = temp; 
}
...

清单 8-3:设置线性内存数据

该循环中的代码通过mem_i32中的 32 位整数视图访问线性内存中的数据。由于循环使用的是 32 位数字,我们创建了一个 32 位版本的stride_bytes,我们称之为stride_i32 1。我们将其设置为stride_bytes / 4,因为每个i32占用四个字节。for循环会一直执行,直到索引i等于obj_count中设置的对象数量乘以由stride_i32 2 定义的步长中的 32 位整数数量。这就在线性内存中创建了圆形数据结构。

在循环内部,我们将四个 32 位整数设置为随机数,这些随机数将表示每个物体的位置和速度。首先,我们设置位置属性。我们从cnvs_size中获取一个介于 0 和画布宽度 3 之间的随机数,并将其存储在线性内存中x位置属性 4 的位置。接下来,生成一个介于 0 和画布高度 5 之间的随机数,并将其存储在线性内存中y属性 6 的位置。然后,我们通过生成一个介于-2 和 2 之间的数字 7 来设置速度属性,将其存储在x速度 8 属性的位置,并对y速度 a 属性做同样的操作 9。

位图图像数据

我们可以直接将位图图像数据渲染到 HTML canvas元素中,使用putImageData函数,传入我们之前定义的ImageData对象。HTML 画布是一个像素网格;每个像素可以通过三个字节表示,其中每个字节代表一种颜色:红色、绿色和蓝色。在位图格式中,一个像素由一个 32 位整数表示,其中整数的每个字节表示一种颜色。整数的第四个字节表示alpha 值,用于像素的不透明度。当 alpha 字节为 0 时,像素完全透明;当其值为 255 时,像素完全不透明。在 WebAssembly 线性内存中,我们将创建一个 32 位整数数组,表示像素数据的数组。这种类型的数组使 WebAssembly 成为操作渲染到 HTML 画布的非常便捷的工具。

script标签中,我们将存储生成该位图数据的 WebAssembly 模块函数,并将其保存在变量animation_wasm中。我们还需要一个 JavaScript 函数来调用该 WebAssembly 函数。然后,我们调用ctx.putImageData将图像数据渲染到canvas元素中。Listing 8-4 包含了你需要添加到 HTML 文件中的下一段 JavaScript 代码。

collide.html(第四部分,共 5 部分)

...
1 var animation_wasm; // the webassembly function we will call every frame

2 function animate() {
  3 animation_wasm();
  4 ctx.putImageData(image_data, 0, 0); // render pixel data
 5 requestAnimationFrame(animate);
  }
...

Listing 8-4:JavaScript animate函数渲染动画帧。

animation_wasm 1 变量保存生成图像数据的 WebAssembly 函数。接下来的animate 2 函数调用 WebAssembly 模块的animation_wasm 3 函数,该函数生成动画的下一帧image_data。然后将image_data对象传递到对ctx.putImageData 4 的调用中,该函数将 WebAssembly 生成的图像渲染到canvas元素中。最后一个函数requestAnimationFrame 5 稍微复杂一些,我们将在下一节更详细地探讨它。

requestAnimationFrame 函数

动画是一种视觉错觉:一系列静止图像快速显示使眼睛误以为有运动发生。你曾观看过的每台电视机、电脑显示器和电影都是这样运作的。JavaScript 提供了方便的requestAnimationFrame函数:当你调用requestAnimationFrame时,传递给requestAnimationFrame的函数会在下一帧渲染时调用。为了requestAnimationFrame,我们传递了我们希望在计算机准备渲染动画帧时调用的函数。

我们在 JavaScript 的结尾调用此函数,并传递了我们在第 8-4 节中定义的animate函数。我们从animate函数的末尾第二次调用requestAnimationFrame,以在后续的帧渲染中将该函数注册为回调函数。必须进行第二次调用,因为requestAnimationFrame函数不会注册一个函数以在每次帧渲染时调用;它只在下一帧渲染时注册。animate函数需要调用 WebAssembly 模块,该模块执行碰撞检测和对象移动计算。WebAssembly 计算放置在画布上的图像数据。然而,它无法直接将该数据渲染到画布上。这就是为什么我们必须从我们的 JavaScript 动画函数中调用putImageData以将像素数据渲染到画布上。对putImageData的调用将我们设置的线性内存块移动到canvas元素上表示像素数据的区域。

第一次调用requestAnimationFrame是在代码的最后一行实例化 WebAssembly 模块之后立即进行的。第 8-5 节显示了 HTML 代码的最后部分。

collide.html (第五部分/共 5 部分)

...
(async () => {
  let obj = await
  1 WebAssembly.instantiateStreaming( fetch('collide.wasm'),
                                     importObject );
  2 animation_wasm = obj.instance.exports.main;
  3 requestAnimationFrame(4animate);
})();
</script>
</body>
</html>

第 8-5 节:实例化 WebAssembly 模块并调用requestAnimationFrame

在异步 IIFE 内部,我们首先调用instantiateStreaming 1 函数。我们将在第 8-4 节中定义的animation_wasm 2 变量设置为 WebAssembly 模块中名为main的导出函数。回想一下,我们从animate函数中调用了animation_wasm函数。最后,调用requestAnimationFrame 3 传递了之前定义的animate 4 函数。因为animate也在自身上调用requestAnimationFrame,所以浏览器每次刷新时都会调用animate

WAT 模块

现在,我们已经定义了 HTML,需要在 WAT 中编写 WebAssembly 模块,该模块将管理对象移动、碰撞检测和位图图像数据。创建名为 collide.wat 的文件。我们将尽可能简单地编写碰撞代码和画布渲染代码。为此,我们将编写多个函数,其中一些可能导致性能不佳。在下一章中,我们将尝试优化此代码。但在本章中,我们将专注于清晰和简单,而不是高性能。该模块将定义从 JavaScript 导入值的全局变量。我们需要定义一系列函数,用于清除画布、计算整数的绝对值、设置单个像素以及绘制碰撞器对象。然后,我们需要定义 main 函数,该函数将使用双循环移动每个碰撞器对象,并测试其是否与另一个对象发生碰撞。

导入的数值

模块的开头,如 Listing 8-6 中所示,通过我们在 JavaScript 中定义的 importObject 导入了传递给模块的常量。这些值包括我们的内存缓冲区、画布大小、对象颜色以及我们可以用来访问线性内存中对象的基础、偏移和步进值。

collide.wat (第一部分,共 12 部分)

(module
1 (global $cnvs_size    (import "env" "cnvs_size")    i32)

2 (global $no_hit_color (import "env" "no_hit_color") i32)
  (global $hit_color    (import "env" "hit_color")    i32)
3 (global $obj_start    (import "env" "obj_start")    i32)
4 (global $obj_size     (import "env" "obj_size")     i32)
5 (global $obj_cnt      (import "env" "obj_cnt")      i32)

6 (global $x_offset     (import "env" "x_offset")     i32)  ;; bytes 00-03
  (global $y_offset     (import "env" "y_offset")     i32)  ;; bytes 04-07
7 (global $xv_offset    (import "env" "xv_offset")    i32)  ;; bytes 08-11
  (global $yv_offset    (import "env" "yv_offset")    i32)  ;; bytes 12-15
 8 (import "env" "buffer" (memory 80))                       ;; canvas buffer
...

Listing 8-6: 声明导入的全局变量和内存缓冲区

我们首先导入全局变量 $cnvs_size 1,它在 JavaScript 中定义为 512,表示画布的宽度和高度。接下来是两个颜色值,$no_hit_color 2,表示非碰撞对象的 32 位颜色,以及 $hit_color,表示碰撞对象的颜色。请记住,我们将它们定义为绿色和红色的十六进制值。

然后,我们有一个 $obj_start 3 变量,其中包含对象数据的基本位置。$obj_size 4 变量是对象的宽度和高度(像素),将是正方形。$obj_cnt 5 变量包含应用程序将渲染并检查碰撞的对象数量。接下来是两个坐标的偏移量,$x_offset 6 和 $y_offset,以及速度值的两个属性 7,$xv_offset$yv_offset。此代码块中的最后一个 import 8 导入了我们在 JavaScript 中定义的 memory buffer

清除画布

接下来,我们将定义一个函数,用于清除整个位图图像缓冲区。如果在每次渲染帧时不清除画布,则每个对象的旧印象将保留在内存中,并且对象将在屏幕上涂抹。$clear_canvas 函数将每个颜色值设置为 0xff_00_00_00,表示黑色且完全不透明。Listing 8-7 显示了 $clear_canvas 函数的代码。

collide.wat (第二部分,共 12 部分)

...
;; clear the entire canvas
(func $clear_canvas 
  (local $i       i32)
  (local $pixel_bytes  i32)

  global.get $cnvs_size
  global.get $cnvs_size
  i32.mul                  ;; multiply $width and $height

  i32.const 4
  i32.mul                  ;; 4 bytes per pixel

1 local.set $pixel_bytes   ;; $pixel_bytes = $width * $height * 4

2 (loop $pixel_loop
  3 (i32.store (local.get $i) (i32.const 0xff_00_00_00)) 

    (i32.add (local.get $i) (i32.const 4))
  4 local.set $i           ;; $i += 4 (bytes per pixel)

 ;; if $i < $pixel_bytes
 5 (i32.lt_u (local.get $i) (local.get $pixel_bytes)) 
  6 br_if $pixel_loop ;; break loop if all pixels set
  )
)
...

Listing 8-7: $clear_canvas 函数定义

$clear_canvas 函数通过将画布大小平方(因为我们选择了一个正方形画布)来计算像素字节数,然后再乘以 4,因为每个像素使用四个字节。接下来,我们将这个值存储在局部变量 $pixel_bytes 中,这就是分配给像素内存的字节数。然后,函数遍历每个像素,存储一个十六进制值 0xff_00_00_00,其中所有像素颜色为 0,而 0xff(完全不透明)用于 alpha 值。函数接着将存储在 $i 中的索引增加 4,因为每个 i32 整数占四个字节。代码检查 $i 索引是否小于像素字节数,如果是,则跳回 loop 的顶部,因为如果 $i 小于像素数,意味着仍然有对象需要被清除。

绝对值函数

在这个应用程序中,我们将使用盒子碰撞检测策略,而不是本书之前使用的圆形碰撞检测,因为我们的对象是正方形的。我们需要切换到矩形碰撞检测算法,这要求代码找到一个有符号整数的绝对值。在 清单 8-8 中,我们将编写一个小的 $abs 函数,它可以接受一个有符号整数,并查看传入的参数是否为负数,如果是,就将其转换为正数,以获得绝对值。

collide.wat(第三部分,共 12 部分)

...
;; this function returns an absolute value when a value is passed in
(func $abs 
  (param $value       i32) 
  (result             i32)

1 (i32.lt_s (local.get $value) (i32.const 0)) ;; is $value negative?
2 if ;; if $value is negative subtract it from 0 to get the positive value
    i32.const 0
    local.get $value
  3 i32.sub
  4 return
  end
5 local.get $value  ;; return original value
)
...

清单 8-8:绝对值函数 $abs

$abs 函数首先查看传入的值,并检查该整数的符号值是否小于 0。如果小于 0,函数就会将这个数减去 0,将其取反并返回正数。如果这个数不是负数,函数将返回原始数值。

设置像素颜色

为了将对象绘制到画布上,我们需要能够在给定 x 和 y 坐标以及颜色值的情况下,在线性内存中设置像素的颜色。这个函数需要进行边界检查,因为我们正在写入一块被分配出来的线性内存区域来表示画布的区域。如果没有这个检查,如果我们尝试写入一个不在画布上的内存位置,函数就会向我们可能用于其他用途的内存区域写入数据。

函数会将坐标与画布的边界进行比较,如果这些坐标超出了边界,则返回。这决定了在何处(在线性内存中)需要更新像素数据。在查看代码之前,让我们快速了解一下画布上的坐标如何转换为线性内存。

画布是一个具有行和列的二维表面。图 8-1 显示了一个简单的画布,四个像素高,四个像素宽。

f08001

图 8-1:一个 4 × 4 的画布

每一行在画布上的纹理不同,原因将在稍后解释。画布有 x 和 y 坐标,其中第一列的 x 坐标为 0,并从左到右递增;y 坐标也从 0 开始,并从上到下递增。图 8-2 展示了我们的 4 × 4 画布及其 x 和 y 坐标。

f08002

图 8-2:4 × 4 画布的 x 和 y 坐标

这就是画布在计算机显示器上的排列方式,但计算机内存并不是按行列排列的。内存是一维的,每个像素都有一个唯一的地址。因此,我们的像素数据在内存中按图 8-3 所示的方式排列。

f08003

图 8-3:画布在线性内存中的 16 个像素

这些行是按顺序排列的,组成一个 16 个像素的数据数组。如果从 x 和 y 坐标的角度来看线性内存如何排列这些像素,它看起来像是图 8-4 所示。

f08004

图 8-4:线性内存中的 x 和 y 坐标

我们的$set_pixel函数已经有了 x 和 y 坐标,并且需要找到内存地址。我们通过公式$y * 4 + $x来计算,这样可以得到线性内存中的值,如图 8-5 所示。

f08005

图 8-5:从 x、y 坐标到线性内存的转换公式

一旦获取到内存位置,我们就可以使用i32.store更新线性内存,将该地址处的值设置为参数$c的颜色值。代码清单 8-9 展示了源代码。

collide.wat(第四部分,共 12 部分)

...
;; this function sets a pixel at coordinates $x, $y to the color $c
(func $set_pixel
  (param $x       i32)    ;; x coordinate
  (param $y       i32)    ;; y coordinate
  (param $c       i32)    ;; color value

 ;; is $x > $cnvs_size
  1 (i32.ge_u (local.get $x) (global.get $cnvs_size)) 
  if    ;; $x is outside the canvas bounds
    return
  end

2 (i32.ge_u (local.get $y) (global.get $cnvs_size))  ;; is $y > $cnvs_size
  if    ;; $y is outside the canvas bounds
    return
  end

  local.get $y
  global.get $cnvs_size
3 i32.mul

  local.get $x
4 i32.add       ;; $x + $y * $cnvs_size (get pixels into linear memory)

  i32.const 4
5 i32.mul       ;; multiply by 4 because each pixel is 4 bytes

  local.get $c  ;; load color value

6 i32.store     ;; store color in memory location
)
...

代码清单 8-9:设置单个像素为给定颜色的函数

这个函数首先进行边界检查,以防用户尝试设置不在画布上的像素颜色。为了验证 x 坐标是否在画布的范围内,我们检查$x是否大于$cnvs_size 1,如果是,则返回函数并不更新内存。我们对 y 坐标 2 也做相同的检查。

在进行边界检查后,我们需要以整数形式获取目标像素的位置。我们通过将$y乘以$cnvs_size 3 来得到像素前面行的内存中像素的数量,然后再加上4乘以$x的值。因为位置值是以 32 位整数(每个像素四个字节)表示的,我们需要将该值乘以4 5,才能得到我们像素在线性内存中的字节位置。这个内存位置就是我们通过调用i32.store 6 语句存储$c的地方。

绘制对象

如果碰撞体对象与其他对象没有碰撞,它们将显示为绿色方块;如果发生碰撞,则显示为红色方块。我们在 JavaScript 代码的常量部分设置了这些方块的大小为 4,因此每个方块的宽度和高度都是四个像素。我们使用一个循环绘制这些像素,该循环会递增 x 值,直到它达到对象的位置加上宽度。这样就绘制了第一行像素。当 x 坐标值超过最大 x 时,代码会递增 y 坐标值。接着,我们绘制第二行像素,并重复此过程,直到超过该对象像素的最大 y 值。然后代码跳出循环。最终我们得到一个 4 × 4 像素的对象。让我们将此函数的代码添加到我们的 WAT 文件中,如 Listing 8-10 所示。

collide.wat(第五部分,共 12 部分)

...
;; draw multi pixel object as a square given coordinates $x, $y and color $c
(func $draw_obj 
  (param $x i32)    ;; x position of the object
  (param $y i32)    ;; y position of the object
  (param $c i32)    ;; color of the object     

  (local $max_x       i32)
  (local $max_y       i32)

  (local $xi          i32)
  (local $yi          i32)

  local.get $x
1 local.tee $xi
  global.get $obj_size
  i32.add
2 local.set $max_x        ;; $max_x = $x + $obj_size

  local.get $y
  local.tee $yi
  global.get $obj_size
  i32.add
3 local.set $max_y        ;; $max_y = $y + $obj_size

  (block $break (loop $draw_loop 

    local.get $xi
    local.get $yi
    local.get $c
  4 call $set_pixel     ;; set pixel at $xi, $yi to color $c

    local.get $xi
    i32.const 1
    i32.add
  5 local.tee $xi       ;; $xi++

    local.get $max_x
  6 i32.ge_u            ;; is $xi >= $max_x

    if
      local.get $x
    7 local.set $xi     ;; reset $xi to $x

      local.get $yi
      i32.const 1
      i32.add
    8 local.tee $yi     ;; $yi++

      local.get $max_y
    9 i32.ge_u          ;; is $yi >= $max_y

 br_if $break

    end
    br $draw_loop
  ))
)
...

Listing 8-10: $draw_obj 函数通过调用 $set_pixel 函数绘制一个像素方块。

$draw_obj 函数以 param i32 变量 $x$y$c 的形式接收参数,分别表示 x 坐标、y 坐标和颜色。该函数从 $x 位置开始绘制像素点,x 坐标为 $x,y 坐标为 $y。它需要循环遍历每个像素,直到达到 $max_x$max_y 的 x 坐标和 y 坐标。函数首先通过使用 local.tee 1 将 $xi 的值设置为传递给函数的 $x 值。然后,它将对象的大小($obj_size)加到此值中,以找到 $max_x 2 的值。之后,函数以相同的方式找到 $max_y 3。

我们首先找出起始和结束的 x 坐标,然后对 y 轴 4 做相同的操作。我选择了 512 作为画布的宽度和高度,因为我假设这种掩码比使用 i32.rem_u 来进行画布边界检查具有更好的性能。在第九章中,我们将测试这个假设,看看这是一个有效的假设,还是一个过早的优化。第四章详细讲解了位掩码的工作原理。

最小值和最大值的 xy 进入一个循环,使用 call $set_pixel 表达式 5 来绘制每个像素。循环增加 $xi 6,并将其与 $max_x 进行比较,如果 $xi 大于或等于 $max_x 7,则将 $xi 重置为 $x,并将 $yi 9 增加 1。然后,当 $yi 超过 $max_y 时,物体完全绘制完毕,代码退出循环。

设置和获取对象属性

让我们创建一些辅助函数,用于在线性内存中设置和获取对象属性值。这些函数接收一个对象编号和一个属性偏移,并返回该对象在内存中的属性值。在 $set_obj_attr 函数中,它还接收一个值,并将该值设置为对象属性。在 $get_obj_attr 函数中,它返回该对象和属性在内存中的值。将 Listing 8-11 中的 $set_obj_attr 代码添加到您的 WAT 模块中。

collide.wat(第六部分,共 12 部分)

...
;; set the attribute of an object in linear memory using the object number,
;; the attributes offset and a value used to set the attribute
(func $set_obj_attr
  (param $obj_number  i32)
  (param $attr_offset i32)
  (param $value       i32)

  local.get $obj_number

 i32.const 16
1 i32.mul                 ;;  16 byte stride multiplied by the object number 

  global.get $obj_start   ;;  add the starting byte for the objects (base)
2 i32.add                 ;;  ($obj_number*16) + $obj_start

  local.get $attr_offset  ;; add the attribute offset to the address
3 i32.add                 ;; ($obj_number*16) + $obj_start + $attr_offset

  local.get $value

 ;; store $value at location ($obj_number*16)+$obj_start+$attr_offset
4 i32.store  
)
...

Listing 8-11: 基于步幅值为 16 在内存中设置一个对象

这个函数中的大部分代码计算了存储特定对象属性的线性内存地址。回顾第六章,我们通过基址(本函数中的 $obj_start)、步幅值 16、对象编号($obj_number)和属性偏移量($attr_offset)计算了属性的内存位置。该函数使用公式 $obj_number*16 1 + $obj_start 2 + $attr_offset 3 来确定我们想要修改的属性的内存位置。然后它调用 i32.store 4 语句将该值存储到内存中。

接下来,我们将创建相应的 $get_obj_attr,它需要计算属性值的地址。你可能已经熟悉软件开发原则 不要重复自己 (DRY)。DRY 代码是一种编写可维护代码的优秀方法,易于其他开发者阅读和更新。不幸的是,有时候 DRY 代码可能会降低性能。在这个示例中,我们将重新做一些在前一个函数中做过的计算。当我们进入性能优化章节时,我们将使代码变得比本章更不 DRY(有时称为 湿代码)。一些能产生 DRY 代码的技术可能会增加需要额外计算周期的抽象层。例如,函数调用需要额外的周期来将值推送到栈上并跳转到代码中的新位置。优化编译器通常能够减轻代码中使用的抽象的影响,但理解它们如何做到这一点以及为什么 DRY 代码在执行时不总是最有效的,仍然是有帮助的。

在其他汇编语言中,宏是保持性能的同时保持代码相对 DRY 的好方法。不幸的是,wat2wasm 当前不支持宏。 Listing 8-12 展示了 $get_obj_attr 函数的代码。

collide.wat(第七部分,共 12 部分)

...
;; get the attribute of an object in linear memory using the object
;; number, and the attributes offset
(func $get_obj_attr
  (param $obj_number  i32)
 (param $attr_offset i32)
  (result i32)

  local.get $obj_number
  i32.const 16
1 i32.mul                  ;; $obj_number * 16

  global.get $obj_start
2 i32.add                  ;; ($obj_number*16) + $obj_start

  local.get $attr_offset
3 i32.add                  ;; ($obj_number*16) + $obj_start + $attr_offset

4 i32.load                 ;; load the pointer above
 ;; returns the attribute
)
...

Listing 8-12:根据步幅值 16 获取内存中的对象

要获取一个对象的偏移字节,我们首先将 $obj_number 乘以 16 的步幅值 1。然后我们加上 2 基址,该基址存储在全局变量 $obj_start 中。接着,添加偏移量,该偏移量存储在 $attr_offset 3 中。此时,栈顶包含了我们想要获取的属性在内存中的位置,因此调用表达式 i32.load 4 会将该值推送到栈上。

$main 函数

$main 函数将在每一帧渲染时从 JavaScript 代码中被调用。它的任务是根据对象的速度移动每个对象,检测对象之间的碰撞,如果对象与其他对象发生碰撞,则将其渲染为红色,如果没有,则渲染为绿色。由于 $main 函数非常长,因此我们将其分为几个部分。

定义本地变量

$main 函数的第一部分,如 Listing 8-13 所示,定义了所有本地变量并调用 $clear_canvas 函数。

collide.wat(第八部分,共 12 部分)

...
;; move and detect collisions between all of the objects in our app
(func $main (export "main")
1 (local $i           i32) ;; outer loop index
  (local $j           i32) ;; inner loop index
2 (local $outer_ptr   i32) ;; pointer to outer loop object
  (local $inner_ptr   i32) ;; pointer to inner loop object

3 (local $x1          i32) ;; outer loop object x coordinate
  (local $x2          i32) ;; inner loop object x coordinate
  (local $y1          i32) ;; outer loop object y coordinate
  (local $y2          i32) ;; inner loop object y coordinate

4 (local $xdist       i32) ;; distance between objects on x axis
  (local $ydist       i32) ;; distance between objects on y axis

5 (local $i_hit       i32) ;; i object hit boolean flag
6 (local $xv          i32) ;; x velocity
  (local $yv          i32) ;; y velocity

7 (call $clear_canvas) ;; clear the canvas to black
...

Listing 8-13:$main 函数的开头声明了局部变量并清除了画布。

该函数包含一个双重循环,用于将每个对象与其他所有对象进行比较。为此,我们定义了两个循环变量,分别是 $i$j,它们将作为外层和内层循环的计数器。我们需要使用 $i 变量循环遍历每个对象,并用 $j 变量与其他每个对象进行比较。然后,我们使用两个指针变量,$outer_ptr$inner_ptr,分别指向这两个碰撞对象的线性内存位置。

接下来的四个局部变量是内层和外层循环对象的 xy 坐标。两个对象的 xy 坐标之间的距离存储在 $xdist$ydist 局部变量中。$i_hit 局部变量是一个布尔标志,如果 $i 对象与其他对象发生碰撞,则设置为 1,否则设置为 0。两个变量 $xv$yv 存储 $i 对象的速度。声明了这些局部变量后,函数执行第一个操作:使用语句 (call $clear_canvas) 将画布的所有像素清除为黑色。

$move_loop

$main 函数的下一部分定义了一个循环,每一帧都将线性内存中的每个对象移动。这部分代码将获取 $i 对象的 $x$y$xv$yv 属性。$xv$yv 变量分别是 xy 速度变量,用于通过改变 $x$y 坐标值来移动对象。代码还强制 xy 值保持在画布的边界内。移动循环之后,$i 变量会被重置为 0。将代码添加到 Listing 8-14 中的 WAT 模块。

collide.wat(第九部分,共 12 部分)

...
1 (loop $move_loop
    ;; get x attribute
    (call $get_obj_attr (local.get $i) (global.get $x_offset))
    local.set $x1

    ;; get y attribute
    (call $get_obj_attr (local.get $i) (global.get $y_offset)) 
    local.set $y1

    ;; get x velocity attribute
    (call $get_obj_attr (local.get $i) (global.get $xv_offset)) 
    local.set $xv
 ;; get y velocity attribute
    (call $get_obj_attr (local.get $i) (global.get $yv_offset)) 
    local.set $yv

    ;; add velocity to x and force it to stay in the canvas bounds
  2 (i32.add (local.get $xv) (local.get $x1))
    i32.const 0x1ff  ;; 511 in decimal
  3 i32.and          ;; clear high-order 23 bits
    local.set $x1

    ;; add velocity to y and force it to stay in the canvas bounds
    (i32.add (local.get $yv) (local.get $y1))
    i32.const 0x1ff  ;; 511 in decimal
    i32.and          ;; clear high-order 23 bits
  4 local.set $y1

    ;; set the x attribute in linear memory
  5 (call $set_obj_attr 
    (local.get $i) 
    (global.get $x_offset)
    (local.get $x1)
  )

    ;; set the y attribute in linear memory
    (call $set_obj_attr 
    (local.get $i) 
    (global.get $y_offset)
    (local.get $y1)
  )

  local.get $i
  i32.const 1
6 i32.add
7 local.tee $i        ;; increment $i

  global.get $obj_cnt
  i32.lt_u            ;; $i < $obj_cnt

8 if  ;; if $i < $obj_count branch back to top of $move_loop
    br $move_loop
  end

 )

  i32.const 0
9 local.set $i
...

Listing 8-14:移动每个对象的循环

Listing 8-14 中的代码遍历所有对象,基于 xy 速度改变它们的 xy 坐标。循环的前几行调用 $get_obj_attr 来获取 x 位置、y 位置、x 速度和 y 速度属性,并传入循环索引和需要设置的属性偏移量。这样做会将属性值推送到栈上。接着,使用 local.set 表达式来设置我们将在 loop 中使用的局部变量。

The `loop` will add the velocity variables (`$xv` and `$yv`) to the position variables `($x1` and `$y1`) with a call to `i32.add` 2. Before `$x1` is set to the new position, an `i32.and` 3 is used to mask the last nine bits of the `x` position. This holds the value of the x-coordinate between 0 and 511, wrapping the value back around to 0 if it exceeds 511\. The same is then done for `$y1` 4 to set the `y` position. Once the new values of `$x1` and `$y1` are set, the `$set_obj_attr` 5 function is called for those values to set them in linear memory. The loop counter `$i` 6 is incremented with a call to `i32.and` and `local.tee` 7. If the value in `$i` is less than the object count (`$obj_count` 8), the code branches back to the top of the `loop`. Otherwise, `local.set` 9 is called to set `$i` to `0`. The squares in Listing 8-14 wrap around on the canvas so when one goes offscreen on the right, it loops back around and appears on the left side of the screen, as in the old-school arcade games. Games like Atari’s *Asteroids* and Namco’s *Pac-Man* didn’t need any extra code to have this effect; instead, their screens were 256 pixels wide and used an 8-bit number to store the x-coordinate. So if a game object had an x-coordinate of 255 and moved one pixel to the right, the single byte value would roll back over to 0, and the game object would reappear on the left side of the screen, as shown in Figure 8-6. ![f08006](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08006.png) Figure 8-6: Player’s character goes off the screen to the right and appears on the left. We can accomplish this effect using `i32.and` to mask all but the bottom nine bits. The code calls `$get_obj_attr` to get the attributes for `$x1`, `$y1`, `$xv`, and `$yv` 1. The new `$x1` value is calculated by adding `$xv` 2 and then using the `i32.and` 3 against the constant `0x1ff`. In binary, `0x1ff` has the lowest nine bits all set to 1, and all the higher bits are set to 0\. Chapter 4 explained how to use an `i32.and` to set specific bits to 0, as illustrated in Figure 8-7. ![f08007](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08007.png) Figure 8-7: The 0s in our mask turn any values in the initial value to 0. When we call `i32.and` 3 on `$x1` and `0x1ff` (binary 00000000000000000000000111111111), the resulting value has the top 23 bits set to 0\. That limits the `$x1` value to nine bits, so if the number goes above what a 9-bit number can hold, the `$x1` value rolls back over, similar to an odometer. That creates an old-school arcade effect where objects that move offscreen to the left reappear on the right, and objects that move offscreen on the top reappear on the bottom of the screen. After making changes to the x- and y-coordinates, the `$i` index is incremented 8, and if `$i` is less than `$obj_count`, the code branches back to the top of the `$move_loop`. When the loop is complete, the `$i` 9 index variable is reset to `0`. #### Beginning of the Outer Loop Now we need to define our double loop, which compares every object against every other object to see whether any collisions have occurred. The next part of the `$main` function defines the outer loop of our double loop, which will determine the first object of our collision test. The loop begins by initializing `$j` to `0`. The `$i` local variable is the increment variable for the outer loop. The inner loop will use `$j` to loop through all the objects to check each one against `$i` until it finds a collision or makes its way through every object. The outer loop starts with the first object, and then the inner loop checks that object for a collision with every other object. This continues until the outer loop has checked every object. Add the code in Listing 8-15 to the `$main` function. **collide.wat (part 10 of 12)** ``` ... (loop $outer_loop (block $outer_break i32.const 0 1 local.tee $j ;; setting j to 0 ;; $i_hit is a boolean value. 0 for false, 1 for true 2 local.set $i_hit ;; setting i_hit to 0 ;; get x attribute for object $i (call $get_obj_attr (local.get $i) (global.get $x_offset)) 3 local.set $x1 ;; get y attribute for object $i (call $get_obj_attr (local.get $i) (global.get $y_offset)) 4 local.set $y1 ... ``` Listing 8-15: The outer loop of a collision detection double loop The beginning of the loop resets `$j` 1 and `$i_hit` 2 to `0`. The code then calls the `$get_obj_attr` function to find the values for `$x1` 3 and `$y1` 4. #### The Inner Loop The next section of the `$main` function is the inner loop, whose function is to detect a collision between two squares. Square collision detection is very simple: you compare the x-coordinates and size to see whether the objects overlap on the x-axis. If the x-axis doesn’t overlap but the y-axis does, it looks like Figure 8-8 and has no collision. If the x-axis overlaps but the y-axis doesn’t, there is no collision between the objects, as in Figure 8-9. ![f08008](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08008.png) Figure 8-8: The x-axis doesn’t overlap but the y-axis does. ![f08009](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08009.png) Figure 8-9: The x-axis overlaps but the y-axis doesn’t. The only collision scenario is when the x-axis and y-axis overlap, as in Figure 8-10. ![f08010](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08010.png) Figure 8-10: The x-axis and y-axis overlap, so there is a collision. The inner loop performs this check against every object in linear memory. Listing 8-16 shows the code. **collide.wat (part 11 of 12)** ``` ... 1 (loop $inner_loop (block $inner_break local.get $i local.get $j i32.eq 2 if ;; if $i == $j increment $j local.get $j i32.const 1 i32.add local.set $j end local.get $j global.get $obj_cnt i32.ge_u 3 if ;; if $j >= $obj_count break from inner loop br $inner_break end ;; get x attribute 4 (call $get_obj_attr (local.get $j)(global.get $x_offset)) local.set $x2 ;; set the x attribute for inner loop object ;; distance between $x1 and $x2 (i32.sub (local.get $x1) (local.get $x2)) call $abs ;; distance is not negative so get the absolute value 5 local.tee $xdist ;; $xdist = the absolute value of ($x1 - $x2) global.get $obj_size i32.ge_u 6 if ;; if $xdist >= $obj_size object does not collide local.get $j i32.const 1 i32.add local.set $j br $inner_loop ;; increment $j and jump to beginning of inner loop end ;; get y attribute (call $get_obj_attr (local.get $j)(global.get $y_offset)) local.set $y2 (i32.sub (local.get $y1) (local.get $y2)) call $abs 7 local.tee $ydist global.get $obj_size i32.ge_u 8 if local.get $j i32.const 1 i32.add local.set $j br $inner_loop end i32.const 1 9 local.set $i_hit ;; exit the loop if there is a collision )) ;; end of inner loop ... ``` Listing 8-16: The inner loop of a collision detection double loop The inner `loop` 1 compares the current object of the outer loop against every other object that hasn’t already been checked against it. First, it needs to make sure it’s not checking an object against itself. An object always completely collides with itself, so if `$i` is equal to `$j` 2, we ignore it and need to increment `$j`. Then we compare `$j` with `$obj_cnt` 3 to see whether the code has tested all the objects yet. If it has, the code exits the inner `loop` . If the code hasn’t tested all objects, the `x` attribute is loaded into the `$x2` variable by calling `$get_obj_attr` 4. We then get the distance between `$x1` and `$x2` by subtracting `$x2` from `$x1`, taking the absolute value and setting the `$xdist` 5 variable. We compare the `x` distance between the two objects to the object size to see whether the two objects overlap on the x-axis. If they don’t overlap because `$xdist` is greater than `$obj_size` 6, the code increments `$j` and jumps back to the top of the `loop`. In the same way, the `y` distance is calculated and stored in the `$ydist` 7 variable, and the code checks whether the `$ydist` variable is greater than `$obj_size` 8. If so, these objects don’t collide, so we increment `$j` and jump back to the top of the `loop`. If we haven’t jumped to the top of the loop at this point, we know that the x- and y-axis of the objects overlap, indicating a collision, so we set `$i_hit` 9 to `1` and exit the inner `loop`. #### Redrawing the Objects When the code has exited the inner loop, either a collision was found or it wasn’t. The code checks the hit variable (`$i_hit`) and calls the `$draw_obj` function with the no collision color (green) if there wasn’t a collision and with the `$hit_color` (red) if there was a collision. Then `$`i is incremented, and if `$i` is less than the number of objects, the code jumps back to the top of the outer loop. Listing 8-17 shows the last section of code to add to the WAT file. **collide.wat (part 12 of 12)** ``` ... local.get $i_hit i32.const 0 i32.eq 1 if ;; if $i_hit == 0 (no hit) (call $draw_obj (local.get $x1) (local.get $y1) (global.get $no_hit_color)) 2 else ;; if $i_hit == 1 (hit) (call $draw_obj (local.get $x1) (local.get $y1) (global.get $hit_color)) end local.get $i i32.const 1 i32.add 3 local.tee $i ;; increment $i global.get $obj_cnt i32.lt_u 4 if ;; if $i < $obj_cnt jump to top of the outer loop br $outer_loop end 5 )) ;; end of outer loop ) ;; end of function ) ;; end of module ``` Listing 8-17: Drawing the object inside the inner loop Immediately after the inner loop ends, the outer loop checks whether the `$i_hit` variable was set to `0` 1 , indicating no collision. If it was, `$draw_obj` is called, passing in the `$no_hit_color` global variable as the last parameter and drawing the square in green. If the `$i_hit` variable is set to `1` 2 (true), `$draw_obj` is called with `$hit_color` (red). At this point, `$i` 3 is incremented, and if it’s less than `$obj_cnt` 4, indicating we’ve not completed drawing our objects, the code jumps back to the top of the loop. If not 5, the code exits the loop and this function is complete. ### Compiling and Running the App Before we run the collider application, we need to compile our WAT into a WebAssembly module. Use the following `wat2wasm` command to compile *collide.wat* into *collide.wasm*: ``` wat2wasm collide.wat ``` When you run a web server and open the *collide.html* file in a web browser from localhost, your screen should look similar to Figure 8-11. ![f08011](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/art-wasm/img/f08011.png) Figure 8-11: The collider app The boxes should move about the canvas, appearing on the left side of the canvas when they move off the right side and on the top when they move off the bottom. Boxes that collide with each other appear in red, and boxes that don’t collide with other boxes appear in green. ## Summary In this chapter, we explored how WebAssembly and the HTML canvas can work together to create fantastic animations on a web page. You should now understand when it’s best to use WebAssembly and when using JavaScript is a better option. Rendering to the canvas from WebAssembly can be done quickly and efficiently by modifying memory locations that represent bitmap data. The collider app we created used many JavaScript constants to define its details. That allowed us to tweak and play with the numbers in the app without recompiling the WebAssembly module. JavaScript can easily generate random numbers that we can use in the app. Generating random numbers from WebAssembly is much more challenging. Because the random numbers only need to be generated once, using JavaScript isn’t a significant performance hit. You learned about bitmap image data and how to use WebAssembly to generate that image data inside linear memory. We used the `requestAnimationFrame` function from within JavaScript to call the WebAssembly module once per frame, using WebAssembly to generate the bitmap image data to be used in the canvas. This image data is moved into the HTML canvas using the `putImageData` function on the canvas context. In the WAT module code, we set up and modified an area of memory dedicated to the image data, and created a canvas clearing function. We drew a specific pixel color to the canvas; then we drew objects larger than a single pixel and made those objects appear at the opposite side of the canvas when they moved out of the element’s bounds. Lastly, we used box collision detection to detect and change the color of our objects if there were collisions between objects. We didn’t spend a lot of time or effort trying to make the collider application fast. In the next chapter, we’ll make this application run as fast as possible with some performance tuning.

第九章:性能优化

本章专门面向那些希望实现极速应用程序并愿意为此付出时间和精力的开发人员。我们将首先讨论性能分析工具,评估 WebAssembly 模块性能,并研究如何将 WebAssembly 的性能与类似的 JavaScript 代码进行比较。我们还将探讨一些提升 WebAssembly 性能的策略,包括内联函数、用位移操作替代乘法和除法、合并常量,以及使用死代码消除(DCE)删除冗余代码。

我们还将探讨确定模块性能的其他方法:我们将使用console.logDate.now来衡量应用程序的性能,并使用测试工具benchmark.js来收集应用程序的详细性能数据。然后,为了好玩,我们将打印出 Chrome JavaScript V8 引擎的中间表示(IR)字节码。JavaScript IR 字节码可以帮助你了解 JavaScript 函数的工作原理,这对于评估是否使用 WebAssembly 或 JavaScript 编写函数非常有帮助。

使用性能分析器

性能分析器是用来分析应用程序性能的工具,包括应用程序的内存使用和执行时间。这可以帮助你做出关于优化的决策,确定优化的方向和重点。你通常需要在不同类型的优化之间做出权衡。例如,你需要决定是优先提升交互时间(TTI),以便用户能够尽快使用你的应用程序,还是在应用程序运行时优化峰值性能。如果你正在开发一款游戏,可能需要较长的加载时间,以确保游戏在下载完成后能够顺畅运行。然而,在线商店可能更倾向于确保用户尽快与网站进行交互。在大多数情况下,你需要在这两者之间找到平衡,使用性能分析器可以帮助你做出这个决策。

性能分析器还非常高效地帮助你发现代码中的瓶颈,帮助你将时间和精力集中在这些关键区域。我们将关注 Chrome 和 Firefox 的性能分析器,因为它们目前对 WebAssembly 提供了最好的支持。我们将对第八章中的碰撞检测应用进行性能分析。

Chrome 性能分析器

使用 Chrome 性能分析器时,你需要使用一个新的隐身浏览器窗口。隐身窗口不会加载网站缓存、Cookies 或 Chrome 插件,这些都会在分析时引发问题,因为它们会运行额外的 JavaScript 代码并影响你想要分析的站点性能。缓存和 Cookies 通常问题不大,但它们会把与你分析的代码无关的数据引入你的环境。你可以通过点击浏览器右上角菜单中的新建隐身窗口来打开隐身窗口,如图 9-1 所示。

f09001

图 9-1:在 Chrome 中打开隐身窗口。

打开隐身浏览器窗口后,确保在命令行中使用node server.js命令运行一个 Web 服务器,然后在浏览器中输入localhost:8080/collide.html。从右上角菜单中点击更多工具开发者工具,如图 9-2 所示。

f09002

图 9-2:在 Chrome 中打开开发者工具。

你应该能看到开发者工具顶部的多个标签。要查看性能分析器,点击性能,如图 9-3 所示。

f09003

图 9-3:在 Chrome 中打开性能标签。

性能标签在你第一次打开时提供两个选项:记录和重新加载。记录按钮会在不重新加载应用程序的情况下开始录制性能数据。这种性能分析最适用于你不太关心应用程序启动时间,而更关注峰值性能的情况。在我们开始性能分析之前,确保在性能标签的顶部勾选了内存复选框。如果没有勾选,内存堆将不会被分析。如果你希望从应用初始化开始进行性能分析,可以点击重新加载按钮。点击记录继续。一旦录制了大约五秒钟,点击停止,如图 9-4 所示。

f09004

图 9-4:在 Chrome 中录制性能数据

当录制停止后,性能分析器会打开,并展示应用程序渲染的每一帧记录。底部的摘要标签显示,应用程序执行的大部分时间都花费在了脚本执行(图 9-5),这包括 JavaScript 和 WebAssembly 时间。

f09005

图 9-5:录制性能数据后的 Chrome 性能标签

在这个主要的性能页面中,一个饼图展示了在脚本执行、渲染、绘制、系统和空闲时间中花费的处理时间。饼图上方是一些标签,包括摘要、从下到上、调用树和事件日志,我们将在本章中进行探讨。以上这些标签的上方显示了分配的 JS 堆内存,此外,还有渲染的帧数、CPU 和 FPS 信息。接下来,我们将快速查看 JavaScript 堆内存。

JavaScript 堆内存

图 9-5 中的分析显示堆内存有增长。我们将花些时间调查为什么会发生这种情况。首先,我们检查在垃圾回收前分配了多少内存。一些开发者认为,因为 JavaScript 是一种垃圾回收语言,所以他们不需要担心内存问题。不幸的是,这并非如此;你的代码仍然可能创建对象的速度超过垃圾回收器能回收它们的速度。也有可能将对象的引用保持得比实际需要的时间更长,这样 JavaScript 无法判断是否应该删除这些对象。如果一个应用程序的内存增长速度像这个一样快,那么监控垃圾回收前分配的内存是很有意义的。然后,尝试理解应用程序在哪里分配了内存。目前,堆内存的大小大约是 1MB。

经过一些额外的性能分析后,我们可以看到 JS 堆内存增长到 2.2MB,而在垃圾回收器运行后,堆内存大小降回到 1.2MB。垃圾回收器运行可能需要几分钟,所以请耐心等待。图 9-6 显示了垃圾回收过程中 JS 堆内存的变化情况。如你所见,在图表的右侧,堆内存大小突然显著下降了 1MB。

f09006

图 9-6:垃圾回收期间的内存下降

最好精确地确定这个内存分配发生的位置,因为如果我们能减缓堆内存的增长,它有可能减少垃圾回收器的负担。

内存分配跟踪

由于堆内存的增长是持续的,我们可以推测内存分配可能发生在每一帧的渲染过程中。这个应用程序的大部分工作都在 WebAssembly 模块中完成,因此我们首先注释掉 WebAssembly 的调用,看看内存是否继续显示相同的 JS 堆增长分析。打开 collide.html,并如 清单 9-1 所示,注释掉 animate 函数内的 animation_wasm() 调用。

collide.html

...
    function animate() {
    **//      animation_wasm();**
      ctx.putImageData(image_data, 0, 0); // render pixel data
      requestAnimationFrame(animate);
    }
...

清单 9-1:注释掉 animation_wasm 函数调用

现在重新加载页面并记录一个新的性能分析。图 9-7 显示了没有 animation_wasm 函数调用的新的 JS 堆内存分析。

f09007

图 9-7:animation_wasm 函数移除后的堆内存分配图

没有对 WebAssembly 模块的调用后,应用程序无法正常运行。然而,你仍然可以看到相同的 JS 堆内存增长分析,因此内存的增长似乎不是来自 WebAssembly 模块。接下来,我们取消注释 WebAssembly 模块的调用,然后注释掉 ctx.putImageData 的调用,并创建另一个性能分析,正如 清单 9-2 所示。

collide.html

 function animate() {
      animation_wasm();
  **//**   **ctx.putImageData(image_data, 0, 0);** // render pixel data
      requestAnimationFrame(animate);
    }

清单 9-2:animation_wasm 函数已恢复,putImageData 被移除。

在注释掉 ctx.putImageData 调用后,我们现在可以创建一个新的分析结果来检查内存增长情况(图 9-8)。

f09008

图 9-8:移除 putImageData 调用时,内存增长变慢。

在没有 ctx.putImageData 调用的情况下,内存增长明显变慢。虽然增长依然存在,但其增长呈现较慢的阶梯型模式,而不是几乎垂直的直线。这表明,ctx.putImageData 调用内部可能正在创建一些大型对象,垃圾回收器最终需要删除它们。现在我们知道内存是如何分配的。由于 ctx.putImageData 是一个内置函数,我们无法优化它。如果内存分配是问题所在,我们就需要寻找其他方式来渲染到画布上。

在分析器窗口中,堆内存上方有一个区域,提供更多的性能信息,包括渲染的每秒帧数(fps)。它还显示了一个显示 CPU 使用率的图表,并展示了每一帧渲染的小缩略图。当你将鼠标悬停在这些帧上时,你可以观察到应用程序如何渲染其动画(图 9-9),这对你调试应用程序是否按预期工作非常有帮助。

f09009

图 9-9:在分析器中查看单独的帧渲染

你可以将鼠标悬停在绿色的Frames框上,查看分析中任意时刻的 fps(图 9-10)。

f09010

图 9-10:在分析器中查看 fps

如你所见,在应用程序执行的这一时刻,帧率为 18 fps。当我们拖动帧时,帧数在 17 和 20 之间徘徊。每秒帧数是碰撞检测应用性能的主要衡量标准,因此我们需要记住大约 18 fps 的分析结果,以便与之后的结果进行比较。请记住,运行分析器似乎会影响应用程序的性能,因此尽管这些结果在相互比较时很有用,但它们可能并不完全准确地反映应用程序在实际环境中的表现。

自下而上

自下而上的标签显示了应用程序内调用的函数、它们运行的总时间以及自时间(Self Time),即函数运行的时间,排除了它调用的其他函数所花费的时间。自时间非常有用,因为调用其他运行时间较长的函数的函数,总时间总是会比较长,正如你在图 9-11 中看到的那样。

f09011

图 9-11:Chrome 的自下而上标签窗口

<wasm-unnamed>的自时间是最长的。总时间在多个函数中更长,例如animate,因为animate函数调用了 WebAssembly 模块。令人有些失望的是,Chrome 没有指出它在 WebAssembly 模块中调用了哪个函数,但我们可以一眼看出,应用程序有超过 90%的处理时间是在执行 WebAssembly。

Firefox 性能分析器

使用 Firefox 性能分析器是收集应用程序性能数据的另一种优秀方式。我建议在运行 Firefox 性能分析器时打开一个私人窗口。可以通过打开浏览器右上角的菜单,点击新建私人窗口来实现(图 9-12)。

f09012

图 9-12:在 Firefox 中打开一个新的私人窗口。

通过点击Web 开发者性能来打开性能分析器(图 9-13)。

f09013

图 9-13:在 Firefox 菜单中点击Web 开发者性能

在性能菜单中,点击开始录制性能按钮以录制性能数据。几秒钟后,停止录制。图 9-14 展示了你应该在性能标签中看到的内容类似的结果。Waterfall 标签(这是录制后的默认视图)显示了顶级函数调用以及它们执行所需的时间。

f09014

图 9-14:Firefox 性能窗口中的 Waterfall 标签

向下滚动以查看垃圾回收发生的位置以及执行所需的时间。对于我们的应用程序来说,这个报告有点无聊,因为它主要执行requestAnimationFrame。窗口顶部的三个标签提供了更多信息。Waterfall 标签让你大致了解任务执行时间过长的地方。我们不会详细讲解 Waterfall 标签,因为它更像是一个一目了然的运行时总结。相反,我们将重点查看 Call Tree 和 JS Flame Chart 标签。

Call Tree

Call Tree 标签显示了应用程序花费大部分时间的函数调用。该界面允许你深入查看每个函数,并查看它们调用了哪些函数。图 9-15 展示了 Call Tree 标签的截图。

f09015

图 9-15:Firefox 的 Call Tree 标签

一个很好的功能是,你可以点击 WebAssembly 文件的名称,链接会将你带到 WebAssembly 代码中的相应函数。函数名称会丢失,但一个显示函数编号的索引会跟随wasm-function标签。这样可以稍微帮助确定函数调用的内容。

JS Flame Chart

JS Flame Chart 标签显示的信息与 Call Tree 标签中看到的信息几乎相同,但它是按照时间线组织的,而不是作为总结。你可以放大图表的特定部分,查看在该时间点上执行的函数(图 9-16)。

f09016

图 9-16:Firefox JS 火焰图标签

这是调用 JavaScript animate 函数的代码。animate 函数大部分时间运行 wasm-function[6],这是我们 WAT 代码中的第七个函数,名为 $main$main 函数调用了 wasm-function[5],即第六个函数($get_obj_attr)和 wasm-function[1]$abs)。

每个标签页都显示了最小和最大帧率在左侧,平均帧率在右侧。分析器的左侧类似于 图 9-17。

f09017

图 9-17:Firefox 最大和最小帧率

如你所见,最大帧率略高于 22,最小帧率略低于 5 fps。如前所述,运行分析器可能会影响帧率。平均帧率显示在分析器的右侧(图 9-18)。

f09018

图 9-18:Firefox 平均帧率

此配置文件的平均帧率约为 14 fps。在下一节中,我们将探讨如何使用 wasm-opt 改善应用性能。

wasm-opt

我们使用 wasm-opt 命令行工具对 WebAssembly 文件进行性能优化。它随 wat-wasmBinaryen.js 一起提供。如果你已经安装了 wat-wasm 来使用 wat2wasm 工具,那么你应该已经有一个版本,可以跳过下一节。如果没有,安装 Binaryen.js,它是 Binaryen WebAssembly 工具的 JavaScript 版本,用于将中间表示(IR)转换为 WebAssembly 代码。它提供了一些有用的选项来优化 WebAssembly 代码。

安装 Binaryen

安装 Binaryen 有几种选择。我推荐使用 Binaryen.js,你可以通过 npm 使用以下命令来安装:

npm install binaryen -g

对于有兴趣从源代码构建的人,可以在 GitHub 上找到它,地址是 github.com/WebAssembly/binaryen。还有一个名为 wasm-optnpm 包,它会为Binaryen 安装特定平台的二进制文件,但我推荐使用 npm 安装 wat-wasmbinaryen.js

运行 wasm-opt

wasm-opt 工具提供了许多标志,你可以使用这些标志来最小化下载大小并优化 WebAssembly 模块的执行。你可以使用这些标志告诉优化器是关注性能还是下载大小。如果有变动可以减少文件大小而不影响性能,那么无论如何都会进行修改。如果有变动可以改善性能而不影响下载大小,也是如此。这些标志告诉编译器在需要权衡时应该优先考虑哪种优化。

我们将使用两种标志运行wasm-opt,首先使用针对文件大小的优化偏好,然后再次编译以优化性能。这些标志适用于任何使用 Binaryen 的工具链,如 Emscripten 或 AssemblyScript。我们首先将查看的两个标志会优化 WAT 文件的大小。

针对下载大小的优化

wasm-opt命令有两个标志可以优化你的 WebAssembly 文件以减小下载大小:-Oz-Os。O 是大写字母 O,而不是数字零。-Oz标志会创建一个更小的 WebAssembly 文件,但缩小文件大小所需的时间更长。-Os标志会创建一个略大的 WebAssembly 文件,但执行时间较短。我们的应用程序很小,因此无论运行哪种优化,所需时间也都非常短。你可能会在创建一个大型 Emscripten 项目时使用-Os,因为它需要较长的编译时间。就我们的目的而言,我们不需要使用-Os。清单 9-3 显示了如何使用-Oz标志优化我们的collide.wasm文件以减小其大小。

wasm-opt collide.wasm -Oz -o collide-z.wasm

清单 9-3:运行wasm-opt以优化collide.wasm文件的下载大小

当你运行此优化时,WebAssembly 文件的大小从 709 字节减少到 666 字节。减少了大约 6%的大小,但我们没有做任何额外的工作来实现这个效果。通常,当你使用此标志和工具链时,会获得更好的大小减少效果。

针对执行时间的优化

当你编写游戏时,你会更关注提升帧率(fps)而不是下载时间。有三个优化标志:-O1-O2-O3。再说一遍,O 是字母 o,而不是数字零。-O3标志提供了最高级别的优化,但执行时间最长。-O1标志执行时间最短,但优化效果最差。-O2标志介于两者之间。由于我们的应用程序非常小,-O1-O3的执行时间差异不大。在清单 9-4 中,我们使用-O3标志来从我们的优化中获取最大收益,优化的是collide.wasm文件。

wasm-opt collide.wasm -O3 -o collide-3.wasm

清单 9-4:使用wasm-opt优化collide.wasm文件的性能

一旦你获得了新的collide.wasm文件版本,修改collide.html文件以运行优化后的版本。现在,当我们通过分析器运行它时,可以了解性能的提升情况。在 Chrome 中进行分析,显示应用现在运行在 35 fps 的帧率上(图 9-19)。

f09019

图 9-19:优化后的collide-3.wasm文件在 Chrome 中的新帧率

图 9-10 显示了原始帧率为 18 fps。仅仅运行wasm-opt就能使你的应用在 Chrome 中帧率翻倍。接下来,让我们看看在 Firefox 中运行分析器时会发生什么(图 9-20)。

f09020

图 9-20:优化后的collide-3.wasm文件在 Firefox 中的新帧率

回顾图 9-18,我们在最初的运行中只达到了平均 14 帧每秒,因此在 Firefox 中,帧率几乎翻倍。在接下来的章节中,我们将查看反汇编后的优化 WAT 代码,看看 wasm-opt 做了哪些优化。

查看优化后的 WAT 代码

你应该已经安装了 VS Code 的 WebAssembly 扩展(我们在第一章中做了这一步)。在 Visual Studio 中,你可以右键点击一个 WebAssembly 文件,选择“显示 WebAssembly”来查看给定 WebAssembly 文件的 WAT。在清单 9-5 中,我们使用命令行中的 wasm2wat 将优化后的 collide-3.wasm 文件转换为 WAT 文件。

wasm2wat collide-3.wasm -f -o collide-3.wat

清单 9-5:运行 wasm2watcollide-3.wasm 反汇编为 WAT。

接下来,在 VS Code 中打开 collide.watcollide-3.wat,查看 wasm-opt 对 WebAssembly 文件所做的更新,如图 9-21 所示。

在优化后的代码中,所有的函数和变量名都消失了。我添加了一些注释来帮助你跟进。你可以很快看到,优化将函数的数量从七个减少到三个。优化通过将许多小函数展开为内联代码实现了这一点。在剩下的函数之一中,优化移除了一个变量。你可能会创建两个不同的变量,而实际上只需要一个,因为这样可以使代码更易于阅读。优化器可以检测到这一点并减少变量的数量。还要注意,优化器将乘法运算替换为二的幂次左移。例如,在图 9-21 中的代码,优化器将乘以 4 替换为了左移 2 位。在接下来的章节中,我们将更详细地探讨这些策略如何提升性能。

f09021

图 9-21:比较优化版本和未优化版本的 collide.wat

提升性能的策略

现在我们来看看一些可以用来提升 WebAssembly 应用程序性能的策略。优化器使用了一些这些技术,你也可以通过编写代码来使优化器的工作变得更加简单。有时,你可能希望查看优化器生成的 WAT 代码,从中获得改进代码的建议。让我们来看看你可以在 WAT 中使用的一些常见优化技术。

内联函数

调用一个函数有一点点开销。除非函数每秒被调用成千上万次,否则这种开销通常不会成为大问题。内联函数是将函数调用替换为该函数相同代码的过程。这样做可以去除执行函数调用所需的额外处理开销,但会增加 WebAssembly 模块的大小,因为它在每个调用该函数的地方都会复制代码。当我们对collide.wasm模块运行优化器时,它内联了七个函数中的四个。让我们看一个内联函数的简单示例。以下的 WAT 代码并不是应用的一部分,它只是一个演示。在 Listing 9-6 中,我们创建了一个将三个数字相加的函数,然后创建了另一个函数来多次调用$add_three

 (func $add_three ;; function adds three numbers together
    (param $a i32)
    (param $b i32)
    (param $c i32)
    (result i32)
 local.get $a
    local.get $b
    local.get $c
    i32.add
    i32.add
  )
  (func (export "inline_test")  ;; I will inline functions in inline_test
    (param $p1 i32)
    (param $p2 i32)
    (param $p3 i32)
    (result i32)
    (call $add_three (local.get $p1) (i32.const 2) (local.get $p2))
    (call $add_three (local.get $p3) (local.get $p1) (i32.const 13))
    ;; add the results together and return
    i32.add
 )

Listing 9-6:用于内联的演示代码

我们将专注于内联作为本节的优化方法。为了内联这些函数,我们将函数的内容复制粘贴到每个调用它的地方。在 Listing 9-7 中,灰色代码是原始的函数调用,后面的代码是内联的函数。

 (func (export "inline_test");; I will inline the functions in inline_test
    (param $p1 i32)
    (param $p2 i32)
    (param $p3 i32)
    (result i32)
;;  (call $add_three (local.get $p1) (i32.const 2) (local.get $p2))
;;  the function above is inlined into the code below
    local.get $p1
    i32.const 2
    local.get $p2
    i32.add
    i32.add
;;  (call $add_three (local.get $p3) (local.get $p1) (i32.const 13))
;;  the function above is inlined into the code below
    local.get $p3
    local.get $p1
    i32.const 13
    i32.add
    i32.add

    i32.add
 )

Listing 9-7:手动内联代码示例

内联函数调用可能会暴露其他优化机会。例如,你可以看到我们在添加2之后又添加了13。由于这两个值都是常量,如果我们直接添加15,代码的性能会更好。

让我们编写一个可能会被内联的模块,编译并优化它,然后查看wasm-opt生成的代码。我们将创建一个包含三个函数的模块:$add_three$square$inline_test。创建一个名为inline.wat的 WAT 文件,并在其中添加 Listing 9-8 中的代码。

inline.wat

(module
  (func $add_three
    (param $a i32)
    (param $b i32)
    (param $c i32)
    (result i32)
    local.get $a
    local.get $b
    local.get $c
    i32.add
    i32.add
  )
  (func $square
    (param $a i32)
    (result i32)
    local.get $a
    local.get $a
    i32.mul
  )
  (func $inline_test (export "inline_test")
    (param $p1 i32)
    (param $p2 i32)
    (param $p3 i32)
    (result i32)
    (call $add_three (local.get $p1) (i32.const 2) (local.get $p2))
    (call $add_three (local.get $p3) (local.get $p1) (i32.const 13))
    call $square
    i32.add
    call $square
  )
)

Listing 9-8:我们将使用wasm-opt来内联这段代码

$add_three函数是我们在 Listing 9-7 中手动内联的相同函数。$square函数将栈顶的值与自身相乘,而$inline_test函数是调用函数。让我们使用wat2wasm编译$inline_test函数:

wat2wasm inline.wat

现在我们可以使用wasm-opt进行优化:

wasm-opt inline.wasm -O3 -o inline-opt.wasm

最后,让我们使用wasm2wat将其转换回 WAT:

wasm2wat inline-opt.wasm

现在我们可以打开inline-opt.wat,查看我们的优化代码是什么样的(Listing 9-9)。

inline-opt.wat

(module
  (type (;0;) (func (param i32 i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32 i32)
    (result i32) ;; $inline_test function
    local.get 0
    local.get 1
    i32.const 2  
    i32.add
    i32.add
    local.get 2
    local.get 0
    i32.const 13
    i32.add
    i32.add
    local.tee 0
    local.get 0
    i32.mul
    i32.add
    local.tee 0
    local.get 0
    i32.mul)
  (export "inline_test" (func 0)))

Listing 9-9:优化后的inline.wat版本,inline-opt.wat,内联了两个函数。

优化器移除了两个函数$add_three$square,并将这些代码内联到inline_test函数中。

乘法和除法与位移

第八章展示了如何通过将整数位向右移动作为乘以 2 的幂的更快方法。例如,向左移动 3 位等同于乘以 2³,即 8。同样,将整数向右移动等同于除以该 2 的幂。例如,右移 4 位等同于除以 2⁴,即 16。让我们看看wasm-opt如何处理 2 的幂的乘法和除法。创建一个新的 WAT 文件,命名为pow2_mul.wat,并将清单 9-10 中的代码添加进去,创建一个模块用于乘以和除以 2 的幂。

pow2_mul.wat

(module
  (func (export "pow2_mul")
    (param $p1 i32)
    (param $p2 i32)
    (result i32)
    local.get $p1
    i32.const 16
    i32.mul ;; multiply by 16, which is 24
    local.get $p2
    i32.const 8
    i32.div_u ;; divide by 8, which is 23
 i32.add
  )
)

清单 9-10:一个用于乘以和除以 2 的幂的函数

使用wat2wasm编译这段代码,使用wasm-opt优化 WebAssembly 文件,然后使用wasm2wat将 WebAssembly 文件反汇编回 WAT 文件。接着在 VS Code 中打开优化后的pow2_mul.wat,如清单 9-11 所示。

pow2_mul_optimized.wat

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 1
    i32.const 8
    i32.div_u
    local.get 0
    i32.const 4
    i32.shl
    i32.add)
  (export "pow2_mul" (func 0)))

清单 9-11:来自清单 9-10 的pow2_mul函数的优化版本

注意,优化后的代码会在执行乘法之前先对第二个参数进行除法操作。当你乘以一个 2 的幂常量时,wasm-opt会将其转换为左移操作。然而,wasm-opt并不总是将 2 的幂的除法替换为右移操作。在本章后面,我们将花些时间通过benchmark.js运行不同版本的代码,看看它们的性能如何。我们将比较由wasm-opt生成的优化代码与我们手动优化的代码,看看是否能够做得更好。

合并常量

通常,优化会合并常量以提高性能。例如,假设你有两个常量偏移量需要加在一起。你原来的代码是x = 3 + 8,但如果你一开始就将x = 11,性能会更好。像这样的情况人眼不总是能轻易看出来,但wasm-opt非常高效,能够帮你找出这些情况。作为例子,创建一个 WAT 文件,命名为combine_constants.wat,并将清单 9-12 中的代码添加进去,该代码仅仅是将三个常量合并在一起。

combine_constants.wat

(module
  (func $combine_constants (export "combine_constants")
    (result i32)
    i32.const 10
    i32.const 20
    i32.add
 i32.const 55
    i32.add
  )
)

清单 9-12:一个将三个常量相加的函数

注意,$combine_constants返回的值将始终是 85。wasm-opt工具足够智能,能够搞清楚这一点。当你通过wat2wasm运行代码,使用wasm-opt优化 WebAssembly 文件,再通过wasm2wat反汇编 WebAssembly 文件时,你会看到清单 9-13 中的代码。

combine_constants_optimized.wat

(module
  (type (;0;) (func (result i32)))
  (func (;0;) (type 0) (result i32)
1 i32.const 85)
  (export "combine_constants" (func 0)))

清单 9-13:三个常量的加法被合并为一个常量。

清单 9-13 中的函数返回85,并且不再执行两个加法操作。

DCE

死代码消除 (DCE) 是一种优化技术,用于删除模块中未被调用或导出的任何代码。这是一种直接的优化方法,虽然不会改善执行时间,但可以减少下载文件的大小。DCE 无论使用哪种优化标志都会发生。我们来看一个简单的例子。打开一个名为dce_test.wat的新文件,并添加清单 9-14 中的代码,这段代码创建了一个包含两个永不使用的函数的模块。

dce_test.wat

(module
1 (func $dead_code_1
    (param $a i32)
    (param $b i32)
    (param $c i32)
    (result i32)
    local.get $a
    local.get $b
    local.get $c
    i32.add
    i32.add
  )
2 (func $dead_code_2
    (param $a i32)
    (result i32)
    local.get $a
    local.get $a
    i32.mul
  )
  (func $dce_test (export "dce_test")
    (param $p1 i32)
 (param $p2 i32)
    (result i32)
    local.get $p1
    local.get $p2
    i32.add
  )
)

清单 9-14:该模块包含两个未使用的函数。

前两个函数$dead_code_1 1 和 $dead_code_2 2 没有被调用,也没有被导出。我们运行的任何优化都会删除这些函数。运行wat2wasm生成代码,使用wasm-opt-O3标志进行优化,再使用wasm2wat将其转换回 WAT 文件。打开新文件,查看优化后的代码,如清单 9-15 所示。

dce_test_optimized.wat

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "dce_test" (func 0)))

清单 9-15:两个函数被 DCE 移除。

唯一剩下的函数是 "dce_test"。使用 DCE 已将模块的大小从 79 字节减少到 46 字节。

使用 JavaScript 比较碰撞检测应用

我们已经看过 WebAssembly 碰撞检测应用的表现。接下来,让我们快速写出相同的代码,用 JavaScript 实现,并比较其与 WebAssembly 版本的性能。创建一个名为 collidejs.html 的新网页。首先,向 collide.html 页面添加头部和 canvas 元素,并将其重新保存为 collidejs.html,如清单 9-16 所示。

collidejs.html(第一部分,共 2 部分)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Collide JS</title>
</head>
<body>
    <canvas id="canvas" width="1024" height="1024"></canvas>
...

清单 9-16:collidejs.html 中的 HTML 头部和 canvas 元素

这段代码与该应用的 WebAssembly 版本相似。主要区别在于script标签,如清单 9-17 所示。在script标签中添加以下 JavaScript 代码。

collidejs.html(第二部分,共 2 部分)

...
    <script type="text/javascript">
        // javascript version
        var animate_callback;
        const out_tag = document.getElementById('out');
        const cnvs_size = 1024 | 0;

        const noh_color = 0xff00ff00 | 0;
        const hit_color = 0xff0000ff | 0;

        const obj_start = cnvs_size * cnvs_size * 4;
        const obj_size = 8 | 0;
        const obj_cnt = 3000 | 0;

        const canvas = document.getElementById("canvas");
        const ctx = canvas.getContext("2d");

        class Collider {
            constructor() {
                this.x = Math.random() * cnvs_size;
                this.y = Math.random() * cnvs_size;
                this.xv = (Math.round(Math.random() * 4) - 2);
                this.yv = (Math.round(Math.random() * 4) - 2);
                this.color = "green";
            }

            move = () => {
                this.x += this.xv;
                this.y += this.yv;
                this.x %= 1024;
                this.y %= 1024;
            }
            draw = () => {
                ctx.beginPath();
                ctx.fillStyle = this.color;
                ctx.fillRect(this.x, this.y, obj_size, obj_size);
                ctx.stroke();
            }
            hitTest = (c2) => {
                let x_dist = this.x - c2.x;
                let y_dist = this.y - c2.y;

                if (Math.abs(x_dist) <= obj_size &&
                    Math.abs(y_dist) <= obj_size) {
                    this.color = "red";
                    return true;
                }
                else {
                    this.color = "green";
                }
                return false;
            }
        }

 let collider_array = new Array();
        for (let i = 0; i < obj_cnt; i++) {
            collider_array.push(new Collider());
        }

        let animate_count = 0;

        function animate() {
            // clear
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            for (let i = 0; i < collider_array.length; i++) {
                collider_array[i].move();
            }

            // loop and render
            for (i = 0; i < collider_array.length; i++) {
                for (let j = 0; j < collider_array.length; j++) {
                    if (i === j) {
                        continue;
                    }
                    if (collider_array[i].hitTest(collider_array[j]))
                    {
                        break;
                    }
                }
                collider_array[i].draw();
            }
            requestAnimationFrame(animate);
        }
        requestAnimationFrame(animate);
    </script>
</body>
</html>

清单 9-17:我们的碰撞检测应用的 JavaScript 版本

我不会详细讲解清单 9-17 中的代码,因为它的目的是为了与第八章中的 WebAssembly 代码进行对比。现在我们可以在 Chrome 和 Firefox 分析器中运行 collidejs.html 来查看它们的表现。图 9-22 显示了 collidejs.html 在 Chrome 分析器中的帧率。

f09022

图 9-22:我们的 JavaScript 应用在 Chrome 分析器中的帧率

Chrome 运行该 JavaScript 版本的应用时帧率约为 9 fps,比未优化的 WebAssembly 版本(在 Chrome 中运行约 18 fps)和优化后的版本(运行帧率为 35 fps)都要慢。优化后的 WebAssembly 代码在 Chrome 中的运行速度几乎是原来的四倍。

现在我们来看看我们的 JavaScript 在 Firefox 中的表现(如图 9-23 所示)。

f09023

图 9-23:我们的 JavaScript 应用在 Firefox 分析器中的帧率

Firefox 在这个应用程序中的表现明显优于 Chrome(几乎快一倍)。它甚至能够超越 WebAssembly 应用程序在 Firefox 上的未优化版本,后者大约运行在 14 fps。这个速度仅为 Firefox 上优化版 WebAssembly 应用程序的一半左右,后者的帧率大约是 31 fps。

在本节中,你学习了如何使用 Firefox 和 Chrome 的性能分析工具将你的 WebAssembly 代码与类似的 JavaScript 代码进行比较。你现在应该能够利用这些知识,在不同浏览器上比较应用程序的不同版本,以了解哪些代码最好使用 WebAssembly 编写,哪些则最好使用 JavaScript 编写。

手动优化 WAT

我花了一些时间手动优化我的 WebAssembly 碰撞应用程序,结果我将帧率进一步提高了。在这本书中无法详细描述所有的更改。然而,我想指出的是,如果你愿意花时间进行手动优化,可能会获得哪些性能提升。我成功地将碰撞应用程序的帧率提高到 Chrome 性能分析工具中的 36 fps (图 9-24)。

f09024

图 9-24:手动优化后的碰撞应用程序在 Chrome 中运行

Firefox 的帧率更高,达到了 52 fps (图 9-25)。

f09025

图 9-25:手动优化后的碰撞应用程序在 Firefox 中运行

你可以在 wasmbook.com/collide.html 上看到我手动优化后的成果,在 wasmbook.com/collide.wat 上查看 WAT 代码。我对精心调优的代码运行了 Binaryen 优化器,但它实际上使帧率下降了几帧。Binaryen 一直在改进它的优化输出。到你阅读这篇文章时,结果可能会有所不同。

性能日志记录

记录 JavaScript 应用程序性能的最简单方法之一是使用 Date 类和 console.log 函数。WebAssembly 无法在不使用 JavaScript 的情况下写入控制台。因此,我们需要使用 JavaScript 来记录 WebAssembly 和 JavaScript 代码的性能到控制台。

让我们看看从 JavaScript 调用 WebAssembly 模块时所涉及的开销。我们将创建一个 WebAssembly 模块,其中包含一些可以从 JavaScript 中反复调用的小函数。创建一个名为 mod_and.wat 的文件,并添加 清单 9-18 中的代码。

mod_and.wat

(module
  (func $mod (export "mod")
    (param $p0 i32)
    (result i32)
    local.get $p0
    i32.const 1000
    i32.rem_u
  )

  (func $and (export "and")
    (param $p0 i32)
    (result i32)
    local.get $p0
    i32.const 0x3ff
    i32.and
  )
)

清单 9-18:比较余数与按位与操作的性能

这个模块中有两个函数,一个是 $mod 函数,它用于计算除以 1000 的余数,另一个是 $and 函数,它使用按位与操作。使用 wat2wasm 编译 mod_and.wat 文件,并使用 wasm-opt 进行优化。

接下来,我们需要创建一个 JavaScript 函数来运行这个 WAT 模块,并将其与等效的 JavaScript 代码进行测试。创建一个名为 mod_and.js 的新文件,并添加 清单 9-19 中的代码。

mod_and.js

const fs = require('fs');
const bytes = fs.readFileSync('./mod_and.wasm');

(async () => {
  const obj =
    await WebAssembly.instantiate(new Uint8Array(bytes));
  let mod = obj.instance.exports.mod;
  let and = obj.instance.exports.and;

 let start_time = Date.now(); // reset start_time
 // The '| 0' syntax is a hint to the JavaScript engine to tell it
 // to use integers instead of floats, which can improve performance in
 // some circumstances
  for (let i = 0 | 0; i < 4_000_000; i++) {
    mod(i);  // call the mod function 4 million times
  }
// calculate the time it took to run 4 million mod calls
  console.log(`mod: ${Date.now() - start_time}`);
  start_time = Date.now(); // reset start_time

  for (let i = 0 | 0; i < 4_000_000; i++) {
    and(i); // call the and function 4 million times
  }
// calculate the time it took to run 4 million and calls
  console.log(`and: ${Date.now() - start_time}`);
  start_time = Date.now(); // reset start_time
  for (let i = 0 | 0; i < 4_000_000; i++) {
    Math.floor(i % 1000);
  }
// calculate the time it took to run 4 million modulo calls
  console.log(`js mod: ${Date.now() - start_time}`);
})();

列表 9-19:使用Date.nowconsole.log记录运行时间

在运行每个代码块之前,我们设置一个变量start_timeDate.now()。这样做将start_time变量设置为当前时间的毫秒数。完成代码后,我们记录Date.now() - start_time,这将给出我们测试的运行时间(毫秒)。我们会对 WebAssembly 模块和 JavaScript 代码执行这个操作,以便比较两者。

现在我们已经有了mod_and.js函数,可以使用以下node命令来运行它:

node mod_and.js

列表 9-20 显示了运行mod_and.js后的输出。

mod: 29
and: 23
js mod: 4

列表 9-20:mod_and.js的输出

mod函数执行四百万次花费了 29 毫秒。and函数执行四百万次花费了 23 毫秒。JavaScript 版本执行四百万次只花费了 4 毫秒。那么,如果 WebAssembly 这么快,为什么它的执行时间却是这些函数的五到七倍呢?问题在于 JavaScript 和 WebAssembly 之间的调用存在一些开销。调用一个小函数四百万次时,也会产生四百万次开销的成本。让我们重写代码,从 WebAssembly 内部执行这些函数,而不是从 JavaScript 的循环中调用它们。

首先,我们将重写 WebAssembly 模块,将循环包含在模块内部,而不是在 JavaScript 内部。创建一个新的 WAT 文件,命名为mod_and_loop.wat,并添加列表 9-21 中的代码。

mod_and_loop.wat

(module
  (func (export "mod_loop")
    (result i32)
    (local $i i32)
    (local $total i32)
    i32.const 100_000_000 ;; loop 100 million times
    local.set $i

    (loop $loop
      local.get $i
      i32.const 0x3ff
      i32.rem_u
      local.set $total

      local.get $i
      i32.const 1
      i32.sub
      local.tee $i  ;; i--

      br_if $loop
    )
    local.get $total
  )

  (func (export "and_loop")
    (result i32)
    (local $total i32)
    (local $i i32)
    i32.const 100_000_000 ;; loop 100 million times
    local.set $i

    (loop $loop
      local.get $i
      i32.const 0x3ff
      i32.and
      local.set $total

      local.get $i
      i32.const 1
      i32.sub
      local.tee $i ;; i--

      br_if $loop
    )
 local.get $total
  )
)

列表 9-21:按位与/取余函数的循环版本

这些函数执行的任务与原始函数相同,但程序将它们执行 1 亿次。我们需要修改 JavaScript 文件,只调用这些函数一次,并让 JavaScript 执行 1 亿次。这样,我们就可以将性能与 WebAssembly 模块进行比较,后者已经修改为执行函数 1 亿次。创建一个名为mod_and_loop.js的新函数,并添加列表 9-22 中的代码。

mod_and_loop.js

const fs = require('fs');
const bytes = fs.readFileSync('./mod_and_loop.wasm');

(async () => {
  const obj =
    await WebAssembly.instantiate(new Uint8Array(bytes));
  let mod_loop = obj.instance.exports.mod_loop;
  let and_loop = obj.instance.exports.and_loop;

  let start_time = Date.now(); // set start_time
  and_loop();
  console.log(`and_loop: ${Date.now() - start_time}`);

  start_time = Date.now(); // reset start_time
  mod_loop();
  console.log(`mod_loop: ${Date.now() - start_time}`);
  start_time = Date.now(); // reset start_time
  let x = 0;
  for (let i = 0; i < 100_000_000; i++) {
    x = i % 1000;
  }
  console.log(`js mod: ${Date.now() - start_time}`);
})();

列表 9-22:运行and_loopmod_loop和相应 JavaScript 代码的 JavaScript

我们调用mod_loopand_loop函数,记录每个循环执行所花的时间。接下来,我们运行一个循环,在其中执行 100 百万次取余操作,并记录所花费的时间。如果我们编译并优化 WebAssembly 模块,然后使用node运行mod_and_loop.js,我们应该看到类似列表 9-23 中的输出。

and_loop: 31
mod_loop: 32
js mod: 52

列表 9-23:mod_and_loop.js的输出

现在 WebAssembly 比相同的 JavaScript 代码快了 67%。虽然按位与操作的性能没有比取余操作好多少,这点让我有些失望,但至少我们现在知道如何使用console.log结合Date.now()进行最简单的性能测试了。

使用 benchmark.js 进行更复杂的测试

如果你想让你的测试比仅使用日志和 Date.now 更复杂一些,可以安装一个性能测试模块,例如benchmark.js。在清单 9-10 中,我们创建了一个 WebAssembly 函数,它先乘以 16 然后除以 8,并通过 wasm-opt 查看 Binaryen 如何优化我们的代码。优化器用位移操作替换了乘法,但没有用位移替换除法操作,它还重新排列了除法和乘法的顺序。

我们将测试这个 WebAssembly 模块的几个版本,包括原始版本和优化器生成的版本,看看是否可以通过一些努力超越优化器的效果。我们将使用benchmark.js来测试所有这些函数的性能。创建一个新的 WAT 文件,命名为pow2_test.wat,并添加清单 9-24 中的代码。

pow2_test.wat(第一部分,共 5 部分)

(module
  ;; this is the original function we wrote
  (func (export "pow2")
    (param $p1 i32)
    (param $p2 i32)
    (result i32)
    local.get $p1
    i32.const 16
    i32.mul
    local.get $p2
    i32.const 8
    i32.div_u
    i32.add
  )
...

清单 9-24:模块开始与原始函数

清单 9-24 显示了我们原始的 2 的幂次测试版本,在其中我们先乘以 16,然后除以 8\。

下一个函数在清单 9-25 中,先进行除法再进行乘法。我想测试这个,因为wasm-opt交换了乘法和除法函数,我很好奇这是否对函数性能产生了积极的影响。

pow2_test.wat(第二部分,共 5 部分)

...
 ;; wasm-opt placed the div before mul, so let's see if that helps
  (func (export "pow2_reverse")
    (param $p1 i32)
    (param $p2 i32)
    (result i32)
    local.get $p2
 i32.const 8
    i32.div_u
    local.get $p1
    i32.const 16
    i32.mul
    i32.add
  )
...

清单 9-25:交换除法和乘法顺序

下一个函数在清单 9-26 中,使用了二进制位移进行 2 的幂次乘法和除法。我们还使用了优化器插入的顺序,其中除法发生在乘法之前。

pow2_test.wat(第三部分,共 5 部分)

...
 ;; change multiply and divide to shifts
  (func (export "pow2_mul_div_shift")
    (param $p1 i32)
    (param $p2 i32)
    (result i32)
    local.get $p2
    i32.const 3
    i32.shr_u
    local.get $p1
    i32.const 4
    i32.shl
    i32.add
  )
...

清单 9-26:将乘法和除法表达式改为二进制位移

接下来,在清单 9-27 中,我们使用了位移操作进行除法和乘法,但这次我们没有改变除法和乘法的顺序,仍然保留了原始代码中的顺序。

pow2_test.wat(第四部分,共 5 部分)

...
 ;; back to original order of multiply and divide
  (func (export "pow2_mul_div_nor")
    (param $p1 i32)
    (param $p2 i32)
    (result i32)
    local.get $p1
    i32.const 4
    i32.shl
    local.get $p2
    i32.const 3
    i32.shr_u
    i32.add
  )
...

清单 9-27:原始顺序,先乘法再除法

下一个函数在清单 9-28 中,这是通过 wasm-opt-O3 标志生成的代码版本。

pow2_test.wat(第五部分,共 5 部分)

...
;; this was what was generated by wasm-opt
  (func (export "pow2_opt") (param i32 i32) (result i32)
    local.get 1
    i32.const 8
    i32.div_u
    local.get 0
    i32.const 4
    i32.shl
    i32.add
  )
)

清单 9-28:wasm-opt优化版本的函数

现在我们可以用wat2wasm编译这个模块,但我们不应该优化它,因为我们要测试原始的 WAT 代码,避免优化器的修改。接下来,我们需要创建我们的benchmark.js代码。首先,我们需要使用 npm 安装 benchmark.js 模块:

npm i --save-dev benchmark

现在我们可以编写一个 JavaScript 程序,使用benchmark.js测试 WebAssembly 函数。让我们把这个程序分成几个部分,一次讲解一个部分。将清单 9-29 中的代码添加到benchmark_test.js中。

benchmark_test.js

// import benchmark.js
1 var Benchmark = require('benchmark');
2 var suite = new Benchmark.Suite();

// use fs to read the pow2_test.wasm module into a byte array
3 const fs = require('fs');
const bytes = fs.readFileSync('./pow2_test.wasm');
const colors = require('colors'); // allow console logs with color

// Variables for the WebAssembly functions
var pow2;
var pow2_reverse;
var pow2_mul_shift;
var pow2_mul_div_shift;
var pow2_mul_div_nor;

console.log(`
================= RUNNING BENCHMARK =================
`.rainbow 4);
...

清单 9-29:benchmark_test.js JavaScript 文件的第一部分

首先,我们引入 1 benchmark 模块,然后从该模块创建一个新的 suite 2 对象。接着,我们引入 fs 3 模块,并用它将 WebAssembly 模块加载到 byte 数组中。然后,我们定义一系列变量来保存 WebAssembly 模块中的函数。我们记录一个 rainbow 4 颜色分隔符,显示 RUNNING BENCHMARK,以便在回滚查看统计数据时更容易找到基准测试的起点。如果你像我一样在基准测试时更改模块,那么在一个显眼的位置标记基准测试开始的地方会很有帮助。

在 列表 9-30 中,我们将添加一个可以调用的函数,用于初始化并运行基准测试套件。将以下函数添加到 benchmark_test.js 中。

benchmark_test.js

...
1 function init_benchmark() {
  // adds the callbacks for the benchmarks
2 suite.add('#1 '.yellow + 'Original Function', pow2);
  suite.add('#2 '.yellow + 'Reversed Div/Mult order', pow2_reverse);
  suite.add('#3 '.yellow + 'Replace mult with shift',
             pow2_mul_shift);
  suite.add('#4 '.yellow + 'Replace mult & div with shift',
             pow2_mul_div_shift);
  suite.add('#5 '.yellow + 'wasm-opt optimized version', pow2_opt);
  suite.add('#6 '.yellow + 'Use shifts with OG order',
             pow2_mul_div_nor);
 // add listeners
3 suite.on('cycle', function (event) {
    console.log(String(event.target));
  });

4 suite.on('complete', function () {
    // when the benchmark has finished, log the fastest and slowest functions
    let fast_string = ('Fastest is ' +
      5 this.filter('fastest').map('name'));
    let slow_string = ('Slowest is ' +
        this.filter('slowest').map('name'));
  6 console.log(`
    ------------------------------------------------------------
    ${fast_string.green}
    ${slow_string.red}
    ------------------------------------------------------------
    `);

    // create an array of all successful runs and sort fast to slow
  7 var arr = this.filter('successful');
  8 arr.sort(function (a, b) { 
    return a.stats.mean - b.stats.mean;
    });

    console.log(`

    `);
    console.log("============ FASTEST ============".green);
  9 while (obj = arr.shift()) {
 let extension = '';
      let count = Math.ceil(1 / obj.stats.mean);

     if (count > 1000) {
       count /= 1000;
       extension = 'K'.green.bold;
     }

     if (count > 1000) {
      count /= 1000;
      extension = 'M'.green.bold;
     }

     count = Math.ceil(count);
     let count_string = count.toString().yellow + extension;
     console.log(
      `${obj.name.padEnd(45, ' ')} ${count_string} exec/sec`
      );
    }
    console.log("============ SLOWEST ============".red);
  });
  // run async
  a suite.run({ 'async': false });
}
...

列表 9-30:init_benchmark 函数

我们定义了 init_benchmark() 1 函数,它为 WebAssembly 模块中的每个函数调用基准测试模块中的 suite.add2。使用 suite.add 告诉基准测试套件测试该函数,并用作为第二个参数传递的字符串记录结果。suite.on 函数设置了一个事件回调,用于处理基准测试期间发生的不同事件。第一次调用 suite.on 3 设置回调,用于每个周期,这将输出我们测试的函数和该测试的统计数据。接下来的调用 suite.on 4 设置回调,用于基准测试完成时,它将使用 filter 5 方法来 log 6 最快和最慢的函数。然后,我们按 'successful' 7 过滤,获取所有成功运行的函数数组。我们根据该周期的 mean(平均)运行时间 sort 8 该数组。这样可以将周期按从最快到最慢的运行时间进行排序。然后,我们可以通过循环 9 遍历这些周期,从最快到最慢地打印它们。在该函数的最后,我们 runsuite

定义了 init_benchmark 函数后,在 列表 9-31 中,我们创建了异步 IIFE 来实例化我们的 WebAssembly 模块并调用 init_benchmark

benchmark_test.js

...
(async () => {
1 const obj = await WebAssembly.instantiate(new Uint8Array(bytes));
2 pow2 = obj.instance.exports.pow2;
  pow2_reverse = obj.instance.exports.pow2_reverse;
  pow2_mul_shift = obj.instance.exports.pow2_mul_shift;
  pow2_mul_div_shift = obj.instance.exports.pow2_mul_div_shift;
 pow2_opt = obj.instance.exports.pow2_opt;
  pow2_mul_div_nor = obj.instance.exports.pow2_mul_div_nor;
3 init_benchmark();
})();

列表 9-31:异步 IIFE 实例化 WebAssembly 并运行 *benchmark.js

在这里,我们 instantiate 1 我们的 WebAssembly 模块,并设置所有我们将从 benchmark.js 中调用的函数 2。然后,通过调用 init_benchmark() 3 运行 benchmark.js。现在,我们可以使用 node 并通过以下命令运行我们的应用程序:

node benchmark_test.js

图 9-26 显示了输出结果。

f09026

图 9-26:benchmark_test.js 的输出

有趣的是,最慢的这些函数是 wasm-opt 优化版本:原始版本和 wasm-opt 优化版本的执行时间差不多。最快的运行是我们将 i32.muli32.div_u 操作替换为位移,并按 wasm-opt 工具重排的顺序重新排序调用。这表明,你不能假设 wasm-opt(或任何程序化优化工具)总是能给你最优性能的代码。始终建议对你的应用程序进行性能测试。

使用 --print-bytecode 比较 WebAssembly 和 JavaScript

在本节中,我们将深入探讨低级字节码。观察 JavaScript JIT 生成的字节码既有趣又令人兴奋。将其与 WebAssembly 进行比较也很有趣,思考如何提高性能更是引人入胜。如果这个话题不感兴趣,你可以跳过并继续看下一节。

我们简要看看如何在 WebAssembly 代码和 JavaScript 之间做更好的对比。V8 将 JavaScript 编译成 IR 字节码,看起来很像汇编语言或 WAT。IR 使用寄存器和累加器,但不是机器特定的。我们可以使用 IR 来比较经过 JIT 编译器运行后的 JavaScript 代码和我们的 WebAssembly 代码。因为它们都是低级字节码,这让我们能进行更好的类比比较。但请记住,JavaScript 代码需要在运行时解析并编译成这种字节码,而 WebAssembly 是提前编译的。

让我们创建一个小的 JavaScript 程序,并使用 node --print-bytecode 标志查看由该 JavaScript 生成的字节码。创建一个名为 print_bytecode.js 的 JavaScript 文件,并添加 Listing 9-32 中的代码。

print_bytecode.js

1 function bytecode_test() {
  let x = 0;
2 for (let i = 0; i < 100_000_000; i++) {
  3 x = i % 1000;
  }
4 return 99;
} 

// 如果我们不调用这个,函数会在 DCE 检查中被移除

5 bytecode_test();

Listing 9-32: 我们将用 --print-bytecode 标志执行的 bytecode_test 函数

这个 bytecode_test 函数类似于我们在 Listing 9-22 中进行性能测试的代码。它是一个简单的 for 循环,取 i 计数器的模数,将其存储在 x 中,然后返回 99。它其实没有做什么有用的事情,但我想用一个易于理解的函数,这样我们可以将其编译成字节码。

然后我们除了定义这个函数,还需要调用它;否则 V8 会将其作为 DCE 的一部分移除。我们可以在 Listing 9-33 中运行 node 命令来打印字节码。

node --print-bytecode --print-bytecode-filter=bytecode_test print_bytecode.js

Listing 9-33: 使用 --print-bytecode 标志运行 node

我们向node传递--print-bytecode标志,指示它打印字节码。我们还传递--print-bytecode-filter标志,将其设置为我们函数的名称,以便打印该函数的字节码。如果不包含过滤器标志,输出的字节码将远超我们需要查看的部分。最后,我们传递node JavaScript 文件的名称。使用来自示例 9-33 的标志运行print_bytecode.js,你应该能在示例 9-34 中看到输出结果。

[generated bytecode for function: bytecode_test]
Parameter count 1
Register count 2
Frame size 16
22 E> 0000009536F965F6 @    0 : a5                **StackCheck**
38 S> 0000009536F965F7 @    1 : 0b                **LdaZero**
      0000009536F965F8 @    2 : 26 fb             **Star r0**
57 S> 0000009536F965FA @    4 : 0b                **LdaZero**
      0000009536F965FB @    5 : 26 fa             **Star r1**
62 S> 0000009536F965FD @    7 : 01 0c 00 e1 f5 05 **LdaSmi.ExtraWide [100000000]**
62 E> 0000009536F96603 @   13 : 69 fa 00          **TestLessThan r1, [0]**
      0000009536F96606 @   16 : 99 16             **JumpIfFalse [22]** (0000009536F9661C @ 38)
44 E> 0000009536F96608 @   18 : a5                **StackCheck**
89 S> 0000009536F96609 @   19 : 25 fa             **Ldar r1**
95 E> 0000009536F9660B @   21 : 00 44 e8 03 01 00 **ModSmi.Wide [1000], [1]**
      0000009536F96611 @   27 : 26 fb             **Star r0**
78 S> 0000009536F96613 @   29 : 25 fa             **Ldar r1**
      0000009536F96615 @   31 : 4c 02             **Inc [2]**
      0000009536F96617 @   33 : 26 fa             **Star r1**
      0000009536F96619 @   35 : 8a 1c 00          **JumpLoop [28], [0]** (0000009536F965FD @ 7)
111 S> 0000009536F9661C @   38 : 0c 63             **LdaSmi [99]**
121 S> 0000009536F9661E @   40 : a9                **Return**
Constant pool (size = 0)
Handler Table (size = 0)

示例 9-34:print_bytecode.js的字节码输出

示例 9-34 中输出的右侧显示了 IR 的操作码。在这里,我列出了这些操作码,并在右侧添加了 WAT 风格的注释。V8 引擎生成的字节码并不是栈机器,而是一个虚拟寄存器机器,它具有一个累加器寄存器。累加器是这个虚拟机器进行计算的地方。快速查看示例 9-35 中的代码,这段代码是 V8 生成的。

;; A = Accumulator R0 = Register 0, R1 = Register 1
StackCheck
LdaZero                                   **;; A = 0**
Star r0                                   **;; R0 = A**
LdaZero                                   **;; A = 0**
Star r1                                   **;; R1 = A**
;; THIS IS THE TOP OF THE LOOP
LdaSmi.ExtraWide [100000000]              **;; A=100_000_000**
TestLessThan r1, [0]                      **;; R1 < A**
JumpIfFalse [22] (0000006847C9661C @ 38)  **;; IF R1 >= A GO 22 BYTES**
                                          **;;   AHEAD [END OF LOOP]**
StackCheck
Ldar r1                                   **;; A = R1**
ModSmi.Wide [1000], [1]                   **;; A %= 1_000**
Star r0                                   **;; R0 = A**
Ldar r1                                   **;; A = R1**
Inc [2]                                   **;; A++**
Star r1                                   **;; R1 = A**
JumpLoop [28], [0] (0000006847C965FD @ 7) **;; 28 BYTES BACK [LOOP TOP]**
LdaSmi [99]                               **;; A = 99 | END OF LOOP**
Return                                    **;; RETURN A**

示例 9-35:带有解释的操作码,解释位于 ;; 字符后

V8 的 IR 使用了一个累加器。累加器机器有一个通用寄存器,所有的计算都由累加器在其中完成,而不是在其他寄存器中进行。带有字母a的操作码通常指的是累加器,而r通常指的是寄存器。例如,StackCheck后的第一个操作码是LdaZero,它将 0(Zero)加载到累加器(a)中。然后,Star r0这一行将累加器(a)中的值存储(St)到寄存器(r)中,并将r0传入定义该寄存器。之所以这么做,是因为 IR 不能直接将Register0设为 0 的值;相反,它需要先将该值加载到累加器中,然后再将累加器中的值移动到Register0。在代码后面,你会看到LdaSmi.ExtraWide。这将一个使用所有 32 位(ExtraWide)的小整数(Smi)加载到累加器(a)中。如果你加载的是一个使用 16 位的数字,它会显示Wide而不是ExtraWide,如果是 8 位的数字,则LdaSmi后面不会跟任何东西。TestLessThan操作码比较指定寄存器(r1)中的值与累加器中的值。JumpIfFalse [22]这一行检查TestLessThan的结果是否为假,如果是,则跳转 22 个字节。

--print-bytecode标志是一个有用的工具,可以帮助你调优 JavaScript 性能。如果你熟悉 WAT 或汇编语言,它并不难理解。它还可以用于比较你的 WAT 代码与 JavaScript 之间的性能调优,适用于 WebAssembly 应用程序的两个部分。

总结

本章中,我们讨论了几种评估 WAT 代码性能的工具。我们还将我们的代码与等效的 JavaScript 性能进行了比较。然后,我们探索了几种提升 WebAssembly 模块性能的策略。

我们查看了 Chrome 浏览器中的性能分析器,讨论了概况页面和 JS 堆内存部分,它们提供了关于内存波动和垃圾回收的信息。我们还查看了我们的分析数据中的 fps,这是评估游戏或 UI 密集型应用程序性能的绝佳方式。

我们使用 Firefox 的性能分析器调查了我们的碰撞检测应用程序。Firefox 性能分析器提供了一些额外的工具,包括调用树和 JS 火焰图。我们通过分析性能分析器中的wasm-function[index],追踪到了被调用的 WAT 函数。

接下来,我们安装了Binaryen.js并使用wasm-opt工具优化了 WebAssembly 模块,以优化下载大小或峰值性能。我们还将其反汇编回 WAT 代码,以便查看优化器所做的更改。

然后,我们研究了提高应用程序峰值性能的各种策略,包括内联函数、用位移操作替代乘法和除法,以及合并常量。我们还讨论了 DCE(死代码消除),它是优化器执行的操作,用于删除我们模块中未使用的函数。

我们创建了一个 JavaScript 版本的应用程序,用以比较 JavaScript 与 WebAssembly 模块的性能。

在本章的大部分内容中使用了性能分析工具后,我们还研究了其他几种确定模块性能的方法。使用console.logDate.now是衡量应用程序性能最简单的方法,而测试工具benchmark.js则提供了更详细的信息,用于评估不同函数的性能。为了好玩,我们还打印了 V8 IR 字节码,进一步评估了 JavaScript 代码,并将其与 WebAssembly 进行了对比。在下一章中,您将学习如何调试 WebAssembly 模块。

第十章:调试 WebAssembly

在本章中,您将学习几种调试 WAT 代码的技术。我们将讨论如何将日志输出到控制台和使用警报,以及如何将堆栈跟踪输出到控制台。我们还将介绍如何在 Firefox 和 Chrome 中使用调试器,它们之间的差异,以及各自调试器的局限性。

源映射将浏览器中运行的代码映射到原始的预编译源代码。它允许开发者在使用 TypeScript 等语言或 React 等框架时,逐步调试他们的原始代码。WebAssembly 工具链(如 Emscripten)将生成的 WebAssembly 二进制文件映射回原始的 C++ 源代码。在撰写本文时,wat2wasm 并不为转换为 WebAssembly 二进制格式的 WAT 代码生成源映射。这并不意味着调试 WAT 代码变得毫无意义,但它确实意味着在转换为二进制后,本地或全局变量的任何名称都会丢失。因此,您在 WAT 中编写的代码看起来与在调试器中看到的代码并不完全一样。您必须手动将变量的特定名称映射到浏览器调试器分配的通用名称。本章稍后您将学到如何理解这种映射。一旦学会了调试您的 WebAssembly 代码,您将能够使用这些工具逐步调试在网上找到的任何 WebAssembly 代码,即使您没有源代码。

从控制台进行调试

调试 WebAssembly 代码的最简单方法是通过将语句记录到浏览器控制台。如前所述,WebAssembly 必须依赖 JavaScript 来实现这一点。在本章中,我们将使用一个 JavaScript 函数来创建调试日志。让我们创建一个简单的 WebAssembly 函数,使用毕达哥拉斯定理计算两点之间的距离。我们将在代码中引入一个错误,并将其用作调试代码。创建一个名为 pythagoras.wat 的新文件,并在其中添加 Listing 10-1 中的代码。

pythagoras.wat

(module
  (import "js" "log_f64" (func $log_f64(param i32 f64)))

  (func $distance (export "distance")
    (param $x1 f64) (param $y1 f64) (param $x2 f64) (param $y2 f64)
    (result f64)
    (local $x_dist f64)
    (local $y_dist f64)

    local.get $x1
    local.get $x2
    f64.sub             ;; $x1 - $x2
    local.tee $x_dist   ;; $x_dist = $x1 - $x2
    local.get $x_dist
    f64.mul             ;; $x_dist * $x_dist on stack

    local.get $y1
    local.get $y2
  1 f64.add             ;; should be $y1 - $y2
    local.tee $y_dist   ;; $y_dist = $y1 - $y2
    local.get $y_dist
    f64.mul             ;; $y_dist * $y_dist on stack
    f64.add             ;; $x_dist * $x_dist + $y_dist * $y_dist on stack

    f64.sqrt            ;; take the square root of x squared plus y squared
  )
)

Listing 10-1:使用毕达哥拉斯定理计算两点之间的距离

为了使用毕达哥拉斯定理,我们在 x 轴和 y 轴之间的两点之间画一个直角三角形。x 轴上的长度是两个 x 值之间的距离。我们也可以用同样的方法计算 y 轴上的距离。我们可以通过对这两个值进行平方、相加,然后取平方根来得到两点之间的距离(图 10-1)。

f10001

图 10-1:使用毕达哥拉斯定理计算游戏对象之间的距离

这个示例中的数学并不特别重要。重要的细节是,我们通过将 $y1$y2 相加,而不是相减 1 来计算 y 坐标之间的距离,从而在代码中引入了一个 bug。将 pythagoras.wat 编译成 pythagoras.wasm,并创建一个名为 pythagoras.html 的新文件。然后将 Listing 10-2 中的代码添加到 pythagoras.html 中。

pythagoras.html

<!DOCTYPE html>
<html lang="en">
<body>
  1 X1: <input type="number" id="x1" value="0">
  2 Y1: <input type="number" id="y1" value="0">
  3 X2: <input type="number" id="x2" value="4">
  4 Y2: <input type="number" id="y2" value="3">
    <br><br>
  5 DISTANCE: <span id="dist_out">??</span>
    <script>
      var distance = null;
      let importObject = {
        js: {
        6 log_f64: function(message_index, value) {
            console.log(`message #${message_index} value=${value}`);
          }
        }
      };

      ( async () => {
        let obj = await WebAssembly.instantiateStreaming(
                            fetch('pythagoras.wasm'), importObject );
        distance = obj.instance.exports.distance;
 })();

      7 function set_distance() {
        8 let dist_out = document.getElementById('dist_out');
          let x1 = document.getElementById('x1');
          let x2 = document.getElementById('x2');
          let y1 = document.getElementById('y1');
          let y2 = document.getElementById('y2');

        9 let dist = distance(x1.value, y1.value, x2.value, y2.value);
          dist_out.innerHTML = dist;
      }
    </script>
    <br>
    <br>
  a <button onmousedown="set_distance()">Find Distance</button>
</body>
</html>

清单 10-2:调用 WebAssembly 距离函数的 Web 应用程序

body 标签内,我们通过添加数字类型的输入标签来设置用户界面,包括 x1 1, y1 2, x2 3 和 y2 4 坐标。我们还添加了一个 span 标签,用于显示 WebAssembly 函数运行后两点之间的距离 5。

script 标签内,importObject 包含一个 log_f64 6 函数,它接受消息索引和一个值作为参数。该函数将这两个值记录到浏览器控制台。由于 WebAssembly 无法直接传递字符串到 JavaScript(必须传递线性内存中的索引),因此通常更容易使用消息代码并在 JavaScript 中定义要记录的字符串。该函数使用模板字符串 `message #${message_index} value=${value}`message_index 和值记录到控制台。你也可以根据 message_index 变量选择其他模板字符串。set_distance 7 函数在用户点击 Find Distance 按钮时执行。该函数会获取 dist_out 8 span 标签的元素 ID,以及 x1x2y1y2 输入字段的 ID。然后它会使用这些输入字段中的值执行 WebAssembly 的 distance 9 函数。

运行一个 Web 服务器,并将 pythagoras.html 页面加载到浏览器中;你应该会看到类似于图 10-2 的内容。

f10002

图 10-2:pythagoras.html 网页截图

在图 10-2 中看到的值是表单中填充的默认值。距离字段下方显示为“??”,用户可以在此输入坐标。当我们点击 Find Distance 时,距离应该是 5。我们使用 3-4-5 三角形来测试这个距离计算器。只要 x 轴的距离为 3,y 轴的距离为 4,那么两点之间的距离就是 5,因为 3² + 4² = 5²,正如图 10-3 所示。

f10003

图 10-3:使用 3-4-5 三角形

当你点击应用中的 Find Distance 按钮时,你将看到 DISTANCE 字段填充了值 5,如图 10-4 所示。

f10004

图 10-4:计算出的 3-4-5 三角形距离

当我们将 X 和 Y 值都改变相同的量时,两点之间的距离应该保持不变。然而,由于我们故意引入的一个 bug,将 1 加到 Y1 和 Y2 后,DISTANCE 字段中显示的值是错误的(见图 10-5)。

f10005

图 10-5:计算距离中的错误

我们应该仍然在 DISTANCE 字段中看到 5,但实际上这是一个完全不同的数字。我们需要追踪出错的原因;第一步是在线程中添加 log 语句。

如我们所知,在 WAT 中直接处理字符串并不是一项简单的任务。因此,为了逐步调试代码,我们使用一个消息 ID 以及从 WebAssembly 模块传递给 JavaScript 的值。使用清单 10-3,修改pythagoras.wat 文件,在 $distance 函数中调用 $log_f64

pythagoras.wat

...
(func $distance (export "distance")
  (param $x1 f64) (param $y1 f64) (param $x2 f64) (param $y2 f64) (result f64)
  (local $x_dist f64)
  (local $y_dist f64)
  (local $temp_f64 f64)

  local.get $x1
  local.get $x2
  f64.sub             ;; $x1 - $x2

  local.tee $x_dist   ;; $x_dist = $x1 - $x2

1 (call $log_f64 (i32.const 1) (local.get $x_dist))

  local.get $x_dist
  f64.mul             ;; $x_dist * $x_dist on stack

2 local.tee $temp_f64 ;; used to hold top of the stack without changing it
3 (call $log_f64 (i32.const 2) (local.get $temp_f64))

  local.get $y1
  local.get $y2
  f64.add             ;; should be $y1 - $y2
  local.tee $y_dist   ;; $y_dist = $y1 - $y2

4 (call $log_f64 (i32.const 3) (local.get $y_dist))

  local.get $y_dist
  f64.mul             ;; $y_dist * $y_dist on stack

5 local.tee $temp_f64 ;; used to hold top of the stack without changing it
6 (call $log_f64 (i32.const 4) (local.get $temp_f64))

  f64.add             ;; $x_dist * $x_dist + $y_dist * $y_dist on stack

7 local.tee $temp_f64 ;; used to hold top of the stack without changing it
8 (call $log_f64 (i32.const 5) (local.get $temp_f64))

  f64.sqrt            ;; take the square root of x squared plus y squared

9 local.tee $temp_f64 ;; used to hold top of the stack without changing it
a (call $log_f64 (i32.const 6) (local.get $temp_f64))
)
...

清单 10-3:更新后的pythagoras.wat文件,添加了 JavaScript 函数调用以记录 f64 变量

我们在这里的几个位置添加了对 $log_f64 函数的调用(13468a)。$log_f64 的第一个参数是消息 ID,这是一个整数,我们将使用它作为此消息的唯一标识符。稍后我们会使用这个 ID 从 JavaScript 输出特定的消息。

第二个参数是一个 64 位浮点值,它可以在多个不同的阶段显示我们的距离计算值。在其中一些调用中,我们希望记录栈顶的值,但将其移除,因此我们使用 local.tee(2579)来设置 $temp_f64 的值,这会设置值但不从栈中移除它。然后我们使用 $temp_f64 中的值在调用 $log_f64(368a)时。

将消息记录到控制台

如前所述,WebAssembly 模块不能直接将消息记录到浏览器的控制台,并且 WAT 没有原生的字符串处理库。我们到目前为止使用的 log_f64 函数是通过 WebAssembly 模块从 JavaScript 导入的。所以,在清单 10-4 中,我们将在 JavaScript 中实现这个函数。

pythagoras.html

log_f64: function(message_index, value) {
  console.log(`message #${message_index} value=${value}`);
}

清单 10-4:pythagoras.wat 调用的 JavaScript 函数

这是一个相当直接的版本,它记录了消息索引和数值,但没有根据任何 message_index 值自定义消息。要在 Chrome 中查看控制台,我们将打开开发者工具。进入浏览器菜单并点击 更多工具(图 10-6)。

f10006

图 10-6:打开 Chrome 开发者工具

点击 开发者工具,然后点击 控制台 标签页以查看控制台,如图 10-7 所示。

f10007

图 10-7:打开 Chrome 控制台

要在 Firefox 中打开控制台,点击 Firefox 浏览器菜单中的 Web 开发者 子菜单,如图 10-8 所示。

f10008

图 10-8:打开 Firefox Web 开发者菜单

点击 Web 控制台,如图 10-9 所示。

f10009

图 10-9:打开 Firefox Web 控制台

你的 Firefox 屏幕应该类似于图 10-10 所示。

f10010

图 10-10:在 Web 控制台中显示消息

所有消息都以 message # 开头,后面跟着消息 ID。

这种消息通常已足够,但我们将对函数进行修改,记录更具体的消息。例如,如果你在跟踪每条消息的意义时遇到困难,可能希望消息更具体。你可以像清单 10-5 那样操作,或者根据不同的情况,拥有一系列不同的日志函数。

pythagoras.html

log_f64: function(message_index, value) {
  switch( message_index ) {
    case 1:
      console.log(`$x_dist=${value}`);
      break;
 case 2:
      console.log(`$x_dist*$x_dist=${value}`);
      break;
     case 3:
       console.log(`$y_dist=${value}`);
       break;
     case 4:
       console.log(`$y_dist*$y_dist=${value}`);
       break;
     case 5:
       console.log(`$y_dist*$y_dist + $x_dist*$x_dist=${value}`);
       break;
     case 6:
       console.log(`dist=${value}`);
       break;
     default:
       console.log(`message #${message_index} value=${value}`);
     }
   }

清单 10-5:更新了pythagoras.html,以显示更详细的消息

有六条消息,因此我们在message_index参数上创建了一个开关,根据message_index的不同值,将不同的消息打印到控制台。该开关有一个默认值,在message_index出现意外值时,显示原始消息。更改这些消息后,控制台输出应类似于图 10-11。

f10011

图 10-11:描述性消息记录到控制台

使用警告

接下来,我们将使用 JavaScript 警告来暂停代码执行,给你时间查看日志消息。在此任务中,我们将使用alert函数,它会打开一个带有错误文本的对话框。请注意,过度使用警告会使检查日志变得耗时,因此最好适度使用它们。

在早期的log_f64示例中,你可能希望在某个特定的情况执行时立即提醒用户。alert会停止代码执行并弹出窗口通知用户。你只应在调试时,在需要立即注意的特殊情况下使用alert。在清单 10-6 中,我们将case 1:的代码更改为在弹出窗口中显示警告,而不是输出到控制台。将log_f64函数的开始部分更改为清单 10-6 所示。

pythagoras.html

log_f64: function(message_index, value) {
  switch( message_index ) {
    case 1:
    1 alert(`$x_dist=${value}`);
      break;

清单 10-6:更新pythagoras.html文件,以调用来自log_f64的警告。

我们将console.log函数调用更改为alert 1,当message_index为 1 时显示警告框。结果如图 10-12 所示,应在浏览器中显示。

f10012

图 10-12:显示警告框

堆栈跟踪

堆栈跟踪显示了调用过的函数列表,直到当前代码的这一点。例如,如果函数 A 调用函数 B,函数 B 又调用函数 C,随后执行堆栈跟踪,那么堆栈跟踪将显示函数 C、B 和 A,以及调用这些函数的行。WebAssembly 并不直接提供这个功能,因此就像向控制台输出日志一样,我们从 JavaScript 调用堆栈跟踪。调用的函数链应类似于图 10-13。

我们通过调用 JavaScript 的console.trace函数来显示堆栈跟踪。当前,Firefox 和 Chrome 提供的堆栈跟踪差异较大。使用 Firefox 中的 console.trace,你可以获取比在 Chrome 浏览器中更多的有关 WAT 文件的信息。Firefox 浏览器将 WebAssembly 二进制文件转换为 WAT 文件,并为你提供一个指向该反汇编 WAT 文件中行号的堆栈跟踪。另一方面,Chrome 给出的只是一个指向函数索引的引用,如果你不熟悉它,可能会显得相当晦涩。

f10013

图 10-13:函数 1 调用函数 2,函数 2 调用函数 3,函数 3 调用函数 4,堆栈跟踪中的函数调用

创建一个名为 stack_trace.wat 的文件,并将 列表 10-7 中的代码添加到文件中。

stack_trace.wat

(module

1 (import "js" "log_stack_trace" (func $log_stack_trace (param i32)))

2 (func $call_level_1 (param $level i32)
    local.get $level
    call $log_stack_trace
  )

3 (func $call_level_2 (param $level i32)
    local.get $level
    call $call_level_1
  )

4 (func $call_level_3 (param $level i32)
    local.get $level
    call $call_level_2
  )

5 (func $call_stack_trace (export "call_stack_trace")
 6 (call $log_stack_trace (i32.const 0))
    (call $call_level_1 (i32.const 1))
    (call $call_level_2 (i32.const 2))
    (call $call_level_3 (i32.const 3)) 
  )
)

列表 10-7:演示堆栈跟踪调用的 WebAssembly 模块

这个 WebAssembly 模块从 JavaScript 导入 log_stack_trace 1 函数,该函数将调用嵌入的 JavaScript 中的 console.trace。我们定义了四个额外的函数,演示每个浏览器如何记录 WebAssembly 调用栈。导入的函数 $log_stack_trace$call_stack_trace$call_level_1 2 调用。函数 $call_level_1$call_stack_trace$call_level_2 3 调用。函数 $call_level_2$call_stack_trace$call_level_3 4 调用。最后,$call_level_3$call_stack_trace 调用。我们通过嵌套这些函数调用,展示从不同函数级别调用时堆栈跟踪的样子。

注意,$call_stack_trace 5 调用了其他每个函数。首先,它直接调用 $log_stack_trace,并传递一个常量 0。接下来,它调用 $call_level_1,该函数调用 $log_stack_trace,并传递一个常量值 1。当堆栈跟踪被记录时,它应显示 $call_level_1$log_stack_trace 6 和 $call_stack_trace 在调用栈中。$call_level_2$call_level_3 函数会分别添加更多层级,这些层级将在堆栈跟踪中显示。

现在,创建一个名为 stack_trace.html 的新文件,并将 列表 10-8 中的代码添加到文件中。

stack_trace.html

<!DOCTYPE html>
<html lang="en">
<body>
    <h1>Stack Trace</h1>
    <script>
      let importObject = {
        js: {
          1 log_stack_trace: function( level ) {
                console.trace(`level=${level}`);
          }
        }
      };

      ( async () => {
        let obj =
          await WebAssembly.instantiateStreaming( fetch('stack_trace.wasm'),
                                                  importObject );
       obj.instance.exports.call_stack_trace();

      })();
    </script>
</body>
</html>

列表 10-8:带有 JavaScript 调用堆栈跟踪的 HTML 文件

这是一个非常基础的 HTML 文件,类似于pythagoras.html。主要的代码是定义在 importObject 内的 log_stack_trace 函数 1,它调用 JavaScript 函数 console.trace,并传递一个字符串,该字符串会在堆栈跟踪之前打印到控制台。保存此 HTML 文件后,在 Firefox 浏览器中打开,你应该会看到类似 图 10-14 的控制台日志。

f10014

图 10-14:在 Firefox 中显示堆栈跟踪

如你所见,第一个堆栈跟踪是通过level=0记录的,因为我们直接将0作为值传递给 WAT 代码中对$log_stack_trace的首次调用。这是从 WebAssembly 函数$call_stack_trace到导入的 JavaScript 函数的直接调用。由于第一次调用是直接调用$log_stack_trace,因此在这个第一个堆栈跟踪中,stack_trace.wasm文件只有一个堆栈帧被记录。该日志表明堆栈跟踪是从stack_trace.wasm的第 98 行执行的。这不一定是你 WAT 文件中的第 98 行;你需要在浏览器中查看 WAT,看看它指的是哪一行。每个堆栈跟踪都会在 WebAssembly 文件中添加一个额外的函数调用,因为我们为每次调用$log_stack_trace添加了额外的函数层。注意,在每个堆栈跟踪中,stack_trace.wasm内部都会出现额外的一行,该行出现在堆栈跟踪中。

点击其中一行,Firefox 会打开stack_trace.wasm文件,并定位到代码中发生函数调用的位置。

如果你还没有在 Firefox 调试器中打开stack_trace.wasm,你可能会被提示刷新浏览器页面以查看作为反汇编 WAT 显示的内容。当stack_trace.wasm打开到字节 98 时,你应该在 Firefox 调试器控制台中看到类似于图 10-15 的内容。

f10015

图 10-15:点击stack_trace.wasm中的位置显示 WAT 代码

发出调用的行会暂时以灰色高亮显示。注意左侧的字节数(62)是十六进制的,不像控制台日志中的字节是十进制数字 98\。

Chrome 不在 WAT 文件中显示每个堆栈跟踪的字节数;相反,它看起来像是图 10-16。

f10016

图 10-16:在 Chrome 中显示堆栈跟踪

在 Chrome 浏览器中,行号总是 1\。但是,当你点击控制台中的链接时,Chrome 会打开该特定函数的反汇编版本。所有 WebAssembly 函数都以wasm-为前缀,后跟函数的索引,再加上:1。点击堆栈跟踪中出现的第一个 WebAssembly 函数时,应该像图 10-17 所示。

f10017

图 10-17:点击 Chrome 中的堆栈跟踪显示 WebAssembly 函数。

在 Chrome 中,反汇编后的函数与 Firefox 中的不同。我们将在下一节中更详细地讨论这些差异。现在,注意 Chrome 使用变量和函数索引而不是标签进行反汇编,这使得阅读起来更具挑战性。

堆栈跟踪在你尝试弄清楚某些函数如何执行时非常有用。当你不确定一个函数是如何被调用时,堆栈跟踪可能会成为救命稻草。接下来,让我们看看 Firefox 和 Chrome 中的调试器代码。

Firefox 调试器

在这一节中,我们将编写一些可以在调试器中逐步执行的代码。首先,花点时间查看 pythagoras.htmlpythagoras.wat 文件。我们故意引入了一个 bug,以便在调试器中追踪它。我们将修改 pythagoras.wat 文件,移除向 JavaScript 输出日志的调用,以便我们可以在调试器中逐步执行。创建一个名为 debugger.wat 的文件,并添加 清单 10-9 中的代码,或者简单地从 pythagoras.wat 中移除日志调用并重新保存文件。

debugger.wat

(module
  (func $distance (export "distance")
    (param $x1 f64) (param $y1 f64) (param $x2 f64) (param $y2 f64)
    (result f64)
    (local $x_dist f64)
    (local $y_dist f64)

    local.get $x1
    local.get $x2
    f64.sub             ;; $x1 - $x2
    local.tee $x_dist   ;; $x_dist = $x1 - $x2    local.get $x_dist
    f64.mul             ;; $x_dist * $x_dist on stack
    local.get $y1
    local.get $y2
   f64.add             ;; Should be $y1 - $y2    local.tee $y_dist   ;; $y_dist = $y1 - $y2
    local.get $y_dist
    f64.mul             ;; $y_dist * $y_dist on stack
    f64.add             ;; $x_dist * $x_dist + $y_dist * $y_dist on stack
    f64.sqrt            ;; take the square root of x squared plus y squared
  )
)

清单 10-9:我们通过移除日志调用来修改 pythagoras.wat

之前,我们通过将 $y1 加到 $y2 上,而不是相减,故意引入了一个有时会给出不正确结果的 bug。将 pythagoras.html 复制到一个名为 debugger.html 的新文件中,并将 <script> 标签内的 JavaScript 代码改为加载 debugger.wasm。然后删除 importObject,使其看起来像 清单 10-10 中的代码。

pythagoras.html

...
<script>
  var distance = null;

  ( async () => {
    let obj = await WebAssembly.instantiateStreaming( fetch(**'debugger.wasm'**) );

    distance = obj.instance.exports.distance;

  })();
  function set_distance() {
    let dist_out = document.getElementById('dist_out');
    let x1 = document.getElementById('x1');
    let x2 = document.getElementById('x2');
    let y1 = document.getElementById('y1');
 let y2 = document.getElementById('y2');

    let dist = distance(x1.value, y1.value, x2.value, y2.value);
    dist_out.innerHTML = dist;
  }
</script>
...

清单 10-10:测试 debugger.wasm 的 HTML 文件

debugger.html 加载到 Firefox 中并打开控制台;然后点击 调试器 标签,进入 Firefox 调试器。在左侧的 Sources 标签下,选择 debugger.wasm,查看反汇编版本的 WAT 代码,应该类似于 图 10-18。

f10018

图 10-18:Firefox 调试器中的 WAT 代码

这段代码是 WebAssembly 二进制的反汇编,因此现在函数和变量的名称不再可用。这个结果类似于你反汇编一个从网上找到的二进制文件。由于在 wat2wasm 中还没有源映射功能,我们无法在调试器中逐步执行原始源代码。相反,你需要将原始代码和反汇编代码进行并排比较。清单 10-11 显示了反汇编代码的样子。

(module

  (type $type0 (func (param f64 f64 f64 f64) (result f64)))
  (export "distance" (func $func0))
1 (func $func0
    (param 2$var0 f64)(param 3$var1 f64)(param 4$var2 f64)(param 5$var3 f64)
    (result f64)
    (local 6$var4 f64) (local 7$var5 f64)
    local.get $var0
 local.get $var2
    f64.sub
    local.tee $var4
    local.get $var4
    f64.mul
    local.get $var1
    local.get $var3
    f64.add
    local.tee $var5
    local.get $var5
    f64.mul
    f64.add
    f64.sqrt
  )
)

清单 10-11:Firefox 反汇编生成的 WAT 代码

这段代码是从 WebAssembly 二进制文件反汇编而来的,并没有意识到我们为变量或函数所给的标签。它也无法识别代码中的任何注释。如果你回头查看原始的 WAT 代码(清单 10-9),你可以看到函数$distance变成了$func0 1。参数变量$x1$y1$x2$y2 分别变成了$var0 2、$var1 3、$var2 4 和$var3 5。局部变量$x_dist$y_dist 变成了$var4 6 和$var5 7。了解原始变量与反汇编变量之间的对应关系后,你就可以逐步执行代码,知道每个变量的用途。要查看这些变量的值,你可以将它们输入到右侧的 Watch 表达式窗口中,去掉 $ 符号。在 Watch 窗口中,你可以通过输入 var0 来查看$var0变量。我使用一个简单的技巧来跟踪哪个变量对应哪个。我在我的 Watch 表达式中添加 JavaScript 注释,标记该变量的原始名称。例如,我可能会在 Watch 表达式中输入 $var0var0 // $x1。图 10-19 展示了 Watch 表达式中的效果。

f10019

图 10-19:在 Firefox 中使用注释的 Watch 表达式

要逐步执行 WAT 代码,请确保已选择 WebAssembly 文件。我们需要创建一个断点,这是调试器停止执行代码的地方,以便你可以逐行查看代码。要设置断点,请点击 WAT 代码左侧的字节编号。在右侧的 Watch 表达式窗口中,你可以看到变量的变化。设置好断点后,通过点击查找距离(图 10-20)来执行 WebAssembly 代码。

f10020

图 10-20:在 Firefox 调试器中设置断点

当执行到达断点时,点击位于 Watch 表达式上方的逐步跳过按钮 i10001。这将允许你逐行执行代码。要进入一个函数而不是执行它,请点击逐步进入按钮 i10002,该按钮位于逐步跳过按钮旁边。如果你想跳出当前的函数,点击逐步跳出按钮 i10003。如果你希望调试器继续执行,直到达到另一个断点,点击恢复按钮 i10004,它看起来像一个播放按钮。

要定位代码中的错误,请点击逐步跳过按钮,直到到达 3D 行。此时,var5 已设置,我们可以在 Watch 表达式窗口中看到其值,如图 10-21 所示。

f10021

图 10-21:在 Firefox 调试器中逐步执行代码

注意,当Y1被设置为1Y2被设置为4时,$y_dist被设置为5。这意味着$y_dist本应是3。之前,我们将编号为 3A 的行从f64.sub改为f64.add,引入了这个错误。通过在调试器中逐行单步执行代码,我们找到了问题所在。

Chrome 调试器

在 Chrome 中调试 WebAssembly 与在 Firefox 中调试相同的代码有所不同。WAT 代码不会按 WebAssembly 文件进行分解;相反,Chrome 会按函数对 WAT 代码进行分组。WebAssembly 函数末尾的数字是一个索引号,基于你在代码中定义函数的位置。

要进入调试器,打开 Chrome 的开发者工具并点击Sources标签。在名为 Page 的部分下,你应该看到一个标有wasm的云图标。展开此分支,可以看到每个在 WebAssembly 模块中定义的函数的页面。因为我们在这个模块中只定义了一个函数,所以只有一个函数存在。点击该函数,在右侧窗口中显示该函数的代码。在该窗口中,在包含代码local.get 0的第 3 行设置断点(图 10-22)。

f10022

图 10-22:在 Chrome 调试器中设置断点

注意到local.get获取的是一个数字而不是变量名。原因是,在local.get后面跟着一个数字时,会根据索引而不是名称获取局部变量。使用local.get 0等同于在 Firefox 浏览器中的local.get $var0。像在 Firefox 中一样,你可以查看代码,并将其与函数中的代码进行匹配。清单 10-12 展示了在 Chrome 调试器中显示的代码。

1 func (param f64 f64 f64 f64) (result f64)
2 (local f64 f64)
  local.get 0
  local.get 2
  f64.sub
  local.tee 4
  local.get 4
  f64.mul
  local.get 1
  local.get 3
  f64.add
  local.tee 5
  local.get 5
  f64.mul
  f64.add
  f64.sqrt
end

清单 10-12:Chrome 调试器中的 WAT 反汇编

注意,Chrome 使用索引来表示局部变量、参数和函数。函数 1 没有与之关联的名称,它的任何参数或局部变量 2 也没有名称。全局变量和类型也是如此。如果我们使用全局变量,我们将使用global.getglobal.set,并传入一个与变量定义顺序相对应的索引号。

Chrome 调试功能的一个优点是你可以在作用域窗口中访问堆栈。在逐步执行代码时,你可以看到值被推送到堆栈中,并从堆栈中弹出。一个缺点是,Watch 窗口的用处远不如在 Firefox 中,因为 Chrome 不会像在 JavaScript 变量那样使变量可用。

和 Firefox 一样,Chrome 有一个 Resume 按钮 i10005、一个 Step over 按钮 i10006、一个 Step into 按钮 i10007和一个 Step out 按钮 i10008,如图 10-23 所示。

f10023

图 10-23:在 Chrome 调试器中查看堆栈

总结

在本章中,我们使用了多种不同的技术在 Chrome 和 Firefox 中调试 WAT 代码。我们对控制台日志记录进行了比之前章节更深入的探讨。接着,我们使用了 JavaScript 的alert函数来暂停执行并等待用户指令。我们还探索了使用console.trace来记录堆栈跟踪,并讨论了 Chrome 和 Firefox 中堆栈跟踪的工作方式差异。最后,我们使用了 Chrome 和 Firefox 内置的调试器。

调试 WebAssembly 有多种可用选项。其中一些选项,例如使用 Chrome 或 Firefox 调试器,仍在开发中。你选择使用的工具将取决于代码和调试时的目标。在下一章中,我们将使用 WebAssembly 构建 Node.js 模块。

第十一章:AssemblyScript

AssemblyScript 是一种高级语言,专门设计用于编译成 WebAssembly 或 WAT。AssemblyScript 比 WAT 更具表现力,但仍然可以编译成 WAT。当你使用 AssemblyScript 时,你失去了一些使用 WAT 时可以进行的细致优化控制,但编写起来要快得多。

本章我们将通过创建一个简单的 AddInt``s 函数开始,类似于我们在第一章中创建的 AddInt。我们将编写一个 AssemblyScript 的 Hello World 应用,并将其编译成 WAT,查看 AssemblyScript 编译器生成的 WebAssembly。我们将研究 AssemblyScript 如何使用长度前缀字符串,然后安装 AssemblyScript 加载器,看看它如何简化在 AssemblyScript 和 JavaScript 之间传递字符串。我们将通过编写一个字符串连接应用来将字符串传入 AssemblyScript。我们还将探索 AssemblyScript 中的面向对象编程(OOP)。我们将创建几个类来演示类继承,并讨论 private 属性,这些属性可以防止 AssemblyScript 将属性导出到嵌入环境。接下来,我们将编写 JavaScript,使我们能够直接创建 publicprivateprotected 成员,并使用 AssemblyScript 加载器。然后,我们将比较直接调用与加载器函数调用的性能。

AssemblyScript 团队设计时参考了 TypeScript 和 JavaScript。与 WAT 不同,AssemblyScript 是一种具有类、字符串和数组等特性的高级语言。除了高级特性外,AssemblyScript 还允许用户使用类似 WAT 的低级内存命令进行编程。AssemblyScript 提供了一个命令行界面 (CLI),可以将 AssemblyScript 编译成 WebAssembly 模块,并在 JavaScript 应用程序中使用。

对于希望使用 WebAssembly 来提高 JavaScript 应用程序性能的 JavaScript 开发者来说,AssemblyScript 是一个很好的工具。不幸的是,正如 WebAssembly 中的所有内容一样,仅仅调整 TypeScript 直到它能够通过 AssemblyScript 编译器编译,并不一定会带来显著的性能提升。了解 AssemblyScript 在幕后是如何工作的,可以让你用看似 JavaScript 的语言编写代码,但运行时表现得像 C++。为了获得这种理解,我们将把 AssemblyScript 代码编译成 WAT,以探索 AssemblyScript 编译器的输出。

AssemblyScript CLI

使用以下命令安装 AssemblyScript:

npm install assemblyscript -g

npm 命令全局安装 AssemblyScript,允许你从命令行使用 AssemblyScript 编译器 asc 命令。运行 asc -h 会提供编译器命令示例和选项的列表。

我不会解释所有的命令行参数,但会提到一些有用的参数。-O选项与第九章中的wasm-opt优化方式相同。你在-O后跟一个数字 0 至 3、s 或 z,指示编译器进行优化,优化目标是大小还是性能,以及应用多少优化。-o标志后跟.wat文件的名称时,会从 AssemblyScript 生成 WAT 代码;而后跟.wasm文件的名称时,会生成 WebAssembly 二进制模块。--sourceMap标志创建一个源映射文件,帮助你从浏览器调试 AssemblyScript。

我们首先创建一个简单的 AssemblyScript 模块。创建文件as_add.ts并添加清单 11-1 中的代码。这是第一章中AddInt函数的一个简化版本。

as_add.ts

1 export function AddInts(2a: i32, 3b: i32 ): i32 {
  4 return a + b;
}

清单 11-1:两个整数相加

我们使用export 1 关键字将function暴露给嵌入的 JavaScript。它接受两个i32类型的参数a 2 和b 3,并返回a + b 4,作为i32类型。使用清单 11-2 中的命令编译 as_add.ts

asc as_add.ts -Oz -o as_add.wat

清单 11-2:将AddInts`编译为 WAT

-Oz标志使输出的二进制文件尽可能小。最后一个标志-o as_add.wat告诉编译器输出 WAT 代码。或者,我们也可以编译一个.wasm文件,比如as_add.wasm,它将输出 WebAssembly 二进制文件。当我们查看输出的as_add.wat文件时,会看到清单 11-3 中的 WAT 代码。

as_add.wat

(module
  (type $i32_i32_=>_i32 (func (param i32 i32) (result i32)))
  (memory $0 0)
1 (export "AddInts" (func $as_add/AddInts))
  (export "memory" (memory $0))
2 (func $as_add/AddInts (param $0 i32) (param $1 i32) (result i32)
  3 local.get $0
  4 local.get $1
  5 i32.add
  )
)

清单 11-3:编译为 WAT 的 AssemblyScript AddInts函数

在 AssemblyScript 中编写代码比直接在 WAT 中编写代码要容易得多。这段代码生成了AddInts 1 函数,它导出了一个接受两个i32参数并返回一个i32的函数。输出函数使用local.get来获取第一个 3 和第二个 4 参数,并使用i32.add5 来将这两个值相加。

AssemblyScript 是一种美丽的小语言,对于熟悉 TypeScript 或 JavaScript 的人来说,相对容易学习。理解 WAT 是从你的 AssemblyScript 或你选择的任何高级语言中获取最大收益的好方法,尤其是用于 WebAssembly 开发时。

Hello World AssemblyScript

接下来,我们将构建一个 AssemblyScript 版本的 WAT Hello World 应用程序,参考第二章的示例。创建一个名为as_hello.ts的新 AssemblyScript 文件,并添加清单 11-4 中的代码。

as_hello.ts

1 declare function console_log( msg: string ):void;

2 export function HelloWorld():void {
3 console_log("hello world!");
}

清单 11-4:一个 Hello World 的 AssemblyScript 应用

AssemblyScript 中的函数声明必须与传入 WebAssembly 模块的 JavaScript 函数相对应。因此,我们需要通过 importObject 传入一个将字符串记录到控制台的函数。declare function 1 从 JavaScript 导入了 console_log 函数。这个函数将把一个字符串从 AssemblyScript 传回调用的 JavaScript 应用程序。我们创建了一个 export function,名为 HelloWorld 2,它调用导入的 console_log 3 函数,并传入字符串 "hello world!"。在将其编译成 WebAssembly 模块之前,我们将使用 asc 编译一个 WAT 文件,这样我们就可以查看创建的 WebAssembly(列表 11-5)。

asc as_hello.ts -Oz -o as_hello.wat

列表 11-5:将 as_hello.ts AssemblyScript 文件编译为 as_hello.wat

然后,我们可以打开 as_hello.wat 文件,在 列表 11-6 中查看 AssemblyScript 生成的 WebAssembly。

;; The comments were added by the author and not generated by asc 1
(module
  (type $none_=>_none (func))
  (type $i32_=>_none (func (param i32)))
 ;; the declare command at the top of the AssemblyScript created an import
 ;; that imports the console_log function inside of the outer as_hello
 ;; object.  AssemblyScript requires its imports in the AssemblyScript file name
 ;; not including the .ts extension
  (import "as_hello" "console_log" (func $as_hello/console_log (param i32))) 2
 ;; using a string automatically creates the memory expression
  (memory $0 1) 3
  ;; the data line below wraps because the line is too long
 ;; The "hello world!" string is preceded by a header and has a hex 00 byte in
 ;; between every letter in the string.  This is because AssemblyScript uses
 ;; the UTF-16 character set instead of ASCII as we did when we were manipulating
 ;; string data in WAT.
  (data (i32.const 16) 4
    "\18\00\00\00\01\00\00\00\01\00\00\00\18\00\00\00h\00e\00l\00l\00o\00 \00w\00o\00r\00l\00d\00!")
  (export "memory" (memory $0))
 ;; The module exports our function with the AssemblyScript name we gave it.
  (export "HelloWorld" (func $as_hello/HelloWorld)) 5
 ;; the function name we gave AssemblyScript is prefixed by the name of our file
 ;; without the .ts extension
  (func $as_hello/HelloWorld (; 1 ;) 6
 ;; 32 is the location in linear memory of the 'h' byte in "hello world"
  i32.const 32 7
 ;; the console_log function is called passing in the location of "hello world"
 ;; in linear memory
  call $as_hello/console_log 8
  )
)

列表 11-6:从 as_hello.ts AssemblyScript 生成的 as_hello.wat 文件。

我已添加注释以澄清代码 1。这个模块导入了 console_log 2,并将其包装在对象 as_hello 中,这是我们 AssemblyScript 文件的名称(不包括 .ts 扩展名)。这是 AssemblyScript 对 importObject 使用的命名约定;当你编写 JavaScript 时,必须在导入的对象中相应地命名你的对象。

AssemblyScript 创建了一个 memory 3 表达式来存储字符串数据。字符串有一个前缀头部,包含了字符串的长度,AssemblyScript 使用它来在 WebAssembly 内部操作数据。字符串 data 4 每个字符使用两个字节,因为 AssemblyScript 使用的是 UTF-16 编码,在这个例子中,每个字符之间由一个空字节 \00 分隔。UTF-16 是 Unicode 字符集的 16 位版本,允许使用许多 ASCII 中不可用的附加字符。

在数据表达式之后,WAT 导出了 5 个函数,函数的名称是我们在 AssemblyScript 中为其指定的,前面加上了 $ 字符,并去掉了 .ts 扩展名。HelloWorld 6 函数调用了 console_log 8,传入了我们 hello world! 字符串在线性内存中的第一个字符的位置,即 32 7。

使用我们编译好的 WAT 文件,我们可以在 列表 11-7 中使用 asc 命令来编译我们的 WebAssembly 模块。

asc as_hello.ts -Oz -o as_hello.wasm

列表 11-7:将我们的 AssemblyScript 编译为 WebAssembly 二进制文件。

接下来,我们编写我们的 JavaScript。

我们的 Hello World 应用的 JavaScript

当前,我们有一个名为 as_hello.wasm 的 WebAssembly 模块。接下来,我们将编写一个 Node.js 应用程序来加载并运行这个模块。在本节中,我们将像在第五章中那样解码字符串数据,以了解 AssemblyScript 如何将字符串传输到 JavaScript。然后,我们将使用 AssemblyScript 加载工具为我们完成这项工作。

首先,我们将编写一个函数,通过 WebAssembly 模块传递的索引从线性内存中提取字符串数据。AssemblyScript 将字符串的长度存储在字符串数据之前的四个字节中。我们可以使用 Uint32Array 获取字符串长度的整数,并利用该长度在 JavaScript 中创建我们的字符串。创建一个名为 as_hello.js 的文件,并添加 清单 11-8 中的代码。

as_hello.js

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/as_hello.wasm');

// The memory object is exported from AssemblyScript
1 var memory = null;

let importObject = {

// 模块的文件名(不带扩展名)用作外部对象名称

2 as_hello: {
 // AssemblyScript passes a length prefixed string with a simple index
 3 console_log: function (index) {
 // in case this is called before memory is set
      if (memory == null) {
        console.log('memory buffer is null');
        return;
      }

    4 const len_index = index - 4;

 // must divide by 2 to get from bytes to 16-bit unicode characters
    5 const len = new Uint32Array(memory.buffer, len_index, 4)[0];
    6 const str_bytes = new Uint16Array(memory.buffer,
        index, len);

 // decode the utf-16 byte array into a JS string
    7 const log_string = new TextDecoder('utf-16').decode(str_bytes);
      console.log(log_string);
    }
  },
  env: {
    abort: () => { } 
  }
};

(async () => {
  let obj = await WebAssembly.instantiate(new Uint8Array(bytes),
    importObject);

 // memory object exported from AssemblyScript
8 memory = obj.instance.exports.memory;
 // call the HelloWorld function
9  obj.instance.exports.HelloWorld();
})();

清单 11-8:从 JavaScript 调用 AssemblyScript 的 HelloWorld

AssemblyScript 生成的 WebAssembly 模块总是会创建并导出它自己的内存,除非使用 --importMemory 标志进行编译。默认情况下,AssemblyScript 会在 WebAssembly 模块中创建自己的线性内存。因此,在 JavaScript 中,我们不创建线性内存对象。相反,我们创建一个名为 memory 1 的 var,稍后我们会将其设置为 WebAssembly 模块导出的线性内存对象。

importObject 中,持有导入数据的对象必须与导入它的 AssemblyScript 文件同名:对于我们的 AssemblyScript 文件 as_hello.ts,就是 as_hello 2。在 as_hello 中是 console_log 3,当从 AssemblyScript 调用时会传递一个字符串参数。当 WebAssembly 模块调用 as_hello 时,JavaScript 函数只接收到一个指向 WebAssembly 线性内存的数字索引,该索引是字符串数据部分(包含长度前缀的字符串)的位置,这是 AssemblyScript 用来定义其字符串类型的方式。

长度是一个 32 位整数,位于 index 前的四个字节中。为了获取长度整数的索引,我们从字符串索引中减去四。通过创建新的 Uint32Array,传入 memory.bufferlen_index 4 和字节数 4,我们可以使用线性内存中存储的长度值。由于 Uint32Array 5 是一个 32 位整数数组,我们需要使用 [0] 获取数组中的第一个也是唯一的项。

我们使用 new Uint16Array 6 从线性内存中获取字符串字节数据,并通过使用新的 TextDecoder 将字节数组转换为 JavaScript 字符串,该解码器用于解码 utf-16 文本数据。代码调用 TextDecoder 7 的 decode 函数,传入字符串数据,返回的 JavaScript 字符串随后被打印到控制台。我们使用一个立即执行函数表达式(IIFE)来实例化 AssemblyScript 的 WebAssembly 模块。请注意,在调用 HelloWorld 9 函数之前,我们必须将 memory 8 对象设置为从 WebAssembly 模块导出的内存对象。console_log 函数使用 memory 对象,如果没有设置,调用 HelloWorld 将不会产生任何效果。

幸运的是,有一种更简单的方法可以在 AssemblyScript 和 JavaScript 之间传递字符串数据,那就是使用 AssemblyScript 加载器。这个代码是由 AssemblyScript 团队提供的。在“加载器与直接 WebAssembly 调用的性能对比”*部分,我们将看到是否能通过我们编写的代码来提升 AssemblyScript 加载器的性能。

使用 AssemblyScript 加载器的 Hello World

AssemblyScript 加载器是一组来自 AssemblyScript 团队的助手函数,旨在简化从 JavaScript 调用 AssemblyScript 的过程。我们将比较之前编写的代码与使用 AssemblyScript 加载器编写的代码。最初,我们将考虑易用性,之后再探讨使用或不使用加载器的性能影响。

我们使用 AssemblyScript 加载器将一个字符串从 AssemblyScript 传回 JavaScript。加载器助手函数将来自 WebAssembly 的索引转换为 JavaScript 字符串。现在,我们将使用 npm 安装加载器:

npm install @assemblyscript/loader --save

现在,我们将创建一个 JavaScript 文件来加载和运行我们的 WebAssembly 模块。创建一个名为 as_hello_loader.js 的文件,并添加 Listing 11-9 中的代码。

as_hello_loader.js

1 const loader = require("@assemblyscript/loader");
const fs = require('fs');
2 var module;

const importObject = {
3 as_hello: {
  4 console_log: (str_index) => {
    5 console.log(module.exports.__getString(str_index));
    }
  }
};

(async () => {
  let wasm = fs.readFileSync('as_hello.wasm');
6 module = await loader.instantiate(wasm, importObject);
7 module.exports.HelloWorld();
})();

Listing 11-9: 使用 AssemblyScript 加载器调用 WebAssembly 模块

这个 JavaScript 函数首先需要 1 个 AssemblyScript 加载器。我们使用这个加载器对象来加载声明为全局的 module 2 对象。module 对象是一个 AssemblyScript 加载器模块,其中包含额外的 AssemblyScript 加载器助手函数。在 importObject 中是一个子对象,命名为 as_hello 3,代表我们的 AssemblyScript 模块。这里是 AssemblyScript 代码期望找到导入函数的位置。在 as_hello 对象中有一个 console_log 4 函数,它将字符串索引 str_index 作为唯一参数。这个函数使用由加载器创建的 module 对象上的 __getString 5 函数。当传入字符串索引时,__getString 函数从线性内存中检索一个 JavaScript 字符串。这个字符串通过 console.log 打印到控制台。IIFE 函数使用 AssemblyScript loader 6 对象加载一个 AssemblyScript 模块。最后,IIFE 调用 HelloWorld 7 函数。当你使用 node 运行这个 JavaScript 文件时,你会在 Listing 11-10 中看到输出。

hello world!

Listing 11-10: AssemblyScript hello world 应用的输出

使用 AssemblyScript 加载器使得 JavaScript 代码显著简化。稍后,在“加载器与直接 WebAssembly 调用的性能对比”部分,我们将探讨性能影响。

AssemblyScript 字符串拼接

现在我们知道如何从 AssemblyScript 接收字符串,接下来我们将把一个字符串发送到 AssemblyScript 模块。这个函数将两个字符串用管道符号(|)连接起来。我们将使用加载器来简化 JavaScript 端的代码编写。字符串连接是 WAT 中很难直接实现的功能,但在 AssemblyScript 中非常简单。创建一个名为as_concat.ts的新文件,并在其中添加列表 11-11 中的代码。

as_concat.ts

1 export function cat( str1: string, str2: string ): string {
2 return str1 + "|" + str2;
}

列表 11-11:使用 AssemblyScript 连接字符串

我们导出一个cat函数,它接收两个字符串参数并返回一个字符串。此函数将两个字符串连接在一起,中间用管道符号(|)分隔。

现在我们可以使用列表 11-12 中的asc命令编译as_concat.ts

asc as_concat.ts --exportRuntime -Oz -o as_concat.wasm

列表 11-12:使用asc编译as_concat.ts文件

我们传递了--exportRuntime标志,这对于将字符串传递到 WebAssembly 模块中是必需的。使用--exportRuntime进行编译时,会添加允许你从 JavaScript 调用__allocString函数的代码。如果我们未能导出运行时,应用执行时会出现以下错误:

TypeError: alloc is not a function

当你将as_concat.ts编译成 WAT 时,会注意到 WAT 文件比我们的as_hello.ts文件大得多。原因在于运行时添加了几个字符串函数,这些函数执行必要的任务,例如复制内存、连接字符串以及获取/设置字符串长度方法。

现在我们可以编写我们的 JavaScript 应用了。列表 11-13 中的代码会在线性内存中创建两个字符串,并调用 WebAssembly 函数cat。创建一个名为as_concat.js的新 JavaScript 文件,并在其中添加列表 11-13 中的代码。

as_concat.js

const fs = require('fs');
const loader = require("@assemblyscript/loader");

(async () => {
  let module = await loader.instantiate(fs.readFileSync('as_concat.wasm'));

  //__newString, __getString functions require
  //compile with --exportRuntime flag
1 let first_str_index = module.exports.__newString("first string");
2 let second_str_index = module.exports.__newString("second string");
3 let cat_str_index = module.exports.cat(first_str_index,second_str_index);
4 let cat_string = module.exports.__getString(cat_str_index);
5 console.log(cat_string);
})();

列表 11-13:JavaScript 使用 AssemblyScript 加载器调用cat AssemblyScript 函数。

我们在 WebAssembly 模块中定义的cat函数并不直接接收字符串作为参数,因此需要一个线性内存中的索引来获取字符串位置。module.exports.__newString加载器助手函数接收一个 JavaScript 字符串,将其复制到线性内存中,并返回一个索引供module.cat使用。我们调用module.exports.__newString两次,第一次传入"first string" 1,然后传入"second string" 2。每次调用都会返回一个索引,我们将其分别存储在first_str_indexsecond_str_index中。接下来,我们调用module.exports.cat,传入这些索引,并返回一个 JavaScript 字符串索引,我们将其存储在cat_str_index 3 中。然后,我们调用module.exports.__getString 4,传入cat_str_index,并将获取到的字符串存储在cat_string 5 中,最后输出到控制台。

现在我们有了 JavaScript 和 WebAssembly,我们可以使用node运行我们的应用:

node as_concat.js

下面是输出到控制台的内容:

first string|second string

AssemblyScript 还有很多内容值得探索。如你所见,AssemblyScript 在处理字符串时比 WAT 更加简单。虽然这并不能直接告诉你何时应该在 WebAssembly 中处理字符串数据,但它提供了这一选项。AssemblyScript,像 WebAssembly 一样,是一个快速发展的项目。花时间从项目主页assemblyscript.org了解更多内容是值得的。

在 AssemblyScript 中使用面向对象编程(OOP)

在 WAT 格式中几乎不可能使用 OOP,但由于 AssemblyScript 是基于 TypeScript 的,它提供了更多的 OOP 选项。在本节中,我们将介绍 AssemblyScript 中 OOP 的一些基础知识,以及一些它的限制,这些限制可能会在未来的版本中不再适用。

我们从创建一个名为vector.ts的 AssemblyScript 文件开始。目前,AssemblyScript 是基于 TypeScript 文件格式的,这在大多数情况下都能正常工作。

Saule Cabrera 为 VS Code 创建了一个 AssemblyScript 语言服务器插件,插件可通过marketplace.visualstudio.com/items?itemName=saulecabrera.asls.下载。

接下来,我们将编写一个 AssemblyScript 的Vector2D类,用于存储类似于第八章碰撞检测应用程序中所写的碰撞器对象的坐标。我们将代码编译成 WAT,以便查看 AssemblyScript 编译器的输出。更好地理解编译器及其输出,在优化 WebAssembly 代码时非常有帮助。将清单 11-14 添加到你的文件中,以创建Vector2D类。

vector.ts

1 export class Vector2D {
2 x: f32;
3 y: f32;

4 constructor(x: f32, y: f32) {
    this.x = x;
    this.y = y;
  }

5 Magnitude(): f32 {
    return Mathf.sqrt(this.x * this.x + this.y * this.y);
  }
}

清单 11-14:在 AssemblyScript 中创建一个向量类

我们导出了一个名为Vector2D的类,它有两个属性,xy。它还有一个constructor,用于从xy参数创建一个新的Vector2D对象。Magnitude方法通过对xy的平方求和并对该和取平方根来计算向量的大小。

如果你熟悉 TypeScript,你会注意到这段代码看起来与 TypeScript 中的class结构非常相似。然而,我们并没有使用 TypeScript 中的number类型,而是使用了f32类型来表示 32 位浮动点数。如果你在 AssemblyScript 中使用number类型,它实际上相当于使用f64类型的 64 位浮动点数,而在大多数情况下,f64的性能是 WebAssembly 类型中最差的。

以下命令使用ascvector.ts编译成 WAT 文件:

asc vector.ts -o vector.wat

这会创建一个我们可以在 VS Code 中查看的 WAT 文件。要进行 asc 编译,我们传递 AssemblyScript 文件的名称,然后传递 -o 标志并指定输出文件的文件名 vector.wat。扩展名决定了输出是 WAT 文件还是 WebAssembly 二进制文件。打开 vector.wat 文件并稍微向下滚动,直到看到在 清单 11-15 中显示的导出内容。

vector.wat

...
  (export "memory" (memory $0))
  (export "Vector2D" (global $vector/Vector2D))
1 (export "Vector2D#get:x" (func $vector/Vector2D#get:x))
2 (export "Vector2D#set:x" (func $vector/Vector2D#set:x))
3 (export "Vector2D#get:y" (func $vector/Vector2D#get:y))
  (export "Vector2D#set:y" (func $vector/Vector2D#set:y))
4 (export "Vector2D#constructor" (func $vector/Vector2D#constructor))
5 (export "Vector2D#Magnitude" (func $vector/Vector2D#Magnitude))
...

清单 11-15:我们 WAT 文件中导出的函数

注意编译器如何为 x 2 和 y 3 属性生成 get 1 和 set 2 访问函数,并将它们导出,以便你可以从嵌入环境访问它们。这表明,当用户通过加载器设置对象属性时,它会调用 WebAssembly 模块中的一个函数。这意味着,如果你一次设置多个属性,可能需要考虑创建一个函数来一次性完成这一操作,以提高性能。这样,你就不需要多次调用 WebAssembly 模块的函数。你还可以看到 WebAssembly 模块导出了 constructor 4 和 Magnitude 5 函数。

如果你想从 JavaScript 中调用 WebAssembly 模块的函数,命名约定是非常重要的。所有方法都以类名和哈希符号(#)作为前缀(Vector2D#)。setget 方法有一个后缀,表示它们设置或获取的属性,例如 :x:y。要在不使用 AssemblyScript 加载器的情况下从 JavaScript 访问这些函数和属性,我们需要遵循这种命名约定。

使用私有属性

如果你不想将所有属性导出到嵌入环境中,需要在 xy 属性前使用 private 关键字。现在在你的 AssemblyScript 中进行此操作,并使用 asc 命令重新编译。清单 11-16 显示了新版本。

vector.ts

export class Vector2D {
  1 private x: f32;
  2 private y: f32;

  constructor(x: f32, y: f32) {
    this.x = x;
    this.y = y;
  }

  Magnitude(): f32 {
    return Mathf.sqrt(this.x * this.x + this.y * this.y);
  }

}

清单 11-16:在 AssemblyScript 中创建私有函数

x 1 和 y 2 前面的 private 修饰符告诉 AssemblyScript 编译器,这些属性不应该对外部公开访问。重新编译 WebAssembly 模块,该模块不再导出设置和获取 xy 变量的访问方法到嵌入环境,如 清单 11-17 所示。

vector.wat

...
(export "memory" (memory $0))
(export "Vector2D" (global $vector/Vector2D))
(export "Vector2D#constructor" (func $vector/Vector2D#constructor))
(export "Vector2D#Magnitude" (func $vector/Vector2D#Magnitude))
...

清单 11-17:WAT 文件中的导出

TypeScript 有三个修饰符,publicprivateprotected,用来定义属性的访问方式。这些修饰符在 AssemblyScript 中的行为与其他语言(如 TypeScript)有所不同。在大多数语言中,protected 属性可以被继承该类的子类访问,但不能在父类或子类之外访问。而在 AssemblyScript 中,protected 方法没有完全实现,其行为与 public 修饰符相同。目前,建议避免使用它以免造成混淆。尽管这些关键字未来可能会像在 TypeScript 中那样工作,但需要注意这些限制仍然存在于 AssemblyScript 版本 0.17.7 中。

private 修饰符阻止 AssemblyScript 在编译模块时导出 getset 方法。

与其他面向对象编程语言不同,AssemblyScript 中的 private 修饰符并不会阻止继承原始类的类访问该属性。

让我们使用以下命令将我们的 AssemblyScript 编译成 WebAssembly 模块,这样我们就可以从 JavaScript 中调用它:

asc vector.ts -o vector.wasm

当我们将 -o 标志改为 vector.wasm 时,我们告诉 asc 编译器输出 WebAssembly 二进制文件。这将允许我们从 JavaScript 嵌入环境加载并运行该模块。接下来,让我们看看如何使用 Node.js 加载和调用 WebAssembly 函数。

JavaScript 嵌入环境

我们将使用 Node.js 来加载和执行 WebAssembly 模块。如果改用浏览器,JavaScript 会使用 WebAssembly.instantiateStreaming ```` and `fetch` instead of using `fs` to load the WebAssembly module from the filesystem and calling `WebAssembly.instantiate``` `.` `` ````

````` ````Create the file *vector.js* and add the code in Listing 11-18. **vector.js** ``` 1 const fs = require('fs'); 2 (async () => { 3 let wasm = fs.readFileSync('vector.wasm'); 4 let obj = await WebAssembly.instantiate(wasm,{env:{abort:()=>{}}}); 5 let Vector2D = { 6 init: function (x, y) { return obj.instance.exports"Vector2D#constructor" }, 7 Magnitude: obj.instance.exports["Vector2D#Magnitude"], } 8 let vec1_id = Vector2D.init(3, 4); let vec2_id = Vector2D.init(4, 5); console.log(` 9 vec1.magnitude=${Vector2D.Magnitude(vec1_id)} vec2.magnitude=${Vector2D.Magnitude(vec2_id)} `); })(); ``` Listing 11-18: Calling functions on the `Vector2D` AssemblyScript class We use the `fs` 1 Node.js module to load 3 the binary WebAssembly data from a file inside an asynchronous IIFE 2. Once we have the binary data, we pass it to `WebAssembly.instantiate` 4, which returns a WebAssembly module object. We then create the JavaScript object `Vector2D` 5, which mirrors the functions inside the WebAssembly module. We create an `init` 6 function that calls the WebAssembly module’s `Vector2D` `constructor`, passing in `0` as the first parameter. Passing this value to the `constructor` function allows some degree of choice of object placement in linear memory. We are passing `0`, which makes the constructor create a new object at the next available memory location.The function will then return the location in linear memory where it created this object. The `Magnitude` 7 attribute in `Vector2D` takes its value from `obj.instance.exports["Vector2D#Magnitude"]`, which is a function in our WebAssembly module. After defining the JavaScript `Vector2D` object, we call `Vector2D.init` 8 twice to create two `Vector2D` WebAssembly objects in linear memory, as well as return the linear memory address of these objects, which we use for method calls. We then call `Vector2D.Magnitude` twice inside a `console.log` template string. We pass in the vector ids (`vec1_id` and `vec2_id`) we saved in Listing 11-18, which tell the WebAssembly module which object it’s using. The `Magnitude` 9 function passes back the magnitude of the given vector, which the app logs to the console. Run this app using `node`: ``` node vector.js ``` Here’s the result: ``` vec1.magnitude=5 vec2.magnitude=6.4031243324279785 ``` The two values are the magnitude of our first vector where x = 3 and y = 4, and the magnitude of the second vector where x = 4 and y = 5\. Now that we know how to make calls into our AssemblyScript app directly, let’s look at how to use the AssemblyScript loader to make coding the JavaScript a little easier. ### AssemblyScript Loader Now we’ll modify our AssemblyScript code to use the AssemblyScript loader library. This will allow us to compare the methods of interfacing with an AssemblyScript module in terms of ease of use and performance. As mentioned previously, it’s important to understand when it’s possible to improve your application’s performance and how much effort that requires. This information helps you make decisions concerning the trade-off between development time and application performance. Open *vector_loader.ts* and add the code in Listing 11-19 to use the AssemblyScript loader. **vector_loader.ts** ``` export class Vector2D { 1 x: f32; 2 y: f32; constructor(x: f32, y: f32) { this.x = x; this.y = y; } Magnitude(): f32 { return Mathf.sqrt(this.x * this.x + this.y * this.y); } 3 add(vec2: Vector2D): Vector2D { this.x += vec2.x; this.y += vec2.y; return this; } } ``` Listing 11-19: Remove the private modifier from the `x` and `y` attributes There are two changes to *vector.ts* that we will add into *vector_loader.ts*. First, we remove the `private` modifiers from the `x` 1 and `y` 2 attributes so we can access `x` and `y` from JavaScript. Second, we create an `add` 3 function that adds a second vector. This function allows us to add two vectors together. In Listing 11-20, we compile *vector_loader.ts* using `asc`. ``` asc vector_loader.ts -o vector_loader.wasm ``` Listing 11-20: Compiling *vector.ts* to a WebAssembly file using `asc` Next, we’ll create a new JavaScript file named *vector_loader.js* so we can run the new WebAssembly module. Add the code in Listing 11-21 to *vector_loader.js*. **vector_loader.js** ``` const fs = require('fs'); 1 const loader = require('@assemblyscript/loader'); (async () => { let wasm = fs.readFileSync('vector_loader.wasm'); // instantiate the module using the loader 2 let module = await loader.instantiate(wasm); // module.exports.Vector2D mirrors the AssemblyScript class. 3 let Vector2D = module.exports.Vector2D; 4 let vector1 = new Vector2D(3, 4); let vector2 = new Vector2D(4, 5); 5 vector2.y += 10; 6 vector2.add(vector1); console.log(` 7 vector1=(${vector1.x}, ${vector1.y}) vector2=(${vector2.x}, ${vector2.y}) vector1.magnitude=${vector1.Magnitude()} vector2.magnitude=${vector2.Magnitude()} `); })(); ``` Listing 11-21: Using the AssemblyScript loader in JavaScript When using the loader, you can interact with AssemblyScript classes almost as if they’re JavaScript classes. There is a slight difference in that you call the demangled constructor function without using the JavaScript `new` operator, as you would do if these classes were created in JavaScript. However, once you’ve instantiated the object, you can interact with it as if it were written in JavaScript. We first require the AssemblyScript `loader` 1. Rather than using the `WebAssembly.instantiate` function from the IIFE, we call the `loader.instantiate` 2 function, which returns a loader module. This module works a little differently than the WebAssembly module object returned by the `WebAssembly.instantiate` call. The AssemblyScript loader adds functionality that allows the JavaScript to work with high-level AssemblyScript objects, such as classes and strings. We then call `loader.demangle`, passing it the module returned by `loader.instantiate`. The `demangle` function returns an object structure that provides us with functions we can use to instantiate objects from our WebAssembly module. We pull the `Vector2D` 3 function out of the object structure so we can use it as a constructor function for creating `Vector2D` objects in JavaScript. Note that we didn't use the `new` operator when instantiating `Vector2D` 4. However, the current loader version supports use of the `new` operator. We use the `Vector2D` function to create a `vector1` and `vector2` object, passing in the `x` and `y` values for those vectors. We can now use these objects as regular JavaScript objects. The loader wires everything up for us. For example, we call `vector2.y += 10` 5 to increase the value of `vector2.y` by 10, and `vector2.add(vector1)` 6 calls the `add` function on the `vector2` object, passing in `vector1`. In our `console.log` 7 call, we can use values like `vector1.x` and `vector1.y`. Run the JavaScript using `node`: ``` node vector_loader.js ``` You should see the following output: ``` vector1=(3, 4) vector2=(7, 19) vector1.magnitude=5 vector2.magnitude=20.248456954956055 ``` The AssemblyScript loader interface allows you to work with WebAssembly modules created in AssemblyScript almost as if they were classes, objects, and functions created in JavaScript. This creates an ergonomic experience that you might not have when you write your own interface with the WebAssembly module. If you have specific performance targets, you’ll need to perform additional testing to see whether the loader meets all your needs. In the next section, we’ll extend our AssemblyScript class through inheritance. ### Extending Classes in AssemblyScript OOP allows developers to extend a class by adding additional attributes or functionality to a base class. The syntax for extending classes in AssemblyScript is the same as it is in TypeScript. In Listing 11-22, we’ll extend the `Vector2D` class with a `Vector3D` class that will add an additional attribute `z`, which will represent a third dimension for our vector. Open the *vector_loader.ts* file and add the code in Listing 11-22 after the `Vector2D` definition. **vector_loader.ts** ``` ... 1 export class Vector3D extends Vector2D { 2 z: f32; constructor(x: f32, y: f32, z: f32) { 3 super(x, y); this.z = z; } 4 Magnitude(): f32 { return Mathf.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } add(vec3: Vector3D): Vector3D { 5 super.add(vec3); 6 this.z += vec3.z; return this; } } ``` Listing 11-22: Extending the `Vector2D` class using the `Vector3D` class The new `Vector3D` 1 class keeps the original `x` and `y` attributes, and adds a third `z` 2 attribute for the third dimension. Its constructor calls `super` 3, which runs the constructor from the `Vector2D` class. It then sets the value of `this.z` to the `z` parameter passed into the constructor. We override the `Magnitude` 4 method from `Vector2D` so it takes the third dimension into account when calculating the magnitude of the vector. Then the `add` function calls the `Vector2D` class’s `add` function using `super.add` 5 and increases the value of `this.z` 6 using the `vec3` parameter’s `z` attribute value. Now we can recompile our WebAssembly module using `asc`: ``` asc vector_loader.ts -o vector_loader.wasm ``` Next, in Listing 11-23, we modify the *vector_loader.js* file to pull in the `Vector3D` class. **vector_loader.js** ``` const fs = require('fs'); const loader = require("@assemblyscript/loader"); (async () => { let wasm = fs.readFileSync('vector_loader.wasm'); let module = await loader.instantiate(wasm); 1let { Vector2D, Vector3D } = await loader.demangle(module).exports; let vector1 = Vector2D(3, 4); let vector2 = Vector2D(4, 5); 2let vector3 = Vector3D(5, 6, 7); vector2.y += 10; vector2.add(vector1); 3vector3.z++; console.log(` vector1=(${vector1.x}, ${vector1.y}) vector2=(${vector2.x}, ${vector2.y}) 4vector3=(${vector3.x}, ${vector3.y}, ${vector3.z}) vector1.magnitude=${vector1.Magnitude()} vector2.magnitude=${vector2.Magnitude()} 5vector3.magnitude=${vector3.Magnitude()} `); })(); ``` Listing 11-23: JavaScript using the AssemblyScript loader to load `Vector2D` and `Vector3D` classes We modify the line that took the `Vector2D` function from the call to `demangle`, and change it to destructure 1 the result, creating a `Vector2D` and `Vector3D` function variable. We create an object `vector3` 2, using the function `Vector3D`, to which we pass `x`, `y`, and `z` values. We increment `vector3.z` 3 for no particular reason other than to show that we can do it. Inside the template string passed to `console.log`, we add a line that displays the `x`, `y`, and `z` 4 values in `vector3`, as well as the magnitude of `vector3` 5. When you run this JavaScript from the command line using `node`, you get the output in Listing 11-24. ``` vector1=(3, 4) vector2=(7, 19) vector3=(5, 6, 8) vector1.magnitude=5 vector2.magnitude=20.248456954956055 vector3.magnitude=11.180339813232422 ``` Listing 11-24: Output from *vector_loader.js* Now let’s look at how the performance of the loader compares to direct calls into the WebAssembly module. ### Performance of Loader vs. Direct WebAssembly Calls The AssemblyScript loader provides a more intuitive structure for interaction between the AssemblyScript module and our JavaScript. The final section of this chapter compares the loader with direct calls into the WebAssembly modules. To run this test, we don’t need to write any additional AssemblyScript. We’ll use the WebAssembly modules created earlier in this chapter, so we only need to create a new JavaScript file to call the existing modules. Create a new file named *vector_perform.js* and add the code in Listing 11-25. **vector_perform.js** ``` const fs = require('fs'); const loader = require("@assemblyscript/loader"); (async () => { let importObject = { env: { abort: () => { } } }; let wasm = fs.readFileSync('vector_loader.wasm'); let module = await loader.instantiate(wasm); let obj = await WebAssembly.instantiate(wasm, importObject); // This JavaScript class will have all the functions // exported from AssemblyScript 1 let dVector2D = { // the init function will call the constructor on Vector2D init: function (x, y) { return obj.instance.exports"Vector2D#constructor" }, getX: obj.instance.exports["Vector2D#get:x"], setX: obj.instance.exports["Vector2D#set:x"], getY: obj.instance.exports["Vector2D#get:y"], setY: obj.instance.exports["Vector2D#set:y"], Magnitude: obj.instance.exports["Vector2D#Magnitude"], add: obj.instance.exports["Vector2D#add"], } // This JavaScript class will have all the functions // exported from AssemblyScript let dVector3D = { // the init function will call the constructor on Vector3D init: function (x, y, z) { return obj.instance.exports"Vector3D#constructor" }, getX: obj.instance.exports["Vector3D#get:x"], setX: obj.instance.exports["Vector3D#set:x"], getY: obj.instance.exports["Vector3D#get:y"], setY: obj.instance.exports["Vector3D#set:y"], getZ: obj.instance.exports["Vector3D#get:z"], setZ: obj.instance.exports["Vector3D#set:z"], Magnitude: obj.instance.exports["Vector3D#Magnitude"], add: obj.instance.exports["Vector3D#add"], } // prepare to log the time it takes to run functions directly 2 let start_time_direct = (new Date()).getTime(); 3 let vec1_id = dVector2D.init(1, 2); let vec2_id = dVector2D.init(3, 4); let vec3_id = dVector3D.init(5, 6, 7); 4 for (let i = 0; i < 1_000_000; i++) { dVector2D.add(vec1_id, vec2_id); dVector3D.setX(vec3_id, dVector3D.getX(vec3_id) + 10); dVector2D.setY(vec2_id, dVector2D.getY(vec2_id) + 1); dVector2D.Magnitude(vec2_id); } 5 console.log("direct time=" + (new Date().getTime() - start_time_direct)); 6 let { Vector2D, Vector3D } = await loader.demangle(module).exports; 7 let start_time_loader = (new Date()).getTime(); 8 let vector1 = Vector2D(1, 2); let vector2 = Vector2D(3, 4); let vector3 = Vector3D(5, 6, 7); 9 for (i = 0; i < 1_000_000; i++) { vector1.add(vector2); vector3.x += 10; vector2.y++; vector2.Magnitude(); } a console.log("loader time=" + (new Date().getTime() - start_time_loader)); })(); ``` Listing 11-25: Comparing loader function calls with direct function calls Now we can see what it costs for us to use that pretty AssemblyScript loader syntax. This JavaScript creates an object to hold the direct calls to the `Vector2D` AssemblyScript class `dVector2D` 1 and one for the `Vector3D` class called `dVector3D`. We then set the variable `start_direct_time` 2 to the current time, which we’ll use to track the performance, and initialize 3 three vector objects. Two of the vector objects are `Vector2D` objects, and one is a `Vector3D` object. After initializing the vectors, we loop one million times 4, making calls to those objects. We didn’t test every function, so this isn’t a perfect performance test. The goal is simply to get some numbers and see how they compare. As long as we make the same calls to the direct and loader versions, we should be able to get a reasonable comparison. We then use `console.log` 5 to log out the amount of time it took to initialize the vectors and run through the loop. This first loop tests the performance of the direct call to the WebAssembly module without using the AssemblyScript loader. Next, the code tests the performance of the module with the loader. We use the `loader.demangle` 6 function to create the `Vector2D` and `Vector3D` factory functions. We then initialize `start_time_loader` 7 to the current time and call the `Vector2D` 8 and `Vector3D` functions to create three objects mirroring the code in the first loop 4 that tested the direct initialization calls. We loop one million times 9, executing the same functions as earlier, except through the loader. Finally, we `log` a the amount of time it took to execute the code using the loader. Run *vector_perform.js* from the command line using `node`: ``` node vector_perform.js ``` This is the output I received when I executed the file: ``` direct time=74 loader time=153 ``` As you can see, the version using the loader took roughly twice as long to execute. The difference is even starker when we include the initialization calls in a loop. If you’re going to use the AssemblyScript loader, it’s best to structure your code to make as few calls as possible between the JavaScript and AssemblyScript. ## Summary In this chapter, you learned about the AssemblyScript high-level language, the AssemblyScript CLI, and the `asc` command you can use to compile AssemblyScript apps. We created an `AddInts` function and a hello world app to show how writing an app in AssemblyScript compares to writing the same app in WAT. We compiled it to WAT format, looked through the code that the AssemblyScript compiler generated, and wrote a JavaScript app that ran the hello world app directly. While doing this, you learned how to use WAT to understand what the WebAssembly, created by the AssemblyScript compiler, is doing under the hood. We then installed the AssemblyScript loader and used the JavaScript functions written by the AssemblyScript team to help us write the JavaScript code. We discussed using strings in AssemblyScript, wrote a string concatenation app, and looked at how we must use additional flags with the `asc` compiler to allow `asc` to include additional WebAssembly libraries when compiling. In the latter half of the chapter, we explored OOP in AssemblyScript. We created a class and looked at the exports from the WAT file it generated. We looked at `private` attributes and how they prevent AssemblyScript from exporting those attributes so they can’t be used by the embedding environment. We wrote JavaScript that allowed us to create the glue classes directly, and then used the AssemblyScript loader to create the glue code for us. We compared the performance of the direct and the loader methods. Finally, we extended our `Vector2D` class with a `Vector3D` class and discussed the differences between class inheritance in AssemblyScript and TypeScript.```` `````

第十二章:最后的思考

感谢阅读我的书!希望到现在为止,你已经了解了 WebAssembly 的底层工作原理,并准备开始在高层和低层 Web 开发中使用它。WebAssembly 是一项年轻的技术,但它已经在所有主流浏览器中可用。通过 Node.js,你还可以将 WebAssembly 模块用于高性能的服务器代码开发。开发 WebAssembly 应用程序的语言不断增加,新的功能和平台也在不断加入。未来将是一个 WebAssembly 支撑下的安全、快速且高效的网络世界。

若要获取本书中代码的更新和更多 WebAssembly 教程,请访问 wasmbook.com

如果你需要帮助或有问题,请随时联系我。

  1. 在 Twitter 上: twitter.com/battagline (@battagline)

  2. 在 LinkedIn 上: www.linkedin.com/in/battagline

  3. 在 AssemblyScript 公共 Discord 上: discord.com/invite/assemblyscript

  4. 在 GitHub 上: github.com/battlelinegames/ArtOfWasm

posted @ 2025-11-27 09:19  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报