Webassembly-实践指南-全-

Webassembly 实践指南(全)

原文:annas-archive.org/md5/cd8a50bdc641bd9667faa923739820fb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

提供高性能应用程序是一个噩梦。JavaScript 是一种动态类型语言。因此,JavaScript 引擎在执行 JavaScript 时假设类型。这些假设导致了不可预测的性能。这使得在 JavaScript 中提供一致的高性能应用程序变得更加困难。

WebAssembly 提供了一种在 JavaScript 引擎中运行类型安全和高性能应用程序的方法。

WebAssembly 非常快

WebAssembly 是网络发生的下一件大事。它承诺提供高且一致的性能,具有可维护的代码,在网络上运行本地代码并提供接近本地性能。

WebAssembly 是类型安全的

当您有多态 JavaScript 代码时,JavaScript 编译器难以提供高性能。另一方面,WebAssembly 在编译时是类型安全的(或单态的)。这不仅提高了性能,而且大大减少了运行时错误,这是一个双赢的局面。

WebAssembly 运行您的本地代码

之前已经尝试通过运行本地代码来使网络更快,但它们都失败了,因为它们要么是供应商特定的,要么与单一语言绑定。网络建立在开放标准之上。作为一个开放标准,WebAssembly 使得所有公司都能轻松采用和支持它。WebAssembly 不是一种语言;它是对其他语言的顶层实现计划,这些语言编译成将在 JavaScript 引擎上运行的字节码。

WebAssembly 是字节码

WebAssembly 不过是运行在 JavaScript 引擎中的字节码。在这本书中,我们将学习如何将本地代码转换为 WebAssembly,以及如何优化它以获得更好的性能。我们还将介绍整个 WebAssembly 如何在 JavaScript 引擎上运行,以及如何使用各种工具以及它们如何帮助我们实现目标。

最重要的是,学习在哪里以及如何使用 WebAssembly 以获得期望的结果。

让我们用 WebAssembly 让网络更加精彩和快速。

这本书面向的对象

这本书是为希望提供更好性能和交付类型安全代码的 JavaScript 开发者而写的。希望构建全栈应用程序而不必过多担心 JavaScript 编程的 Rust 开发者或后端工程师也会发现这本书很有用。

阅读这本书需要基本的 JavaScript 理解。Rust 知识是首选,但不是强制性的。代码示例简单,任何开发者都能轻松跟随。

这本书涵盖的内容

第一章理解 LLVM,简要介绍了 LLVM,它是什么,以及如何使用它。

第二章理解 Emscripten,向您介绍 Emscripten,您将在其中构建和运行您的第一个 WebAssembly 模块。

第三章, 探索 WebAssembly 模块,探讨了 WebAssembly 模块,模块由什么组成,以及不同的部分是什么。

第四章, 理解 WebAssembly 二进制工具包,探讨了如何安装和使用WebAssembly 二进制工具包WABT)。

第五章, 理解 WebAssembly 模块中的部分,探讨了 WebAssembly 二进制文件内部的各个部分及其用途。

第六章, 安装和使用 Binaryen,探讨了如何安装和使用 Binaryen。

第七章, 将 Rust 与 WebAssembly 集成,首先查看 Rust 以及将 Rust 转换为 WebAssembly 模块的各种方法,最后查看wasm_bindgen

第八章, 使用 wasm_pack 打包 WebAssembly,探讨了wasm-pack以及它是如何简化构建 Rust 和 WebAssembly 应用程序的。

第九章, 跨越 Rust 和 WebAssembly 之间的边界,重点关注wasm-bindgen以及如js-sysweb-sys之类的 crate 如何帮助在 WebAssembly 和 JavaScript 之间共享实体。

第十章, 优化 Rust 和 WebAssembly,介绍了使用示例优化 Rust 和 WebAssembly 的各种方法。

要充分利用本书

本书假设您对 JavaScript 有基本的了解。代码示例大多是用 C++/Rust 编写的。请在开始之前安装 Rust(在第七章将 Rust 与 WebAssembly 集成中简要介绍)和 Node.js。

如果您使用的是本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将有助于您避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub(github.com/PacktPublishing/Practical-WebAssembly)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块设置如下:

html, body, #map {
 height: 100%; 
 margin: 0;
 padding: 0
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)

任何命令行输入或输出都应如下编写:

$ mkdir css
$ cd css

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“从管理面板中选择系统信息。”

小贴士或重要注意事项

应像这样显示。

联系我们

我们始终欢迎读者的反馈。

customercare@packtpub.com 并在邮件主题中提及书籍标题。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问 www.packtpub.com/support/errata 并填写表格。

copyright@packt.com 并附上相关材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

分享您的想法

一旦您阅读了《实用 WebAssembly》,我们很乐意听听您的想法!请 点击此处直接访问此书的亚马逊评论页面 并分享您的反馈。

您的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。

第一部分:WebAssembly 简介

本节简要介绍了 LLVM 和 Emscripten。你将了解它们是什么以及为什么在了解 WebAssembly 之前需要理解它们。本节以 WebAssembly 模块和 WebAssembly 文本格式的介绍结束。

在本节之后,你将理解 WebAssembly 是什么以及它是如何工作的。

本节包括以下章节:

  • 第一章, 理解 LLVM

  • 第二章, 理解 Emscripten

  • 第三章, 探索 WebAssembly 模块

第一章:第一章:理解 LLVM

JavaScript 是最受欢迎的编程语言之一。然而,JavaScript 有两个主要缺点:

  • 不可预测的性能

JavaScript 在 JavaScript 引擎提供的环境和运行时中执行。存在各种 JavaScript 引擎(V8、WebKit 和 Gecko)。它们都是不同构建的,并以不同的方式运行相同的 JavaScript 代码。此外,JavaScript 是动态类型的。这意味着 JavaScript 引擎在执行 JavaScript 代码时应该猜测类型。这些因素导致 JavaScript 执行性能不可预测。针对一种 JavaScript 引擎的优化可能会对其他类型的 JavaScript 引擎产生不希望的影响。这导致性能不可预测。

  • 包大小

JavaScript 引擎会等待下载整个 JavaScript 文件后再进行解析和执行。JavaScript 文件越大,等待时间越长。这将降低应用程序的性能。捆绑器如 webpack 有助于最小化捆绑大小。但当您的应用程序增长时,捆绑大小会呈指数增长。

有没有一种工具提供原生性能,并且体积要小得多?是的,WebAssembly。

WebAssembly 是 Web 和 Node 开发的未来。WebAssembly 是静态类型和预编译的,因此它比 JavaScript 提供更好的性能。二进制的预编译提供了生成小型二进制捆绑包的选项。WebAssembly 允许 Rust、C 和 C++等语言编译成在 JavaScript 引擎内与 JavaScript 一起运行的二进制文件。所有 WebAssembly 编译器都使用 LLVM 在底层将本地代码转换为 WebAssembly 二进制代码。因此,了解 LLVM 是什么以及它是如何工作的是非常重要的。

在本章中,我们将学习编译器的各种组件及其工作原理。然后,我们将探讨 LLVM 是什么以及它是如何帮助编译型语言的。最后,我们将看到 LLVM 编译器如何编译本地代码。本章将涵盖以下主题:

  • 理解编译器

  • 探索 LLVM

  • LLVM 的实际应用

技术要求

我们将使用Clang,这是一个将 C/C++代码编译成本地代码的编译器。

对于 Linux 和 Mac 用户,Clang 应该直接可用。

对于 Windows 用户,可以从以下链接安装 Clang:llvm.org/docs/GettingStarted.html?highlight=installing%20clang%20windows#getting-the-source-code-and-building-llvm 以安装 Clang。

您可以在 GitHub 上找到本章中存在的代码文件,链接为github.com/PacktPublishing/Practical-WebAssembly

理解编译器

编程语言被广泛分为编译型和解释型语言。

在编译世界中,代码首先被编译成目标机器码。将代码转换为二进制的过程称为编译。将代码转换为目标机器码的软件程序称为编译器。在编译过程中,编译器会对编写的代码运行一系列的检查、通过和验证,并生成一个高效且优化的二进制文件。编译语言的一些例子包括 C、C++和 Rust。

在解释世界中,代码在单次遍历中读取和执行。由于编译是在运行时发生的,因此生成的机器码不如编译后的版本优化。解释语言比编译语言慢得多,但它们提供了动态类型和更小的程序大小。

在这本书中,我们将只关注编译语言。

编译语言

编译器是一种将源代码翻译成机器码(或者更抽象地说,将代码从一种编程语言转换到另一种编程语言)的翻译器。编译器很复杂,因为它应该理解源代码所写的语言(其语法、语义和上下文);它还应该理解目标机器码(其语法、语义和上下文),并应该创建一个表示,将源代码映射到目标机器码。

编译器有以下组件:

  • 前端 – 前端负责处理源语言。

  • 优化器 – 优化器负责优化代码。

  • 后端 – 后端负责处理目标语言。

![ 图 1.1 – 编译器的组件

![ 图 1.1 – 编译器的组件

图 1.1 – 编译器的组件

前端

前端专注于处理源语言。前端在接收到代码后对其进行解析。然后检查代码中是否存在任何语法或语法错误。之后,代码被转换(映射)成中间表示IR)。可以将 IR 视为表示编译器处理的代码的格式。IR 是编译器版本的你的代码。

优化器

编译器的第二个组件是优化器。这是可选的,但正如其名称所示,优化器分析 IR 并将其转换为一个更高效的版本。少数编译器有多个 IR。编译器在每次遍历 IR 时都会高效地优化代码。优化器是 IR 到 IR 的转换器。优化器分析、运行遍历并重写 IR。这里的优化包括移除冗余计算、消除死代码(无法到达的代码)以及各种其他优化选项,这些将在未来的章节中探讨。需要注意的是,优化器不必是语言特定的。由于它们作用于 IR,因此可以作为通用组件构建并用于多种语言。

后端

后端专注于生成目标语言。后端接收生成的(优化后的)中间表示(IR)并将其转换为另一种语言(例如机器码)。也有可能链式连接多个后端,将代码转换为其他语言。后端负责从 IR 生成目标机器码。这种机器码是实际在裸机上运行的代码。为了生成高效的机器码,后端应该理解代码执行的架构。

机器码是一组指令,指示机器将某些值存储在寄存器中并对它们进行一些计算。例如,生成的机器码负责在 32 位架构中高效地将 64 位数字存储在空闲寄存器中(以及类似的事情)。后端应该理解目标环境,以便高效地创建一组指令,并正确选择和调度指令以提高应用程序执行的效率。

编译器效率

执行越快,性能越好。

编译器的效率取决于它如何选择指令、分配寄存器以及在给定架构中调度指令执行。指令集是处理器支持的运算集,这种整体设计被称为指令集架构ISA)。ISA 是计算机的抽象模型,通常被称为计算机架构。不同的处理器以不同的实现方式转换 ISA。不同的实现可能在性能上有所不同。ISA 是硬件和软件之间的接口。

如果你正在实现一种新的编程语言,并且希望这种语言能在不同的架构(或更抽象地说,不同的处理器)上运行,那么你应该为这些架构/目标中的每一个构建后端。但是,为每个架构构建这些后端是困难的,并且会花费时间、成本和努力来开始语言创建之旅。

如果我们创建一个通用的 IR 并构建一个编译器,将这个 IR 转换为在各种架构上高效运行的机器码,那会怎么样?让我们称这个编译器为低级虚拟机。现在,你的前端在编译器链中的角色仅仅是把源代码转换为与低级虚拟机(如 LLVM)兼容的 IR。现在,低级虚拟机的一般目的是成为一个通用的可重用组件,将 IR 映射到各种目标的本机代码。但是,低级虚拟机只会理解通用的 IR。这个 IR 被称为LLVM IR,编译器被称为LLVM

探索 LLVM

LLVM 是 LLVM 项目的一部分。LLVM 项目托管编译器和工具链技术。LLVM 核心 是 LLVM 项目的一部分。LLVM 核心负责提供源和目标无关的优化,并为许多 CPU 架构生成代码。这使得语言开发者只需创建一个前端,即可从源语言生成与 LLVM 兼容的 IR 或 LLVM IR。

你知道吗?

LLVM 不是一个缩写。当该项目作为一个研究项目开始时,它代表低级虚拟机(Low-Level Virtual Machine)。但后来,决定使用这个名字而不是缩写。

LLVM 的主要优势如下:

  • LLVM 使用一种类似于 C 的简单低级语言。

  • LLVM 是强类型的。

  • LLVM 具有严格定义的语义。

  • LLVM 具有精确和精确的垃圾回收。

  • LLVM 提供了各种优化,您可以根据需求选择。它有 激进标量跨过程简单循环基于配置文件 的优化。

  • LLVM 提供了各种编译模型。它们是 链接时间安装时间运行时离线

  • LLVM 为各种目标架构生成机器代码。

  • LLVM 提供了 DWARF 调试信息。

    注意

    DWARF 是许多编译器和调试器使用的调试文件格式,用于支持源级调试。DWARF 是架构无关的,适用于任何处理器或操作系统。它使用一种称为 调试信息条目DIE)的数据结构来表示每个变量、类型、过程等。

    如果你想了解更多关于 DWARF 的信息,请参阅 dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf

    重要提示

    LLVM 不是一个单一的项目。它是一系列子项目和其它项目的集合。这些项目被各种语言使用,如 Ruby、Python、Haskell、Rust 和 D,用于编译。

现在我们已经了解了编译器和 LLVM,我们将看到它是如何被使用的。

LLVM 在行动

在本节中,让我们使用 LLVM 的 Clang 编译器将原生代码编译成 LLVM IR。这将更好地了解 LLVM 的工作原理,并在未来章节中理解编译器如何使用 LLVM 时非常有用。

我们首先创建一个名为 sum.c 的 C 文件,并输入以下内容:

 $ touch sum.c
 // sum.c
 unsigned sum(unsigned a, unsigned b) {
    return a + b;
}

sum.c 文件包含一个简单的 sum 函数,该函数接受两个无符号整数并返回它们的和。LLVM 提供了 Clang LLVM 编译器来编译 C 源代码。为了生成 LLVM IR,请运行以下命令:

$ clang -S -O3 -emit-llvm sum.c

我们向 Clang 编译器提供了 -S-O3-emit-llvm 选项:

  • -S 选项指定编译器只运行预处理和编译步骤。

  • -O3 选项指定编译器生成一个经过良好优化的二进制文件。

  • -emit-llvm 选项指定编译器在生成机器代码时输出 LLVM IR。

上述代码将打印出以下 LLVM IR:

define i32 @sum(i32, i32) local_unnamed_addr #0 {
  %3 = add i32 %1, %0
  ret i32 %3
}

LLVM IR 的语法在结构上与 C 语言非常接近。define关键字定义了函数的开始。旁边是函数的返回类型,i32。接下来是函数的名称,@sum

重要提示

注意那里的@符号吗?LLVM 使用@来标识全局变量和函数。它使用%来标识局部变量。

在函数名之后,我们声明输入参数的类型(在这种情况下为i32)。local_unnamed_addr属性表示地址在模块内不具有重要意义。LLVM IR 中的变量是不可变的。也就是说,一旦你定义了它们,就不能更改它们。因此,在block内部,我们创建一个新的局部值%3,并将其赋值为addadd是一个操作码,它接受参数的类型,然后是两个参数,%0%1%0%1表示第一个和第二个局部变量。最后,我们使用ret关键字返回%3,后面跟着type

这个 IR 是可以转换的;也就是说,IR 可以从文本表示转换为内存,然后转换为在裸金属上运行的实际位码。此外,从位码,你可以将它们转换回文本表示。

假设你正在编写一种新的语言。这种语言的成功取决于它在各种架构上执行时的灵活性。为各种架构(如 x86、ARM 等)生成优化的字节码需要很长时间,而且并不容易。LLVM 提供了一种简单的方法来实现这一点。不是针对不同的架构,而是创建一个编译器前端,将源代码转换为 LLVM 兼容的 IR。然后,LLVM 将 IR 转换为在任何架构上运行的效率高和优化的字节码。

注意

LLVM 是一个伞形项目。它有如此多的组件,你几乎可以为他们写一套书。涵盖整个 LLVM 以及如何安装和运行它们超出了本书的范围。如果你对学习 LLVM 的各个组件、它们的工作原理以及如何使用它们感兴趣,请查看网站:llvm.org

摘要

在本章中,我们了解了编译型语言是如何工作的,以及 LLVM 是如何帮助编译它们的。我们已经使用 LLVM 编译了一个示例程序,以了解它是如何工作的。在下一章中,我们将探讨 Emscripten,这是一个将 C/C++转换为 WebAssembly 模块的工具。Emscripten 使用 LLVM 后端进行编译。

第二章:第二章:理解 Emscripten

在本章中,我们将了解Emscripten,这是一个将 C/C++代码转换为 WebAssembly 模块的工具链。

Emscripten 由两个组件组成:

  • Emscripten 编译器前端

  • Emscripten SDKemsdk

Clang编译器前端将 C/C++代码编译成LLVM 中间表示LLVM IR),然后使用 LLVM 后端将 LLVM IR 转换为本地代码。Clang 编译器速度快,内存使用少,与GNU 编译器集合GCC)兼容。Emscripten 与 Clang 类似;前者生成wasm二进制文件,而后者生成本地二进制文件。Emscripten 编译器前端emcc)是将 C/C++转换为 LLVM IR(二进制和可读形式)以及 WebAssembly 二进制文件或asm.js(如 JavaScript)的编译器前端。

![Figure 2.1 – Emscripten 编译器前端img/Figure_2.1_B14844.jpg

Figure 2.1 – Emscripten 编译器前端

emsdk帮助管理和维护 Emscripten 工具链组件,并设置运行时/终端环境以运行 emcc。

在本章中,我们将学习如何安装 Emscripten。然后,我们将使用 Emscripten 生成 asm.js,这是一个在 Node.js 和浏览器上运行的 WebAssembly 模块。之后,我们将探索 emsdk 工具。最后,我们将探索 Emscripten 提供的各种优化。本章将涵盖以下主题:

  • 使用 emsdk 安装 Emscripten

  • 使用 Emscripten 生成 asm.js

  • 在 Node.js 中使用 Emscripten 运行 Hello World

  • 在浏览器中使用 Emscripten 运行 Hello World

  • 探索 emsdk 中的其他选项

  • 理解各种优化级别

    你知道吗?

    asm.js 是 JavaScript 的一个子集,经过优化,在浏览器中可以接近本地性能运行。asm.js 规范并未被所有浏览器厂商接受。asm.js 已演变成 WebAssembly。

技术要求

在本章中,我们将展示如何设置 Emscripten,您需要在系统上安装以下内容:

  • Python >= 3.7

  • Node.js > 12.18

    注意

    emsdk 预装了兼容的 Node.js 版本。

您可以在 GitHub 上找到本章中提供的代码文件,链接为github.com/PacktPublishing/Practical-WebAssembly

使用 emsdk 安装 Emscripten

emsdk 提供了一种简单的方式来安装、管理和切换 Emscripten 工具链的版本。emsdk 负责设置环境、工具和 SDK,以便将 C/C++编译成 LLVM IR,然后以 asm.js 或 WebAssembly 二进制文件的形式转换为 JavaScript。

让我们安装 Emscripten 并开始编写代码:

  1. 克隆 emsdk 仓库并进入emsdk文件夹:

    $ git clone https://github.com/emscripten-core/emsdk
    $ cd emsdk
    
  2. 在机器上安装 emsdk,请运行以下命令:

对于*nix 用户,请使用以下命令:

$ ./emsdk install latest

对于 Windows 用户,请使用以下命令:

$ emsdk install latest

注意

前面的命令可能需要一段时间运行;它将构建和设置整个工具链。

接下来,我们将激活最新的 emsdk。激活更新了本地 shell 中的必要环境引用,并使最新的 SDK 在当前 shell 中对用户生效。它将 Emscripten 工具链的路径和其他必要信息写入用户主目录下的名为 .emscripten 的文件中:

  1. 要激活已安装的 emsdk,请运行以下命令:

对于 *nix 用户,请使用以下命令:

 $ ./emsdk activate latest

对于 Windows 用户,请使用以下命令:

 $ emsdk activate latest
  1. 现在是时候运行以下命令来确保配置和路径被激活,以确保配置和路径被激活:

对于 *nix 用户,请使用以下命令:

 $ source ./emsdk_env.sh

对于 Windows 用户,请使用以下命令:

 $ emsdk_env.bat

恭喜,Emscripten 工具链已安装!使用 emsdk 更新工具链与安装一样简单。

要更新,请运行以下命令:

对于 *nix 用户,请使用以下命令:

$ ./emsdk update

对于 Windows 用户,请使用以下命令:

$ emsdk update

emsdk 在 Emscripten 配置文件中设置以下路径。Emscripten 配置文件(.emscripten)位于主文件夹中。它由以下内容组成:

  • LLVM_ROOT – 指定 LLVM Clang 编译器的路径

  • NODE_JS – 指定 Node.js 的路径

  • BINARYEN_ROOT – 指定 Emscripten 编译器的优化器

  • EMSCRIPTEN_ROOT – 指定 Emscripten 编译器的路径

我们可以使用以下命令检查 emcc 的安装是否成功:

 $ emcc --version

现在我们已经完成了 Emscripten 编译器的安装,让我们继续使用它。

使用 Emscripten 生成 asm.js

我们将使用 Emscripten 将 C/C++ 程序移植到 asm.js 或 WebAssembly 二进制文件,然后在 JavaScript 引擎中运行它们。

注意

如 Lua 和 Python 等编程语言具有 C/C++ 运行时。使用 Emscripten,我们可以将运行时作为 WebAssembly 模块进行移植,并在 JavaScript 引擎中执行它们。这使得在 JavaScript 引擎上运行 Lua/Python 代码变得容易。因此,Emscripten 和 WebAssembly 允许在 JavaScript 引擎中运行原生代码。

首先,让我们创建一个 sum.cpp 文件:

 // sum.cpp
extern "C" {
  unsigned sum(unsigned a, unsigned b) {
      return a + b;
  }
}

extern "C" 视为一个类似于 导出 的机制。所有这些函数都作为导出函数可用,无需更改其名称。然后,我们定义一个普通的 sum 函数,它接受两个数字并返回一个数字。

为了从 sum.cpp 生成类似于 JavaScript 的 asm.js 代码,请使用以下命令:

$ emcc -O1 ./sum.cpp -o sum.html -s WASM=0 -s EXPORTED_FUNCTIONS='["_sum"]'

注意

如果您是第一次运行 emcc,可能需要几秒钟才能完成。后续运行将更快。

我们将 -O1 选项传递给 emcc 编译器,指示编译器生成较少优化的代码(我们将在本章后面看到更多优化选项)。接下来,我们传递要转换的文件,即 sum.cpp。然后,使用 -o 标志,我们提供所需的输出名称,即 sum.html

最后,我们使用 -s 标志向 emcc 编译器发送更多信息。-s 标志接受键和值作为它们的参数。emcc 编译器默认生成 WebAssembly 模块。WASM=0 指示编译器生成类似 JavaScript 的 asm.js 而不是 WebAssembly。

然后,我们使用 EXPORTED_FUNCTIONS 选项指定导出的函数。EXPORTED_FUNCTIONS 选项接受一个参数数组。为了导出 sum 函数,我们指定 _sum

这将生成以下代码:

function _sum($0,$1) {
    $0 = $0|0;
    $1 = $1|0;
    var $2 = 0, label = 0, sp = 0;
    sp = STACKTOP;
    $2 = (($1) + ($0))|0;
   return ($2|0);
}

注意

|0 指定类型为数字。

现在,在浏览器中打开 sum.html 并打开开发者控制台。为了调用导出的函数,我们将在控制台中运行以下表达式:

 ccall("sum", "number", "number, number", [10, 20])
// outputs 30

ccall 是通过 JavaScript 从 C/C++ 代码中调用导出函数的方式。该函数接受函数名称、返回值类型、参数类型以及作为数组的输入参数。这将调用 sum 函数以产生结果。我们将在后面的章节中看到更多关于 ccallcwrap 的内容。但就现在而言,将 ccall 视为调用 C 函数的方式。

github.com/emscripten-core/emscripten 了解更多关于 Emscripten 源代码的信息。

到目前为止,我们已经看到了如何使用 emscripten 生成 asm.js 文件。让我们使用 emscripten 创建一个在 Node.js 上运行的 WebAssembly 模块。

在 Node.js 中使用 Emscripten 运行 Hello World

在本节中,我们将了解如何通过 Emscripten 将 C/C++ 代码转换为 WebAssembly 二进制文件,并与其在 Node.js 中一起运行。

注意

如果终端出现 emcc 命令未找到 的错误,您的终端环境可能已被重置。要设置环境,请在 emsdk 文件夹内运行以下命令:

source ./emsdk_env.sh

让我们遵循布莱恩·科尼汉的传统,用一点小小的变化来编写 "Hello, world"。让我们来做 "Hello, Web":

  1. 首先,我们创建一个 hello_web.c 文件:

    $ touch hello_web.c
    
  2. 启动您喜欢的编辑器并添加以下代码:

     #include <stdio.h>
    
    int main() {
        printf("Hello, Web!\n");
        return 0;
    }
    

这是一个简单的 C 程序,包含一个 main 函数。main 函数是运行时的入口点。当此代码使用 Clang (clang sum.c && ./a.out) 编译和执行时,将打印 "Hello, Web!"。现在,我们不再使用 Clang(或任何其他编译器),而是用 emcc 编译代码。

  1. 我们输入以下命令使用 emcc 编译代码:

     $ emcc hello_web.c
    

完成后,将生成以下文件:

  • a.out.js

  • a.out.wasm

生成的 JavaScript 文件非常大。它有超过 2,000 行,大小为 109 KB。我们将在本章后面学习如何优化文件大小。

  1. 让我们使用 Node 运行生成的 JavaScript 文件,这将打印出 "Hello, Web!":

    $ node a.out.js
    Hello, Web!
    

恭喜!您刚刚运行了您的第一个 WebAssembly 二进制文件!

注意

在浏览器世界中,二进制大小很重要。即使你的算法以纳秒级运行,如果你有一大块代码,那也无济于事。浏览器会等待收到所有必要的信息后才开始解析和编译。因此,检查文件大小是强制性的。Closure Compiler可以帮助进一步最小化字节码的大小。Closure Compiler 不仅减少了代码大小,还试图使代码更加高效。

生成的 JavaScript 文件包含其自己的运行时和配置,这些配置是 JavaScript 引擎在 JavaScript 引擎内部执行 WebAssembly 模块所需的。生成的 JavaScript 文件创建了一个 JavaScript 模块,并为浏览器和 Node.js 初始化了代码:

  • 在 Node.js 中,生成的 JavaScript 文件通过从本地文件系统读取文件来创建一个模块。它获取传递给 node 命令的参数,并在创建的模块中设置它们。

  • 在浏览器中,生成的 JavaScript 文件通过发送请求并从 URL 获取字节来创建一个模块。浏览器从托管服务器或位置获取 WebAssembly 二进制文件,然后实例化该模块。

生成的 JavaScript 文件还创建了栈、内存、导入和导出部分。我们将在本书的后续章节中深入探讨这些部分。

这个生成的 JavaScript 文件被称为绑定文件。绑定文件的主要功能是创建或设置一个环境,使得可以在 JavaScript 引擎内部执行 WebAssembly 模块。绑定文件充当 JavaScript 和 WebAssembly 之间的翻译器。所有值都通过这个绑定文件传入和传出。

当通过 node 执行 JavaScript 文件时,它会做以下事情。

JavaScript 引擎首先加载模块,然后设置 WebAssembly 执行所需的常量和各种函数。然后,模块检查代码正在何处执行,模块是在浏览器内部还是在Node环境中。基于这一点,它获取文件。由于我们在这里通过 node 运行 WebAssembly 模块,它从本地文件系统获取文件。然后,模块会检查是否提供了任何调用参数。如果没有,JavaScript 引擎将检查是否有任何未处理/未捕获的异常。然后,JavaScript 引擎将print out/print err函数映射到控制台。JavaScript 引擎检查加载的模块是否具有所有必需的访问权限和全局变量以及导入,以便执行。

模块继续初始化栈和其他所需的常量,以及解码器和编码器,分别用于解码和编码缓冲区。编码器负责将 JavaScript 值转换为 WebAssembly 可理解的价值。解码器负责将 WebAssembly 值转换为 JavaScript 可理解的价值。

Node.js 运行时会检查文件的可访问性,然后初始化文件。模块会检查所有与 WebAssembly 相关的函数可用性。一旦一切初始化完成,并且模块包含所有所需的函数,我们将调用 run 函数。

run 函数实例化 WebAssembly 二进制文件。在这种情况下,由于我们在 C 中定义了 main 函数,绑定文件在实例化时会直接调用 main 函数。

绑定文件包含 ccall 函数。ccall 函数是访问 C 中定义的底层函数的接口:

function ccall(ident, returnType, argTypes, args, opts)  {
  // the code is elided
}

ccall 函数接受以下参数:

  • ident – 调用的函数;它是 C 语言中定义的函数标识符。

  • returnType – 函数的返回类型。

  • argTypes – 参数类型。

  • args – 随函数调用一起传递的参数。

  • opts – 所需的任何其他选项。

JavaScript 模块除了 ccall 外还导出 cwrap 函数。cwrapccall 函数的包装函数。虽然 ccall 是函数调用,但 cwrap 提供了一个调用 ccall 的函数:

 function cwrap(ident, returnType, argTypes, opts) {
    return function() {
        return ccall(ident, returnType, argTypes,
          arguments, opts);
    }
}

生成的 WebAssembly 文件包含用于指导运行时打印 "Hello, Web!" 的二进制操作码。WebAssembly 文件以 00 61 73 6d 01 00 00 00 开头。

webassembly.github.io/spec/ 了解更多关于 WebAssembly 规范的信息。

到目前为止,我们已经看到了如何生成在 Node.js 上运行的 WebAssembly 模块。让我们使用 emscripten 创建一个在浏览器中运行的 WebAssembly 模块。

使用 Emscripten 在浏览器中运行 Hello World

在本节中,我们将看到如何通过 Emscripten 将 C/C++ 代码转换为 WebAssembly 二进制文件并在浏览器中运行。

注意

如果终端显示 emcc 命令未找到,那么很可能你遗漏了设置环境变量。要设置环境变量,请在 emsdk 文件夹内运行以下命令:source ./emsdk_env.sh

让我们使用在 使用 Emscripten 生成 asm.js 部分中使用的相同代码示例。现在,我们不仅运行 emcc,还传递 -o 选项并指示 emcc 生成 .html 文件:

$ emcc hello_web.c -o helloweb.html

完成后,将生成以下文件:

  • helloweb.js

  • helloweb.wasm

  • helloweb.html

与 Node.js 示例类似,生成的 JavaScript 文件非常大。我们将在本章后面学习如何优化文件大小。

注意

-o 选项确保所有生成的文件都命名为 helloweb

为了在浏览器中运行生成的 HTML 文件,我们需要一个网络服务器。网络服务器通过 HTTP 协议提供 HTML 文件。解释网络服务器及其工作原理超出了本书的范围;有关更多详细信息,请参阅 en.wikipedia.org/wiki/Web_server

Python 提供了一种简单的方式来运行网络服务器。为了使用 Python 运行网络服务器,请运行以下命令:

$ python -m http.server <port number>

打开 http://localhost:<端口号> 来在浏览器中查看 WebAssembly 的实际运行情况。

![图 2.2 – 浏览器运行 WebAssembly图片

图 2.2 – 浏览器运行 WebAssembly

当 JavaScript 文件通过浏览器执行时,它会打印出 run 函数。run 函数实例化了 WebAssembly 二进制文件。在这种情况下,由于我们在 C 中定义了 main 函数,绑定文件在实例化时会直接调用 main 函数。

Emscripten 还提供了 emrun 来运行 HTML 文件。更多信息请查看 emscripten.org/docs/compiling/Running-html-files-with-emrun.html

了解如何在 emscripten.org/docs/compiling/Deploying-Pages.html 上部署 Emscripten 编译的页面。

我们已经使用 Emscripten 生成 WebAssembly 模块。让我们继续探索 emsdk 还能做什么。

探索 emsdk 中的其他选项

emsdk 是一个一站式商店,用于安装、维护和管理使用 Emscripten 所需的所有工具和工具链。emsdk 使启动环境、升级到最新版本、切换到各种版本、更改或配置各种工具等变得更加容易。

emsdk 命令在 emsdk 文件夹内可用。转到 emsdk 文件夹并运行 emsdk 命令。

注意

对于本章中的所有命令,对于 *nix 系统,使用 ./emsdk,对于 Windows,使用 emsdk

要查找 emsdk 命令中可用的各种选项,请运行以下命令:

$ ./emsdk --help
emsdk: Available commands:
emsdk list [--old] [--uses] - To list down the tools
emsdk update - To update the emsdk to the latest version.
emsdk update-tags - To fetch the latest tags from the GitHub 
  repository.
emsdk install - To install the tools and SDK.
emsdk uninstall - To uninstall the tools and SDK installed 
  previously.
emsdk activate - To activate the currently installed version.

一个 emsdk 命令采用以下格式:

emsdk <option> <Tool / SDK > --<flags>

emsdk 命令由以下部分组成:

  • <选项>

这可以是以下之一:list、update、update-tags、install、uninstall 或 activate。

  • <工具/SDK>

这指的是库,它包括 Emscripten 和 LLVM。SDK 指的是 emsdk 本身。

  • --<标志>

这指的是各种配置选项。

让我们探索 emsdk 命令支持的每个选项和标志。

列出工具和 SDK

在这里,我们展示了如何列出 emsdk 可用的工具和 SDK。运行以下命令:

$ ./emsdk list

The *recommended* precompiled SDK download is 2.0.6 
  (4ba921c8c8fe2e8cae071ca9889d5c27f5debd87).

To install/activate it, use one of:
        latest                  [default (llvm) backend]
        latest-fastcomp         [legacy (fastcomp) backend]

Those are equivalent to installing/activating the following:
         2.0.6             INSTALLED
         2.0.6-fastcomp

All recent (non-legacy) installable versions are:
         2.0.6    INSTALLED
         ...

The additional following precompiled SDKs are also available 
  for download:
         sdk-fastcomp-1.38.31-64bit

The following SDKs can be compiled from source:
         sdk-upstream-master-64bit
          ...
The following precompiled tool packages are available for 
  download:
        ...
     *     node-12.18.1-64bit         INSTALLED
     *     python-3.7.4-2-64bit       INSTALLED
           emscripten-1.38.30
        ...
The following tools can be compiled from source:
           llvm
           clang
           emscripten
           binaryen

Items marked with * are activated for the current user.

To access the historical archived versions, type 'emsdk list 
  --old'

Run "git pull" followed by "./emsdk update-tags" to pull 
  in the latest list.

emsdk list 列出所有可用的工具包和 SDK。这个工具和 SDK 列表包括 LLVM、Clang、Emscripten 和 Binaryen 的最新几个版本。它们甚至有 Node 版本 8 和 12 以及 Python 3.7。emsdk 维护和管理 emsdk。这意味着我们需要了解我们正在使用的当前版本的信息以及如何更新它。emsdk list 命令还提供了关于 SDK 组件的更多详细信息,以及从源编译的列表。

管理工具和 SDK

emsdk 提供了安装、更新和卸载工具和 SDK 的选项。

为了安装工具、SDK 或 emsdk 本身,请使用以下命令:

$ ./emsdk install <tool / SDK to install>

要安装 SDK 的最新版本,可以运行以下命令:

./emsdk install latest

注意

latest 指的是 emsdk 的最新版本。

要使用emsdk install命令安装多个工具,请使用以下命令:

./emsdk install <tool1> <tool2> <tool3>

您也可以为install命令指定多个选项。您可以通过以下方式向install命令传递选项:

 ./emsdk install [options] <tools / SDK>

可用的各种 options 如下所示:

  • 构建所需的核数

  • 构建类型

  • 工具和 SDK 的激活

  • 卸载

构建所需的核数

初始设置将花费较长时间构建和安装所需的工具和 SDK。根据您的需求,您可以控制构建和安装所需工具和 SDK 所需的核数:

./emsdk install -j<number of cores to use for building> <tools 
  / SDK>

构建类型

您指导emsdk使用哪种构建类型来使 LLVM 执行:

./emsdk install --build=<type> <tools / SDK>

type 接受以下选项:

  • 调试

    • 此类型用于调试。

    • 它生成符号文件。

    • 最终构建将不会生成优化、快速代码。

  • 发布版

    • 此类型将生成优化、快速代码。
  • MinSizeRel

    • 此类型与发布版相同。

    • 此类型将最小化大小并最大化速度。

    • 这使用了优化选项,例如 -O1(最小化大小)和 -O2(最大化速度)。

  • RelWithDebInfo

    • 此类型与发布版相同。

    • 此类型还将生成符号文件。这将有助于调试。

工具和 SDK 的激活

在工具和 SDK 安装后,我们可以激活不同的版本来使用它们。activate 命令会生成必要的配置文件,映射路径,并带有构建的可执行文件。

要激活工具和 SDK,请运行以下命令:

./emsdk activate <tools / SDK to activate>

activate 命令接受一些选项;如下所示:

  • --embedded – 此选项确保所有构建的文件、配置、缓存和临时文件都位于 emsdk 命令所在的目录内。

如果未指定,此命令将配置文件移动到用户的主目录。

  • --build=<type> – 与 LLVM 支持的构建类型相似。例如,调试、发布、MinSizeRel、RelWithDebInfo。

工具和 SDK 的卸载

要卸载工具和 SDK,我们可以运行以下命令:

./emsdk uninstall < tools / SDK to uninstall> 

emscripten.org/docs/tools_reference/index.html了解更多关于工具的信息。

我们已经探讨了 emscripten 如何帮助我们管理工具和 SDK;让我们继续探索 Emscripten 提供的各种优化。

理解各种优化级别

C/C++程序通过 Clang 或 GCC 编译器编译并转换为本地代码。Clang 或 GCC 编译器根据目标转换 C/C++程序。这里的“目标”是指代码执行的最后机器。emcc 内置了 Clang 编译器。emcc 编译器负责将 C 或 C++源代码转换为 LLVM 字节码。

在本节中,我们将看到如何提高生成的 WebAssembly 二进制代码的优化和代码大小。

为了提高效率和生成的代码大小,Emscripten 编译器有以下选项:

  • 优化

  • Closure Compiler

首先让我们谈谈优化。

优化

编译器的目标是减少编译成本,即编译时间。使用 -O 优化标志,编译器试图在编译时间上做出牺牲,以改善代码大小和/或性能。在编译器优化方面,代码大小和性能是互斥的。编译时间越快,优化级别越低。要指定优化,我们使用 -O<0/1/2/3/s/z> 标志。每个选项都包括各种断言、代码大小优化和代码性能优化,以及其他优化。

以下是可以用的各种优化:

  • -O0 – 这是默认选项,是一个完美的实验起点。这个选项意味着“没有优化”。这个优化级别编译速度最快,生成的代码最易于调试。这是一个基本的优化级别。此选项尝试内联函数。

  • -O1 – 这个选项添加了简单的优化,并尝试生成最小的代码大小。此选项在生成的代码中移除了运行时断言,并且构建速度比 -O0 选项慢。此选项还尝试简化循环。

  • -O2 – 这个选项比 -O1 添加了更多的优化。它比 -O1 慢,但生成的代码比 -O1 选项更优化。此选项基于 JavaScript 优化进行代码优化,并移除不属于 JavaScript 模块的代码。此选项移除了内联函数,并将 vectorize-loop 选项设置为开启。此选项添加了适度的优化级别。此选项还添加了死代码消除。

向量化将指示处理器以块的形式执行操作,而不是逐个执行。

  • -O3 – 这个选项添加了更多选项,编译时间更长,生成的代码比 -O2 选项更优化。

这个选项生成最优的生产就绪代码。这个选项类似于 -O2,但开启了需要更长时间执行或可能生成更大代码的优化(试图使程序运行更快)。

  • -Os – 这个选项与 -O2 类似。它添加了额外的优化并减少了代码大小。减少代码大小反过来会降低性能。此选项生成的代码比 -O2 更小。

  • -Oz – 这个选项类似于 -Os,但进一步减少了代码大小。此选项生成二进制代码需要更多的编译时间。

我们现在将探索 Emscripten 提供的各种优化选项:

  1. 首先,我们创建一个名为 optimization_check 的 C 文件:

    $ touch optimization_check.c
    
  2. 然后,打开你最喜欢的编辑器并添加以下代码。以下是一个简单的 C 文件,包含一个 main 函数和其他几个函数:

    #include <stdio.h>
     int addSame(int a) {
        return a + a;
    }
    
    int add(int a, int b) {
        return a + b;
    }
    
    int main() {
        printf("Hello, Web!\n");
    
        int a;
        int sum = 0;
        /* for loop execution */
        for( a = 0; a < 20; a = a + 1 ){
           sum = sum + a;
        }
         addSame(sum);
         add(1, 2);
         return 0;
    }
    
  3. 然后,我们使用 emcc 将其编译成 WebAssembly 代码:

    $ time emcc optimization_check.c
    emcc optimization_check.c  0.32s user 0.14s system 
      90% cpu 0.514 total
    
  4. 然后,检查生成的文件大小:

     $ l
    324B optimization_check.c
    13K a.out.wasm
    109K a.out.js
    

我们可以看到生成的 WebAssembly 文件大约有 13 KB,总共花费了 0.514 秒来编译。这是一个快速的编译,但代码大小很大。

在编译器的世界中,编译速度越快,代码大小越大,执行速度越慢。

  1. 现在,让我们使用 -O1 选项进一步优化它:

    $ time emcc -O1 optimization_check.c
    emcc -O1 optimization_check.c  0.31s user 0.13s system
      86% cpu 0.519 total
    

检查生成的文件大小:

$ l
324B optimization_check.c
3.4K a.out.wasm
59K a.out.js

生成的 WebAssembly 文件大约是 3.4 KB(比 -O0 版本少 3.8 倍),并且花费了几乎相同的时间,大约 0.519 秒。

  1. 现在,让我们使用 -O2 选项进一步优化它:

    $ time emcc -O2 optimization_check.c
    emcc -O2 optimization_check.c  0.53s user 0.16s system
      111% cpu 0.620 total
    

检查生成的文件大小:

$ l
324B optimization_check.c
2K a.out.wasm
20K a.out.js

生成的 WebAssembly 文件大约是 2 KB(比 -O0 少约 6.5 倍),花费了大约 0.62 秒。

  1. 现在,让我们使用 -O3 选项进一步优化它:

    $ time emcc -O3 --profiling optimization_check.c
    emcc -O3 --profiling optimization_check.c  1.03s user
      0.21s system 110% cpu 1.117 total
    

emscripten.org/docs/tools_reference/emcc.html#emcc-profiling 了解更多关于 --profiling 标志的信息。

检查生成的文件大小:

$ l
324B optimization_check.c
2.0K a.out.wasm
17K a.out.js

生成的 WebAssembly 文件与 -02 的大小相同,但生成的 JavaScript 文件少 3 KB,编译时间大约为 1.117 秒。

  1. 现在,让我们使用 -Os 选项进一步优化它:

    $ time emcc -Os optimization_check.c
    emcc -Os optimization_check.c  1.03s user 0.22s system
      46% cpu 2.655 total
    

检查生成的文件大小:

$ l
324B optimization_check.c
1.7K a.out.wasm
14K a.out.js

生成的 WebAssembly 文件大约是 1.7 KB(比 -O0 少约 7.5 倍),花费了大约 2.655 秒。

  1. 现在,让我们使用 -Oz 选项进一步优化它:

    $ time emcc -Oz optimization_check.c
    emcc -Oz optimization_check.c  1.03s user 0.21s system
      110% cpu 1.123 total
    

检查生成的文件大小:

$ l
324B optimization_check.c
1.7K a.out.wasm
14K a.out.js

生成的 WebAssembly 文件大约是 1.7 KB(比 -O0 少约 7.5 倍),花费了大约 1.123 秒。

接下来,我们将看到 Emscripten 编译器提供的另一种提高效率和减少生成代码大小的手段:Closure Compiler

Closure Compiler

Closure Compiler 是一个将 JavaScript 编译成更优 JavaScript 的工具。它解析、分析、删除死代码、重写和最小化 JavaScript。对生成的绑定 JavaScript 文件和 WebAssembly 模块进行的进一步优化使用 Closure Compiler 完成。使用 Closure Compiler,我们可以对 Emscripten 代码进行更好的优化。为了进一步优化 WebAssembly 模块和 JavaScript,我们可以使用 --closure <优化类型>

优化 类型有以下选项:

  • --closure 0 – 此选项不添加任何 Closure Compiler 优化。

  • --closure 1 – 此选项减少生成的 JavaScript 代码大小。此选项不优化 asm.js 和 WebAssembly 二进制文件。此选项添加了一个额外的编译步骤,增加了编译时间。

  • --closure 2 – 此选项优化 JavaScript、asm.js,但不优化 WebAssembly 二进制文件,并且显著减少了文件的代码大小。

我们将使用 –closure 1 选项来优化 WebAssembly 二进制文件,同时使用 –O3/s Emscripten 优化选项:

$ time emcc -O3 --closure 1 optimization_check.c
emcc -O3 --closure 1 optimization_check.c  2.40s user 0.42s 
  system 105% cpu 2.681 total

生成的文件大小如下:

 $ l
324B optimization_check.c
1.8K a.out.wasm
6.5K a.out.js

除了 emcc –O3,我们还传递 –closure 1 以进一步优化生成的文件。与 emcc -O3 选项相比,Closure Compiler 将 JavaScript 文件的大小减少了 50%,编译时间为 2.681 秒:

time emcc -Os --closure 1 optimization_check.c
emcc -Os --closure 1 optimization_check.c  2.53s user 0.42s 
  system 106% cpu 2.778 total

让我们列出当前文件夹中的文件,以检查生成的文件及其大小:

$ l
324B optimization_check.c
1.7K a.out.wasm
6.5K a.out.js

除了emcc –Os之外,我们还会传递–closure 1来进一步优化生成的二进制文件。使用emcc -Os选项,Closure Compiler 可以进一步减少.wasm文件的大小,编译耗时 2.778 秒。

注意

在优化大小的时候,尝试同时使用-O3-Os以及--closure 1来优化 JavaScript 和 WebAssembly 模块。

emscripten.org/docs/tools_reference/emcc.html clang.llvm.org/docs/CommandGuide/clang.html查看更多选项和标志。

docs.microsoft.com/en-us/cpp/build/reference/o-options-optimize-code?view=vs-2017了解更多关于各种可用的优化选项。

developers.google.com/closure/compiler了解更多关于 Closure Compiler 的信息。

emscripten.org/docs/optimizing/Optimizing-Code.html#very-large-codebases了解更多关于使用 Emscripten 优化大型代码库的信息。

摘要

在本章中,我们学习了如何安装和使用 Emscripten 将 C/C++编译成 WebAssembly 模块。我们还探讨了 emsdk 工具以及生成 WebAssembly 模块时的各种优化级别。在下一章中,我们将探索 WebAssembly 模块。

第三章:第三章:探索 WebAssembly 模块

WebAssembly 是一种低级类似汇编的代码,旨在高效执行和紧凑表示。WebAssembly 在所有 JavaScript 引擎中(包括现代桌面和移动浏览器以及 Node.js)以接近原生速度运行。二进制的紧凑表示使得生成的二进制文件尽可能小。

备注

WebAssembly 的主要目标是实现高性能应用程序。

每个 WebAssembly 文件都是一个高效、最优且自给自足的模块,称为WebAssembly 模块WASM)。WASM 是安全的,也就是说,二进制在内存安全和沙箱环境中运行。WASM 没有权限访问沙箱之外的任何内容。WASM 是语言、硬件和平台无关的。

WebAssembly 是一个虚拟的指令集架构ISA)。WebAssembly 规范定义了以下内容:

  • 指令集

  • 二进制编码

  • 验证

  • 执行语义

WebAssembly 规范还定义了 WebAssembly 二进制的文本表示。

在本章中,我们将探索 WASM 以及 JavaScript 引擎如何执行 WASM。然后我们将探索 WebAssembly 文本格式及其用途。理解 WASM 执行和 WebAssembly 文本格式将使我们能够轻松理解模块并在 JavaScript 引擎中调试它。本章将涵盖以下主要主题:

  • 理解 WebAssembly 的工作原理

  • 探索 WebAssembly 文本格式

技术要求

你可以在 GitHub 上找到本章中包含的代码文件,网址为github.com/PacktPublishing/Practical-WebAssembly

理解 WebAssembly 的工作原理

让我们先探索 JavaScript 和 WebAssembly 如何在 JavaScript 引擎中执行。

理解 JavaScript 在 JavaScript 引擎中的执行

JavaScript 引擎首先获取完整的 JavaScript 文件(请注意,引擎必须等待整个文件下载/加载完成)。

备注

JavaScript 文件越大,加载所需的时间就越长。无论你的 JavaScript 引擎有多快,或者你的代码有多高效,都没有关系。如果你的 JavaScript 文件非常大(即,大于 170 KB),那么你的应用程序在加载时将会很慢。

图 3.1 – JavaScript 在 JavaScript 引擎中的执行

图 3.1 – JavaScript 在 JavaScript 引擎中的执行

一旦加载,JavaScript 就会被解析成抽象语法树ASTs)。这个阶段称为解析。由于 JavaScript 既是解释型语言又是编译型语言,JavaScript 引擎在解析后启动执行。解释器执行代码更快,但每次都会编译代码。这个阶段称为解释

JavaScript 引擎有 监视器(在一些浏览器中称为 分析器)。监视器跟踪代码执行。如果一个特定的代码块经常执行,那么监视器将其标记为热代码。引擎使用 即时编译JIT)编译这个代码块。引擎花费一些时间进行编译,比如说在纳秒级别。这里花费的时间是值得的,因为下次函数被调用时,执行会更快,因为编译版本总是比解释版本快。这个阶段被称为 优化

JavaScript 引擎添加一个(或两个)更多的优化层。监视器继续监视代码执行。然后,监视器将调用频率更高的代码命名为 非常热代码。引擎进一步优化此代码。这种优化需要很长时间(考虑类似于 -O3 级别的优化)。这个阶段产生高度优化的代码,运行速度极快。此代码比之前优化的代码和解释版本快得多。显然,引擎在这个阶段花费更多的时间,比如说在毫秒级别。这是通过代码性能和执行频率来补偿的。

JavaScript 是一种动态类型语言,引擎能做的所有优化都是基于 类型 的假设。如果假设被打破,那么代码将被解释并执行,优化的代码将被移除而不是抛出运行时异常。JavaScript 引擎实现了必要的类型检查,并在假设的类型发生变化时退出优化的代码。但是,在优化阶段花费的时间是徒劳的。

我们可以通过使用诸如 TypeScript 这样的工具来防止这些 类型 相关的问题。TypeScript 是 JavaScript 的超集。使用 TypeScript,我们可以防止多态代码(接受不同类型的代码)。在 JavaScript 引擎中,单态代码(只接受一种类型的代码)总是比其多态对应物运行得更快。

如果 JavaScript 文件很大,那么拥有高度优化的单态 JavaScript 代码是没有用的。JavaScript 引擎必须等待整个文件下载完成。在糟糕的连接下,这需要很长时间才能完成。

注意

将 JavaScript 包拆分成更小的块非常重要。异步包含 JavaScript(换句话说,懒加载)可以提高应用程序的性能。我们需要找到一个正确的平衡点,并知道要加载、缓存和重新验证哪个 JavaScript 模块/文件。更大的文件大小(负载)将大大降低应用程序的性能。

最后一步是垃圾回收,其中移除内存中所有活动的对象。JavaScript 引擎中的垃圾回收基于引用。在垃圾回收周期中,JavaScript 引擎从根对象(类似于 Node.js 中的全局对象)开始。它找到所有从根对象引用的对象,并将它们标记为可达对象。它将剩余的对象标记为不可达对象。最后,它清除不可达对象。由于这是由 JavaScript 引擎自动完成的,因此垃圾回收过程效率不高,速度较慢。

理解 JavaScript 引擎中的 WebAssembly 执行

WASM 是二进制格式,并且已经编译和优化。JavaScript 引擎获取 WASM。然后,它解码 WASM 并将其转换为模块的内部表示(即 AST)。这个阶段称为解码。解码阶段比 JavaScript 的解析阶段快得多。

![Figure 3.2 – WebAssembly execution inside the JavaScript engine]

![img/Figure_3.2_B14844.jpg]

Figure 3.2 – WebAssembly execution inside the JavaScript engine

接下来,解码后的 WASM 进入编译阶段。在这个阶段,模块被验证,在验证过程中,代码会检查某些条件以确保模块是安全的,并且没有有害的代码。在验证过程中,函数、指令序列和堆栈的使用都会进行类型检查。验证后的代码随后被编译成机器可执行代码。由于 WASM 已经编译和优化,这个编译阶段更快。在这个阶段,WASM 被转换成机器代码。

编译后的代码随后进入执行阶段。在执行阶段,模块被实例化和调用。在实例化过程中,引擎实例化了状态和执行栈(存储所有与程序相关的信息的内存),然后执行模块。

WebAssembly 的另一个优点是模块从第一字节开始就可以准备编译和实例化。因此,JavaScript 引擎无需等待整个模块下载完成。这进一步提高了 WebAssembly 的性能。WebAssembly 之所以快速,是因为它的执行步骤比 JavaScript 执行步骤少,所以二进制文件已经优化和编译,并且可以流式编译。

注意

WASM 并不总是提供高性能。在某些场景中,JavaScript 的表现更好。因此,有必要理解这一点,并在使用 WebAssembly 之前进行思考。

medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4了解更多关于 JavaScript 性能和加载时间如何涉及的信息。

webpack.js.org/guides/code-splitting/了解更多关于 webpack 中的代码拆分和分块的信息。

我们已经看到了 WebAssembly 在浏览器中的工作方式;现在,让我们探索 WebAssembly 文本格式。

探索 WebAssembly 文本格式

机器理解一串 1 和 0。我们优化二进制以使其运行更快、更高效。指令越简洁、越优化,机器将越高效、性能越好。但对于人类来说,很难在上下文中分析和理解一大堆 1 和 0。这正是我们开始抽象和创建高级编程语言的原因。

在 WebAssembly 世界中,我们将可读性编程语言,如 Rust、Go 和 C/C++,转换为二进制代码。这些二进制是一系列带有操作码和操作数的指令。这些指令使机器运行得非常高效,但在上下文中也使得我们难以理解。

我们为什么要担心生成的二进制的可读性?因为它有助于我们理解代码,这在调试代码时很有帮助。

WebAssembly 提供了 WebAssembly 文本格式,WAST 或 WAT。WAST 是 WebAssembly 二进制的可读格式。JavaScript 引擎(无论是在浏览器中还是在 Node.js 中),在加载 WebAssembly 文件时,可以将二进制转换为 WebAssembly 文本格式。这有助于理解代码内容并进行调试。文本编辑器可以以 WebAssembly 文本格式显示二进制,这比其二进制对应物更易读。

二进制格式的基本 WASM 如下所示:

00 61 73 6d 01 00 00 00

这对应于以下内容:

 00 61 73 6d 01 00 00 00
\0  a  s  m  1  0  0  0 (ascii value of the character)
|         |  |
---------  version
    |
Magic Header

这个基本模块有一个魔法头(\0asm),后面跟着 WebAssembly 的版本(01)。

文本格式是用 () 写的。S-表达式在定义嵌套列表或结构化树时常用。许多关于基于树的数据结构的研究论文使用这种符号来展示他们的代码。s-表达式从 XML 中移除了所有不必要的仪式,提供了一个简洁的格式。

注意

这个表达式(定义括号内的所有内容)看起来熟悉吗?你曾经使用过 LISP(或受 LISP 启发的语言)吗?

模块是 WASM 的基本构建块。基本 WASM 的文本表示如下:

(module ) 

WASM 由一个头和零个或多个部分组成。头以一个魔法头和 WASM 的版本开始。在头之后,WASM 可能包含零个或多个以下部分:

  • 类型

  • 函数

  • 内存

  • 全局变量

  • 元素

  • 数据

  • 开始函数

  • 导出

  • 导入

所有这些部分在 WASM 中都是可选的。WASM 的结构如下所示:

 module ::= {
    types vec<funcType>,
    funcs vec<func>,
    tables vec<table>,
    mems vec<mem>,
    globals vec<global>,
    elem vec<elem>,
    data vec<data>,
    start start,
    imports vec<import>,
    exports vec<export>
 } 

WASM 内部的每个部分都是一个包含零个或多个相应类型值的向量(数组),除了 start。我们将在本书的后面部分探讨 start 部分。目前,start 保存一个索引,该索引引用 funcs 部分中的一个函数。

WASM 中的每个部分都采用以下格式:

<section id><u32 section size><Actual content of the section> 

第一个字节指的是一个唯一的节 ID。每个节都有一个唯一的节 ID。紧随唯一节 ID 的是定义节大小的 无符号 32 位u32)整数。剩余的字节是节内容。

注意

由于节大小由 u32 整数定义,因此节的最大大小限制在大约 4.2 GB 的内存(即 2³² - 1)内。

在 WebAssembly 文本格式中,我们使用节的名字来表示节中的每个段。

例如,函数节包含一个函数列表。以下是一个 WebAssembly 文本格式中的示例函数定义:

 (func <name>? <func_type> <local>* <inst>* )

与其他表达式一样,我们定义的所有内容都在括号 () 内。首先,我们使用 func 关键字定义函数块。在 func 关键字之后,我们添加函数的名称。在这里,函数名称是可选的,因为在二进制中,函数是通过函数节内函数块的索引来识别的。

名字后面跟着 func_typefunc_type 在规范中被称为 type_use。在这里,type_use 指的是类型定义。func_type 包含所有输入参数(及其类型)和函数的返回类型。因此,对于一个 add 函数,它接受两个输入操作数并返回结果,func_type 将看起来像这样:

(param $lhs i32) (param $rhs i32) (result i32) 

注意

类型可以是 i32i64f32f64(32 位和 64 位整数或浮点数)。类型信息可能会在未来改变,当 WebAssembly 增加对更多类型的支持时。

param 关键字表示定义的表达式包含一个参数。$lhs 是变量名。请注意,在 WebAssembly 文本格式中定义的所有变量都将有 $ 作为前缀。接下来,我们有参数的类型,i32。同样,我们为第二个操作数定义了另一个表达式,$rhs。最后,返回类型被提到为 (result i32)result 关键字表示表达式是一个返回类型,后面跟着类型,i32

func_type 之后,我们定义任何将在函数内部使用的局部变量。最后,我们有一个指令/操作的列表。

让我们以前面的代码片段为参考,定义一个 add 函数:

 (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add) 

整个块被括号包围。函数块以 func 关键字开始。然后,我们有一个可选的函数名($add)。WebAssembly 二进制模块将使用函数节内的函数索引来识别函数,而不是名称。然后,我们定义操作数和返回类型。

注意

在二进制格式中,参数和结果通过 type 节定义,因为这有助于优化生成的函数。但在文本格式中,为了简洁和易于理解,类型信息将显示在每个函数定义中。

然后,我们有一系列指令。第一条指令get_local获取(从堆中)的局部值$lhs。然后,我们获取$rhs的局部值。之后,我们使用i32.add指令将它们相加。最后,关闭括号结束。

没有单独的return语句/表达式。那么,函数是如何知道要返回什么的?

如我们之前所见,WebAssembly 是一种栈机器。当调用一个函数时,它会为它创建一个空栈。然后,函数使用这个栈来推送和弹出数据。因此,当执行get_local指令时,它将值推入栈中。在执行了两个get_local调用之后,栈中将包含$lhs$rhs。最后,i32.add将从栈中弹出两个值,执行add操作,并将元素推入。当函数结束时,栈顶的值将被取出并提供给函数调用者。

如果我们想将此函数导出到外部世界,则可以添加一个export块:

 (export <export_name> (func <function_reference>))

export块定义在()内。export块以export关键字开始。export关键字后面跟着函数的名称。名称之后,我们引用该函数。函数块由以下func关键字组成。然后,我们有function_reference,它引用模块内部定义/导入的函数的名称。

为了导出add函数,我们定义以下内容:

 (export "add" (func $add)) 

"add"指的是函数在模块外部导出的名称,后面跟着(func $add),指的是该函数。

函数和export部分都应该包裹在module部分内,以使其成为有效的 WASM:

(module
    (func $add (param $lhs i32) (param $rhs i32) 
      (result i32)
        get_local $lhs
        get_local $rhs
        i32.add)
    (export "add" (func $add))
) 

前面的内容是有效的 WASM。想象它是一个树结构,模块作为其根,函数和导出作为其子节点。

我们已经看到了如何在 WebAssembly 文本格式中创建一个简单的函数。现在,让我们在 WebAssembly 文本格式中定义一个复杂函数。

在 WebAssembly 文本格式中构建函数

为了这个目的,我们将使用递归的斐波那契数列生成器。我们将编写的斐波那契函数将具有以下格式:

 # Sample code in C for reference
int fib(n) {
    if (n <= 1)
        return 1;
    else
        return fib(n-1)+ fib(n-2);
} 

让我们首先使用 WebAssembly 文本格式定义给定fib函数的函数签名。fib函数类似于其 C 语言对应版本,接受一个数字参数并返回一个数字。因此,函数定义遵循 WebAssembly 文本格式中的相同签名:

 (func $fib (param $n i32) (result i32)
    ...
)

我们在括号()内定义函数。函数以func关键字开始。关键字之后,我们添加函数名称,$fib。然后,我们向函数添加参数;在我们的例子中,函数只有一个参数,n;我们将其定义为(param $n i32)。然后,函数返回一个数字,(result i32)

WebAssembly 没有内存来处理临时变量。为了有局部值,我们应该将值推入栈中,然后检索它。所以,要检查n<=1,我们必须首先创建一个局部变量并在其中存储1,然后进行检查。要定义局部变量,我们使用local块。local块以local关键字开始。这个关键字后面跟着变量的名称。在变量名称之后,我们定义变量的类型:

 (local <name> <type>)

让我们创建一个名为$tmplocal变量:

 (local $tmp i32) 

注意

(local $tmp i32)不是一个指令。它是函数声明的一部分。记住,前面的函数语法包括local

我们接下来必须将$tmp的值设置为1。要设置值,我们首先必须将值1推入栈中,然后从栈中弹出值并将其设置为$tmp

i32.const 1
set_local $tmp 

i32.const创建一个i32常量值并将其推入栈中。所以,在这里,我们创建一个值为1的常量并将其推入栈中。

然后,我们使用set_local设置$tmp中的值。set_local从栈顶获取最高值,在我们的例子中是 1,并将$tmp的值设置为 1。

现在,我们必须检查给定的参数是否小于 2。WebAssembly 提供了i32.<some_action>来对i32执行一些操作。例如,要添加两个数字,我们使用了i32.add。同样,要检查它是否小于某个特定值,我们有i32.lt_s。这里的_s表示我们正在检查一个有符号数字。

i32.lt_s期望两个操作数。对于第一个操作数(即$n),我们使用get_local表达式从$n中获取值并将其放在栈顶。然后,我们使用i32.const 2创建一个常量并将其推入栈中。最后,我们使用i32.lt_s比较$n的值与2

get_local $n
i32.const 2
i32.lt_s

但我们如何定义条件?WebAssembly 提供了br_ifblock

在 WebAssembly 文本格式中,一个块是通过block关键字定义的,后面跟着一个用于识别块的名称。我们使用end来结束块。块看起来如下:

block $block
... ; some code goes in here.
end

我们将提供这个块给br_if。如果条件成功,br_if将调用块:

get_local $n
i32.const 2
i32.lt_s
br_if $block ; calls the $block` only when the condition
  succeeds.

WebAssembly 文本格式目前看起来是这样的:

 (module
  (func $fib (param $n i32) (result i32) (local $tmp i32)
    i32.const 1
    set_local $tmp
    ; block
    block $block
      ; if condition
      get_local $n
      i32.const 2
      i32.lt_s
      br_if $block
    ... ; some code
    end
    ; return value
    get_local $tmp
  )
) 

所有的内容都被包裹在module中。在$block的末尾,值将被存储在$tmp中。我们使用get_local $tmp来获取$tmp的值。唯一剩下要做的就是创建循环。

循环时间

首先,我们将$tmp设置为1

i32.const 1
set_local $tmp 

然后,我们将创建一个循环。要创建循环,WebAssembly 文本格式使用loop关键字:

loop $loop
end 

loop关键字后面跟着循环的名称。循环以end关键字结束。loop是一个特殊的块,它将一直运行,直到我们使用一些条件表达式(如br_if)退出:

get_local $n
i32.const -2
i32.add
call $fib
get_local $tmp
i32.add
set_local $tmp
get_local $n
i32.const -1
i32.add
tee_local $n
i32.const 1
i32.gt_s
br_if $loop

我们得到$n并给它加上-2,然后调用fib函数。要调用一个函数,我们使用call关键字后跟函数名。在这里,call $fib返回值并将值推入栈中。

现在,使用get_local $tmp获取$tmp。这会将$tmp推入栈中。然后,我们使用i32.add从栈中弹出两个值并将它们相加。最后,我们使用set_local $tmp设置$tmpset_local $tmp从栈中取出最顶部的值并将其分配给$tmp。我们得到$n并给它加上-1

我们在这里使用tee_local是因为tee_localset_local类似,但不同之处在于它不是将值推入栈中,而是返回值。最后,我们运行循环直到$n大于 1。如果它小于 1,我们使用br_if $loop跳出循环。完整的 WebAssembly 文本格式将看起来像这样:

(module
  (func (export $fib (param $n i32) (result i32) 
    (local $tmp i32)
    i32.const 1
    set_local $tmp
    ; block
    block $block
      ; if condition
      get_local $n
      i32.const 2
      i32.lt_s
      br_if $block
      ; loop
      loop $loop
        get_local $n
        i32.const -2
        i32.add
        call $fib
        get_local $tmp
        i32.add
        set_local $tmp
        get_local $n
        i32.const -1
        i32.add
        tee_local $n
        i32.const 1
        i32.gt_s
        br_if $loop
      end
    end
    ; return value
    get_local $tmp
  )
)

在未来的章节中,我们将看到如何将这个 WebAssembly 文本格式转换为 WASM 并执行它。

如果你对学习更多关于 s 表达式感兴趣,请查看en.wikipedia.org/wiki/S-expression

要了解更多关于 WebAssembly 文本格式设计的信息,请查看github.com/WebAssembly/design/blob/master/Semantics.md中的规范。

webassembly.github.io/spec/core/text/instructions.html查看更多文本指令。

webassembly.github.io/spec/core/binary/instructions.html参考各种指令及其操作码。

github.com/WebAssembly/design/blob/master/BinaryEncoding.md了解更多关于二进制编码的信息。

摘要

在本章中,我们看到了 WebAssembly 如何在 JavaScript 引擎中执行,并探讨了 WebAssembly 文本格式是什么以及如何使用 WebAssembly 文本格式定义 WASM。在下一章中,我们将探索 WebAssembly 二进制工具包。

第二部分:WebAssembly 工具

本节介绍了 WebAssembly 中可用的各种工具以及我们可以用它们实现什么。它还解释了 WebAssembly 模块内部的各个部分。

在完成本章后,您将了解如何从不同的来源生成 WebAssembly,以及如何使用 WebAssembly 生态系统中的各种工具。

本节包括以下章节:

  • 第四章理解 WebAssembly 二进制工具包

  • 第五章理解 WebAssembly 模块中的部分

  • 第六章安装和使用 Binaryen

第四章:第四章:理解 WebAssembly 二进制工具包

Rust 编译器链将 Rust 代码转换为 WebAssembly 二进制。但生成的二进制文件在大小和性能上都进行了优化。理解、调试和验证二进制代码(它是一堆十六进制数字)是困难的。将 WebAssembly 二进制转换回原始源代码非常困难。WebAssembly 二进制工具包WABT)有助于将 WebAssembly 二进制转换为人类可读的格式,例如WebAssembly 文本WAST)格式或 C 本地代码。

注意

这里的本地代码并不指代原始的真实来源;相反,它指的是机器解释的 C 本地代码。

WebAssembly 二进制工具包(WebAssembly Binary Toolkit)简称为 WABT,发音为"wabbit"。WABT 提供了一套用于转换、分析和测试 WebAssembly 二进制文件的工具。

在本章中,我们将探讨 WABT 以及它是如何帮助将 WebAssembly 二进制转换为各种格式,以及为什么它是有用的。本章将涵盖以下主要主题:

  • 开始使用 WABT

  • 将 WAST 转换为 WASM

  • 将 WASM 转换为 WAST

  • 将 WASM 转换为 C

  • 将 WAST 转换为 JSON

  • 了解 WABT 提供的其他一些工具

技术要求

你可以在 GitHub 上找到本章中存在的代码文件,链接为github.com/PacktPublishing/Practical-WebAssembly

开始使用 WABT

让我们先安装 WABT,然后探索 WABT 工具提供的各种选项。

安装 WABT

为了安装 WABT,首先从 GitHub 克隆仓库:

$ git clone --recursive https://github.com/WebAssembly/wabt

注意

我们在这里使用--recursive标志,因为它确保在创建克隆之后,仓库中的所有子模块(如test-suite)都被初始化。

进入克隆的仓库,创建一个名为build的文件夹,然后进入build文件夹。这是我们生成二进制文件的地方:

$ cd wabt
$ mkdir build
$ cd build

注意

你还需要安装 CMake。有关更多说明,请参阅cmake.org/download/

要使用 CMake 构建二进制文件,我们首先需要生成构建系统。我们指定cmake命令的源。然后,CMake 将构建树并为指定的源生成一个构建系统,使用CMakeLists.txt文件。

Linux 或 macOS

为了生成项目构建系统,我们运行带有wabt文件夹路径的cmake命令。cmake命令接受相对路径和绝对路径。我们在这里使用相对路径(..):

$ cmake ..

现在我们可以使用cmake build来构建项目。cmake build利用生成的项目二进制树来生成二进制文件:

Usage: cmake --build <dir> [options] [-- [native-options]]
Options:
  <dir> = Project binary directory to be built.
  --parallel [<jobs>], -j [<jobs>]
        = Build in parallel using the given number of jobs.
                   If <jobs> is omitted the native build
                   tool's
                   default number is used.
                   The CMAKE_BUILD_PARALLEL_LEVEL
                   environment variable
                   specifies a default parallel level when
                   this option
                   is not given.
  --target <tgt>..., -t <tgt>...
                 = Build <tgt> instead of default targets.
  --config <cfg> = For multi-configuration tools, choose
    <cfg>.
  --clean-first  = Build target 'clean' first, then build.
                   (To clean only, use --target 'clean'.)
  --verbose, -v  = Enable verbose output - if supported –
    including the build commands to be executed.
  --             = Pass remaining options to the native
    tool.

cmake build命令需要<dir>选项来生成二进制文件。cmake build命令接受前面代码块中列出的标志:

$ cmake --build .
....
[100%] Built target spectest-interp-copy-to-bin

Windows

安装 CMake 和 Visual Studio(>= 2015)。然后,在build文件夹内运行cmake

$ cmake [wabt project root] -DCMAKE_BUILD_TYPE=[config] –
  DCMAKE_INSTALL_PREFIX=[install directory] -G [generator]

[config]参数可以是DEBUGRELEASE

[安装目录]参数应该是您想要安装二进制文件的文件夹。

[generator]参数应该是您想要生成的项目类型,例如,Visual Studio 14 2015。您可以通过运行cmake –help来查看可用生成器的列表。

这将在指定的文件夹内构建和安装所有必需的可执行文件:

$ cd build
$ cmake --build .. --config RELEASE --target install

一旦成功安装了所有 WABT 工具,您可以将它们添加到您的路径中或从它们的路径中调用它们。

build文件夹包含以下二进制文件:

$ tree -L 1
├── dummy
├── hexfloat_test
├── spectest-interp
├── wabt-unittests
├── wasm-c-api-global
├── wasm-c-api-hello
├── wasm-c-api-hostref
├── wasm-c-api-memory
├── wasm-c-api-multi
├── wasm-c-api-reflect
├── wasm-c-api-serialize
├── wasm-c-api-start
├── wasm-c-api-table
├── wasm-c-api-threads
├── wasm-c-api-trap
├── wasm-decompile
├── wasm-interp
├── wasm-objdump
├── wasm-opcodecnt
├── wasm-strip
├── wasm-validate
├── wasm2c
├── wasm2wat
├── wast2json
├── wat-desugar
└── wat2wasm

这确实是一个庞大的二进制文件列表。让我们详细看看每个二进制文件能做什么:

  • wat2wasm – 这个工具帮助将 WAST 格式转换为WebAssembly 模块WASM)。

  • wat-desugar – 这个工具读取 WASM S 表达式文件并格式化它。

  • wast2json – 这个工具验证并将 WAST 格式转换为 JSON 格式。

  • wasm2wat – 这个工具将 WASM 转换为 WAST 格式。

  • wasm2c – 这个工具将 WASM 转换为 C 原生代码。

  • wasm-validate – 这个工具验证给定的 WebAssembly 是否按照规范构建。

  • wasm-strip – 正如我们在上一章中看到的,WASM 由多个部分组成。模块中的自定义部分仅用于关于模块及其生成过程中使用的工具的额外元信息。wasm-strip从 WASM 中移除自定义部分。

  • wasm-opcodecnt – 这个工具读取 WASM 并计算 WebAssembly 模块中指令操作码的使用情况。

  • wasm-objdump – 这个工具帮助打印关于 WASM 二进制文件的信息。它与 objdump (en.wikipedia.org/wiki/Objdump)类似,但用于 WebAssembly 模块。

  • wasm-interp – 这个工具使用基于堆栈的解释器解码并运行 WebAssembly 二进制文件。

  • wasm-decompile – 这个工具帮助将 WASM 二进制文件反编译成可读的类似 C 的语法。

以下是一些测试二进制文件:

我们已经构建了 WABT 并生成了工具。现在,让我们探索最重要和最有用的工具。

将 WAST 转换为 WASM

wat2wasm帮助将 WAST 格式转换为 WASM。让我们试一试:

  1. 创建一个名为wabt-playground的新文件夹并进入该文件夹:

    $ mkdir wabt-playground
    $ cd wabt-playground
    
  2. 创建一个名为add.wat.wat文件:

    $ touch add.wat
    
  3. 将以下内容添加到add.wat

    (module
    (func $add (param $lhs i32) (param $rhs i32) (result i32)
     get_local $lhs 
      get_local $rhs
        i32.add
    )
    )
    
  4. 使用wat2wasm二进制文件将 WAST 格式转换为 WASM:

    $ /path/to/build/directory/of/wabt/wat2wasm add.wat
    

这将在add.wasm文件中生成有效的 WebAssembly 二进制文件:

00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 017f 03
 02 01 00 0a 09 01 07 00 20 00 20 01 6a 0b

注意,生成的二进制文件大小为 32 字节。

  1. WABT 读取 WAST 格式文件(.wat)并将其转换为 WebAssembly 模块(.wasm)。wat2wasm首先验证给定的文件(.wat),然后将其转换为.wasm文件。要检查wat2wasm支持的选项,我们可以运行以下命令:

    $ /path/to/build/directory/of/wabt/wat2wasm --help
    usage: wat2wasm [options] filename
      read a file in the wasm text format, check it for
      errors, and
      convert it to the wasm binary format.
    examples:
      # parse and typecheck test.wat
      $ wat2wasm test.wat
      # parse test.wat and write to binary file test.wasm
      $ wat2wasm test.wat -o test.wasm
      # parse spec-test.wast, and write verbose output to
        stdout (including
      # the meaning of every byte)
      $ wat2wasm spec-test.wast -v
    options:
          --help              Print this help message
          --version           Print version information
      -v, --verbose       Use multiple times for more info
          --debug-parser      Turn on debugging the parser
                              of wat files
      ...
          --debug-names       Write debug names to the
                              generated binary file
          --no-check          Don't check for invalid
                              modules
    

如果我们需要以不同的名称生成 WASM 文件,可以使用 -o 选项和文件名。例如,wat2wasm add.wat -o add.wasm 将生成 add.wasm

  1. wat2wasm 还提供了详细的输出,清楚地解释了 WASM 的结构。为了查看 WASM 的结构,我们使用 -v 选项运行它:

     $ /path/to/build/directory/of/wabt/wat2wasm add.wat -v
    0000000: 0061 736d     ; WASM_BINARY_MAGIC
    0000004: 0100 0000     ; WASM_BINARY_VERSION
    ; section "Type" (1)
    0000008: 01            ; section code
    0000009: 00            ; section size (guess)
    000000a: 01            ; num types
    ; type 0
    000000b: 60            ; func
    000000c: 02            ; num params
    000000d: 7f            ; i32
    000000e: 7f            ; i32
    000000f: 01            ; num results
    0000010: 7f            ; i32
    0000009: 07            ; FIXUP section size
    ; section "Function" (3)
    0000011: 03            ; section code
    0000012: 00            ; section size (guess)
    0000013: 01            ; num functions
    0000014: 00            ; function 0 signature index
    0000012: 02            ; FIXUP section size
    ; section "Code" (10)
    0000015: 0a            ; section code
    0000016: 00            ; section size (guess)
    0000017: 01            ; num functions
    ; function body 0
    0000018: 00            ; func body size (guess)
    0000019: 00            ; local decl count
    000001a: 20            ; local.get
    000001b: 00            ; local index
    000001c: 20            ; local.get
    000001d: 01            ; local index
    000001e: 6a            ; i32.add
    000001f: 0b            ; end
    0000018: 07            ; FIXUP func body size
    0000016: 09            ; FIXUP section size
    

前面的输出是对生成的二进制的详细描述。最左边的七个数字是索引,后面跟着一个冒号。接下来的两个字符是实际的二进制代码,然后是注释。注释描述了二进制(操作)码的功能。

前两行指定 wasm_magic_header 及其版本。下一个部分是 Type 部分。Type 部分定义了部分 ID,后面跟着部分大小,然后是类型块的数量。在我们的例子中,我们只有一个类型。type 0 部分定义了 add 函数的类型签名。

然后,我们有 Function 部分。在 Function 部分,我们有部分 ID,后面跟着部分大小,然后是函数的数量。函数部分没有函数体。函数体在 Code 部分定义。

在生成二进制时,我们可以通过使用适当的 enable-*disable-* 选项分别启用新功能和禁用现有功能来让编译器包含新功能和禁用现有功能。

注意

你也可以在网上查看版本,在 webassembly.github.io/wabt/demo/wat2wasm/ 探索 WABT 工具。

我们已经将 WAST 转换成了 WASM。现在,让我们探索如何使用 wasm2wat 将 WASM 转换为 WAST。

将 WASM 转换为 WAST

有时,为了调试或理解,我们需要知道 WASM 正在做什么。WABT 有一个 wasm2wat 转换器。它可以帮助将 WASM 转换为 WAST 格式:

$ /path/to/build/directory/of/wabt/wasm2wat add.wasm
(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))

运行前面的命令将 add.wasm 转换回 WAST 格式,并在控制台打印输出。

如果你想将其保存为文件,可以使用 -o 标志:

$ /path/to/build/directory/of/wabt/wasm2wat add.wasm -o new_
  add.wat

此命令创建一个 new_add.wat 文件。

要检查 wasm2wat 支持的各种选项,我们可以运行以下命令:

$ wasm2wat --help
usage: wasm2wat [options] filename

  Read a file in the WebAssembly binary format, and convert 
  it to
  the WebAssembly text format.

examples:
  # parse binary file test.wasm and write text file test.wast
  $ wasm2wat test.wasm -o test.wat

  # parse test.wasm, write test.wat, but ignore the debug names, if any
  $ wasm2wat test.wasm --no-debug-names -o test.wat

options:
      --help                                Print this help 
        message
      --version                             Print version 
        information
  -v, --verbose                             Use multiple times 
   for more info
  -o, --output=FILENAME                     Output file for the 
   generated wast file, by default use stdout
  -f, --fold-exprs                          Write folded 
   expressions where possible
  ....
      --no-debug-names                      Ignore debug names 
        in the binary file
      --ignore-custom-section-errors        Ignore errors in 
        custom sections
      --generate-names                      Give auto-generated
        names to non-named functions, types, etc.
      --no-check                            Don't check for 
        invalid modules

注意

wasm2watwat2wasm 几乎有相同的选项。

使用 -v 选项运行前面的命令将打印 WAST 格式的 AST 语法:

$ /path/to/build/directory/of/wabt/wasm2wat add.wasm -o new_
  add.wat -v
BeginModule(version: 1)
  BeginTypeSection(7)
    OnTypeCount(1)
    OnType(index: 0, params: [i32, i32], results: [i32])
  EndTypeSection
  BeginFunctionSection(2)
    OnFunctionCount(1)
    OnFunction(index: 0, sig_index: 0)
  EndFunctionSection
  BeginCodeSection(9)
    OnFunctionBodyCount(1)
    BeginFunctionBody(0, size:7)
    OnLocalDeclCount(0)
    OnLocalGetExpr(index: 0)
    OnLocalGetExpr(index: 1)
    OnBinaryExpr("i32.add" (106))
    EndFunctionBody(0)
  EndCodeSection
  BeginCustomSection('name', size: 28)
    BeginNamesSection(28)
      OnNameSubsection(index: 0, type: function, size:6)
      OnFunctionNameSubsection(index:0, nametype:1, size:6)
      OnFunctionNamesCount(1)
      OnFunctionName(index: 0, name: "add")
      OnNameSubsection(index: 1, type: local, size:13)
      OnLocalNameSubsection(index:1, nametype:2, size:13)
      OnLocalNameFunctionCount(1)
      OnLocalNameLocalCount(index: 0, count: 2)
      OnLocalName(func_index: 0, local_index: 0, name: "lhs")
      OnLocalName(func_index: 0, local_index: 1, name: "rhs")
    EndNamesSection
  EndCustomSection
EndModule

整个代码块被包裹在 BeginModuleEndModule 之间。BeginModule 包含 WebAssembly 二进制的版本。

BeginModule 内部,BeginTypeSection 从类型的部分索引(即 7)开始,后面跟着 OnTypeCount,定义的类型数量。然后,我们有 OnType 的实际类型定义。我们通过 EndTypeSection 结束类型部分。

然后,我们有由 BeginFunctionSectionEndFunctionSection 标记的 Function 部分。这部分包含函数计数(OnFunctionCount)和函数定义(OnFunction)。

最后,我们有代码部分,它包含函数的实际主体。代码部分以BeginCodeSection开始,以EndCodeSection结束。

有时,WASM 可能包含调试名称。我们可以使用--no-debug-names标志来忽略它们:

 $ /path/to/build/directory/of/wabt/wasm2wat add.wasm -o new_
  add.wat -v --no-debug-names
BeginModule(version: 1)
  BeginTypeSection(7)
    OnTypeCount(1)
    OnType(index: 0, params: [i32, i32], results: [i32])
  EndTypeSection
  BeginFunctionSection(2)
    OnFunctionCount(1)
    OnFunction(index: 0, sig_index: 0)
  EndFunctionSection
  BeginCodeSection(9)
    OnFunctionBodyCount(1)
    BeginFunctionBody(0, size:7)
    OnLocalDeclCount(0)
    OnLocalGetExpr(index: 0)
    OnLocalGetExpr(index: 1)
    OnBinaryExpr("i32.add" (106))
    EndFunctionBody(0)
  EndCodeSection
  BeginCustomSection('name', size: 28)
  EndCustomSection
EndModule

注意BeginCustomSectionEndCustomSection。与之前的输出比较,它缺少NamesSection。现在,让我们查看wasm2wat工具提供的各种选项。

-f 或--fold-exprs

作为功能编程的大粉丝,这是可用的最酷的选项之一。它折叠表达式;也就是说,它将表达式 1 >> 表达式 2 >> 操作转换为操作 >> 表达式 1 >> 表达式 2。

让我们看看实际操作:

  1. 创建一个名为fold.wat的 WAST 文件,并填充以下内容:

    (module
        (func $fold (result i32)
            i32.const 22
            i32.const 20
            i32.add
        )
    )
    
  2. 首先,使用wat2wasm将其转换为 WASM:

    $ /path/to/build/directory/of/wabt/wat2wasm -v
      fold.wat
    ; some contents
    0000018: 41                                        ;
      i32.const
    0000019: 16                                        ;
      i32 literal
    000001a: 41                                        ;
      i32.const
    000001b: 14                                        ;
      i32 literal
    000001c: 6a                                        ;
      i32.add
    ; other contents
    

这将创建fold.wasm

  1. 现在,使用wasm2wat将 WASM 转换为 WAST 格式,并传递-f选项:

    $ /path/to/build/directory/of/wabt/wasm2wat -v
      fold.wasm -o converted_fold.wat -f
    

这将创建一个名为converted_fold.wat的文件:

(module
    (type (;0;) (func (result i32)))
    (func (;0;) (type 0) (result i32)
        (i32.add
            (i32.const 1)
            (i32.const 2))))

而不是使用i32.const 1(表达式 1)和i32.const 2(表达式 2)然后执行i32.add(操作),这生成一个输出,i32.add(操作),然后是i32.const 1(表达式 1)和i32.const 2(表达式 2)。

在生成wat时,我们可以通过使用适当的enable-*disable-*选项来启用编译器包括新的和闪亮的功能,并禁用各种现有功能。

我们已将 WASM 转换为 WAST。现在,让我们探索如何使用wasm2c将 WASM 转换为本地代码(C)。

将 WASM 转换为 C

WABT 有一个wasm2c转换器,可以将 WASM 转换为 C 源代码和头文件。

让我们创建一个simple.wat文件:

$ touch simple.wat

将以下内容添加到simple.wat中:

(module
    (func $uanswer (result i32)
        i32.const 22
        i32.const 20
        i32.add
    )
)

wat在这里定义了一个uanswer函数,将2220相加得到42作为答案。让我们使用wat2wasm创建一个 WebAssembly 二进制文件:

$ /path/to/build/directory/of/wabt/wat2wasm simple.wat -o 
  simple.wasm

这生成了simple.wasm二进制文件。现在,使用wasm2c将二进制文件转换为 C 代码:

$ /path/to/build/directory/of/wabt/wasm2c simple.wasm -o 
  simple.c

这生成了simple.csimple.h

注意

simple.csimple.h可能看起来很大。记住这是一个自动生成的文件,它包含了程序运行所需的所有必要的头文件和配置。

simple.h

simple.h(头文件)包括头文件的标准化样板代码。它还包括_cplusplus条件,以防止 C++中的名称修饰:

#ifndef SIMPLE_H_GENERATED_
#define SIMPLE_H_GENERATED_
#ifdef __cplusplus
extern "C" {
#endif

...

#ifdef __cplusplus
}
#endif
#endif

由于我们使用了i32.consti32.add,头文件也导入了stdint.h。它包括wasm-rt.hwasm-rt.h头文件导入了必要的 WASM 运行时变量。

接下来,我们可以指定一个模块前缀。当使用多个模块时,模块前缀很有用。由于我们只有一个模块,我们使用一个空前缀:


#ifndef WASM_RT_MODULE_PREFIX
#define WASM_RT_MODULE_PREFIX
#endif

#define WASM_RT_PASTE_(x, y) x ## y
#define WASM_RT_PASTE(x, y) WASM_RT_PASTE_(x, y)
#define WASM_RT_ADD_PREFIX(x) WASM_RT_PASTE(WASM_RT_MODULE_PREFIX, x)

接下来,我们有一些针对 WASM 支持的多种数字格式的 typedef:

typedef uint8_t u8;
typedef int8_t s8;
typedef uint16_t u16;
typedef int16_t s16;
typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;
typedef float f32;
typedef double f64;

simple.c

simple.c提供了从 WASM 二进制生成的实际 C 代码。生成的代码具有以下内容:

  1. 我们将需要以下库列表在代码中使用:

    #include <math.h>
    #include <string.h>
    
    #include "simple.h"
    
  2. 接下来,我们定义当发生错误时调用的陷阱:

    #define TRAP(x) (wasm_rt_trap(WASM_RT_TRAP_##x), 0)
    
  3. 然后,我们定义 PROLOGUEEPILOGUEUNREACHABLE_TRAP,分别是在执行开始前、执行后以及执行遇到不可达异常时调用的:

    #define FUNC_PROLOGUE                                            \
      if (++wasm_rt_call_stack_depth >
        WASM_RT_MAX_CALL_STACK_DEPTH) \
        TRAP(EXHAUSTION)
    
    #define FUNC_EPILOGUE --wasm_rt_call_stack_depth
    
    #define UNREACHABLE TRAP(UNREACHABLE)
    

WASM_RT_MAX_CALL_STACK_DEPTH 是堆栈的最大深度。默认值为 500,但我们可以更改它。注意,如果达到限制,则会抛出异常。

  1. 接下来,我们定义内存操作:

        addr) {   \
        addr, t2 value) { \
        MEMCHECK(mem, addr, t1);       
    \
        t1 wrapped = (t1)value;      
    \
        __builtin_memcpy(&mem->data[addr], &wrapped,
          sizeof(t1));          \
      }
    

MEMCHECK 检查内存。DEFINE_LOADDEFINE_STORE 块定义了如何在内存中加载和存储值。

  1. 接下来,我们为示例中具有的各种数据类型定义了一组加载和存储操作:

    DEFINE_LOAD(i32_load, u32, u32, u32);
    DEFINE_LOAD(i64_load, u64, u64, u64);
    ...
    DEFINE_LOAD(i64_load32_u, u32, u64, u64);
    DEFINE_STORE(i32_store, u32, u32);
    DEFINE_STORE(i64_store, u64, u64);
    ...
    DEFINE_STORE(i64_store32, u32, u64);
    

然后,我们定义各种数据类型支持的各种函数,例如 TRUNCDIVREM

  1. 接下来,我们使用 func_types 初始化函数类型:

    static u32 func_types[1];
    
    static void init_func_types(void) {
      func_types[0] = wasm_rt_register_func_type(0, 1,
        WASM_RT_I32);
    }
    

这在 WASM 中注册了(结果 i32)类型。

  1. 接下来,我们初始化 globalsmemorytableexports

    static void init_globals(void) {
    }
    
    static void init_memory(void) {
    }
    
    static void init_table(void) {
      uint32_t offset;
    }
    
    static void init_exports(void) {
    }
    
  2. 现在,我们实现实际的功能:

    static u32 w2c_f0(void) {
      FUNC_PROLOGUE;
      u32 w2c_i0, w2c_i1;
      w2c_i0 = 22u;
      w2c_i1 = 20u;
      w2c_i0 += w2c_i1;
      FUNC_EPILOGUE;
      return w2c_i0;
    }
    

函数是静态的。它在执行前调用 FUNC_PROLOGUE。然后,它创建两个变量(都是无符号 u32)。然后,我们定义两个变量的值,分别是 2220。之后,我们将它们相加。一旦执行完成,我们调用 FUNC_EPILOGUE。最后,我们返回值。

注意

由于我们在 wat 中没有导出任何内容,因此 init_exports 是空的。

我们已经将 WASM 转换为 C。生成的代码与原始代码略有不同。让我们探索如何使用 wast2json 将 WAST 转换为 JSON。

将 WAST 转换为 JSON

wast2json 工具读取 WAST 格式并解析它,检查错误,然后将 WAST 转换为 JSON 文件。它为 WAST 文件生成一个 JSON 和 WASM 文件。然后,它将 JSON 中的 WASM 链接起来:

$ /path/to/build/directory/of/wabt/wast2json add.wat -o add.
  json
$ cat add.json
{"source_filename": "add.wat",
"commands": [
{"type": "module", "line": 1, "filename": "add.0.wasm"}]}

要检查 wast2json 支持的各种选项,请运行以下命令:

$ /path/to/build/directory/of/wabt/wast2json --help
usage: wast2json [options] filename

  read a file in the wasm spec test format, check it for 
  errors, and
  convert it to a JSON file and associated wasm binary files.

examples:
  # parse spec-test.wast, and write files to spec-test.json. 
  Modules are
  # written to spec-test.0.wasm, spec-test.1.wasm, etc.
  $ wast2json spec-test.wast -o spec-test.json

options:
      --help                                Print this help 
        message
      --version                             Print version 
        information
  -v, --verbose                             Use multiple times 
   for more info
      --debug-parser                        Turn on debugging 
        the parser of wast files
      --enable-exceptions                   Enable Experimental 
        exception handling
      --disable-mutable-globals             Disable Import/
        export mutable globals
      --disable-saturating-float-to-int     Disable Saturating
        float-to-int operators
      --disable-sign-extension              Disable Sign-
        extension operators
      --enable-simd                         Enable SIMD support
      --enable-threads                      Enable Threading 
        support
      --disable-multi-value                 Disable Multi-value
      --enable-tail-call                    Enable Tail-call 
        support
      --enable-bulk-memory                  Enable Bulk-memory 
        operations
      --enable-reference-types              Enable Reference 
        types (externref)
      --enable-annotations                  Enable Custom 
        annotation syntax
      --enable-gc                           Enable Garbage 
        collection
      --enable-memory64                     Enable 64-bit 
        memory
      --enable-all                          Enable all features
  -o, --output=FILE                         output JSON file
  -r, --relocatable                         Create a 
   relocatable wasm binary (suitable for linking with e.g. lld)
      --no-canonicalize-leb128s             Write all LEB128 
        sizes as 5-bytes instead of their minimal size
      --debug-names                         Write debug names 
        to the generated binary file
      --no-check                            Don't check for 
        invalid modules

这些是常用的 WABT 工具。WABT 还提供了一些其他工具,有助于调试和更好地理解 WASM。

理解 WABT 提供的几个其他工具

除了转换器之外,WABT 还提供了一些工具,帮助我们更好地理解 WASM。在本节中,让我们探索 WABT 提供的以下工具:

  • wasm-objdump

  • wasm-strip

  • wasm-validate

  • wasm-interp

wasm-objdump

目标代码不过是计算机语言中的一系列指令或语句。目标代码是编译器产生的。目标代码随后收集在一起,并存储在目标文件中。目标文件是链接和调试信息的元数据持有者。目标文件中的机器代码不能直接执行,但在调试时提供有价值的信息,并有助于链接。

注意

objdump 是 POSIX 系统中可用的工具,它提供了一种解汇编二进制格式并打印运行代码汇编格式的方法。

wasm-objdump 工具提供以下选项:

$ /path/to/build/directory/of/wabt/wasm-objdump --help
usage: wasm-objdump [options] filename+

  Print information about the contents of wasm binaries.

examples:
  $ wasm-objdump test.wasm

options:
      --help                   Print this help message
      --version                Print version information
  -h, --headers                Print headers
  -j, --section=SECTION        Select just one section
  -s, --full-contents          Print raw section contents
  -d, --disassemble            Disassemble function bodies
      --debug                  Print extra debug information
  -x, --details                Show section details
  -r, --reloc                  Show relocations inline with 
   disassembly

至少应提供以下选项之一给 wasm-objdump 命令:

-d/--disassemble
-h/--headers
-x/--details
-s/--full-contents

-h 选项打印 WASM 中所有可用的头信息。例如,在我们的 add 示例(add.wasm)中,我们有以下内容:

$ /path/to/build/directory/of/wabt/wasm-objdump add.wasm -h
add.wasm: file format wasm 0x1

Sections:

     Type start=0x0000000a end=0x00000011 (size=0x00000007)
  count: 1
Function start=0x00000013 end=0x00000015 (size=0x00000002) 
  count: 1
     Code start=0x00000017 end=0x00000020 (size=0x00000009) 
  count: 1

这里,我们有三个部分可用:

  • Type

  • Function

  • Code

-d 选项打印函数的实际主体:


$/path/to/build/directory/of/wabt/wasm-objdump add.wasm -d
add.wasm: file format wasm 0x1

Code Disassembly:

000019 func[0]:
00001a: 20 00 | local.get 0
00001c: 20 01 | local.get 1
00001e: 6a | i32.add
00001f: 0b | end

它反汇编汇编函数并仅打印函数主体。

-x 选项打印 WebAssembly 二进制文件的段详细信息:

$ /path/to/build/directory/of/wabt/wasm-objdump add.wasm -x

add.wasm: file format wasm 0x1

Section Details:

Type[1]:
- type[0] (i32, i32) -> i32
Function[1]:
- func[0] sig=0
Code[1]:
- func[0] size=7

-s 选项打印所有可用的段的内容:

$ /path/to/build/directory/of/wabt/wasm-objdump add.wasm -s

add.wasm: file format wasm 0x1

Contents of section Type:
000000a: 0160 027f 7f01 7f .`.....

Contents of section Function:
0000013: 0100 ..

Contents of section Code:
0000017: 0107 0020 0020 016a 0b ... . .j.

wasm-strip

WASM 中的自定义部分包含有关函数和所有在 WASM 中定义的局部变量的名称的信息。它可能包含有关构建和 WASM 如何创建的信息。这是附加信息。它会使二进制文件膨胀,但不会添加任何功能。

我们可以使用 wasm-strip 工具删除自定义部分以减小二进制文件大小:

  1. 创建一个包含以下内容的 wat 文件:

    $ touch simple.wat
    (module
        (func $fold (result i32)
            i32.const 22
            i32.const 20
            i32.add
        )
    )
    
  2. 现在,使用 wat2wasm 将其转换为 WASM:

    $ /path/to/build/directory/of/wabt/wat2wasm simple.wat
      --debug-names
    $ l simple.wasm
    51B simple.wasm
    

注意

--debug-names 选项提供的生成自定义部分并将其添加到生成的二进制文件中。

之前的命令生成了一个 simple.wasm 文件,其大小为 51 字节。

  1. 现在,让我们使用以下命令从二进制文件中移除自定义部分:

    $ /path/to/build/directory/of/wabt/wasm-strip add.wasm
    $ l simple.wasm
    30B simple.wasm
    

如您所见,它删除了 21 字节的不必要信息。一些 WASM 生成器添加自定义部分以获得更好的调试体验,但在生产部署时,我们不需要自定义部分。使用 wasm-strip 来删除它。

wasm-validate

我们可以使用 wasm-validate 验证 WASM:

  1. 使用以下内容创建 error.wasm:

    00 61 73 6d 03 00 00 00
                |
            Error value
    
  2. 现在,运行 wasm-validate 检查 WASM 是否有效:

    $ /path/to/build/directory/of/wabt/wasm-validate
      error.wasm
    0000004: error: bad magic value
    
  3. wasm-validate 工具提供以下选项:

    usage: wasm-validate [options] filename
    
  4. 读取 WebAssembly 二进制格式的文件并验证它:

    examples:
      # validate binary file test.wasm
      $ wasm-validate test.wasm
    
    options:
          --help                Print this help message
          --version             Print version information
    -v, --verbose             Use multiple times for 
       more info
          --enable-exceptions   Enable Experimental
            exception handling
          --disable-mutable-globals     Disable
            Import/export mutable globals
          --disable-saturating-float-to-int        Disable
            Saturating float-to-int operators
          --disable-sign-extension                 Disable
            Sign-extension operators
          --enable-simd                            Enable
            SIMD support
          --enable-threads                         Enable
            Threading support
          --disable-multi-value                    Disable
            Multi-value
          --enable-tail-call                       Enable
            Tail-call support
          --enable-bulk-memory                     Enable
            Bulk-memory operations
          --enable-reference-types                 Enable
            Reference types (externref)
          --enable-annotations                     Enable
            Custom annotation syntax
          --enable-gc                              Enable
            Garbage collection
          --enable-memory64                        Enable
            64-bit memory
          --enable-all                             Enable
            all features
          --no-debug-names                         Ignore
            debug names in the binary file
          --ignore-custom-section-errors           Ignore
            errors in custom sections
    

wasm-interp

wasm-interp 读取 WebAssembly 二进制格式的文件并在基于堆栈的解释器中运行它。wasm-interp 工具解析二进制文件,然后进行类型检查:

$ /path/to/build/directory/of/wabt/wasm-interp add.wasm -v
BeginModule(version: 1)
  BeginTypeSection(7)
    OnTypeCount(1)
    OnType(index: 0, params: [i32, i32], results: [i32])
  EndTypeSection
  BeginFunctionSection(2)
    OnFunctionCount(1)
    OnFunction(index: 0, sig_index: 0)
  EndFunctionSection
  BeginCodeSection(9)
    OnFunctionBodyCount(1)
    BeginFunctionBody(0, size:7)
    OnLocalDeclCount(0)
    OnLocalGetExpr(index: 0)
    OnLocalGetExpr(index: 1)
    OnBinaryExpr("i32.add" (106))
    EndFunctionBody(0)
  EndCodeSection
EndModule
   0| local.get $2
   8| local.get $2
  16| i32.add %[-2], %[-1]
  20| drop_keep $2 $1
  32| return

最后五行是堆栈解释器执行代码的方式。

wasm-interp 工具提供以下选项:


usage: wasm-interp [options] filename [arg]...

  read a file in the wasm binary format and run it in a stack-
  based
  interpreter.

examples:
  ...

options:
      --help                                Print this help 
        message
      --version                             Print version 
        information
  ...

WABT 提供了一系列工具,使 WASM 更易于理解、调试和转换为各种可读格式。它是允许开发者更好地探索 WASM 的最重要的工具包之一。

摘要

在本章中,我们了解了 WABT 是什么以及如何安装和使用它提供的各种工具。WABT 工具在 WebAssembly 生态系统中非常重要,因为它提供了一个将不可读的紧凑二进制文件转换为可读的展开源代码的简单选项。

在下一章中,我们将探索 WASM 内部的各种部分。

第五章:第五章:理解 WebAssembly 模块中的部分

WebAssembly 模块由零个或多个部分组成。每个部分都有其自身的功能。在前几章中,我们看到了如何在 WebAssembly 模块内部定义函数。函数是 WebAssembly 模块内部的一个部分。

在本章中,我们将探讨 WebAssembly 模块内部的各个其他部分。了解 WebAssembly 模块内部的各个部分将使我们更容易识别、调试和编写高效的 WebAssembly 模块。在本章中,我们将涵盖以下部分:

  • 导出和导入

  • 全局变量

  • 开始

  • 内存

技术要求

你可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Practical-WebAssembly/tree/main/05-wasm-sections

导出和导入

WebAssembly 模块由导出和导入部分组成。这些部分负责将函数导出和导入到 WebAssembly 模块中。

导出

为了从 JavaScript 中调用 WebAssembly 模块中定义的函数,我们需要从 WebAssembly 模块中导出这些函数。导出部分是我们将定义所有从 WebAssembly 模块导出的函数的地方。

让我们回到前一章中的经典add.wat示例:

; add.wat
(module
    (func $add (param $lhs i32) (param $rhs i32) 
      (result i32)
        get_local $lhs
        get_local $rhs
        i32.add)
    (export "add" (func $add))
)

这里,我们使用(export "add" (func $add))语句导出了add函数。为了导出一个函数,我们使用了export关键字,后跟函数名称,然后是导出函数本身的指针。

记住 WebAssembly 是紧凑型的。因此,我们可以将导出语句及其本身的函数定义一起表示,如下所示:

; add.wat
(module
    (func $add (export "add") (param $lhs i32) 
      (param $rhs i32) (result i32)
        get_local $lhs
        get_local $rhs
        i32.add)
)

让我们使用 WABT 的wat2wasm工具,通过以下命令将 WebAssembly 文本格式转换为 WebAssembly 模块:

$ /path/to/wabt/bin/wat2wasm add.wat

让我们使用hexdump工具分析生成的字节码:

$ hexdump add.wasm
0000000 00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 01
0000010 7f 03 02 01 00 07 07 01 03 61 64 64 00 00 0a 09
0000020 01 07 00 20 00 20 01 6a 0b
0000029

如预期,第一个字节是二进制文件的魔术头和版本00 61 73 6d 01 00 00 00

0000000: 0061 736d                   ; WASM_BINARY_MAGIC
0000004: 0100 0000                   ; WASM_BINARY_VERSION

接下来的位是01,代表类型部分的索引。随后,我们有类型部分的大小,为07。接下来的七个位是类型部分。01代表可用的类型定义数量:

; section "Type" (1)
0000008: 01                          ; section code
0000009: 07                          ; section size
000000a: 01                          ; num types

然后,我们有60,代表func。随后,我们有02,代表两个参数。7f是定义 i32 类型的操作码。由于两个参数都是 i32 类型,所以我们有连续的7f操作码。随后,最后两个位代表返回类型,还有一个7f代表 i32:

; type 0
000000b: 60                          ; func
000000c: 02                          ; num params
000000d: 7f                          ; i32
000000e: 7f                          ; i32
000000f: 01                          ; num results
0000010: 7f                          ; i32

type 部分之后,我们有 func 部分。func 部分的唯一标识符是 03。随后是 02,它定义了函数部分的大小。这意味着函数部分的大小仅为 2 位。但我们在 WebAssembly 文本格式中定义了 add 的函数定义,而函数的大小超过 2 位。那么这是怎么可能的呢?原因是函数部分没有函数体;相反,它只是定义了可用的函数。函数定义在代码部分。下一个 01 定义了模块中定义了一个函数:

; section "Function" (3)
0000011: 03                          ; section code
0000012: 02                          ; section size
0000013: 01                          ; num functions
0000014: 00                          ; function 0 signature
  index

然后,我们有导出部分,它以 07 开头。下一个 07 代表导出部分的大小。然后,我们定义导出部分中导出的数量。下一个位表示导出函数名的长度。接下来的 03 位表示函数名,add。然后,导出部分有导出类型和导出函数的索引:

; section "Export" (7)
0000015: 07                          ; section code
0000016: 07                          ; section size
0000017: 01                          ; num exports
0000018: 03                          ; string length
0000019: 6164 64                add  ; export name
000001c: 00                          ; export kind
000001d: 00                          ; export func index

最后一段以 0a 开头。0a 是代码部分的唯一标识符。代码部分的长度为 09。接下来,01 代表代码块中定义的函数数量。

接下来,07 代表函数定义的长度。接下来的七个位实际上定义了函数块。00 表示函数块没有任何局部声明。20get_local 的操作码,我们使用 00 索引,然后再次使用 20 操作码来 get_local 并使用 01 索引。然后,我们使用 i32.add 将它们相加。i32 加法的操作码是 6a。最后,我们使用 0b 来结束函数代码块:

; section "Code" (10)
000001e: 0a                          ; section code
000001f: 09                          ; section size
0000020: 01                          ; num functions
  ; function body 0
0000021: 07                          ; func body size

0000022: 00                          ; local decl count
0000023: 20                          ; local.get
0000024: 00                          ; local index
0000025: 20                          ; local.get
0000026: 01                          ; local index
0000027: 6a                          ; i32.add
0000028: 0b                          ; end

我们已经看到了导出部分在 WebAssembly 模块中的表示。在下一节中,让我们看看导入部分在 WebAssembly 模块中的表示。

导入

为了从另一个 WebAssembly 模块或 JavaScript 模块导入一个函数,我们需要在 WebAssembly 模块中导入这些函数。导入部分是我们将所有外部依赖项导入 WebAssembly 模块的地方。

现在,让我们想象一个名为 jsAdd 的 JavaScript 模块导出了一个函数。我们可以使用 import 关键字导入 jsAdd 函数。创建一个名为 jsAdd.wat 的文件,并向其中添加以下内容:

(module
    (func $i (import "imports" "jsAdd") (param i32))
)

在这里,我们使用 func 关键字定义了一个函数,后面跟着函数的名称,$i。我们使用 $i 在 WebAssembly 模块内部调用该函数。然后,我们有了 import 关键字。import 关键字后面跟着模块名称。这里的模块名称指的是 JavaScript 模块,然后是我们要从 JavaScript 模块中导入的函数名称。

最后,我们有 param。由于 WebAssembly 模块是强类型的,我们必须在函数定义中定义输入参数和返回类型。

让我们使用 WABT 的 wat2wasm 将 WebAssembly 文本格式转换为以下命令的 WebAssembly 模块:

$ /path/to/wabt/bin/wat2wasm jsAdd.wat

让我们使用 hexdump 工具分析生成的字节码:

$ hexdump jsAdd.wasm
0000000 00 61 73 6d 01 00 00 00 01 05 01 60 01 7f 00 02
0000010 11 01 07 69 6d 70 6f 72 74 73 05 6a 73 41 64 64
0000020 00 00
0000022

二进制文件由导入部分组成,从索引 16 开始。导入部分以 02 开始,因为导入部分的唯一部分索引是 02。之后,我们有 11,表示二进制中导入部分的大小。下一个位表示导入的数量,01

然后,我们有导入的定义。07 这里表示导入函数的长度。接下来的七个位表示导入模块的名称。下一个位表示函数名称的长度,05,接下来的五个位表示函数名称。最后,我们有索引的类型和签名:

; Other information
; section "Import" (2)
000000f: 02                          ; section code
0000010: 11                          ; section size
0000011: 01                          ; num imports
; import header 0
0000012: 07                          ; string length
0000013: 696d 706f 7274 73           imports  ; import
  module name
000001a: 05                          ; string length
000001b: 6a73 4164 64         jsAdd  ; import field name
0000020: 00                          ; import kind
0000021: 00                          ; import signature
  index

现在,你可以像在 WebAssembly 模块内部的其他函数一样使用 $i 标识符调用 jsAdd 函数。

我们已经探讨了如何在 WebAssembly 模块内部定义导入和导出部分,以及它们如何帮助导入和导出函数。现在,让我们探讨如何导入和导出 WebAssembly 模块中的值。

全局变量

全局部分是我们可以在 WebAssembly 模块中导入和导出值的地方。在 WebAssembly 模块中,你可以从 JavaScript 导入可变或不可变值。此外,WebAssembly 还支持 wasmValue,这是 WebAssembly 模块内部的内部不可变值。

让我们创建一个名为 globals.wat 的文件,并将以下内容添加到其中:

$ touch globals.wat
(module
     (global $mutableValue (import "js" "mutableGlobal")
       (mut i32))
     (global $immutableValue (import "js"
       "immutableGlobal") i32)
     (global $wasmValue i32 (i32.const 10))
     (func (export "getWasmValue") (result i32)
        (global.get $wasmValue))
     (func (export "getMutableValue") (result i32)
        (global.get $mutableValue))
     (func (export "getImmutableValue") (result i32)
        (global.get $immutableValue))
     (func (export "setMutableValue") (param $v i32)
        (global.set $mutableValue
            (local.get $v)))
)

我们创建了一个模块(module)和三个全局变量:

  • $mutableValue – 这个值是从 js JavaScript 模块和 mutableGlobal 全局变量导入的。我们还定义全局变量为 mut i32 类型。

  • $immutableValue – 这个值是从 js JavaScript 模块和 immutableGlobal 全局变量导入的。我们还定义全局变量为 i32 类型。

  • $wasmValue – 这是一个全局常量。我们定义 global 关键字后跟全局变量的名称 $wasmValue,然后是 i32 类型,最后是实际值(i32.const 10)。

    注意

    $wasmValue 是不可变的,不能导出到外部世界。

然后,我们有一组帮助获取和设置全局变量的函数。getWasmValuegetImmutableValuegetMutableValue 分别获取 wasmValue 全局常量、immutableValue 全局常量和 mutableValue 全局变量的值。

最后,一个将 mutableValue 设置为新值的函数是 setMutableValuesetMutableValue 接收 param $v 参数,将值设置为 $mutableValue

使用 WABT 将 WebAssembly 文本格式转换为 WebAssembly 模块,可以使用以下命令:

$ /path/to/wabt/bin/wat2wasm globals.wat

创建一个包含以下内容的 globals.html 文件:

// globals.html
<html>
    <head> </head>
    <body>
        <script>
            async function run() {  }
            run()
        </script>
    </body>
</html>

让我们在 <script> 标签内定义 run 函数。

WebAssembly.Global对象代表一个全局变量实例,可以从 JavaScript 访问,并在一个或多个WebAssembly.Module实例之间导入/导出。WebAssembly.Global构造函数期望一个描述符和值。描述符定义了全局变量的类型和可变性:

注意

这个全局变量构造函数提供了一个选项,可以动态链接多个 WebAssembly 模块。

let immutableGlobal = new WebAssembly.Global({value:'i32',
  mutable:false}, 1000)
let mutableGlobal = new WebAssembly.Global({value:'i32',
  mutable:true}, 0)

我们使用WebAssembly.Global构造函数创建两个全局值。它们是immutableGlobalmutableGlobal。前者是mutable:false,而后者是mutable:true。因此,我们可以使用mutableGlobal.value更改后者的值,但不能更改前者。如果我们尝试更改immutableGlobal的值,我们将收到一个错误:

mutableGlobal.value = 1337  // valid.
immutableGlobal.value = 7331 // Error

之后,我们获取globals.wasm WebAssembly 模块。然后,我们使用响应和arrayBuffer以及WebAssembly.instantiate构造函数实例化arrayBuffer。此外,WebAssembly.instantiate构造函数接受importsObject。我们可以通过importsObject发送 JavaScript 模块:

const response = await fetch('./globals.wasm')
const bytes = await response.arrayBuffer()
const wasm = await WebAssembly.instantiate(bytes, { js: {
  mutableGlobal, immutableGlobal } })

在这种情况下,我们发送了js模块以及mutableGlobalimmutableGlobal值。wasm变量现在持有 WebAssembly 模块。我们调用wasm.instance.exports以获取 WebAssembly 模块中所有导出的函数:

const {
    getWasmValue,
    getMutableValue,
    setMutableValue,
    getImmutableValue
} =  wasm.instance.exports

getWasmValuegetMutableValuesetMutableValuegetImmutableValue是 WebAssembly 模块导出的函数。

getWasmValue函数返回 WebAssembly 模块内wasmValue的值:

console.log(getWasmValue()) // 10

getMutableValuesetMutableValue函数返回并设置在 JavaScript 中定义并传递到 WebAssembly 模块中的mutableGlobal字段:

console.log(getMutableValue()) // 1337
setMutableValue(1338)
console.log(getMutableValue()) // 1338

最后,我们使用getImmutableValue函数获取不可变值:

console.log(getImmutableValue()) // 1000

让我们在浏览器中使用以下命令运行一个示例:

$ python -m http.server

现在,启动 URL http://localhost:8000/globals.html并打开开发者工具。

WebAssembly 二进制包含一个导入部分。导入部分有一个唯一的标识符02,后面是部分的长度,为2b(十进制为 43)。接下来的 43 位代表导入部分。

000015索引处的02代表导入的数量。然后,我们有两个部分定义了导入的全局函数:


; section "Import" (2)
0000013: 02                          ; section code
0000014: 2b                          ; section size
0000015: 02                          ; num imports

每个全局段由模块字符串长度和模块名称组成,接着是函数字符串长度和函数名称。最后,它包含导入的类型、变量类型和可变性:


; import header 0
0000016: 02                          ; string length
0000017: 6a73                    js  ; import module name
0000019: 0d                          ; string length
000001a: 6d75 7461 626c 6547 6c6f 6261 6c
         mutableGlobal  ; import field name
0000027: 03                          ; import kind
0000028: 7f                          ; i32
0000029: 01                          ; global mutability
  ; import header 1
000002a: 02                          ; string length
000002b: 6a73                    js  ; import module name
000002d: 0f                          ; string length
000002e: 696d 6d75 7461 626c 6547 6c6f 6261 6c
    immutableGlobal  ; import field name
000003d: 03                          ; import kind
000003e: 7f                          ; i32
000003f: 00                          ; global mutability

之后,我们有Global部分。Global部分具有唯一的部分 ID 6。下一个位定义了Global部分的大小,为06

之后,我们有可用的全局变量数量。全局变量的数量是01。这是因为其他两个全局变量被导入。类型、可变性和值是接下来的 4 个字节:

; section "Global" (6)
0000047: 06                          ; section code
0000048: 06                          ; section size
0000049: 01                          ; num globals
000004a: 7f                          ; i32
000004b: 00                          ; global mutability
000004c: 41                          ; i32.const
000004d: 0a                          ; i32 literal
000004e: 0b                          ; end

代码部分内的第一个function体如下所示:

; function body 0
000009c: 04                          ; func body size
000009d: 00                          ; local decl count
000009e: 23                          ; global.get
000009f: 02                          ; global index
00000a0: 0b                          ; end

function 的长度为四位。前两位 00 表示该函数没有局部声明。接下来的 23 是获取全局值的操作码。接下来的 02 定义了全局值的索引。尽管前面的全局部分指定只有一个全局值,但整个模块会考虑导入的全局值。由于有两个导入的全局值,我们在导入的全局值之后索引局部全局值。因此,$wasmValue 全局值的索引为 3。最后,我们使用 0b 操作码结束函数代码。同样,第二个和第三个函数体定义了如何获取其他两个导入的全局值:

; function body 3
00000ab: 06                          ; func body size
00000ac: 00                          ; local decl count
00000ad: 20                          ; local.get
00000ae: 00                          ; local index
00000af: 24                          ; global.set
00000b0: 00                          ; global index
00000b1: 0b                          ; end

在函数体 4 中,我们使用 global.set 设置全局值,其操作码为 24

我们已经探讨了如何在 WebAssembly 模块中导入和导出值。现在,让我们探索 WebAssembly 模块中的特殊 start 函数。

起始

Start 是一个特殊函数,它在 WebAssembly 模块初始化后运行。让我们使用与全局变量相同的示例。我们将以下内容添加到 globals.wat 文件中:

(module
    ; Code is elided
    (func $initMutableValue
          (global.set $mutableValue
               (i32.const 200))) 
     (start $initMutableValue)
)

我们定义了 initMutableValue 函数,该函数将 mutableValue 设置为 200。之后,我们添加一个起始块,该块以 startkeyword 开头,后跟函数的名称。

注意

在起始处引用的函数不应返回任何值。

让我们使用 WABT 将 WebAssembly 文本格式转换为 WebAssembly 模块,以下命令:

$ /path/to/wabt/bin/wat2wasm globals.wat

使用以下命令在浏览器中运行示例:

$ python -m http.server

现在,启动 URL http://localhost:8000/globals.html 并打开开发者工具。

起始函数与其他函数类似,但它们没有被分类到任何类型中。类型可能在函数初始化时初始化,也可能不初始化。WebAssembly 模块的起始部分指向一个函数索引(函数部分在函数组件内的位置索引)。

起始函数的节 ID 为 8。解码后,起始函数表示模块的起始组件:

; section "Start" (8)
0000085: 08                          ; section code
0000086: 01                          ; section size
0000087: 03                          ; start func index

注意

目前,工具如 webpack 不支持 start 函数。起始部分被重写为一个普通函数,然后当打包器本身初始化 JavaScript 时调用该函数。

start 是一个有趣且有用的函数,它允许在模块初始化时设置一些值,以防止模块可能引起的不必要的副作用。现在,让我们探索内存部分。内存部分负责在 JavaScript 和 WebAssembly 之间传输内存。

内存

在 JavaScript 和 WebAssembly 之间传输数据是一个昂贵的操作。为了减少 JavaScript 和 WebAssembly 模块之间的数据传输,WebAssembly 使用 sharedArrayBuffer。使用 sharedArrayBuffer,JavaScript 和 WebAssembly 模块都可以访问相同的内存,并使用它来在两者之间共享数据。

WebAssembly 模块的内存部分是一个线性内存的向量。线性内存模型是一种内存寻址技术,其中内存组织在一个单一的连续地址空间中。它也被称为平面内存模型。线性内存模型使得理解、编程和表示内存变得更加容易。但是,线性内存模型存在一个巨大的缺点,即内存中元素重新排列的执行时间很高,并且会浪费内存空间。在这里,内存代表了一个未解释数据的原始字节的向量。它们使用可调整大小的数组缓冲区来存储内存的原始字节。我们使用 sharedArrayBuffers 来定义和维护这个内存。

注意

重要的是要注意,这个内存可以通过 JavaScript 和 WebAssembly 访问和修改。

我们使用 WebAssembly.Memory() 构造函数来分配内存。构造函数可以接受一个参数,用于定义内存的初始值和最大值,如下所示:

$ touch memory.html
$ vi memory.html
let memory = new WebAssembly.Memory({initial: 10, maximum: 100})

在这里,我们定义 WebAssembly.Memory 具有初始内存 10 和最大内存 100。然后,我们使用以下代码实例化 WebAssembly 模块:

const response = await fetch('./memory.wasm')
const bytes = await response.arrayBuffer()
const wasm = await WebAssembly.instantiate(bytes, { js: { memory } })

与全局示例类似,这里我们传递 importObject,它接受 js 模块和内存对象。

让我们创建一个名为 memory.wat 的新文件,并将以下内容添加到其中:

(module
     (memory (import "js" "memory") 1)
     (func (export "sum") (param $ptr i32) (param $len i32)
       (result i32)
          (local $end i32)
          (local $sum i32)
          (local.set $end (i32.add (local.get $ptr)
            (i32.mul (local.get $len) (i32.const 4))))
          (block $break (loop $top
               (br_if $break (i32.eq (local.get $ptr)
               (local.get $end)))
               (local.set $sum (i32.add (local.get $sum)
                 (i32.load (local.get $ptr))))
               (local.set $ptr (i32.add (local.get $ptr)
                 (i32.const 4)))
               (br $top)
          ))
          (local.get $sum)
     )
)

在模块内部,我们使用名为 memory 的名称从 js 模块导入内存。之后,我们定义一个名为 sum 的函数并将该函数导出至模块外部。该函数接受两个参数作为输入,并返回一个 i32 类型的输出。第一个参数命名为 $ptr,它是一个指向 sharedArrayBuffer 中值所在索引的指针。下一个参数是 $len,它定义了共享内存的长度。

然后,我们创建两个局部变量,$end$sum。首先,我们将 $end 设置为 $ptr 加上 $len 的四倍值。然后,我们创建一个块并开始一个循环。循环在 $end 的值等于 $ptr 的值时结束。然后,我们将 $sum 的值通过将现有的 $sum 值与 $ptr 的值相加来设置。然后,我们将 $ptr 增加到下一个值。最后,我们退出循环并返回 $sum

以下代码与 JavaScript 中的以下代码类似:

function sum(ptr, len) {
    let end = ptr + (len * 4)
    let tmp = 0
    while (ptr < end) {
        tmp = memory[ptr]
        ptr = ptr + 4
    }
    return tmp;
}

让我们回到 memory.html 并初始化缓冲区:

let i32Arr = new Uint32Array(memory.buffer)
for (var i = 0; i < 50; i++) {
    i32Arr[i] = i * i * i
}

我们使用 Uint32Array 和我们创建的内存对象创建一个无符号数组。然后,我们将从 1 到 50 的数字的立方填充到数组缓冲区中:

var sum = wasm.instance.exports.sum(0, 50)
console.log(sum) // 1500625

最后,我们在 WebAssembly 模块内部调用 sum 函数,并要求它提供从 0 开始到长度为 50 的共享数组缓冲区中所有立方数的总和。

让我们使用 WABT 将 WebAssembly 文本格式转换为 WebAssembly 模块,使用以下命令:

$ /path/to/wabt/bin/wat2wasm memory.wat

让我们在浏览器中使用以下命令运行一个示例:

$ python -m http.server

现在,启动 URL http://localhost:8000/globals.html 并打开开发者工具。

当我们需要在两个世界之间传输大量数据时,内存部分非常有用。内存部分使得在 WebAssembly 和 JavaScript 世界之间定义、共享和访问内存变得更加容易。

摘要

在本章中,我们学习了 WebAssembly 模块中的导入、导出、启动和内存部分。我们看到了它们在 WebAssembly 模块中的结构和定义方式。每个部分都承载着一个特定的功能,理解、分析和调试 WebAssembly 模块时,这些部分是至关重要的。在下一章中,我们将探索 Binaryen。

第六章:第六章:安装和使用 Binaryen

在编译过程中,编译语言会产生它们自己的中间表示IR)。然后编译器会优化 IR 以生成优化代码。在传递给 LLVM 之前,编译器应将此 IR 转换为 LLVM 理解的内容(LLVM IR)。LLVM 优化 LLVM IR 并生成原生代码(如 WebAssembly 二进制文件)。这些不同级别的多个 IR 生成和优化使得编译过程变慢且不太有效。Binaryen 试图消除这些多个 IR 生成,并使用自己的 IR。

(WebAssembly) 二进制 + Emscripten = Binaryen

Binaryen 是一个用于 WebAssembly 的编译器和工具链基础设施库,用 C++ 编写。它的目标是使编译到 WebAssembly 变得简单、快速和有效。

Binaryen 使用自己的 IR 版本。Binaryen 的 IR 是 WebAssembly 的一个子集。因此,它使得将 Binaryen 编译到 WebAssembly 变得更快、更简单。Binaryen 的 IR 使用紧凑的数据结构,并考虑到现代 CPU 架构。也就是说,可以使用所有可用的 CPU 核心并行生成和优化 WebAssembly 二进制文件。

此外,Binaryen 的优化器有许多可以通过显著改进代码的遍历。Binaryen 的优化器使用诸如局部着色以合并局部变量、删除死代码以及在可能的情况下预计算表达式等技术。Binaryen 还提供了一种缩小 WebAssembly 二进制文件的方法。

Binaryen 使用简单。它接受 WebAssembly 二进制文件,甚至控制图来生成高度优化的 WebAssembly 二进制文件。Binaryen 还提供了 Binaryen.js,它使得从 JavaScript 使用 Binaryen 成为可能。类似于 WABT,Binaryen 包含一套不同的工具,这些工具在处理 WebAssembly 时非常有用。

这些工具链实用程序有助于解析 WebAssembly 二进制文件,然后进一步优化它,并最终输出高度优化的 WebAssembly 二进制文件(换句话说,wasm-to-wasm 优化器),当浏览器没有 WebAssembly 支持时,提供 WebAssembly 的 polyfill。

在本章中,我们将了解如何安装和使用 Binaryen 提供的各种工具。了解 Binaryen 及其提供的工具将帮助您在性能和大小方面优化 WebAssembly 二进制文件。本章将涵盖以下部分:

  • 安装和使用 Binaryen

  • wasm-as

  • wasm-dis

  • wasm-opt

  • wasm2js

技术要求

我们将需要安装 Binaryen 和 Visual C++。您可以在 GitHub 上找到本章中提供的代码文件,网址为 github.com/PacktPublishing/Practical-WebAssembly

安装和使用 Binaryen

为了安装 Binaryen,首先从 GitHub 克隆仓库:

$ git clone https://github.com/WebAssembly/binaryen

在将仓库克隆后,进入以下文件夹:

$ cd binaryen

Linux/macOS

通过运行带有 binaryen 文件夹路径的 cmake 命令来生成项目构建系统:

$ cmake .

接下来,使用 make 命令构建项目:

$ make .

这将在 bin 文件夹中生成所有二进制文件。

Windows

对于 Windows,一旦克隆了存储库,我们将创建一个 build 目录并进入其中:

$ mkdir build
$ cd build

默认情况下,Windows 没有可用的 cmake 命令。安装 Visual C++ 工具以使 cmake 命令在系统中可用。要安装 Visual C++ 工具,请查看以下链接:docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-160&viewFallbackFrom=vs-2019。然后,在 build 文件夹中运行以下命令:

$ "<path-to-visual-studio-root>\Common7\IDE\CommonExtensions\
  Microsoft\CMake\CMake\bin\cmake.exe" ..

上述命令将在 build 目录中生成所有必要的构建文件。然后,我们可以使用 cmake 生成的 binaryen.vcxproj 文件构建项目:

$ msbuild binaryen.vcxproj

生成的二进制文件包括以下内容:

$ tree -L 1
├── wasm-as
├── wasm-ctor-eval
├── wasm-dis
├── wasm-emscripten-finalize
├── wasm-metadce
├── wasm-opt
├── wasm-reduce
├── wasm-shell
└── wasm2js 

Binaryen 生成的各种工具如下:

  • wasm-as – 此工具与 WABT 中的 wat2wasm 类似。该工具将 WebAssembly 文本格式 (.wast) 转换为 WebAssembly 二进制格式 (.wasm)。

  • wasm-ctor-eval – 此工具在预编译时执行 C++ 全局构造函数,并使其就绪。这种优化加快了 WebAssembly 的执行速度。

  • wasm-dis – 此工具与 wabt 中的 wasm2wat 类似。也就是说,它将 WebAssembly 二进制格式 (.wasm) 转换为 WebAssembly 文本格式 (.wat)。

  • wasm-emscripten-finalize – 此工具对给定的 .wasm 文件执行 Emscripten 特定的转换。

  • wasm-metadce – 此工具从提供的 WebAssembly 二进制文件中删除死代码。

  • wasm-opt – 此工具优化提供的 WebAssembly 二进制文件。

  • wasm-reduce – 此工具将给定的 WebAssembly 二进制文件减小到更小的二进制文件。

  • wasm-shell – 此工具创建一个可以加载和解释 WebAssembly 代码的壳。

  • wasm2js – 此工具在 polyfill 中非常有用。它将 WebAssembly 转换为 JavaScript 编译器。

  • binaryen.js – 一个独立的 JavaScript 库,它公开 Binaryen 方法以创建和优化 WebAssembly 模块。此 JavaScript 文件就像任何其他可以加载到浏览器中的 JavaScript 文件一样。

现在我们已经构建并生成了 Binaryen 提供的工具,让我们探索生成的工具。

wasm-as

wasm-as 工具将 WAST 转换为 WASM。让我们看看步骤:

  1. 让我们创建一个名为 binaryen-playground 的新文件夹,并进入该文件夹:

    $ mkdir binaryen-playground
    $ cd binaryen-playground
    
  2. 创建一个名为 add.wat.wat 文件:

    $ touch add.wat
    
  3. 将以下内容添加到 add.wat

    (module
        (func $add (param $x i32) (param $y i32) 
          (result i32)
            (i32.add
                (local.get $x)
                (local.get $y)
            )
        )
    )
    
  4. 使用 wasm-as 二进制将 Web Assembly 文本格式转换为 WebAssembly 模块:

    $ /path/to/build/directory/of/binaryen/wasm-as add.wat
    

生成 add.wasm 文件:

00 61 73 6d 01 00 00 00 01 07 01 60 02 7f 7f 01
7f 03 02 01 00 0a 09 01 07 00 20 00 20 01 6a 0b

注意

生成的二进制文件大小仅为 32 字节。

wasm-as 首先验证给定的文件(.wat),然后将其转换为 .wasm 文件。要检查 wasm-as 支持的各种选项,我们可以运行以下命令:

$ /path/to/build/directory/of/binaryen/wasm-as --help
wasm-as INFILE
Assemble a .wat (WebAssembly text format) into a .wasm (WebAssembly binary
format)
Options:
  --version                        Output version information     and exit
  --help,-h                        Show this help message and     exit
  --debug,-d                       Print debug information to     stderr
....
  --output,-o                      Output file (stdout if not 
    specified)
  --validate,-v                    Control validation of the 
    output module
  --debuginfo,-g                   Emit names section and debug
    info
  --source-map,-sm                 Emit source map to the 
    specified file
  --source-map-url,-su             Use specified string as 
    source map URL
  --symbolmap,-s                   Emit a symbol map (indexes 
    => names)

如果我们需要将 WebAssembly 模块文件生成在不同的名称下,我们将使用 -o 选项和文件名。例如,wasm-as add.wat -o customAdd.wasm 将生成 customAdd.wasm

wasm-as 也提供了详细的输出,清楚地解释了 WebAssembly 模块的结构。为了查看 WebAssembly 模块的结构,我们使用 -d 选项运行它:

$ /path/to/build/directory/of/binaryen/wasm-as add.wat -d
Loading 'add.wat'...
s-parsing...
w-parsing...
Validating...
writing...
writing binary to add.wasm
Opening 'add.wasm'
== writeHeader
...
== writeTypes
...
== writeFunctionSignatures
...
== writeFunctions
...
finishUp
Done.

之前的输出是关于如何生成二进制的详细描述。首先,它加载了给定的 .wat 文件。之后,它解析并验证文件。最后,它读取了头部、类型、函数签名和函数后创建了 add.wasm 并写入了头部、类型、函数签名和函数。在生成二进制文件时,我们可以通过使用适当的 enable-*disable-* 选项来启用编译器包括新的和闪亮的功能,并禁用各种现有功能。此外,您可以使用 --sm 选项生成 sourcemap

现在我们已经看到了如何将 WAST 转换为 WASM,让我们看看如何将 WASM 转换为 WAST。

wasm-dis

wasm-dis 工具将 WAST 转换为 WASM。在这里,我们将使用之前示例中创建的 add.wasm 文件。让我们看看步骤:

  1. 为了将 WebAssembly 模块转换为 WebAssembly 文本格式,使用 wasm-dis 二进制文件,运行以下命令:

    $ /path/to/build/directory/of/binaryen/wasm-dis add.wasm -o gen-add.wast
    
  2. 我们使用 -o 选项和文件名(gen-add.wast)生成 gen-add.wast 文件:

    (module
    (type $i32_i32_=>_i32 (func (param i32 i32) 
      (result i32)))
    (func $0 (param $0 i32) (param $1 i32) (result i32)
               (i32.add  (local.get $0)  (local.get $1) )
    )
    )
    
  3. wasm-dis 首先验证给定的文件(.wasm),然后将其转换为 .wat 文件。要检查 wasm-dis 支持的各种选项,请运行以下命令:

    $ /path/to/build/directory/of/binaryen/wasm-dis --help
    wasm-dis INFILE 
    Un-assemble a .wasm (WebAssembly binary format) into a .wat (WebAssembly text
    format)
    Options:
      --version        Output version information and exit
      --help,-h        Show this help message and exit
      --debug,-d       Print debug information to stderr
      --output,-o      Output file (stdout if not specified)
      --source-map,-sm Consume source map from the specified file to add location
    
  4. wasm-dis 还提供了详细的输出,清楚地解释了 WebAssembly 模块的结构。为了查看 WebAssembly 模块的结构,我们使用 -d 选项运行它:

    $ /path/to/build/directory/of/binaryen/wasm-dis
      add.wat -o gen-add.wast -d
    parsing binary...
    reading binary from add.wasm
    Loading 'add.wasm'...
    == readHeader
    ...
    == readSignatures
    ...
    == readFunctionSignatures
    ...
    == processExpressions
    ...
    == processExpressions finished
    end function bodies
    Printing...
    Opening 'gen-add.wast'
    Done.
    

之前的输出是关于如何生成 .wast 文件的详细描述。首先,它加载了给定的 .wasm 文件。之后,它解析并验证文件。最后,在读取了头部、类型、函数签名和函数后创建了 gen-add.wast

在生成文件时,我们可以通过使用适当的 enable-*disable-* 选项来启用编译器包括新的和闪亮的功能,并分别禁用各种现有功能。

此外,我们还可以使用 --sm <filename> 选项输入 sourcemap

现在我们已经看到了如何将 WASM 转换为 WAST,让我们看看如何进一步优化 WebAssembly 二进制文件。

wasm-opt

wasm-opt 工具是一个 wasm-to-wasm 优化器。它将接收一个 WebAssembly 模块作为输入,并在其上运行转换过程以优化并生成优化的 WebAssembly 模块。让我们看看步骤:

  1. 让我们先创建 inline-optimizer.wast 文件并添加以下内容:

    (module
        (func $parent (export "parent") (result i32)
            (i32.add
                (call $child)
                (i32.const 13)
            )
        )
        (func $child (result i32) (call $grandChild))
        (func $grandChild (result i32) (call
           $greatGrandChild))
        (func $greatGrandChild (result i32) (i32.const 7))
    )
    
  2. 要生成 WebAssembly 模块,我们将运行以下命令:

      optimizer.wast -o inline.wasm --print
    (module
    (type $0 (func (result i32)))
    (export "parent" (func $parent))
    (func $parent (; 0 ;) (type $0) (result i32)
    (i32.add
    (call $child)
    (i32.const 13)
      )
    )
    (func $child (; 1 ;) (type $0) (result i32)
      (call $grandChild)
    )
    (func $grandChild (; 2 ;) (type $0) (result i32)
      (call $greatGrandChild)
    )
    (func $greatGrandChild (; 3 ;) (type $0) (result i32)
      (i32.const 7)
    )
    )
    

这将生成 inline.wasm--print 选项在转换成 WebAssembly 二进制之前打印出 WebAssembly 文本格式。我们还传递了 -o 选项,将 WebAssembly 模块输出为 inline.wasm

60B inline-optimize.wasm
273B inline-optimize.wat

这个生成的二进制文件在内存中占用 60 字节。

  1. 我们可以使用 --inlining-optimizing 选项进一步优化二进制文件:

      optimizer.wast -o inline.wasm --print --inlining-
      optimizing
    

这将优化函数并将函数内联到二进制文件被调用的地方。让我们检查生成的文件大小:

39B inline-optimize.wasm
273B inline-optimize.wat

生成的文件只有 39 字节,比原始二进制文件少了 35%。

  1. 要检查 wasm-opt 支持的各种选项,请运行以下命令:

    /path/to/bin/folder/of/binaryen/wasm-opt –help
    

wasm-opt 工具帮助我们进一步优化 WebAssembly 二进制文件。接下来,让我们探索 wasm2js 工具。

wasm2js

wasm2js 工具将 WASM/WAST 文件转换为 JavaScript 文件。让我们看看步骤:

  1. 创建一个名为 add-with-export.wast 的文件:

    $ touch add-with-export.wast
    

然后,添加以下代码:

(module
    (export "add" (func $add))
    (func $add (param $x i32) (param $y i32) 
      (result i32)
        (i32.add
            (local.get $x)
            (local.get $y)
        )
    )
)
  1. 为了使用 wasm2js 将 WebAssembly 文本格式转换为 JavaScript,运行以下命令:

    $ /path/to/build/directory/of/binaryen/wasm2js add-
      with-export.wast
    

这将打印出生成的 JavaScript:

function asmFunc(global, env) {
var Math_imul = global.Math.imul;
var Math_fround = global.Math.fround;
var Math_abs = global.Math.abs;
var Math_clz32 = global.Math.clz32;
var Math_min = global.Math.min;
var Math_max = global.Math.max;
var Math_floor = global.Math.floor;
var Math_ceil = global.Math.ceil;
var Math_sqrt = global.Math.sqrt;
var abort = env.abort;
var nan = global.NaN;
var infinity = global.Infinity;
function add(x, y) {
  x = x | 0;
  y = y | 0;
  return x + y | 0 | 0;
}
return {
  "add": add
};
}
var retasmFunc = asmFunc({
    Math,
    Int8Array,
    Uint8Array,
    Int16Array,
    Uint16Array,
    Int32Array,
    Uint32Array,
    Float32Array,
    Float64Array,
    NaN,
    Infinity
  }, {
    abort: function() { throw new Error('abort'); }
  });
export var add = retasmFunc.add;

asmFunc 函数被定义了。在 asmFunc 中,我们从全局对象中导入数学函数。之后,我们有一个 add 函数。add 函数初始化 xy。该函数返回两个值的和。最后,我们返回 add 函数。

注意

生成的 JavaScript 是 asmjs 而不是常规 JavaScript。我们还在 JavaScript 中从全局命名空间导入了大量函数到 asmFunc

wasm2js 工具使得从 WebAssembly 模块生成 JavaScript 变得容易。生成的 JavaScript 模块比其常规 JavaScript 对应物更快。这可以用作不支持 WebAssembly 的浏览器的 polyfill。

摘要

在本章中,我们看到了如何安装 Binaryen 以及 Binaryen 工具包提供的各种工具。Binaryen 使得将 WebAssembly 模块转换为各种格式变得更加容易。这是一个使您的 WebAssembly 之旅更加轻松和高效的工具。

在下一章中,我们将开始我们的 Rust 和 WebAssembly 之旅。

第三部分:Rust 和 WebAssembly

本节介绍了 Rust 以及如何轻松地从 Rust 生成 WebAssembly 模块。你将了解 Rust 生态系统如何使 WebAssembly 成为一等公民,以及可用的各种工具以及如何使用它们。

到本章结束时,你将完全理解如何在 JavaScript 应用程序中使用 Rust 和 WebAssembly,以及一点 WASI。

本节包括以下章节:

  • 第七章, 将 Rust 与 WebAssembly 集成

  • 第八章, 使用 wasm-pack 打包 WebAssembly

  • 第九章, 跨越 Rust 和 WebAssembly 之间的边界

  • 第十章, 优化 Rust 和 WebAssembly

第七章:第七章:将 Rust 与 WebAssembly 集成

Rust 是一种系统级编程语言。作为系统级编程语言,Rust 提供了低级内存管理和高效表示数据的能力。因此,它为程序员提供了完全的控制权,并提高了性能。

此外,Rust 还提供了以下功能:

  • 友好的编译器 – Rust 编译器是您编写 Rust 时的伴侣。编译器会纠正您,指导您,并确保您几乎总是编写内存安全的代码。

  • 所有权模型 – 所有权模型确保我们不需要垃圾回收。这保证了 Rust 中的线程和内存安全。

  • 安全性、速度和并发性 – Rust 确保安全性和并发性,并让您远离风险、崩溃和漏洞。

  • 现代语言 – Rust 提供了现代语言语法,并且语言是为了提供更好的开发者体验而构建的。

这些特性(以及成千上万的其它特性)确保 Rust 是一种通用编程语言。Rust 语言的亮点在于其编译器和社区总是乐于助人。

Rust 为 WebAssembly 提供了一级支持。Rust 丰富的工具链使得开始使用 WebAssembly 更加容易。Rust 不需要运行时,这使得它成为 WebAssembly 的理想候选者。在本章中,我们将看到如何安装 Rust 并探索将 Rust 转换为 WebAssembly 模块的各种方法。本章将涵盖以下部分:

  • 安装 Rust

  • 通过 rustc 将 Rust 转换为 WebAssembly

  • 通过 Cargo 将 Rust 转换为 WebAssembly

  • 安装 wasm-bindgen

  • 通过 wasm-bindgen 将 Rust 转换为 WebAssembly

现在,让我们进入 Rust 和 WebAssembly 的世界。

技术要求

您可以在 GitHub 上找到本章中存在的代码文件,网址为 github.com/PacktPublishing/Practical-WebAssembly

安装 Rust

Rust 是一种编译型语言,其编译器被称为 Rust 编译器rustc)。Rust 还有自己的包管理器,称为 Cargo。Cargo 与 Node.js 的 npm 类似。Cargo 下载包依赖项并构建、编译、打包并将工件上传到 crate(Rust 的包版本)。

Rust 语言提供了一个简单的方法通过 rustup 安装和管理 Rust。rustup 帮助安装、更新和删除 rustc、Cargo 和 rustup 本身。这使得安装和管理 Rust 的各种版本变得容易。

让我们使用 rustup 工具安装 Rust,并看看我们如何使用 rustup 管理 Rust 版本。

在 Linux 或 macOS 上,请使用以下命令:

$ curl https://sh.rustup.rs --sSf | sh

该脚本将下载并安装 Rust 语言。rustc 和 Cargo 都安装在 ~/.cargo/bin 中,并委托对底层工具链的任何访问。

对于 Windows,从这里下载并安装二进制文件:forge.rust-lang.org/infra/other-installation-methods.htmlrustc 和 Cargo 都安装在 users 文件夹中。

注意

你将需要 Visual Studio 2013 或更高版本的 C++ 构建工具。你可以从这里安装它们:visualstudio.microsoft.com/downloads/

一旦安装成功完成,你可以通过运行以下命令来检查:

$ rustc –version
rustc 1.58.1 (db9d1b20b 2022-01-20)

rustup 是一个工具链多路复用器。它安装并管理许多 Rust 工具链,并通过位于主目录 .cargo/bin 中的单个工具集进行代理。一旦安装了 rustup,我们就可以轻松地管理 rustccargo 编译器。rustup 还使得在夜间、稳定和测试版本之间切换 Rust 更加容易。

Rust 在其稳定版本中提供了 WebAssembly 编译支持。我们还将切换到夜间构建,以确保我们获得所有最新的好处。

要切换到夜间版本,我们必须运行以下命令:

$ rustup default nightly

此命令将默认的 rustc 切换到夜间版本。位于 ~/.cargo/binrustc 代理将运行夜间编译器而不是稳定编译器。

要更新到最新版本的夜间版本,我们可以运行以下命令:

$ rustup update

一旦成功更新,我们可以通过运行以下命令来检查当前安装的版本:

$ rustc --version
rustc 1.55.0 (c8dfcfe04 2021-09-06)

Rust 将 WebAssembly 作为一等公民支持。因此,rustc 能够将 Rust 代码编译成 WebAssembly 模块。让我们看看如何通过 rustc 将 Rust 转换为 WebAssembly。

通过 rustc 将 Rust 转换为 WebAssembly

Rust 使用我们即将创建的 LLVM 编译器来生成机器原生代码。rustc 使用 LLVM 的能力将原生代码转换为 WebAssembly 模块。我们在上一节中安装了 Rust;现在让我们使用 rustc 开始将 Rust 转换为 WebAssembly 模块。

我们将从 Hello World 开始:

  1. 让我们创建一个名为 hello_world.rs 的文件:

    $ touch hello_world.rs
    
  2. 启动你最喜欢的编辑器并开始编写 Rust 代码:

    fn main() {    
    println!("Hello World!");
    }
    

我们已经定义了一个 main 函数。类似于 C,main 是一个特殊函数,它在编译成可执行文件后标记程序的入口点。

fn 是 Rust 中的函数关键字。main() 是函数名。

println! 是一个宏。Rust 中的宏允许我们在语法级别抽象代码。宏调用是“展开”的语法形式的简写。这种展开发生在编译的早期阶段,在静态检查之前。

注意

宏是一个有趣的功能,但解释它们超出了本书的范围。你可以在以下位置找到更多信息:doc.rust-lang.org/book/ch19-06-macros.html

  1. 我们将 Hello World! 字符串传递给 println! 宏函数。我们可以通过运行以下命令来编译并生成二进制文件:

    $ rustc hello.rs
    
  2. 这将生成一个 hello 二进制文件。我们可以执行该二进制文件,它将打印 Hello World!

    $ ./hello
    Hello World!
    
  3. 现在,使用 rustc 将 Rust 编译成 WebAssembly 模块:

    $ rustc --target wasm32-unknown-emscripten
      hello_world.rs -o hello_world.html
    

这将生成 WebAssembly 模块。

  1. 使用以下命令在浏览器中运行生成的代码:

    $ python -m http.server
    

打开浏览器并转到 http://localhost:8000。打开开发者控制台以查看其中打印的 Hello World!

要将 Rust 转换为 WebAssembly 模块,我们使用了 --target 标志。此标志指示编译器编译和构建二进制文件,使其在提供的运行时上运行。

我们将 wasm32-unknown-emscripten 作为值传递给 --target 标志。

wasm32 表示地址空间大小为 32 位。unknown 告诉编译器您不知道要编译到的系统。最后的 emscripten 通知编译器您的目标。

因此,使用 wasm32-unknown-emscripten 值,编译器将在几乎任何机器上编译,但仅在 Emscripten 运行时上运行。然后,我们指定需要编译成 WebAssembly 模块的需要编译的输入文件。最后,我们使用 -o 标志指定输出。

理解 rustc 做了什么是很重要的。

图 7.1 – Rust 编译步骤

图 7.1 – Rust 编译步骤

rustc 首先解析输入并生成 抽象语法树AST)。一旦生成了 AST,编译器随后递归解析路径,展开宏和其他引用。一旦 AST 完全解析,它将被转换为 高级中间表示HIR)。这种中间表示类似于 AST 的去糖版本。

然后对 HIR 进行类型检查。类型检查后,HIR 将进行后处理并转换为 中间表示MIR)。从 MIR 中,编译器生成 LLVM 中间表示LLVM IR)。之后,LLVM 对它们进行所需的优化。

现在,有了 LLVM IR,将 LLVM IR 转换为 WebAssembly 模块变得更容易。这与 Emscripten 将 C 或 C++ 代码转换为 WebAssembly 模块的方式类似。

注意

由于我们在这里使用了 wasm32-unknown-emscripten 标志,我们需要 emcc 可用,以便将 Rust 代码生成的 LLVM IR 转换为 WebAssembly 模块。

我们已经看到了如何使用 rustc 生成 WebAssembly 模块。它在幕后使用 Emscripten 创建它们。但 Rust 提供了另一种抽象来生成 WebAssembly 模块,通过 Cargo。

在下一节中,我们将看到如何使用 Cargo 将 Rust 转换为 WebAssembly。

通过 Cargo 将 Rust 转换为 WebAssembly

Cargo 使创建、运行、下载、编译、测试和运行项目变得更容易。cargo 命令提供了一个包装器,它调用 rustc 编译器以启动编译。为了使用 Rust 的工具链创建 WebAssembly 模块,我们将使用不同的目标,wasm32-unknown-unknown

wasm32-unknown-unknown目标添加了零运行时和工具链占用。wasm32使编译器假设只有wasm32指令集存在。unknown-unknown中的第一个unknown表示代码可以在任何机器上编译,第二个表示代码可以在任何机器上运行。

为了看到它的实际效果,让我们使用 Cargo 创建一个新的项目:

$ cargo new --lib fib_wasm
Created library `fib_wasm` package

创建了一个名为fib_wasm的新项目。新选项创建一个 Rust 项目。--lib标志通知 Cargo 创建一个新的库项目而不是默认的二进制项目。

二进制项目将生成可执行的二进制文件。库项目将创建库模块。

启动您最喜欢的文本编辑器,并将src/lib.rs文件的内容替换为以下内容:

#[no_mangle]
fn add(x: i32, y:i32) -> i32 {    x + y}

#[no_mangle]是一种注释。这种注释通知编译器在生成库时不要更改名称。

然后,我们定义add函数。add函数接受两个参数,xy。我们使用i32在变量后跟一个冒号(:)来定义它们的类型。最后,我们使用-> i32定义它们的返回类型。

函数体只有x + y。注意,在 Rust 中我们不需要在最后一条语句的末尾使用return关键字和;,这可以简写为返回。

Cargo 还会生成Cargo.toml文件。此文件包含有关项目的所有元信息,如何编译 Rust 代码以及它们的依赖项。

Cargo.toml文件看起来像这样:

[package]
name = "fib_wasm"
version = "0.1.0"
authors = ["Sendil Kumar"]
edition = "2018"

它定义了包名、版本、作者以及我们正在使用的 Rust 版本。

在这里,我们必须指导编译器我们正在编译哪种类型的 crate。我们可以在[lib]部分和crate-type属性中指定它。

打开Cargo.toml文件,并在其中添加crate-type信息:

[package]
name = "fib_wasm"
version = "0.1.0"
authors = ["Sendil Kumar"]
edition = "2018"
[lib]
crate-type = ["cdylib"]

cdylib在这里指定将生成一个动态系统库。当库需要从其他语言加载时,将使用此动态系统库。

让我们编译 Rust 到 WebAssembly 模块:

$ cargo build --target wasm32-unknown-unknown

这将调用rustc并指定目标。这将生成/target/wasm32-unknown-unknown/中的 WebAssembly 模块。现在,为了在浏览器上运行 WebAssembly 模块,让我们手动创建 HTML 文件并使用 JavaScript 加载它。

让我们创建一个 HTML 文件:

$ touch index.html

将以下内容添加到文件中:

<script> 
(async () => {     
const bytes = await fetch("target/wasm32-unknown-
  unknown/debug/fib_wasm.wasm");     
const response = await bytes.arrayBuffer();     
const result = await WebAssembly.instantiate(response, {});
  console.log(result.instance.exports.add(10,3)); 
})();
</script>

我们在<script>标签内定义了脚本。在 HTML 中,我们在<script>标签内定义 JavaScript。我们添加了async关键字。async关键字指定函数是异步的。

首先,我们获取 WebAssembly 模块。WebAssembly 模块在target/wasm32-unknown-unknown/debug/文件夹中生成,其名称与Cargo.toml中定义的包名相同。

await关键字确保执行被挂起,直到我们获取整个 WebAssembly 模块。

然后,我们使用bytes.arrayBuffer()将收集的字节(来自 fetch 调用)进行转换。现在response对象将包含在ArrayBuffer中的 WebAssembly 模块。

然后,我们使用 WebAssembly.instantiate 函数实例化字节数组。result 对象包含整个 WebAssembly 模块。

WebAssembly 模块 result 包含 instance 属性。该实例具有 exports 属性。exports 属性包含 WebAssembly 模块导出的所有函数。

由于我们添加了 #[no_mangle],导出的函数名称没有改变。因此,exports 属性中定义了 add 函数。

我们在这里使用了 async-await 来使语法更加优雅,并使上下文更容易理解。

如预期的那样,前面的代码将输出 13。您可以在浏览器控制台中检查输出。

在这里,cargo build 命令调用 rustc 并将 Rust 代码编译成 MIR,然后编译成 LLVM IR。生成的 LLVM IR 然后转换为 WebAssembly 模块。让我们使这个函数更复杂一些。我们可以使用 Rust 创建一个斐波那契数生成器并在浏览器上运行 WebAssembly 模块:

  1. 打开 src/lib.rs 并将其替换为以下内容:

    #[no_mangle]
    fn fibonacci(num: i32) -> i32 {    
    match num {        
    0 => 0,        
    1 => 1,        
    _ => fibonacci(num-1) + fibonacci(num-2),    
    }
    }
    

使用 cargo build --target wasm32-unknown-unknown 构建。

  1. 然后,将 index.html 替换为调用斐波那契而不是 add

    <script>
     (async () => {     
    const bytes = await fetch("target/wasm32-unknown-
      unknown/debug/fib_wasm.wasm");     
    const response = await bytes.arrayBuffer();     
    const result = await WebAssembly.instantiate(response,
      {});  
         result.instance.exports.fibonacci(20); 
    })();
    </script>
    
  2. 现在,启动 HTML 服务器并检查浏览器的控制台以获取斐波那契值。

到目前为止,我们已经看到了简单的示例。但如何将函数和类从 JavaScript 传递到 WebAssembly,反之亦然?为了进行更高级的绑定,Rust 为我们提供了 wasm-bindgen

安装 wasm-bindgen

wasm-bindgen 用于将 Rust 中的实体绑定到 JavaScript,反之亦然。

wasm-bindgen 使得从 Rust 导出实体到 JavaScript 更加自然。JavaScript 开发者会发现 wasm-bindgen 对 WebAssembly 的使用与 JavaScript 类似。

这使得在将 Rust 转换为 WebAssembly 模块时可以使用更丰富和更简单的 API。wasm-bindgen 使用这些功能并提供了一个简单的 API 以供使用。它确保 wasm 模块和 JavaScript 之间发生高级交互。

wasm-bindgen 提供了 JavaScript 和 WebAssembly 之间的通道,用于传递除了数字之外的内容,例如对象、字符串和数组。

要安装 wasm-bindgen-cli,请使用以下 cargo 命令:

$ cargo install wasm-bindgen-cli

安装成功后,让我们运行 wasm-bindgen CLI:

$ wasm-bindgen  --help
Generating JS bindings for a wasm file
Usage:
    wasm-bindgen [options] <input>
    wasm-bindgen -h | --help
    wasm-bindgen -V | --version
Options:
    -h --help                 Show this screen.
    --out-dir DIR             Output directory
    --out-name VAR            Set a custom output filename 
      (Without extension. Defaults to crate name)
    --target TARGET           What type of output to generate, 
      valid
      values are [web, bundler, nodejs, no-modules],
        and the default is [bundler]
    --no-modules-global VAR   Name of the global variable to 
      initialize
    --browser                 Hint that JS should only be 
      compatible with a browser
    --typescript              Output a TypeScript definition 
      file (on by default)
    --no-typescript           Don't emit a *.d.ts file
    --debug                   Include otherwise-extraneous 
      debug checks in output
    --no-demangle             Don't demangle Rust symbol names
    --keep-debug              Keep debug sections in wasm files
    --remove-name-section     Remove the debugging `name` 
      section of the file
    --remove-producers-section   Remove the telemetry 
      `producers` section
    --encode-into MODE        Whether or not to use 
      TextEncoder#encodeInto,
      valid values are [test,z always, never]
    --nodejs                  Deprecated, use `--target nodejs`
    --web                     Deprecated, use `--target web`
    --no-modules              Deprecated, use `--target 
      no-modules`
    -V --version              Print the version number of 
     wasm-bindgen

让我们看看 wasm-bindgen 支持的各种选项。

要在特定目录和特定名称下生成文件,该工具分别有 --out-dir--out-name。为了减少或优化生成的 WebAssembly 模块大小,wasm-bindgen 有以下标志:

  • --debug--debug 选项在生成的 WebAssembly 模块中包含额外的调试信息。这将增加 WebAssembly 模块的大小,但在开发中很有用。

  • --keep-debug: 这个 WebAssembly 模块可能包含自定义部分,也可能不包含。这些自定义部分可以用来存储调试信息。这些自定义部分在调试应用程序时(例如在浏览器开发者工具中)将非常有用。这将增加 WebAssembly 模块的大小。这在开发中很有用。

  • --no-demangle: 这个标志告诉 wasm-bindgen 不要对 Rust 符号名称进行去混淆。去混淆允许最终用户使用他们在 Rust 文件中定义的相同名称。

  • --remove-name-section: 这将移除文件的调试名称部分。我们将在稍后了解更多关于 WebAssembly 模块中各种部分的内容。这将减小 WebAssembly 模块的大小。

  • --remove-producers-section: WebAssembly 模块可以有一个生产者部分。这个部分将包含有关文件是如何生成或由谁生成的信息。默认情况下,生产者部分会被添加到生成的 WebAssembly 模块中。使用这个标志,我们可以移除它。这可以节省更多字节。

wasm-bindgen 为 Node.js 和浏览器环境提供生成绑定文件的选项。让我们看看那些标志:

  • --nodejs: 这将生成仅适用于 Node.js 的输出。不使用 ES 模块。

  • --browser: 这将生成仅适用于浏览器的输出。使用 ES 模块。

  • --no-modules: 这将生成仅适用于浏览器的输出。不使用 ES 模块。适用于尚不支持 ES 模块的浏览器。

可以使用 --no-typescript 标志关闭类型定义文件(*.d.ts)。

现在我们已经安装了 wasm-bindgen,让我们试一试。

通过 wasm-bindgen 将 Rust 转换为 WebAssembly

让我们从使用 wasm-bindgen 的 Hello World 示例开始:

  1. 使用 Cargo 创建一个新项目:

     $ cargo new --lib hello_world
    Created library `hello_world` package
    

这将创建一个新的 Rust 项目,包含所有必要的文件。

  1. 在您最喜欢的编辑器中打开项目。打开 Cargo.toml 文件以添加 crate-type 并添加 wasm-bindgen 依赖项:

    [package]
    name = "hello_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    [lib]
    crate-type = ["cdylib"]
    [dependencies]
    wasm-bindgen = "0.2.38"
    
  2. 我们在 toml 文件中的 [dependencies] 表下定义依赖项。打开 src/lib.rs 文件,将其内容替换为以下内容:

    use wasm_bindgen::prelude::*;
    #[wasm_bindgen]
    pub fn hello() -> String {
    "Hello World".to_string()
    }
    

我们使用 use wasm_bingen::prelude::* 导入 wasm_bindgen 库,然后使用 # [wasm_bindgen] 注解函数。hello 函数返回 String

要生成 WebAssembly 模块,我们首先运行以下命令:

$ cargo build --target=wasm32-unknown-unknown

这将生成 WebAssembly 模块。但这个模块不能单独运行。WebAssembly 只支持在本地代码和 JavaScript 之间传递数字。但我们在这里返回的是一个 String

为了传递任何值(除了数字),我们需要创建一个绑定 JavaScript 文件。这个绑定文件不过是一个翻译器,它将 String(和其他类型)转换为 startlengtharrayBuffer

为了生成绑定文件,我们需要在生成的 WebAssembly 模块上运行 wasm-bindgen CLI 工具:

$ wasm-bindgen target/wasm32-unknown-
  unknown/debug/hello_world.wasm --out-dir .

我们运行 wasm-bindgen 并将其传递给生成的 target/wasm32-unknown-unknown/debug/hello_world.wasm WebAssembly 模块。--out-dir 标志告诉 wasm-bindgen CLI 工具在哪里保存生成的文件。在这里,我们要求在当前文件夹中生成文件。

我们可以看到文件夹内生成的文件:

$ ls -lrta
-rw-r--r-- 1 sendilkumar staff 1769 hello_world.js
-rw-r--r-- 1 sendilkumar staff 88 hello_world.d.ts
-rw-r--r-- 1 sendilkumar staff 227 hello_world_bg.d.ts
-rw-r--r-- 1 sendilkumar staff 67132 hello_world_bg.wasm 

cargo build 命令生成 WebAssembly 模块。wasm-bindgen CLI 将此 WebAssembly 模块作为输入并生成必要的绑定。绑定 JavaScript 文件的大小约为 1.8 KB。

生成的文件如下:

  • WebAssembly 模块(hello_world_bg.wasm

  • JavaScript 绑定文件(hello_world.js

  • WASM 的类型定义文件(hello_world.d.ts

  • JavaScript 的类型定义文件(hello_world_bg.d.ts

JavaScript 绑定文件就足够我们加载和运行 WebAssembly 模块了。

注意

还生成了一个 TypeScript 文件。

现在我们来检查绑定文件包含的内容:

import * as wasm from './hello_world_bg.wasm';

绑定文件导入了 WebAssembly 模块:

const lTextDecoder = typeof TextDecoder === 'undefined' ?
  require('util').TextDecoder : TextDecoder;
let cachedTextDecoder = new lTextDecoder('utf-8');

然后,它定义了 TextDecoder,用于从共享的 ArrayBuffer 解码字符串。

注意

由于没有可用的输入参数,不需要 TextEncoder(即从 JavaScript 编码字符串到共享内存)。wasm-bindgen 将只在绑定文件中生成必要的东西。

现代浏览器内置了 TextDecoderTextEncoder 支持。wasm-bindgen 会检查是否存在解码器,如果存在则使用它;否则,使用 polyfill 加载它。

JavaScript 和 WebAssembly 模块之间的共享内存不需要每次都初始化。我们可以初始化一次,并在执行期间一直使用它。我们有以下两种方法来加载内存一次并在执行期间使用它:

function getUint8Memory() { ... }
function getUint32Memory() { ... }

然后,我们从 Rust 获取一个 String 到 JavaScript。这个 String 通过共享内存传递。因此,我们可以使用偏移量的指针和 String 的长度来检索它。以下函数用于从 WebAssembly 模块中检索 String

function getStringFromWasm(ptr, len) { ....  }

我们在最后定义堆。这是我们将在其中存储所有从 WebAssembly 模块可引用的 JavaScript 变量的地方。__wbindgen_object_drop_ref 函数用于释放由 JavaScript 引用计数器占用的槽位。

最后,我们有 hello 函数:

export function hello() {
    const retptr = globalArgumentPtr();
    wasm.hello_world(retptr);
    const mem = getUint32Memory();
    const rustptr = mem[retptr / 4];
    const rustlen = mem[retptr / 4 + 1];
    const realRet = getStringFromWasm(rustptr,
      rustlen).slice();
    wasm.__wbindgen_free(rustptr, rustlen * 1);
    return realRet;
}

hello 函数被导出。我们首先获取参数的指针。这个指针指向共享数组缓冲区中的一个位置。然后,我们在 WebAssembly 模块中调用 hello 函数。

注意,我们在这里传递了一个(指向)参数。但在 Rust 端,我们没有为该函数定义任何参数。我们将简要地看看 rustc 如何重写代码。

然后,我们获取共享内存。注意,这是一个 32 位数组。我们获取存储结果的指针和输出字符串的长度。注意,这些是连续存储的。

最后,我们将从rustptrrustlen获取字符串。一旦我们收到输出,我们将使用wasm.__wbindgen_free清除分配的内存。

要理解 Rust 端发生的情况,让我们使用cargo-expand命令展开宏,看看代码是如何生成的。

注意

检查github.com/dtolnay/cargo-expand了解如何安装cargo-expand。这不是本书课程的强制性要求。但cargo-expand将帮助您理解wasm-bindgen实际上生成了什么。

打开您的终端,进入项目的根目录,并运行以下命令:

cargo expand --target=wasm32-unknown-unknown > expanded.rs

上述命令将创建一个名为expanded.rs的文件。如果您查看生成的文件,您将看到简单的#[wasm_bindgen]注解是如何改变函数暴露的详细部分的。wasm-bindgen 添加了编译器将 Rust 代码转换为 WebAssembly 模块所需的所有必要元数据。为了加载和运行生成的文件,我们可以使用 webpack 或 Parcel 等打包器。我们将在后面的章节中更详细地了解这些打包器是如何帮助的。现在,让我们看看如何运行和加载生成的文件:

注意

以下设置是常见的,我们将在未来的示例中将其称为“默认”webpack 设置。创建一个webpack-config.js文件来告诉 webpack 如何处理文件。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ],
    mode: 'development'
};

这是一个标准的 webpack 配置文件,包含一个HTMLWebpackPlugin插件。此插件帮助我们生成默认的index.html,而不是手动创建它。

让我们添加一个package.json文件来打包运行 webpack 所需的依赖项:

{
    "scripts": {
        "build": "webpack",
        "serve": "webpack-dev-server"
    },
    "devDependencies": {
        "html-webpack-plugin": "³.2.0",
        "webpack": "⁴.29.4",
        "webpack-cli": "³.1.1",
        "webpack-dev-server": "³.1.0"
    }
}

创建一个index.js文件来加载绑定 JavaScript,它反过来加载生成的 WebAssembly 模块:

import("./hello_world").then(module => {
    console.log(module.hello_world());
});

现在,转到终端并使用以下命令安装 npm 依赖项:

$ npm install

运行webpack-dev-server

$ npm run serve

前往 URL http://localhost:8080,并在浏览器中打开开发者控制台以查看打印的“Hello World”。

摘要

在本章中,我们看到了如何使用rustup安装 Rust。rustup帮助我们安装、更新、删除和切换 Rust 的不同版本。我们看到了rustc是如何工作的,然后使用rustc将 Rust 转换为 WebAssembly。之后,我们探讨了 Rust 的包管理器 Cargo。最后,我们安装了wasm-bindgen,并使用wasm-bindgen将 Rust 代码编译成 WebAssembly 模块。

在下一章中,我们将探讨wasm-pack是什么以及它是如何帮助构建和打包 WebAssembly 模块的。

第八章:第八章:使用 wasm-pack 打包 WebAssembly

JavaScript 无处不在,但无处不在既是优点也是缺点。有许多不同的生态系统,它们有不同的标准和目的。为所有生态系统构建独特的解决方案并不实际。

尽管如此,JavaScript 社区在这里做得非常出色。社区的付出使 JavaScript 成为首选语言之一。对于像 JavaScript 这样多才多艺的语言,总会有些奇怪的地方(当然,每种语言都有)。当您编写 JavaScript 时,这些地方需要额外的关注和注意。

JavaScript 是动态类型的。这使得避免运行时异常变得困难(几乎不可能)。虽然 TypeScript、Flow 和 Elm 试图在 JavaScript 的动态类型上提供(类型化的)超集,但它们无法完全解决根本问题。

任何语言要成长,都必须快速演变,JavaScript 就是这样做的。快速演变而不破坏现有用法也同样重要,JavaScript 提供了 polyfills 来实现向后兼容。

但创建 polyfills 是一项平凡的任务。还有许多其他平凡的任务,例如打包和包装库、压缩包和懒加载库,仅举几例。打包器为大多数这些问题提供了解决方案。它们充当前端编译器。

到目前为止,我们已经看到了 Rust 如何使创建和运行 WebAssembly 模块变得容易。在本章中,我们将探讨 wasm-pack,这是一个使打包和发布 WebAssembly 模块更容易的工具。本章将涵盖以下部分:

  • 使用 webpack 打包 WebAssembly 模块

  • 使用 Parcel 打包 WebAssembly 模块

  • 介绍 wasm-pack

  • 使用 wasm-pack 打包和发布

技术要求

您可以在 GitHub 上找到本章中存在的代码文件,网址为github.com/PacktPublishing/Practical-WebAssembly

使用 webpack 打包 WebAssembly 模块

webpack 是现代 JavaScript 应用程序的静态模块打包器。那么,它做什么呢?

您可以将 webpack 视为前端的一个非正式编译器。webpack 接受一个应用程序的入口点,逐步运行模块,并构建依赖图。依赖图包含所有模块。这些模块对于应用程序的运行是必要的。

一旦构建了依赖图,webpack 就会输出一个或多个包。webpack 非常灵活,帮助我们根据需要打包或包装 JavaScript,webpack 配置中提供了选项。根据提供的选项,webpack 创建输出。

听起来很简单,对吧?

几年前,当我们需要唯一的库就是 jQuery 时,事情很简单。

但由于 JavaScript 的快速演变,现在有很多不同的事情正在发生。底层运行时不尽相同。存在三种不同的浏览器引擎和多种目标。

浏览器引擎的演进速度不同,浏览器支持各种版本的 JavaScript。在某些工作场所的机器上,升级到最新版本的浏览器是被禁止的。这意味着运行的 JavaScript 应用需要在不同的时间进行调整和填充。

基础目标系统需要对您的 JavaScript 代码进行一定的调整才能运行。手动完成所有这些工作将花费很长时间,并且容易出错。

JavaScript 有多种变体,包括 TypeScript 和 CoffeeScript。它们各不相同,但在运行之前都会编译成 JavaScript。基于浏览器的开发需要 CSS、SCSS、SASS 和 LESS。支持所有这些变体并在每次更改后手动编译它们并不是一件容易的事情。

JavaScript 对此的回应是打包器。无论你讨厌它们还是喜欢它们,打包器都能在用 JavaScript 开发时减少负担和混乱。

webpack 为所有这些问题以及更多问题提供了一个解决方案。

webpack 是一个用于打包 JavaScript 应用的工具。它包含加载器和插件,可以帮助转换、添加、删除和操作输出包。webpack 最有趣的部分是其加载器和插件,它们将 webpack 的能力发挥到极致。

加载器允许我们像在 JavaScript 中加载或导入任何其他模块一样加载或导入 Rust、CSS 或 TypeScript 文件。然后 webpack 负责生成支持指定目标环境的包。

插件允许我们优化和管理生成的包。需要注意的是,webpack 完全建立在插件系统之上。

webpack 如何帮助 WebAssembly?

webpack 内部依赖于 webassemblyjs 库。因此,所有使用 webpack 的应用都已经为 WebAssembly 准备就绪。你只需像加载普通 JavaScript 文件一样开始加载 WebAssembly 文件,webpack 就会处理其余部分。

在 webpack 配置中,我们将定义入口点。然后 webpack 加载入口文件。入口文件中的 import 语句根据 JavaScript 的模块解析算法被加载为一个模块。如果导入的模块是 WebAssembly 模块,它将获取模块的内容并将其交给 webassemblyjs 编译器。

编译器负责解析和修改 WebAssembly 模块。

你知道吗?

webassemblyjs 可以直接解析 WebAssembly 文本格式和 WebAssembly 二进制格式。

编译器生成 抽象语法树AST)。生成的 AST 然后进行验证。一旦验证成功,WebAssembly 模块中的任何自定义部分都将被移除。

自定义部分是 WebAssembly 模块内部的一个部分,用户可以在此存储关于 WebAssembly 模块的自定义信息。这些信息可能包括函数和局部变量的名称。浏览器可以使用这些信息来改善调试过程。

webpack 也不支持起始部分。起始部分是 WebAssembly 模块中的一个部分,将在 WebAssembly 模块加载时立即调用。

相反,webpack 创建一个函数,并在 WebAssembly 模块加载后调用它。webassemblyjs 移除了起始部分,并将起始函数转换为 WebAssembly 模块上的普通函数。然后,webpack 负责生成调用该函数的包装器,以便模块加载后立即调用。

最后,webassemblyjs 还负责优化二进制文件,并从 WebAssembly 模块中消除死代码。

webassemblyjs 内置了解释器和 CLI,这使得实验 WebAssembly 模块变得容易。

代码,刷新,重复。

这长期以来一直是 Web 开发的流程。实时重新加载为 Web 开发者提供了额外的帮助。一旦代码保存,实时重新加载可以自动编译和重新加载更改。代码可以在多个设备、因素和方向之间共享。在一个地方进行的交互可以自动与其他设备同步。虽然 Web 提供了一种轻松交付软件的媒介,但它以各种形式存在。这些形式包括功能手机、智能手机、平板电脑、笔记本电脑、计算机、超宽显示器、360 度虚拟世界等等。支持所有或其中一些是一项艰巨的任务。实时重新加载就像一双额外的手。

webpack 为添加实时重新加载到您的应用程序提供了多个选项。它为实时重新加载工具,如 BrowserSync,提供了插件。webpack 生态系统在其配置中也提供了监视模式。

一旦启用,监视模式会查找源文件及其目录中发生的任何更改。一旦检测到更改,它将自动重新编译。但监视模式是为了将输入重新编译为输出。

自动重新加载网页是由一个名为 webpack-dev-server 的库提供的。webpack-dev-server 是一个内存中的 Web 服务器。内容是在内存中生成和放置的,而不是在文件系统中的实际文件中。

此外,webpack-dev-server 还支持热模块替换。这允许服务器仅修补浏览器中的更改,而不是进行完整的页面刷新。

让我们看看如何在 WebAssembly 项目中启用实时重新加载:

  1. 首先,我们将创建一个新的 Rust 项目:

    $ cargo new --lib live_reload
      Created library `live_reload` package
    
  2. 一旦创建了项目,就在您喜欢的编辑器中打开它。为了为项目定义 wasm-bindgen 依赖项,打开 Cargo.toml 文件:

    [package]
    name = "live_reload"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    

首先,添加 [lib] 部分,并添加 crate-type = ["cdylib"]。通过 crate-type 选项,我们指示编译器该库是动态的。之后,将 wasm-bindgen 依赖项添加到 [dependencies] 标签中。

  1. 然后,打开 src/lib.rs 文件,并用以下内容替换其内容:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn hello_world() -> String {
    "Hello World".to_string()
    }
    
  2. 我们将在这里重用前几章中的简单 Hello World 示例。使用以下命令构建 WASM 模块:

    $ cargo build --target wasm32-unknown-unknown
    $ wasm-bindgen target/wasm32-unknown-
      unknown/debug/live_reload.wasm --out-dir .
    
  3. 然后创建一个webpack.config.js文件来指导 webpack 如何处理和编译文件:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-
      plugin');
    
    module.exports = {
        entry: './index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'bundle.js',
        },
        plugins: [
            new HtmlWebpackPlugin(),
        ],
        experiments: {
           syncWebAssembly: true,
        },
        mode: 'development'
    };
    
  4. 添加一个package.json文件来下载 webpack 依赖项:

    {
        "scripts": {
            "build": "webpack",
            "serve": "webpack-dev-server"
        },
        "devDependencies": {
            "html-webpack-plugin": "⁵.5.0",
            "webpack": "⁵.64.1",
            "webpack-cli": "⁴.9.1",
            "webpack-dev-server": "⁴.5.0"
        }
    }
    

    注意

    请使用此处适用的最新版本的依赖项。

  5. 创建一个index.js文件来加载绑定 JavaScript,该 JavaScript 反过来加载生成的 WebAssembly 模块:

    import("./live_reload").then(module => {
        console.log(module.hello_world());
    });
    
  6. 现在,转到终端并使用以下命令安装 npm 依赖项:

    $ npm install
    

使用以下命令运行 webpack-dev-server:

$ npm run serve 

我们已经使用 webpack-dev-server 来启用自动重新编译。现在我们可以去更改 HTML、CSS 或 JavaScript 文件。一旦我们保存更改,webpack 服务器将编译一切。一旦编译完成,更改将在浏览器中反映出来。

但等等,如果你更改 Rust 文件会发生什么?让我们尝试更改它:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello_world() -> String {
"Hello Universe".to_string()
}

我们在main.rs文件中进行了重大更改。是的,我们从world更改为universe;这不是很大吗?但一旦你保存文件,你将不会在浏览器中看到任何更改。事实上,甚至 webpack 编译器也没有重新编译东西。

webpack 编译器默认查找将在 HTML、CSS 和 JavaScript 文件中发生的更改(在配置文件中定义的东西以及包含在依赖图中的东西)。但它对 Rust 代码一无所知。

我们需要以某种方式告诉 webpack 在 Rust 中查找代码更改。我们可以使用一个插件来完成这个任务,该插件将检查指定文件类型的指定位置中的任何更改。然后,它将重新触发构建过程。我们将使用wasm-pack-plugin来完成这个任务。

使用以下命令将wasm-pack-plugin依赖项添加到应用程序中:

$ npm i @wasm-tool/wasm-pack-plugin -D

然后,通过webpack.config.js文件将此插件钩入 webpack 的插件系统:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-
  plugin');

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    plugins: [
        new HtmlWebpackPlugin(),
        new WasmPackPlugin({
                crateDirectory: path.resolve(__dirname)
        }),
    ],
    experiments: {
       syncWebAssembly: true,
    },
    mode: 'development'
};

我们导入wasm-pack-plugin。我们指定包含Cargo.toml文件的 crate 目录,然后插件将负责自动重新加载部分。要看到它的实际效果,让我们使用npm run serve停止并启动 webpack 服务器。

现在,让我们用 Hello Galaxy 编辑src/main.rs文件。打开浏览器以查看控制台日志已更改为Hello Galaxy

那么,这里发生了什么?

wasm-pack-plugin通过 webpack 的插件系统连接到 webpack。这将与 webpack 编译器一起运行。如果src目录中发生任何更改,wasm-pack-plugin将运行wasm-pack编译来自动将 Rust 代码编译成 WebAssembly 模块。这将触发 webpack 编译器的重新编译。一旦 webpack 编译器重新编译,它将通知webpack-dev-server在浏览器中重新加载更改。然后浏览器将自动重新加载更改。

wasm-pack-plugin使得在 webpack 中运行 Rust 和 WebAssembly 变得容易。现在,让我们检查如何使用 Parcel 运行 Rust 和 WebAssembly。

使用 Parcel 打包 WebAssembly 模块

Parcel 是一个快速、零配置的 Web 应用程序打包器index.html),然后从那里构建整个图。

虽然 Webpack 具有基于插件的架构,但 Parcel 具有基于工作进程的架构。这使得 Parcel 比 Webpack 更快,因为它使用多核编译和缓存。

Parcel 还内置了配置以支持 JavaScript、CSS 和 HTML 文件。就像 Webpack 一样,它也有各种插件,我们可以使用这些插件来配置打包器以生成所需的输出。

当需要时,它还内置了对标准 Babel、PostCSS 和 PostHTML 的转换支持。如果需要,我们可以通过插件扩展和更改它们。

Parcel 还具有自动的、开箱即用的热模块替换功能,用于跟踪和记录文件的更改(这些更改由依赖关系图记录)。让我们使用 Parcel 作为打包器来构建 WebAssembly 模块:

  1. 我们将首先创建一个新的 Rust 项目:

    $ cargo new --lib live_reload_parcel
      Created library `live_reload_parcel` package
    

一旦创建项目,就在您最喜欢的编辑器中打开项目。

  1. 要为项目定义wasm-bindgen依赖项,打开Cargo.toml文件:

    [package]
    name = "live_reload_parcel"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    

首先,删除[dependencies]标签,并用上面的粗体行替换它。我们正在告诉编译器,生成的库将是动态的,并且它依赖于wasm-bindgen库。

  1. 然后,我们打开src/lib.rs文件,并将其内容替换为以下内容:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn hello_world() -> String {
    "Hello World".to_string()
    }
    
  2. 我们将重用简单的 Hello World 示例。使用以下命令构建wasm模块:

    $ cargo build --target wasm32-unknown-unknown
    $ wasm-bindgen target/wasm32-unknown-
      unknown/debug/live_reload_parcel.wasm --out-dir .
    
  3. 由于 Parcel 支持零配置,我们所需做的只是将 Parcel 依赖项添加到package.json中:

    {
        "scripts": {
            "build": "parcel index.html",
            "serve": "parcel build index.html"
        },
        "devDependencies": {
            "parcel-bundler": "¹.12.3"
        }
    }
    

由于 Parcel 是零配置打包器,我们只需定义它的入口点。我们在scripts部分定义入口点。serve命令是我们用于开发目的运行代码的命令。当我们定义parcel build index.html时,我们正在通知 Parcel 入口点是index.html

注意

请使用此处适用的最新版本。

  1. 然后,我们将创建入口点。我们将创建一个index.html文件,正如package.json脚本中指定的那样:

    <html>
        <head>
            ...
            <script src="img/index.js"> </script>
        </head>
        <body> ... </body>
    </html>
    
  2. 创建一个index.js文件来加载绑定 JavaScript,它反过来加载生成的 WebAssembly 模块:

    import { module } from './live_reload_parcel.js';
    module.hello_world();
    
  3. 现在,转到终端。运行以下命令来安装依赖项:

    $ npm install
    

使用以下命令运行 Parcel 应用程序:

$ npm run serve

Parcel 的零配置特性使得开始使用 WebAssembly 非常容易。默认情况下,Parcel 支持 .wasm 文件。我们甚至可以像导入任何其他 .js 文件一样导入 .wasm 文件。

重要的是要注意,同步导入 WebAssembly 模块仍然不受支持。但我们可以将导入写为同步导入。内部,Parcel 将生成必要的额外代码,在 JavaScript 执行开始前预加载文件。

这意味着 WebAssembly 文件将是一个单独的包,而不是与打包的 JavaScript 文件一起。

  1. 让我们更改 Rust 文件并看看会发生什么:

     use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn hello_world() -> String {
    "Hello Universe".to_string()
    }
    
  2. 保存文件后,您将看不到任何变化。Parcel 无法得知您已更改源代码,编译器也不会做出反应。

要使 Parcel 对 Rust 源代码更改做出反应,我们需要添加一个插件。该插件是 parcel-plugin-wasm.rs

  1. 要安装插件,我们可以运行以下命令:

    npm install -D parcel-plugin-wasm.rs
    

这将把插件下载到 node_modules。这也会在 package.jsondevDependencies 中保存插件。

  1. 安装完成后,我们需要更改 index.js,使其直接查看源代码,而不是从 Cargo.toml 文件中引用:

    import { hello_world } from './src/lib.rs';
    // import { hello_world } from './Cargo.toml'; 
    hello_world();
    

在这里,我们不是从 WebAssembly 模块导入,而是指定入口 Rust 文件。我们甚至可以指定 Cargo.toml 文件的位置,以便 Parcel 在相应位置查找更改。

  1. 现在,让我们用 Hello Galaxy 编辑 src/main.rs 文件。打开浏览器查看控制台日志如何变为 Hello Galaxy

那么,这里发生了什么?

Parcel 只需要我们应用程序的起点。它将从那里生成依赖图。Parcel 插件会持续查找文件夹中的任何更改。它基本上会查看包含 Cargo.toml 文件的文件夹。Cargo.toml 的位置是通过 index.js 传递给 Parcel 打包器和其插件的。

因此,对 Rust 文件所做的任何更改都将导致以下过程。

当 Rust 文件被保存时,parcel-plugin-wasm.rs 内部的监视器会被触发。然后,parcel-plugin-wasm.rs 将通过 wasm-pack 启动 Rust 代码编译成 WebAssembly 的过程。一旦 wasm-pack 编译并生成新的 WebAssembly 代码,插件将通知 Parcel 编译器依赖图中的某个部分发生了变化。

然后,Parcel 编译器会重新编译,这将导致浏览器刷新。浏览器现在显示更改后的消息。

注意,对于 Parcel,我们实际上使用了同步模块导入,而对于 webpack,我们依赖于异步导入。

parcel-plugin-wasm.rs 插件使得在 Parcel 中运行 Rust 和 WebAssembly 变得容易。现在,让我们看看如何安装和使用 wasm-pack 来打包和发布 WebAssembly 模块。

介绍 wasm-pack

为了与 JavaScript 兼容,基于 Rust 的 WebAssembly 应用程序应该能够完全与 JavaScript 世界互操作。没有这一点,开发者将难以在 JavaScript 中启动他们的 WebAssembly 项目。

节点模块完全改变了 JavaScript 世界的视角。它们使得在浏览器和 Node 环境之间开发和共享模块变得更加容易。世界各地的开发者可以在任何地方和任何时候使用这些库。

wasm-pack 工具旨在成为构建和与 Rust 生成的 WebAssembly 交互的一站式商店,这些 WebAssembly 可以在浏览器或 Node.js 中与 JavaScript 交互。- wasm-pack 网站。github.com/rustwasm/wasm-pack

你为什么需要 wasm-pack?

wasm-pack 使得构建和打包基于 Rust 和 WebAssembly 的项目变得简单。一旦打包,模块就准备好通过 npm 注册表与世界共享——就像那里数百万(甚至数十亿)的 JavaScript 库一样。

如何使用 wasm-pack

wasm-pack 可作为 Cargo 库使用。如果您正在跟随这本书学习,那么您可能已经安装了 Cargo。要安装 wasm-pack,请运行以下命令:

$ cargo install wasm-pack

上述命令将下载、编译并安装 wasm-pack 库。一旦安装完成,wasm-pack 命令将可用。

要检查 wasm-pack 是否正确安装,请运行以下命令:

$ wasm-pack --version
wasm-pack 0.6.0

一旦您安装了 wasm-pack,让我们看看如何使用 wasm-pack 来构建和打包 Rust 和 WebAssembly 项目:

  1. 我们将首先使用 Cargo 生成一个新的项目。要生成项目,请使用以下命令:

    $ cargo new --lib wasm_pack_world
      Created library `wasm_pack_world` package
    

一旦项目创建完成,请使用您最喜欢的编辑器打开它。

  1. 要为项目定义 wasm-bindgen 依赖项,请打开 cargo.toml 文件:

    [package]
    name = "wasm_pack_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    

首先,删除 [dependencies] 标签,并用 wasm-bindgen 库替换它。我们告诉编译器,正在生成的库将是动态的,并且它依赖于 wasm-bindgen 库。

  1. 然后,我们打开 src/lib.rs 文件,并将内容替换为以下内容:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn get_me_universe_answer() -> i32 {
        42
    }
    

再次强调,这是一个简单的函数,它返回一个数字(这是万能的答案)。

以前,我们使用 rustc 或 Cargo 构建 Rust 和 WebAssembly 应用程序。这会产生一个 WebAssembly 二进制文件。但二进制文件本身并没有用;它需要一个绑定文件。使用 wasm-bindgen,我们将生成绑定文件以及 WebAssembly 二进制文件。

这两个步骤是强制性的,但它们很平凡。我们可以用 wasm-pack 替换它们。

  1. 要使用 wasm-pack 构建 WebAssembly 应用程序,请运行以下命令:

    $ wasm-pack build
    

当我们运行 wasm-pack build 时,会发生以下情况:

  1. wasm-pack 首先检查 Rust 编译器是否已安装。如果已安装,那么它会检查 Rust 编译器版本是否大于 1.30。

  2. wasm-pack 检查 crate 配置以及库是否指示我们正在生成动态库。

  3. wasm-pack 验证是否有任何 wasm-target 可用于构建。如果 wasm32-unknown-unknown 目标不可用,wasm-pack 将下载并添加该目标。

  4. 一旦环境准备就绪,wasm-pack 然后开始编译模块并构建 WebAssembly 模块和绑定 JavaScript 文件。

注意,wasm-pack 命令也会生成 package.json 文件。package.json 文件看起来类似于以下内容:

{
"name": "wasm_pack_world",
"collaborators": [
"Sendil Kumar"
],
"version": "0.1.0",
"files": [
     "wasm_pack_world_bg.wasm",
     "wasm_pack_world.js",
     "wasm_pack_world.d.ts"
],
"module": "wasm_pack_world.js",
"types": "wasm_pack_world.d.ts",
"sideEffects": "false"
}
  1. 最后,如果有的话,它会复制 Readme 和 LICENSE 文件,以确保 Rust 和 WebAssembly 版本之间有共享文档。

wasm-pack 还会检查 wasm-bindgen-cli 的存在。如果不存在,将使用 Cargo 进行安装。

  1. 当构建成功完成后,它将创建一个 pkg 目录。在 pkg 目录内,它将通过 wasm-bindgen 将输出重定向:

    pkg
    ├── package.json
    ├── wasm_pack_world.d.ts
    ├── wasm_pack_world.js
    ├── wasm_pack_world_bg.d.ts
    └── wasm_pack_world_bg.wasm
    

现在,这个 pkg 文件夹可以像任何其他 JavaScript 模块一样打包和共享。我们将在未来的菜谱中看到如何实现这一点。

wasm-pack 是一个打包和发布 WebAssembly 模块的强大工具。现在,让我们来看看如何使用它。

使用 wasm-pack 打包和发布

对于库开发者来说,最令人惊叹(当然,也是最重要的)的事情就是打包和发布工件。这就是我们日夜精心制作应用程序,将其发布到世界,接收反馈(无论是负面还是正面),然后根据这些反馈增强应用程序的原因。

任何项目的关键点是其首次发布,这决定了项目的命运。即使它只是一个 MVP,它也会让世界一瞥我们正在做什么,并让我们一瞥未来我们必须做什么。

wasm-pack 帮助我们将基于 Rust 和 WebAssembly 的项目构建、打包和发布到 npm 注册表。我们已经看到了 wasm-pack 如何通过底层的 wasm-bindgen 使将 Rust 构建到 WebAssembly 二进制文件以及绑定 JavaScript 文件变得更加简单。让我们进一步探索我们可以使用其 packpublish 标志做什么。

wasm-pack 提供了一个 pack 标志来打包使用 wasm-pack 构建命令生成的工件。尽管使用 wasm-pack 构建二进制文件不是必需的,但它生成了我们将需要将工件打包成 Node 模块的所有样板代码。

为了使用 wasm-pack 打包构建的工件,我们必须运行以下命令,并参考 pkg(或我们生成构建工件的那个目录):

$ wasm-pack pack pkg

我们也可以通过传递 project_folder/pkg 作为其参数来运行命令。如果没有参数,wasm-pack pack 命令将在它运行的当前工作目录中搜索 pkg 目录。

wasm-pack pack 命令首先确定提供的文件夹是否是 pkg 目录或包含一个 pkg 目录作为其直接子目录。如果检查通过,那么 wasm-pack 将调用底层的 npm pack 命令,将库打包成一个 npm 包。

为了捆绑 npm 包,我们只需要一个有效的 package.json 文件。该文件由 wasm-pack 构建命令生成。

我们可以在之前的菜谱中的 cg-array-world 示例内部运行 pack 命令,并检查会发生什么:

$ wasm-pack pack
npm notice
npm notice 📦 cg-array-world@0.1.0
npm notice === Tarball Contents ===
npm notice 313B package.json
npm notice 32.7kB cg_array_world_bg.wasm
npm notice 135B cg_array_world.d.ts
npm notice 1.6kB cg_array_world.js
npm notice 1.5kB README.md
npm notice === Tarball Details ===
npm notice name: cg-array-world
npm notice version: 0.1.0
npm notice filename: cg-array-world-0.1.0.tgz
npm notice package size: 16.0 kB
npm notice unpacked size: 36.4 kB
npm notice shasum: 243488f1f5a859b60bb34f39146b35ba720dd8ea
npm notice integrity: sha512-9SFuObzEpi254[...]/lkHq6RKSgnNw==
npm notice total files: 5
npm notice
cg-array-world-0.1.0.tgz
| 🎒 packed up your package!

正如你所见,pack 命令在 npm pack 命令的帮助下,创建了一个包含 pkg 文件夹内容的 tarball 包。

一旦我们打包了我们的应用程序,下一步显然就是发布它。为了发布生成的 tarball,wasm-pack 有一个 publish 选项。

为了发布包,我们必须运行以下命令:

$ wasm-pack publish

wasm-pack publish命令将首先检查提供的目录中是否已经存在pkg目录。

如果pkg目录不存在,那么它会询问你是否想要首先创建包:

$ wasm-pack publish
Your package hasn't been built, build it? [Y/n]

如果你回答Y来回答这个问题,那么它会要求你输入你想要生成构建工件文件夹的位置。我们可以给出任何文件夹名称或使用默认值:

$ wasm-pack publish
Your package hasn't been built, build it? yes
out_dir[default: pkg]:

然后,它会询问你的目标,即构建应该生成的目标。你可以在这里选择各种选项,如构建配方中所述:

$ wasm-pack publish
Your package hasn't been built, build it? yes
out_dir[default: pkg]: .
target[default: browser]:
> browser
nodejs
no-modules

根据提供的选项,它将在指定的文件夹中生成工件。

一旦生成工件,它们就准备好使用 npm publish 进行发布了。为了 npm publish 能够正确工作,我们需要进行认证。你可以通过使用 npm login 或wasm-pack login 来对 npm 进行认证。

wasm-pack login命令将调用底层的 npm login 命令然后创建一个会话:

$ wasm-pack login
Username: sendilkumarn
Password: *************
login succeeded.

wasm-pack publish命令还支持两个选项,即以下内容:

  • -a--access用于确定要部署的包的访问级别。

这接受publicrestricted

  • public – 使包公开

  • restricted – 使包内部化

  • -t--target用于支持在构建中产生的各种目标。

因此,wasm-pack使得打包和发布 WebAssembly 二进制文件变得简单。

摘要

在本章中,我们看到了如何使用 webpack 和 Parcel 等打包器运行 WebAssembly 项目。Parcel 和 webpack 使得 JavaScript 开发者能够轻松运行和开发 Rust 和 WebAssembly 项目。然后,我们安装了wasm-pack并使用它来运行项目。最后,我们使用wasm-pack将 WebAssembly 模块打包并发布到 npm。

在下一章中,我们将探讨如何使用wasm-bindgen在 Rust 和 WebAssembly 之间共享复杂对象。

第九章:第九章:Rust 和 WebAssembly 之间的边界跨越

到目前为止,我们只看到了在 JavaScript 和 WebAssembly 之间共享简单数字的示例。在上一节中,我们看到了 wasm-bindgen 如何轻松地将字符串从 Rust 传递到 JavaScript。在本章中,我们将探讨 wasm-bindgen 如何通过 Rust 使在 JavaScript 和 WebAssembly 之间转换更复杂的数据类型变得更加容易。本章将涵盖以下内容:

  • 使用 Rust 与 JavaScript 共享类

  • 使用 JavaScript 与 Rust 共享类

  • 通过 WebAssembly 调用 JavaScript API

  • 通过 WebAssembly 调用闭包

  • 将 JavaScript 函数导入 Rust

  • 通过 WebAssembly 调用 Web API

技术要求

您可以在 GitHub 上找到本章中包含的代码文件,链接为 github.com/PacktPublishing/Practical-WebAssembly/tree/main/09-rust-wasm-boundary

使用 Rust 与 JavaScript 共享类

wasm-bindgen 通过简单的注解使 JavaScript 与 Rust 以及反之亦然共享类变得容易。它处理所有样板代码,例如将值从 JavaScript 转换为 WebAssembly 或从 WebAssembly 转换为 JavaScript,复杂的内存操作以及容易出错的指针算术。因此,wasm-bindgen 使一切变得简单。

让我们看看在 JavaScript 和 WebAssembly(从 Rust)之间共享类有多简单:

  1. 创建一个新的项目:

    $ cargo new --lib class_world
    Created library `class_world` package
    
  2. 为项目定义 wasm-bindgen 依赖项。打开 cargo.toml 文件并添加以下内容:

    [package]
    name = "class_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    [lib]
    crate-type = ["cdylib"]
    [dependencies]
    wasm-bindgen = "0.2.68"
    
  3. 打开 src/lib.rs 文件,并将其内容替换为以下内容:

    use wasm_bindgen::prelude::*;
    #[wasm_bindgen]
    pub struct Point {
        x: i32,
        y: i32,
    }
    #[wasm_bindgen]
    impl Point {
        pub fn new(x: i32, y: i32) -> Point {
            Point { x: x, y: y}
        }
        pub fn get_x(&self) -> i32 {
            self.x
        }
        pub fn get_y(&self) -> i32 {
            self.y
        }
    
        pub fn set_x(&mut self, x: i32) {
            self.x = x;
        }
    
        pub fn set_y(&mut self, y:i32) {
            self.y = y;
        }
    
        pub fn add(&mut self, p: Point) {
            self.x = self.x + p.x;
            self.y = self.y + p.y;
         }
    }
    

    注意

    参数前的 &mut 指定参数(在这种情况下,self)是一个可变引用。

Rust 没有类,但我们可以通过结构体定义一个类。Point 结构体包含获取器、设置器和 add 函数。这是只有添加了 #[wasm_bindgen] 注解的正常 Rust 代码。

注意

函数和结构体被显式标记为 pubpub 修饰符表示函数是公共的,并将被导出。

  1. 使用 Cargo 生成 WebAssembly 模块:

    $ cargo build --target=wasm32-unknown-unknown
    
  2. 使用 wasm-bindgen CLI 生成 WebAssembly 模块对应的绑定文件:

    $ wasm-bindgen target/wasm32-unknown-
      unknown/debug/class_world.wasm --out-dir .
    

这将生成与上一章中看到的类似的绑定文件和类型定义文件。让我们首先看看 class_world.js 文件。此文件将与之前章节中生成的文件类似,除了 Point 类。Point 类中包含所有获取器、设置器和 add 函数。这些函数使用它们的引用指针。

此外,wasm-bindgen 生成一个名为 __wrap 的静态方法,它创建 Point 类对象并将其指针附加到它。它添加了一个自由方法,该方法反过来调用 WebAssembly 模块内的 __wbg_point_free 方法。此方法负责释放 Point 对象或类占用的内存。

创建以下文件。我们将在其他部分也使用它们:

  1. 创建webpack.config.js。它包含 webpack 配置:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-
      plugin');
    module.exports = {
        entry: './index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'bundle.js',
        },
        plugins: [
            new HtmlWebpackPlugin(),
        ],
        mode: 'development'
    };
    
  2. 创建package.json并添加以下内容:

    {
        "scripts": {
            "build": "webpack",
            "serve": "webpack-dev-server"
        },
        "dependencies": {
            "html-webpack-plugin": "³.2.0",
            "webpack": "⁴.41.5",
            "webpack-cli": "³.3.10",
            "webpack-dev-server": "³.10.1"
        }
    }
    
  3. 创建一个index.js文件:

    $ touch index.js
    
  4. 然后,再次运行npm install。用以下内容修改index.js

    import("./class_world").then(({Point}) => {
    const p1 = Point.new(10, 10);
    console.log(p1.get_x(), p1.get_y());
    const p2 = Point.new(3, 3);
    p1.add(p2);
    console.log(p1.get_x(), p1.get_y());
    });
    

我们在Point类中调用新方法,并传递xy参数。我们打印xy坐标。这将打印10, 10。然后,我们将创建另一个点(p2)。最后,我们调用add函数,并传递点p2。这将打印13, 13

  1. 获取器方法使用指针从共享数组中获取值:

    get_x() {
        return wasm.point_get_x(this.ptr);
    }
    
  2. 在设置器方法中,我们传递指针和值。由于我们在这里只是传递一个数字,所以不需要额外的转换:

    set_x(arg0) {
        return wasm.point_set_x(this.ptr, arg0);
    }
    
  3. add函数的情况下,我们获取参数,获取Point对象的指针,并将其传递给 WebAssembly 模块:

    add(arg0) {
        const ptr0 = arg0.ptr;
        arg0.ptr = 0;
        return wasm.point_add(this.ptr, ptr0);
    }
    

wasm-bindgen使将类转换为 WebAssembly 模块变得简单。我们已经看到了如何在 Rust 中与 JavaScript 共享一个类。现在,我们将看到如何从 JavaScript 与 Rust 共享一个类。

使用 JavaScript 与 Rust 共享类

使用#[wasm_bindgen],JavaScript 类与 Rust 的共享也变得简单。让我们看看如何实现它。

JavaScript 类是具有一些方法的对象。Rust 是一种强类型语言。这意味着 Rust 编译器需要具体的绑定。如果没有它们,编译器会报错,因为它需要了解对象的生命周期。我们需要一种方法来确保编译器在运行时可以访问这个 API。

外部 C 函数块在这里很有帮助。extern C 使得函数名在 Rust 中可用。

在这个例子中,让我们看看如何将 JavaScript 中的类与 Rust 共享:

  1. 让我们创建一个新的项目:

    $ cargo new --lib class_from_js_world
    Created library `class_from_js_world` package
    
  2. 为项目定义wasm-bindgen依赖项。打开cargo.toml文件,并添加以下内容:

    [package]
    name = "class_from_js_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    [lib]
    crate-type = ["cdylib"]
    [dependencies]
    wasm-bindgen = "0.2.68"
    

请将上一节中的package.jsonindex.jswebpack-config.js复制过来。然后,运行npm install

  1. 打开src/lib.rs文件,并用以下内容替换其内容:

    use wasm_bindgen::prelude::*;
    #[wasm_bindgen(module = "./point")] . // 1
    extern "C" {
         pub type Point; // 2
    
        #[wasm_bindgen(constructor)] //3
        fn new(x: i32, y: i32) -> Point;
    
        #[wasm_bindgen(method, getter)] //4
        fn get_x(this: &Point) -> i32;
    
        #[wasm_bindgen(method, getter)]
        fn get_y(this: &Point) -> i32;
    
        #[wasm_bindgen(method, setter)] //5
        fn set_x(this: &Point, x:i32) -> i32;
    
        #[wasm_bindgen(method, setter)]
        fn set_y(this: &Point, y:i32) -> i32;
    
        #[wasm_bindgen(method)] // 6
        fn add(this: &Point, p: Point);
    }
    
    #[wasm_bindgen]
    fn get_precious_point() -> Point { //7
        let p = Point::new(10, 10);
        let p1 = Point::new(3, 3);
        p.add(p1); // 8
        p
    }
    

//1处,我们正在导入 JavaScript 模块。这将导入一个 JavaScript 文件,point.js。注意,这个文件应该位于与Cargo.toml相同的目录中。然后,我们创建一个 extern C 块来定义我们需要使用的方法。

我们首先在块中声明一个类型(pub type Point;)。现在,我们可以在 Rust 代码中像使用任何其他类型一样使用它。之后,我们定义一系列函数。我们首先定义构造函数。我们将构造函数作为参数传递给#[wasm_bindgen]注解。定义一个接受参数并返回之前声明的类型的函数。这将绑定到 Point 类型的命名空间,我们可以在 Rust 函数内部调用Point::new(x, y);

然后,我们定义获取器和设置器(分别对应//4//5)。我们甚至可以定义一个方法;这些与 JavaScript 侧上的函数类似。然后,我们有add函数。

注意

外部 C 块内的所有函数都是完全类型化的。

最后,我们使用 #[wasm_bindgen] 注解导出 get_precious_point() 函数。在 get_precious_point 函数中,我们使用 Point::new(x, y) 创建两个 Point,然后使用 p1.add(p2) 添加两个点。

我们可以像之前一样从 JavaScript 中调用它。我们还需要在 JavaScript 端定义一个 Point 类。

  1. 使用以下内容创建 Point.js

    export class Point {
        constructor(x, y) {
            this.x = x;
            this.y = y;
        }
    
        get_x() {
            return this.x;
        }
    
        get_y() {
            return this.y;
        }
    
        set_x(x) {
            this.x = x;
        }
    
        set_y(y) {
            this.y = y;
        }
    
        add(p1) {
            this.x += p1.x;
            this.y += p1.y;
        }
    }
    
  2. 最后,用以下内容替换 index.js

    import("./class_from_js_world").then(module => {
        console.log(module.get_precious_point());
    });
    
  3. 现在,运行以下命令以启动服务器:

    $ npm run serve
    
  4. 打开浏览器并运行 http://localhost:8000。打开开发者控制台以查看打印的对象类。

  5. 让我们看看 #[wasm_bindgen] 宏是如何扩展代码的:

    $ cargo expand --target=wasm32-unknown-unknown >
      expanded.rs
    

这里发生了一些有趣的事情。

首先,type 点被转换成一个结构体。这与我们在上一个示例中所做的是类似的。但是,结构体的成员是 JsValue 而不是 xy。这是因为 wasm_bindgen 不会知道这个 Point 类正在实例化什么。因此,它创建一个 JavaScript 对象并将其作为成员:

pub struct Point {
    obj: ::wasm_bindgen::JsValue,
}

它还定义了如何构造 Point 对象以及如何解引用它。这对于 WebAssembly 运行时知道何时分配和何时解引用它是有用的。

所定义的所有方法都被转换成 Point 结构体的实现。正如你所看到的,方法声明中有大量的 unsafe 代码。这是因为 Rust 代码直接与原始指针交互:

fn new(x: i32, y: i32) -> Point {
#[link(wasm_import_module =
  "__wbindgen_placeholder__")]
extern "C" {
fn __wbg_new_3ffc5ccd013f4db7(x:<i32 as
 ::wasm_bindgen::convert::IntoWasmAbi>::Abi, y:<i32 as
 ::wasm_bindgen::convert::IntoWasmAbi>::Abi) -> <Point
 as ::wasm_bindgen::convert::FromWasmAbi>::Abi;
}

unsafe {
let _ret = {
let mut __stack =
  ::wasm_bindgen::convert::GlobalStack::new();
let x = <i32 as
  ::wasm_bindgen::convert::IntoWasmAbi>::into_abi
  (x, &mut __stack);
let y = <i32 as
  ::wasm_bindgen::convert::IntoWasmAbi>::into_abi
  (y, &mut __stack);
__wbg_new_3ffc5ccd013f4db7(x, y)
};
<Point as
 ::wasm_bindgen::convert::FromWasmAbi>::from_abi(_ret,
 &mut ::wasm_bindgen::convert::GlobalStack::new())
}
}

在前面的代码中展示了由 #[wasm_bindgen(constructor)] 宏生成的代码。它首先将代码与 extern C 块链接起来。然后,参数被转换,以便在 WebAssembly 中推断。

然后,我们有 unsafe 块。首先,在全局栈中预留空间。然后,xy 都被转换成 IntoWasmAbi 类型。

IntoWasmAbi 是一个 trait,用于任何可以转换为可以直接跨越 WebAssembly ABI 的类型的任何东西,例如 u32 或 f64。然后,调用 JavaScript 中的函数。然后,使用 FromWasmAbi 将返回值转换成 Point 类型。

FromWasmAbi 是一个 trait,用于任何可以从 WebAssembly ABI 边界恢复值的任何东西;例如,Rust u8 可以从 WebAssembly ABI u32 类型中恢复。

我们已经看到了如何使用 Rust 与 JavaScript 共享一个类。现在,我们将看到我们如何在 Rust 中调用 JavaScript API。

通过 WebAssembly 调用 JavaScript API

JavaScript 提供了丰富的 API 来处理对象、数组、映射、集合等。如果我们想在 Rust 中使用或定义它们,那么我们需要提供必要的绑定。手工制作这些绑定将是一个巨大的过程。但是,如果我们已经有了这些 API 的绑定呢?这是一个既适用于 Node.js 也适用于浏览器环境的通用 API,它将创建一个平台,我们可以在这个平台上完全用 Rust 编写代码,并使用 wasm_bindgen 来创建必要的代码。

rustwasm 团队对此的答案是 js-sys crate。

js-sys 包包含所有由 ECMAScript 标准保证在每一个 JavaScript 环境中存在的全局 API 的原始 #[wasm_bindgen] 绑定。 – RustWASM

它们提供了对 JavaScript 的标准内置对象的绑定,包括它们的方法和属性。

在这个例子中,让我们看看如何通过 WebAssembly 调用 JavaScript API:

  1. 使用 cargo new 命令创建一个默认项目:

    $ cargo new --lib jsapi
    
  2. 按照上一个示例,复制 webpack.config.jsindex.jspackage.json。然后,在您最喜欢的编辑器中打开生成的项目。

  3. 修改 Cargo.toml 的内容:

    [package]
    name = "jsapi"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.68"
    js-sys = "0.3.45"
    
  4. 现在,打开 src/lib.rs 并将其替换为以下内容。我们可以在 Rust 中使用以下代码片段创建一个 JavaScript 映射:

    use wasm_bindgen::prelude::*;
    
    use js_sys::Map;
    
    #[wasm_bindgen]
    pub fn new_js_map() -> Map {
        Map::new()
    }
    

wasm_bindgen 导入中,我们使用 use js_sys::Map;js_sys 包中导入了 map。

  1. 然后,我们定义 new_js_map 函数,它将返回一个新的映射:

    #[wasm_bindgen]
    pub fn set_get_js_map() -> JsValue {
        let map = Map::new();
        map.set(&"foo".into(), &"bar".into());
        map.get(&"foo".into())
    }
    

set_get_js_map 函数创建一个新的映射,在映射中设置一个值,然后返回设置的值。

注意,这里的返回类型是 JsValue。这是 Rust 中用于指定 JavaScript 值的包装器。另外,请注意,我们将字符串传递给 trait 函数 get 和 set。当在 JavaScript 中调用时,这将返回 bar 作为输出。

  1. 现在,我们也在 Rust 代码中使用 for_each 运行映射,如下所示:

    #[wasm_bindgen]
    pub fn run_through_map() -> f64 {
        let map = Map::new();
        map.set(&1.into(), &1.into());
        map.set(&2.into(), &2.into());
        map.set(&3.into(), &3.into());
        map.set(&4.into(), &4.into());
        map.set(&5.into(), &5.into());
        let mut res: f64 = 0.0;
    
        map.for_each(&mut |value, _| {
            res = res + value.as_f64().unwrap();
        });
    
        res
    }
    

这创建了一个映射,然后使用值 12345 加载映射。然后,它遍历创建的映射并添加值。这将产生输出 15(即 1 + 2 + 3 + 4 + 5)。

  1. 最后,我们将 index.js 替换为以下内容:

    import("./jsapi").then(module => {
        let m = module.new_js_map();
        m.set("Hi", "Hi");
        console.log(m); // prints Map { "Hi" ->  "Hi" }
        console.log(module.set_get_js_map());  // prints
          "bar"
        console.log(module.run_through_map()); // prints
          15
    });
    

在浏览器上运行此代码将打印结果。请参阅控制台日志附近的注释。

让我们从生成的 JavaScript 绑定文件开始。生成的绑定 JavaScript 文件的结构几乎与上一节相同,但导出了更多函数。

堆对象在这里用作栈。所有与 WebAssembly 模块共享或引用的 JavaScript 对象都存储在这个堆中。还重要的是要注意,一旦访问了值,它就会从堆中弹出。

function takeObject(idx) {
    const ret = getObject(idx);
    dropObject(idx);
    return ret;
}

takeObject 函数用于从堆中获取对象。它首先获取给定索引处的对象。然后,它从该堆索引(即弹出)中移除对象。最后,它返回值 ret

同样,我们可以在 Rust 中使用 JavaScript API。仅针对常见的 JavaScript API(包括 Node.js 和浏览器)生成绑定。

我们已经看到了如何在 Rust 中调用 JavaScript API。现在,我们将看到如何通过 WebAssembly 调用 Rust 闭包。

通过 WebAssembly 调用闭包

官方的 Rust 书籍将闭包定义为如下:

闭包是无名函数,你可以将其保存到变量中,或者将其作为参数传递给其他函数。- 《Rust 编程语言》(涵盖 Rust 2018)由 Steve Klabnik 和 Carol Nichols 编著 (doc.rust-lang.org/book/ch13-00-functional-features.html)

MDN 将 JavaScript 中的闭包定义为如下:

闭包是函数和其声明时所处词法环境的组合。- MDN Web 文档 (developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#closure)

通常,闭包是自包含的功能块,可以在代码中传递和使用。它们可以捕获并存储定义它们上下文中的变量引用。

闭包和函数相似,除了一个细微的区别。闭包会在第一次创建时捕获状态。然后,每次调用闭包时,它都会覆盖这个捕获的状态。

闭包是有状态的函数。当你创建闭包时,它会捕获状态。然后,我们可以像传递任何其他函数一样传递闭包。当闭包被调用时,它会覆盖这个捕获的状态并执行(即使闭包在捕获状态之外被调用)。这是闭包在 JavaScript 函数式编程方面使用越来越多的一个重要原因。

闭包使得数据封装、高阶函数和记忆化变得容易。(听起来像是函数式编程,对吧? 😉)

让我们看看如何从 JavaScript 到 Rust 以及从 Rust 到 JavaScript 共享闭包:

  1. 创建一个新的项目:

    $ cargo new --lib closure_world
         Created library `closure_world` package
    
  2. 为项目定义 wasm-bindgen 依赖项。让我们打开 cargo.toml 文件,并添加加粗的内容:

    [package]
    name = "closure_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    js-sys = "0.3.15"
    

我们需要 js-sys 包来将闭包从 JavaScript 复制到 Rust。请从上一节复制 package.jsonindex.jswebpack-config.js。然后,运行 npm install

  1. 然后,我们打开 src/lib.rs 文件,并添加来自我们的 Point 类示例的内容,以及一个接受 JavaScript 闭包函数作为参数的额外方法:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub struct Point {
        x: i32,
        y: i32,
    }
    
    #[wasm_bindgen]
    impl Point {
        pub fn new(x: i32, y: i32) -> Point {
            Point { x: x, y: y}
        }
    
        pub fn get_x(&self) -> i32 {
            self.x
        }
    
        pub fn get_y(&self) -> i32 {
            self.y
        }
    
        pub fn set_x(&mut self, x: i32) {
            self.x = x;
        }
    
        pub fn set_y(&mut self, y:i32) {
            self.y = y;
        }
    
        pub fn add(&mut self, p: Point) {
            self.x = self.x + p.x;
            self.y = self.y + p.y;
         }
    
        pub fn distance(&self, js_func: js_sys::Function)
          -> JsValue {
            let this = JsValue::NULL;
            let x = JsValue::from(self.x);
            let y = JsValue::from(self.y);
            js_func.call2(&this, &x, &y).unwrap()
        }
    }
    

现在,我们将更改 index.js 以使用闭包调用 distance 函数:

import("./closure_world").then(({Point}) => {
     const p1 = Point.new(13, 10);
     console.log(p1.distance((x, y) => x - y));
});

让我们使用 npm run serve 启动 webpack 服务器。这将打印出 3

js-sys 包提供了一种使用 apply 和 call 方法调用 JavaScript 函数的选项。这正是我们通过调用 js_func.call2(&this, &x, &y) 来实现的。

Rust 没有函数重载。这意味着我们必须根据传递的参数数量使用不同的方法名。因此,js-sys 为我们提供了 call1call2call3 等等,分别接受 123 等等个参数。

在 Rust 中调用 JavaScript 函数将返回 Result<JsValue, Error>。我们将展开结果以获取 JsValue 并返回它。wasm-bindgen 将创建必要的绑定,以便将值作为数字在 JavaScript 中返回。

另一方面,将闭包从 Rust 传递到 JavaScript 需要一些额外的信息和选项。

wasm-bindgen 在这里支持两种变体:

  • 堆栈生命周期闭包

  • 堆分配的闭包

让我们看看它们实际上意味着什么:

  • 一旦将闭包传递给导入的 JavaScript 函数后,堆栈生命周期闭包不应再次被 JavaScript 调用。这是因为一旦函数(闭包)返回,Rust 将使闭包失效。任何未来的调用都将导致异常。换句话说,堆栈生命周期闭包是短暂的,一旦被访问,它们就会超出上下文。

  • 另一方面,堆分配的闭包对于多次调用内存非常有用。在这里,其有效性与 Rust 中闭包的生命周期相关联。一旦 Rust 中的闭包被丢弃,闭包将进行解分配,垃圾回收器将进行垃圾回收。这将反过来使 JavaScript 中的闭包(函数)失效。一旦失效,任何进一步尝试访问闭包或内存的操作都将引发异常。

堆栈生命周期和堆分配的闭包都支持 FnFnMut 闭包、参数和返回值。

我们已经看到了如何调用闭包函数。现在,我们将了解如何将 JavaScript 中的函数导入 Rust。

将 JavaScript 函数导入 Rust

在某些地方,JavaScript 比 WebAssembly 快,因为没有边界跨越和实例化单独运行时环境的开销。JavaScript 在其自身环境中运行得更自然。

JavaScript 生态系统非常庞大。有成千上万的库是用 JavaScript 创建并经过实战检验的(当然,并非全部),这使得 JavaScript 变得容易(这里的“容易”是主观的)。

WebAssembly 解决了前端世界最重要的一个问题,即“一致”的性能问题。但它并不是 JavaScript 的完全替代品。WebAssembly 帮助 JavaScript 提供更好和更一致的性能。

JavaScript 将在大多数地方成为默认选择。提供允许两个系统无缝集成的生态系统很重要。我们已经看到了如何将 JavaScript 中的类导入 Rust。同样,我们可以使用 wasm-bindgen 将任何内容从 JavaScript 导入 Rust。最重要的是,我们可以在 Rust 代码中更自然地使用这些导入的 JavaScript 函数。

在这个例子中,让我们看看如何将 JavaScript 函数导入 Rust:

  1. 创建一个新的项目:

    $ cargo new --lib import_js_world
         Created library `import_js_world` package
    
  2. 为项目定义 wasm-bindgen 依赖项。让我们打开 cargo.toml 文件,并添加以下加粗内容:

    
    [package]
    name = "import_js_world"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    
  3. 请将上一节中的 package.jsonindex.jswebpack-config.js 复制过来。然后,运行 npm install。接着,打开 src/lib.rs 文件,并将其内容替换为以下内容:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen(module = "./array")]
    extern "C" {
        fn topArray() -> f64;
        fn getNumber() -> i32;
        fn lowerCase(str: &str) -> String;
    }
    
    #[wasm_bindgen]
    pub fn sum_of_square_root() -> f64 {
        let n = getNumber();
        let mut sum = 0;
    
        for _ in 0..n {
            sum = sum + (topArray().sqrt() as i64);
        } 
        sum
    }
    
    #[wasm_bindgen]
    pub fn some_string_to_share() -> String {
        lowerCase("HEYA! I AM ALL CAPS")
    }
    

我们首先导入wasm_bindgen库。然后,我们定义 extern C 块来定义 FFI 函数(即我们从 JavaScript 中导入的函数)。在 extern C 块内部,我们定义与 Rust 编译器理解相似的函数签名。我们还使用#[wasm_bindgen(module = "./array")]来注释 extern C 块。这有助于wasm-bindgen CLI 理解函数的定义和导出位置。它将使用这些信息并创建必要的链接。

  1. array.js 文件与 cargo.toml 文件位于同一目录下。我们将如下定义 array.js

    let someGlobalArray = [1, 4, 9, 16, 25];
    
    export function getNumber() {
        return someGlobalArray.length;
    }
    
    export function topArray() {
        return someGlobalArray.sort().pop();
    }
    
    export lowerCase(str) {
        return str.toLowerCase();
    }
    

之前提到的功能应该在 JavaScript 文件中导出。

我们然后在 Rust 中声明一个函数(sum_of_square_root),并将其作为生成的 WebAssembly 模块中的函数导出。我们首先从 JavaScript 中调用 getNumber() 方法。我们使用返回值,然后运行 for 循环遍历数组的长度。对于每个循环,我们调用 topArray 从数组中获取最小元素。然后,我们取这个数字的平方根(这在 Rust 代码中发生)。将它们加起来并返回总和(例如我们之前看到的 15)。

  1. 我们将用以下内容替换index.js

    import("./import_js_world").then(module => {
        console.log(module.sum_of_square_root());
        console.log(module.some_string_to_share());
    });
    
  2. 打开生成的绑定 JavaScript 文件。你会发现,getNumbertopArray 函数在生成的绑定 JavaScript 文件中不可用。这主要是因为我们只是在 JavaScript 和 WebAssembly 模块之间共享数字。因此,在这种情况下,边界跨越更加自然发生。

  3. wasm-bindgen 还会根据内存对象字节数进行必要的移位和解析字节缓冲区。对于 Uint32Array,指针和内存的计算如下:

    const rustptr = mem[retptr / 4];
    const rustlen = mem[retptr / 4 + 1];
    
  4. 对于 BigInt64Array,指针和内存的计算如下:

    const rustptr = mem[retptr / 8];
    const rustlen = mem[retptr / 8 + 1];
    

我们已经了解了如何在 Rust 中导入 JavaScript 函数。现在,我们将看看如何在 Rust 中调用 web API。

通过 WebAssembly 调用 Web API

网络的发展是惊人的,其增长归功于其开放标准。今天,网络提供了数百个 API,这使得网络开发者能够轻松地为音频、视频、画布、SVG、USB、电池等开发。

网络是普遍存在的。它不断地被实验和改变,以使其对开发者和公司来说既吸引人又易于使用。web-sys包提供了对目前网络上几乎所有 API 的访问。

web-sys包提供了对 Web 所有 API 的原始绑定:从 DOM 操作到 WebGL、Web Audio、计时器、fetch 等! – web-sys crates.io (crates.io/crates/web-sys)

WebIDL 接口定义被转换为wasm-bindgen的内部抽象语法树ASTs)。然后,这些 ASTs 被用来创建零开销的 Rust 和 JavaScript 粘合代码。

在这个绑定代码的帮助下,我们可以调用和操作网络 API。将网络 API 转换为 Rust 确保了参数和返回值的类型信息被正确且安全地处理。

在这个例子中,让我们通过 WebAssembly 调用一个网络 API:

  1. 使用cargo new命令创建一个默认项目:

    $ cargo new --lib web_sys_api
        Created library `web_sys_api` package
    
  2. webpack.config.jsindex.jspackage.json的内容与jsapi部分(在上面的部分)类似地复制过来。现在,我们将打开生成的项目到我们最喜欢的编辑器中。让我们更改cargo.toml的内容:

    [package]
    name = "web_sys_api"
    version = "0.1.0"
    authors = ["Sendil Kumar"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.38"
    
    [dependencies.web-sys]
    version = "0.3.4"
    features = [
        'Document',
        'Element',
        'HtmlElement',
        'Node',
        'Window',
    ]
    

这里的主要区别在于,我们不仅定义了依赖及其版本,还定义了我们将在此示例中使用的功能。

我们为什么需要它?由于网络生态系统中存在大量的 API,我们不希望携带所有这些 API 的绑定。绑定文件仅用于列出的功能。

  1. 让我们打开src/lib.rs并将文件替换为以下内容:

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn draw(percent: i32) -> Result<web_sys::Element,
      JsValue> {
        let window = web_sys::window().unwrap();
        let document = window.document().unwrap();
    
        let div = document.create_element("div")?;
        let ns = Some("http://www.w3.org/2000/svg");
    
        div.set_attribute("class", "pie")?;
    
        let svg = document.create_element_ns( ns, "svg")?;
        svg.set_attribute("height", "100")?;
        svg.set_attribute("width", "100")?;
        svg.set_attribute("viewBox", "0 0 32 32")?;
    
        let circle = document.create_element_ns(ns,
          "circle")?;
        circle.set_attribute("r", "16")?;
        circle.set_attribute("cx", "16")?;
        circle.set_attribute("cy", "16")?;
        circle.set_attribute("stroke-dasharray",
          &(percent.to_string().to_owned() +" 100"))?;
    
        svg.append_child(&circle)?;
    
        div.append_child(&svg)?;
    
        Ok(div)
    }
    

我们首先使用web_sys::window()获取窗口。最后的 unwrap 确保窗口可用。如果不可用,它将抛出一个错误。之后,我们从窗口对象中获取文档。然后,我们使用document.createElement创建一个div元素。然后,我们创建一个 SVG 和圆形文档元素,并将圆形添加到 SVG 元素中。最后,我们将 SVG 作为子元素添加到div元素中,并返回div元素。

API 与 Web API 非常相似,只是方法名使用的是蛇形命名法而不是驼峰命名法。

  1. 我们将更改index.js以使用此元素作为 Web 组件:

    import("./web_sys_api").then(module => {
        class Pie extends HTMLElement {
            constructor() {
                super();
                let shadow = this.attachShadow({ mode:
                  'open' });
                let style =
                  document.createElement('style');
    
                style.textContent = `
                        svg {
                            width:100px;
                            height: 100px;
                            background: yellowgreen;
                            border-radius: 50%;
                        }
    
                        circle {
                            fill: yellowgreen;
                            stroke: #655;
                            stroke-width: 32;
                        }`;
    
               shadow.appendChild(module.draw(this.
               getAttribute
               ('value'));
               shadow.appendChild(style);
           }
       }
    
        customElements.define('pie-chart', Pie);
    
        setInterval(() => {
            let r = Math.floor(Math.random() * 100);
            document.getElementsByTagName('body')[0].
              innerHTML = `
                <pie-chart value='${r}' />`;
        }, 1000);
    });
    

那么,我们在这里做了什么?我们首先导入绑定文件,这将反过来初始化 WebAssembly 模块。一旦 WebAssembly 模块初始化完成,我们创建一个扩展 HTML 元素的Pie类。在类的构造函数中,我们调用super方法。然后,我们创建一个阴影 DOM。我们在阴影 DOM 中添加一个样式元素,然后定义元素的样式。

我们继续将样式元素附加到阴影元素上,然后添加从 Rust 代码导出的元素。然后我们将其注册为名为pie-chart的自定义元素。最后,我们将自定义元素附加到文档的主体中,以查看饼图被显示出来。

  1. 现在,运行以下命令:

    $ npm run serve
    

打开浏览器查看饼图。

摘要

在本章中,我们看到了wasm-bindgen如何使 JavaScript 和 Rust 之间共享复杂对象变得容易。注释使得标记一个函数以导出/导入 JavaScript 和 WebAssembly 变得简单。我们还看到了如何使用 js-sys 和 web-sys Cargo 在 Rust 代码中轻松调用 JavaScript 和 Web API。

在下一章中,我们将看到如何在 Rust 中优化生成的 WebAssembly 模块。

第十章:第十章:优化 Rust 和 WebAssembly

到目前为止,我们已经看到了 Rust 如何使创建和运行 WebAssembly 模块变得容易,以及 Rust 社区提供的各种工具。在本章中,我们将涵盖以下部分:

  • 最小化 WebAssembly 模块

  • 分析 WebAssembly 模块中的内存模型

  • 使用 Twiggy 分析 WebAssembly 模块

技术要求

您可以在 GitHub 上找到本章中存在的代码文件,地址为github.com/PacktPublishing/Practical-WebAssembly

最小化 WebAssembly 模块

wasm-bindgen是一个完整的套件,为 WebAssembly 模块生成绑定 JavaScript 文件(包括 polyfills)。在前几章中,我们看到了wasm-bindgen如何提供库,并使得在 JavaScript 和 WebAssembly 之间传递复杂对象变得容易。但在 WebAssembly 的世界里,优化生成的二进制文件的大小和性能非常重要。

让我们看看我们如何进一步优化 WebAssembly 模块:

  1. 使用所有必要的工具链创建 WebAssembly 应用程序:

    $ npm init rust-webpack wasm-rust
    🦀 Rust + 🕸 WebAssembly + Webpack = ❤
    

此前命令创建了一个新的基于 Rust 和 JavaScript 的应用程序,webpack 作为打包器。

  1. 进入生成的wasm-rust目录:

    cd wasm-rust
    

Rust 源文件位于src目录中,JavaScript 文件位于js目录中。我们已经为运行应用程序配置了 webpack。

  1. src/lib.rs中删除所有代码,并用以下内容替换:

    use wasm_bindgen::prelude::*;
    
    #[cfg(feature = "wee_alloc")]
    #[global_allocator]
    static ALLOC: wee_alloc::WeeAlloc =
      wee_alloc::WeeAlloc::INIT;
    #[wasm_bindgen]
    pub fn is_palindrome(input: &str) -> bool {
        let s = input.to_string().to_lowercase();
        s == s.chars().rev().collect::<String>()
    }
    

我们导入wasm_bindgen并启用wee_alloc,它进行更小的内存分配。

我们继续定义is_palindrome函数,它接受&str作为输入并返回bool。在这个函数内部,我们检查给定的字符串是否是回文。

注意

users.rust-lang.org/t/whats-the-difference-between-string-and-str/10177/9了解更多关于&strString之间的区别。

  1. 现在,从js/index.js中删除所有行,并用以下内容替换:

    const rust = import('../pkg/index.js');
    rust.then(module => {
        console.log(module.is_palindrome('tattarrattat'));
      // returns true
    });
    

    注意

    我们在这里从../pkg/index.js导入。wasm-pack命令将在pkg文件夹内生成binding文件和wasm文件。

  2. 接下来,使用以下命令构建应用程序:

    $ npm run build
    // comments, logs are elided
       Asset     Size   Chunks    Chunk Names
       0.js   9.84 KiB       0  [emitted]
       0fd5cbc32a547ac3295c.module.wasm    115 KiB       0
         [emitted] [immutable]
       index.html  179 bytes          [emitted]
       index.js    901 KiB   index  [emitted]
         index
    

您可以使用npm run start命令运行应用程序。此命令打开浏览器并加载应用程序。

  1. 现在,打开开发者工具并检查控制台中的日志。

Rust 编译器生成的 WebAssembly 模块并未完全优化。我们可以进一步优化 WebAssembly 模块。在 JavaScript 的世界里,每个字节都很重要。

  1. 现在,打开Cargo.toml并添加以下内容:

    [profile.dev]
    opt-level = 'z'
    lto = true
    debug = false
    

此外,完全删除[profile.release]部分。[profile.dev]部分指导编译器如何对 dev 构建中生成的代码进行性能分析。[profile.release]部分仅用于发布构建。

我们指示编译器使用opt-level = z来生成代码。opt-level设置类似于 LLVM 编译器的-O1/2/3/...

opt-level设置的合法选项如下:

  • 0 – 没有优化;同时开启cfg(debug_assertions)

  • 1 – 基本优化

  • 2 – 一些优化

  • 3 – 所有优化

  • s – 优化二进制大小

  • z – 优化二进制大小,但关闭循环向量化

LLVM 支持链接时优化,通过使用整个程序分析来更好地优化代码。但链接时优化是以更长的链接时间为代价的。我们可以通过使用 lto 选项来启用 LLVM 的链接时优化。

LTO 支持以下选项:

  • false – 执行“thin local LTO”。这意味着链接时优化仅在本地 crate 上完成。注意:当 Codegen 单元数量为 1 或opt-level为 0 时,将不会进行链接时优化。

  • true或“fat” – 执行“fat”LTO。这意味着链接时优化是在依赖图中的所有 crates 上完成的。

  • thin – 执行thinLTO。这是“fat”的一个更快版本,优化速度更快。

  • off – 禁用 LTO(链接时优化)。

  1. 接下来,运行npm run build

    $ npm run build
    // comments, logs are elided
       Asset   Size   Chunks   Chunk Names
       0.js  9.84 KiB     0  [emitted]
       b5e867dd3d25627d7122.module.wasm  50.8 KiB       0
         [emitted] [immutable]
       index.js   901 KiB   index  [emitted]
         index
    

生成的 WebAssembly 二进制文件大小为 50.8 KB。生成的二进制文件大小减少了约 44%。这对我们来说是一个巨大的胜利。我们可以使用 Binaryen 的wasm-opt工具进一步优化二进制文件:

$ /path/to/build/directory/of/binaryen/wasm-opt -Oz
  b5e867dd3d25627d7122.module.wasm -o opt-gen.wasm
$ l
-rw-r--r--    1 sendilkumar  staff    45K May  8 17:43
 opt-gen.wasm

它又减少了 5 KB。我们已经使用了-Oz传递,但我们还可以传递其他传递来进一步优化生成的二进制文件。

我们已经看到了如何使用 Rust 最小化 WebAssembly 模块。接下来,我们将分析 WebAssembly 模块中的内存模型。

分析 WebAssembly 模块中的内存模型

在 JavaScript 引擎内部,WebAssembly 和 JavaScript 在不同的位置运行。跨越 JavaScript 和 WebAssembly 之间的边界总会有一些成本。浏览器厂商实现了酷炫的技巧和解决方案来减少这种成本,但当你应用程序跨越这个边界时,这个边界跨越很快就会成为你应用程序的主要性能瓶颈。设计 WebAssembly 应用程序时,减少边界跨越非常重要。但一旦应用程序增长,管理这个边界跨越就变得困难。为了防止边界跨越,WebAssembly 模块附带内存模块。

WebAssembly 模块中的内存部分是一个线性内存的向量。

线性内存模型是一种内存寻址技术,其中内存组织在一个单一的连续地址空间中。它也被称为平面内存模型。

线性内存模型使得理解、编程和表示内存变得更加容易。但它也有巨大的缺点,例如在内存中重新排列元素的高执行时间和浪费大量内存区域。

在这里,内存代表一个包含未解释数据的原始字节的向量。WebAssembly 使用可调整大小的数组缓冲区来存储内存的原始字节。需要注意的是,创建的这种内存可以从 JavaScript 和 WebAssembly 中访问和修改。

使用 Rust 在 JavaScript 和 WebAssembly 之间共享内存

我们已经看到了如何在 JavaScript 和 WebAssembly 之间共享内存。让我们在这个例子中使用 Rust 来共享内存:

  1. 使用 Cargo 创建一个新的 Rust 项目:

    $ cargo new --lib memory_world
    
  2. 在您喜欢的编辑器中打开项目,并将 src/lib.rs 替换为以下内容:

    #![no_std]
    
    use core::panic::PanicInfo;
    use core::slice::from_raw_parts_mut;
    
    #[no_mangle]
    fn memory_to_js() {
        let obj: &mut [u8];
    
        unsafe {
          obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
        }
    
        obj[0] = 13;
    }
    
    #[panic_handler]
    fn panic(_info: &PanicInfo) -> !{
        loop{}
    } 
    

Rust 文件以 #![no_std] 开始。这指示编译器在生成 WebAssembly 模块时不要包含 Rust 标准库。这将大大减少二进制文件的大小。接下来,我们定义一个名为 memory_to_js 的函数。这个函数在内存中创建一个 obj 并与 JavaScript 共享。在函数定义中,我们创建一个名为 obju32 切片。接下来,我们将一些原始内存分配给 obj。在这里,我们处理原始内存。因此,我们将代码包裹在一个 unsafe 块中。内存对象是全局的,并且可以被 JavaScript 和 WebAssembly 同时修改。因此,我们使用 from_raw_parts_mut 来实例化对象。最后,我们将共享数组缓冲区的第一个元素赋值为一个值。

  1. 创建一个 index.html 文件,并添加以下内容:

    <script>
        ( async() => {
             const bytes = await fetch("target/wasm32-
             unknown-unknown/debug/memory_world.wasm");
             const response = await bytes.arrayBuffer();
             const result = await
               WebAssembly.instantiate(response, {});
             result.exports.memory_to_js();
             const memObj = new
               UInt8Array(result.exports.memory.buffer, 0)
               .slice(0, 1);
             console.log(memObj[0]); // 13
        })();
    </script>
    

我们创建一个匿名异步 JavaScript 函数,该函数在脚本加载时立即被调用。在匿名函数中,我们获取 WebAssembly 二进制文件。接下来,我们创建 ArrayBuffer 并将模块实例化到 result 对象中。然后,我们在 WebAssembly 模块中调用 memory_to_js 方法(注意 exports 关键字,因为该函数是从 WebAssembly 模块导出的)。这实例化了内存并将共享数组缓冲区的第一个元素赋值为 13

const memObj = new
 UInt8Array(result.exports.memory.buffer, 0)
   .slice(0, 1);
console.log(memObj[0]); // 13

接下来,我们使用 result.export.memory.buffer 调用从 WebAssembly 导出的内存对象,并使用一个新的 UInt8Array() 将其转换为 UInt8Array。然后,我们使用 slice(0,1) 提取第一个元素。这样,我们可以在 JavaScript 和 WebAssembly 之间传递和检索值,而无需任何开销。内存通过 loadstore 二进制指令访问。load 操作将数据从主内存复制到寄存器。store 操作将数据从主内存复制。这些二进制指令通过偏移量和对齐方式访问。对齐是以 2 为底的对数表示。内存地址应该是 4 的倍数。这被称为对齐限制。这种对齐限制使硬件运行得更快。

注意

需要注意的是,WebAssembly 目前只提供 32 位地址范围。在未来,WebAssembly 可能会提供 64 位地址范围。

我们已经看到如何通过在 Rust 中创建内存来在 JavaScript 和 WebAssembly 之间共享内存。接下来,我们将在 JavaScript 端创建内存对象并在 Rust 应用程序中使用它。

在 JavaScript 中创建内存对象以在 Rust 应用程序中使用

与 JavaScript 不同,Rust 不是动态类型。在 JavaScript 中创建的内存无法告诉 WebAssembly(或 Rust 代码)要分配什么以及何时释放它们。我们需要明确告知 WebAssembly 如何分配内存,最重要的是,何时以及如何释放它们(以避免任何泄漏)。

我们使用 WebAssembly.memory() 构造函数在 JavaScript 中创建内存。内存构造函数接收一个对象来设置默认值。该对象具有以下选项:

  • initial – 内存初始大小

  • maximum – 内存的最大大小(可选)

  • shared – 表示是否使用共享内存

initialmaximum 的单位是 WebAssembly 页面,其中页面指的是 64 KB。

我们按如下方式更改 HTML 文件:

<script>
     ( async() => {
        const memory = new WebAssembly.Memory({initial: 10,
          maximum:100}); // -> 1
        const bytes = await fetch("target/wasm32-unknown-
          unknown/debug/memory_world.wasm");
        const response = await bytes.arrayBuffer();
        const instance = await
          WebAssembly.instantiate(response, 
          { js: { mem: memory } }); // ->2
        const s = new Set([1, 2, 3]);
        let jsArr = Uint8Array.from(s); // -> 3
        const len = jsArr.length;
        let wasmArrPtr = instance.exports.malloc(length);
          // -> 4
        let wasmArr = new
          Uint8Array(instance.exports.memory.buffer,
          wasmArrPtr, len); // -> 5
        wasmArr.set(jsArr); // -> 6
        const sum = instance.exports.accumulate
          (wasmArrPtr, len); // -> 7
        console.log(sum);
    })();
</script>

// -> 1 中,我们使用 WebAssembly.Memory() 构造函数初始化内存。我们传递了内存的初始和最大大小,即 640 KB 和 6.4 MB。

// -> 2 中,我们正在实例化 WebAssembly 模块以及内存对象。

// -> 3 中,我们随后创建具有值 123typedArray (UInt8Array)。

// -> 4 中,我们看到,由于 WebAssembly 模块对从内存中创建的对象没有任何线索,因此需要分配内存。我们必须在 WebAssembly 中手动编写内存的分配和释放。在这个步骤中,我们发送数组的长度并分配该内存。这给了我们内存位置的指针。

// -> 5 中,我们使用缓冲区(总可用内存)、内存偏移量(wasmAttrPtr)和内存长度创建一个新的 typedArray

// -> 6 中,我们将本地创建的 typedArray(在第 3 步中创建)设置为在第 5 步中创建的 typedArray

//-> 7 中,最后,我们将内存指针和长度发送到 WebAssembly 模块,通过使用内存指针和长度从内存中获取值。

在 Rust 端,将 src/lib.rs 的内容替换为以下内容:

use std::alloc::{alloc, dealloc,  Layout};
use std::mem;

#[no_mangle]
fn accumulate(data: *mut u8, len: usize) -> i32 {
    let y = unsafe { std::slice::from_raw_parts(data as
      *const u8, len) };
    let mut sum = 0;
    for i in 0..len {
        sum = sum + y[i];
    }
    sum as i32
}

#[no_mangle]
fn malloc(size: usize) -> *mut u8 {
    let align = std::mem::align_of::<usize>();
    if let Ok(layout) = Layout::from_size_align(size,
      align) {
        unsafe {
            if layout.size() > 0 {
                let ptr = alloc(layout);
                if !ptr.is_null() {
                    return ptr
                }
            } else {
                return align as *mut u8
            }
        }
    }
    std::process::abort
}

我们从 std::allocstd::mem 中导入了 allocdeallocLayout 来操作原始内存。第一个函数 accumulate 接收 data,这是数据开始的指针,以及 len,要读取的内存长度。首先,我们使用 std::slice::from_raw_parts 通过传递指针 data 和长度 len 从原始内存中创建一个切片。请注意,这是一个不安全操作。接下来,我们遍历数组中的项并将所有元素相加。最后,我们将值作为 i32 返回。

malloc函数用于自定义分配内存,因为 WebAssembly 模块对发送的信息类型以及如何读取/理解它一无所知。malloc帮助我们按需分配内存,而无需任何恐慌。

使用python -m http.server运行前面的代码,并在浏览器中加载网页以在开发者工具中查看结果。

使用 Twiggy 分析 WebAssembly 模块

Rust 到 WebAssembly 的二进制文件更有可能创建一个臃肿的二进制文件。在创建 WebAssembly 二进制文件时,应采取适当的注意。在生成二进制文件时,应考虑优化级别、编译时间以及各种其他因素之间的权衡。但大多数先前的工作默认由编译器完成。无论是 Emscripten 还是rustc编译器,都会确保消除死代码,并提供各种优化级别的选项(从-O0z)。我们可以选择适合我们的一个。

Twiggy 是一个代码大小分析器。它使用调用图来确定函数的来源,并提供关于函数的元信息。元信息包括每个函数的二进制大小及其成本。Twiggy 提供了二进制内容的概述。有了这些信息,我们可以进一步优化二进制文件。让我们安装并使用 Twiggy 来优化二进制文件:

  1. 通过运行以下命令安装 Twiggy:

    $ cargo install twiggy
    
  2. 安装完成后,twiggy命令将在命令行中可用,我们可以通过以下命令来检查:

    $ twiggy
    twiggy-opt 0.6.0
    ...
    Use `twiggy` to make your binaries slim!
    
    USAGE:
        twiggy <SUBCOMMAND>
    
    FLAGS:
        -h, --help Prints help information
        -V, --version Prints version information
    
    SUBCOMMANDS:
        diff         Diff the old and new versions of a
                     binary to see what sizes changed.
        dominators   Compute and display the dominator
                     tree for a binary's call graph.
        garbage      Find and display code and data that
                     is not transitively referenced by any
                     exports or public functions.
        help         Prints this message or the help of
                     the given subcommand(s)
        monos        List the generic function
                     monomorphizations that are
                     contributing to code bloat.
    paths        Find and display the call paths 
                     to a function in the given binary's
                     call graph.
        top          List the top code size offenders in a
                     binary.
    
  3. 创建一个文件夹来测试驱动 Twiggy:

    $ mkdir twiggy-world
    
  4. 创建一个名为add.wat的文件,并添加以下内容:

    $ touch add.wat
    (module
        (func $add (param $lhs i32) (param $rhs i32)
          (result i32)
            get_local $lhs
            get_local $rhs
            i32.add)
        (export "add" (func $add))
    )
    
  5. 一旦定义了 WebAssembly 文本格式,就可以使用wabt将其编译为 WebAssembly 模块:

    $ /path/to/build/directory/of/wabt/wat2wasm add.wat
    
  6. 前面的命令生成一个add.wasm文件。要获取二进制中的调用路径,请使用paths选项运行 Twiggy:

    $ twiggy paths add.wasm
    Shallow Bytes │ Shallow % │ Retaining Paths
    ───────────────┼───────────┼───────────────────────────
    9 ┊21.95% ┊ code[0]
    ┊┊⬑ export "add"
    8 ┊19.51% ┊ wasm magic bytes
    6 ┊14.63% ┊ type[0]: (i32, i32) -> i32
    ┊┊⬑ code[0]
    ┊┊⬑ export "add"
    6 ┊14.63% ┊ export "add"
    6 ┊14.63% ┊ code section headers
    3 ┊7.32% ┊ type section headers
    3 ┊7.32% ┊ export section headers
    

twiggy paths命令显示函数的调用路径、它们在二进制文件中占用的字节数以及它们的百分比。实际添加的代码是 9 字节,它占整个二进制文件大小的 21.95%。

让我们探索 Twiggy 的各种子命令:

  • top

  • monos

  • garbage

top

twiggy top命令将列出每个代码块的大小。它按降序列出函数的大小、在最终二进制文件中的大小百分比以及块部分:

$ twiggy top add.wasm
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼───────────────────────────
9 ┊21.95% ┊ code[0]
8 ┊19.51% ┊ wasm magic bytes
6 ┊14.63% ┊ type[0]: (i32, i32) -> i32
6 ┊14.63% ┊ export "add"
6 ┊14.63% ┊ code section headers
3 ┊7.32% ┊ type section headers
3 ┊7.32% ┊ export section headers
41 ┊100.00% ┊Σ [7 Total Rows]
The usage of the twiggy top is as follows
USAGE: twiggy top <input> -n <max_items> -o
 <output_destination> --format <output_format> --mode
 <parse_mode>

使用-n后跟要显示的条目数来列出前 n 个详细信息:

$ twiggy top add.wasm -n 3
Shallow Bytes │ Shallow % │ Item
───────────────┼───────────┼───────────────────────────
9 ┊21.95% ┊ code[0]
8 ┊19.51% ┊ wasm magic bytes
             6 ┊14.63% ┊ type[0]: (i32, i32) -> i32
18 ┊43.90% ┊ ... and 4 more.
41 ┊100.00% ┊Σ [7 Total Rows]

类似地,我们可以使用--format标志将输出格式化为 JSON 格式:

$ twiggy top add.wasm -n 3 --format json
[{"name":"code[0]","shallow_size":9,"shallow_size_percent":
21.951219512195124},{"name":"wasm magic
bytes","shallow_size":8,"shallow_size_percent":19.512195121
95122},{"name":"type[0]: (i32, i32) ->
i32","shallow_size":6,"shallow_size_percent":14.63414634146
3413}]

当你想追踪最大的代码块并单独优化它们时,top命令非常有用。

monos

在 JavaScript 世界中,单态化可以提高性能。但它也会增加代码大小(例如,在泛型中)。由于我们必须为每种类型动态创建泛型函数的实现,因此在使用泛型和单态代码时必须非常小心。

Twiggy 有一个名为monos的子命令,它将列出由于单态化导致的代码膨胀:

$ twiggy monos pkg/index_bg.wasm
Apprx. Bloat Bytes │ Apprx. Bloat % │ Bytes │ %      │ Monomorphizations
────────────────────┼────────────────┼───────┼────────┼───────────────────────────────────────────────────────────────────
                  4 ┊          0.01% ┊    32 ┊  0.06% ┊ core::ptr::drop_in_place
                    ┊                ┊    28 ┊  0.05% ┊     core::ptr::drop_in_place::h9684ba572bb4c2f9
                    ┊                ┊     4 ┊  0.01% ┊     core::ptr::drop_in_place::h00c08aab80423b88
                  0 ┊          0.00% ┊  5437 ┊ 10.44% ┊ dlmalloc::dlmalloc::Dlmalloc::malloc
                    ┊                ┊  5437 ┊ 10.44% ┊     dlmalloc::dlmalloc::Dlmalloc::malloc::hb0329e71e24f7e2f
                  0 ┊          0.00% ┊  1810 ┊  3.48% ┊ <char as core::fmt::Debug>::fmt
                    ┊                ┊  1810 ┊  3.48% ┊     <char as core::fmt::Debug>::fmt::h5472f29c33f4c4c9
                  0 ┊          0.00% ┊  1126 ┊  2.16% ┊ dlmalloc::dlmalloc::Dlmalloc::free
                    ┊                ┊  1126 ┊  2.16% ┊     dlmalloc::dlmalloc::Dlmalloc::free::h7ab57ecacfa2b1c3
                  0 ┊          0.00% ┊  1123 ┊  2.16% ┊ core::str::slice_error_fail
                    ┊                ┊  1123 ┊  2.16% ┊     core::str::slice_error_fail::h26278b2259fb6582
                  0 ┊          0.00% ┊   921 ┊  1.77% ┊ core::fmt::Formatter::pad
                    ┊                ┊   921 ┊  1.77% ┊     core::fmt::Formatter::pad::hb011277a1901f9f7
                  0 ┊          0.00% ┊   833 ┊  1.60% ┊ dlmalloc::dlmalloc::Dlmalloc::dispose_chunk
                    ┊                ┊   833 ┊  1.60% ┊     dlmalloc::dlmalloc::Dlmalloc::dispose_chunk::he00c681454a3c3b7
                  0 ┊          0.00% ┊   787 ┊  1.51% ┊ core::fmt::write
                    ┊                ┊   787 ┊  1.51% ┊     core::fmt::write::hb395f946a5ce2cab
                  0 ┊          0.00% ┊   754 ┊  1.45% ┊ core::fmt::Formatter::pad_integral
                    ┊                ┊   754 ┊  1.45% ┊     core::fmt::Formatter::pad_integral::h05ee6133195a52bc
                  0 ┊          0.00% ┊   459 ┊  0.88% ┊ alloc::string::String::push
                    ┊                ┊   459 ┊  0.88% ┊     alloc::string::String::push::he03a5b89b77597a1
                  0 ┊          0.00% ┊  4276 ┊  8.21% ┊ ... and 64 more.
                  4 ┊          0.01% ┊ 17558 ┊ 33.73% ┊ Σ [85 Total Rows]
....

我们正在使用本章“最小化 WebAssembly 模块”部分中的 index_bg.wasm 示例。

monos 对于我们理解由泛型参数引起的任何膨胀现象非常有用,这些膨胀现象随后可以被更简单的泛型函数所替代。

garbage

有时,找到不再使用但出于某些其他原因保留在最终二进制文件中的代码是很重要的。这些函数在某个地方被引用,但未在任何地方使用,编译器将不知道何时何地删除它们。

我们可以使用 Twiggy 的 garbage 命令列出所有非传递性引用的代码和数据:

$ twiggy garbage add.wasm
Bytes │ Size % │ Garbage Item
───────┼────────┼─────────────────────────────────────────
   109 ┊  0.21% ┊ custom section 'producers'
   109 ┊  0.21% ┊ Σ [1 Total Rows]
27818 ┊ 53.44% ┊ 1 potential false-positive data segments

WebAssembly 模块由一个数据部分组成。但有时,我们可能不会立即在 WebAssembly 模块中使用这些数据,而是在其他导入数据的地方使用。正如你所看到的,Twiggy 的 garbage 子命令显示了这些可能错误的数据值。

摘要

在本章中,我们看到了如何使用 Rust 优化 WebAssembly 二进制文件,如何映射 JavaScript 和 Rust 之间的内存,最后是如何使用 Twiggy 分析 WebAssembly 模块。

WebAssembly 生态系统仍处于早期阶段,它承诺提供更好的性能。WebAssembly 二进制文件解决了 JavaScript 生态系统中的几个差距,例如大小高效的紧凑二进制文件、启用流式编译和正确类型化的二进制文件。这些功能使 WebAssembly 更小、更快。另一方面,Rust 提供了生成 WebAssembly 模块的一流支持,而 wasm-bindgen 是目前最好的工具,它使得在 Rust 和 WebAssembly 之间传输复杂对象变得更加容易。

我希望你现在已经理解了 WebAssembly 的基础知识以及 Rust 如何使其生成 WebAssembly 模块变得更加容易。我迫不及待地想看看你将使用 Rust 和 WebAssembly 发行的内容。

Packt.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书和视频,以及领先的行业工具,帮助您规划个人发展并推进您的职业生涯。有关更多信息,请访问我们的网站。

第十一章:为什么订阅?

  • 使用来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,多花时间编码

  • 通过为您量身定制的技能计划提高您的学习效果

  • 每月免费获得一本电子书或视频

  • 完全可搜索,便于轻松访问关键信息

  • 复制粘贴、打印和收藏内容

你知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在 packt.com 升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。有关更多信息,请联系我们 customercare@packtpub.com

www.packt.com,您还可以阅读一系列免费的技术文章,注册各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

您可能还会喜欢的其他书籍

如果您喜欢这本书,您可能还会对 Packt 的这些其他书籍感兴趣:

《Rust 程序员创意项目》

Carlo Milanesi

ISBN: 978-1-78934-622-0

  • 访问 TOML、JSON 和 XML 文件以及 SQLite、PostgreSQL 和 Redis 数据库

  • 使用 JSON 有效载荷开发 RESTful 网络服务

  • 使用 HTML 模板和 JavaScript 创建一个网络应用程序,或使用 WebAssembly 创建一个前端网络应用程序或网络游戏

  • 构建 2D 桌面游戏

  • 为编程语言开发解释器和编译器

  • 创建一个机器语言模拟器

  • 使用可加载模块扩展 Linux 内核

《使用 Rust 和 WebAssembly 进行游戏开发》

Eric Smith

ISBN: 978-1-80107-097-3

  • 使用 WebAssembly 将 Rust 应用程序构建和部署到网络

  • 使用 wasm-bindgen 和 Canvas API 绘制实时图形

  • 编写游戏循环并获取键盘输入以进行动态操作

  • 探索碰撞检测,创建一个可以在平台上跳上跳下并能掉入洞中的动态角色

  • 使用状态机管理动画

  • 为无尽跑酷生成程序化关卡

  • 加载并显示精灵和精灵表,用于动画

  • 测试、重构并保持您的代码干净和可维护

Packt 正在寻找像您这样的作者

如果你有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们与成千上万的开发者和技术专业人士合作,就像你一样,帮助他们将见解分享给全球技术社区。你可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交你自己的想法。

分享你的想法

现在你已经完成了《Practical WebAssembly》,我们非常想听听你的想法!如果你从亚马逊购买了这本书,请点击此处直接跳转到亚马逊评论页面并分享你的反馈或在该购买网站上留下评论。

你的评论对我们和整个技术社区都至关重要,并将帮助我们确保我们提供高质量的内容。

你可能还会喜欢的其他书籍

你可能还会喜欢的其他书籍

posted @ 2025-09-06 13:43  绝不原创的飞龙  阅读(3)  评论(0)    收藏  举报