WebAssembly - 快速指南


WebAssembly - 概述

WebAssembly 是一种新的网络计算机编程语言。WebAssembly 代码是一种低级二进制格式,与 Web 兼容,并且可以在现代 Web 浏览器中轻松运行。生成的文件很小,加载和执行速度更快。您现在可以将 C、C++、Rust 等语言编译为二进制格式,并且它可以像 javascript 一样在网络上运行。

WebAssembly 的定义

根据 WebAssembly 的官方网站(https://webassemble.org/),它被定义为 WebAssembly(缩写为 Wasm)是基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为编译 C/C++/Rust 等高级语言的便携式目标,支持在 Web 上部署客户端和服务器应用程序。

WebAssembly 不是开发人员必须编写的东西,但代码是用 C、C++、Rust 等语言编写的,并且可以编译为 WebAssembly(wasm)。相同的代码可以在网络浏览器中运行。

WebAssembly是一种新语言,代码是低级汇编语言,但凭借其文本格式特性,代码具有可读性,并且在必要时可以进行调试。

WebAssembly 的目标

WebAssembly 的开放标准是在 W3C 社区组中开发的,该社区组包括来自所有主要浏览器的代表以及 W3C 工作组。

WebAssembly 的主要目标如下:

  • 更快更高效、更便携- WebAssembly 代码旨在利用可用的硬件在不同平台上更快地运行。

  • 易于阅读和调试- WebAssembly 是一种低级汇编语言,具有文本格式支持,允许您调试代码以解决任何问题,并在必要时重写代码。

  • 安全性- WebAssembly 可以安全地在 Web 浏览器上运行,因为它负责权限和同源策略。

WebAssembly 的优点

以下是 WebAssembly 的优点 -

  • Run is Modern Browsers - WebAssembly 能够在可用的现代 Web 浏览器上执行,没有任何问题。

  • 多语言支持- C、C++、Rust、Go 等语言现在可以将代码编译为 WebAssembly 并在 Web 浏览器中运行相同的代码。因此,以前无法在浏览器中运行的语言现在可以在浏览器中运行。

  • 更快、更高效、更便携- 由于代码较小,因此加载和执行速度更快。

  • 易于理解- 开发人员不必在理解 WebAssembly 编码方面花费太多精力,因为他们不必在 WebAssembly 中编写代码。相反,在 WebAssembly 中编译代码并在 Web 上执行相同的代码。

  • 易于调试- 虽然最终代码是低级汇编语言,但您也可以以文本格式获取它,这很容易阅读和调试。

WebAssembly 的缺点

以下是 WebAssembly 的缺点 -

  • WebAssembly 仍在开发中,现在决定它的未来还为时过早。

  • WebAssembly 依赖于 javascript 与文档对象模型 (DOM) 进行交互。

WebAssembly - 简介

WebAssembly 也称为 WASM,于 2017 年首次推出。WebAssembly 起源背后的大型科技公司是 Google、Apple、Microsoft、Mozilla 和 W3C。

有传言称 WebAssembly 将取代 Javascript,因为它的执行速度更快,但事实并非如此。WebAssembly 和 Javascript 旨在共同解决复杂的问题。

需要 WebAssembly

到目前为止,我们只有 Javascript 可以在浏览器中成功运行。有一些非常繁重的任务很难在浏览器中使用 JavaScript 执行。

仅举几例,包括图像识别、计算机辅助设计 (CAD) 应用程序、实时视频增强、VR 和增强现实、音乐应用程序、科学可视化和模拟、游戏、图像/视频编辑等。

WebAssembly 是一种具有二进制指令的新语言,可以更快地加载和执行。上述任务可以用 C、C++、Rust 等高级语言轻松完成。我们需要一种方法,可以编译 C、C++、Rust 中的代码并在 Web 浏览器中使用它。使用 WebAssembly 也可以实现同样的效果。

当 WebAssembly 代码加载到浏览器中时。然后,浏览器负责将其转换为处理器可以理解的机器格式。

对于 javascript,必须下载、解析代码并将其转换为机器格式。它需要花费大量时间,对于像我们之前提到的繁重任务可能会非常慢。

WebAssembly 的工作原理

C、C++ 和 Rust 等高级语言被编译为二进制格式,即.wasm和文本格式.wat

WebAssembly 的工作原理

使用C、C++ 和 Rust 编写的源代码使用编译器编译为.wasm 。您可以使用 Emscripten SDK 将 C/C++ 编译为.wasm

流程如下 -

瓦斯姆

可以使用 Emscripten SDK 将 C/C++ 代码编译为.wasm。稍后,可以在 html 文件中的 javascript 的帮助下使用.wasm代码来显示输出。

WebAssembly 的关键概念

关键概念解释如下 -

模块

模块是由浏览器编译为可执行机器代码的对象。模块被认为是无状态的,可以在 Windows 和 Web Worker 之间共享。

记忆

WebAssembly 中的内存是保存数据的数组缓冲区。您可以使用 Javascript api WebAssembly.memory() 分配内存。

桌子

WebAssembly 中的表是一个类型化数组,位于 WebAssembly 内存之外,并且主要包含对函数的引用。它存储函数的内存地址。

实例

实例是一个对象,它具有可以从 javascript 调用并在浏览器内部执行的所有导出函数。

WebAssembly-WASM

WebAssembly 也称为 wasm,是对 Javascript 的改进。它被设计为在浏览器内运行,就像 JavaScript 一样,也可以与 NodeJS 一起运行。当编译任何高级语言(如 C、C++、Rust)时,您碰巧会得到 wasm 输出。

考虑以下 C 程序 -

int factorial(int n) {
   if (n == 0) 
      return 1; 
   else 
      return n * factorial(n-1); 
}

利用 WasmExplorer(可在https://mbebenita.github.io/WasmExplorer/获取)来获取编译后的代码,如下所示 -

瓦斯姆探索者

阶乘程序的 WebAssembly 文本格式如下所示 -

(module 
   (table 0 anyfunc) 
   (memory $0 1) 
   (export "memory" (memory $0)) (export "factorial" (func $factorial)) 
   (func $factorial (; 0 ;) (param $0 i32) (result i32)
      (local $1 i32) 
      (local $2 i32) 
      (block $label$0 
         (br_if $label$0 
            (i32.eqz 
               (get_local $0) 
            )
         )
         (set_local $2 
            (i32.const 1) 
         ) 
         (loop $label$1 
            (set_local $2 
               (i32.mul 
                  (get_local $0) (get_local $2) 
               ) 
            ) 
            (set_local $0 
               (tee_local $1        (i32.add 
                  (get_local $0) (i32.const -1) 
               ) 
               ) 
            ) 
            (br_if $label$1      (get_local $1) 
            ) 
         ) 
         (return 
            (get_local $2)
         ) 
      ) 
      (i32.const 1) 
   )
)

使用 Wat2Wasm 工具,您可以查看 WASM 代码,就像下面提到的那样 -

Wat2Wasm 工具

开发人员不应该在 wasm 中编写代码或学习在其中编写代码,因为它主要是在编译高级语言时生成的。

堆垛机型号

在 WASM 中,所有指令都被压入堆栈。参数被弹出,结果被推回堆栈。

考虑以下添加 2 个数字的 WebAssembly 文本格式 -

(module
   (func $add (param $a i32) (param $b i32) (result i32) 
      get_local $a 
      get_local $b 
      i32.add
   )
   (export "add" (func $add))
)

函数的名称是$add,它接受 2 个参数 $a 和 $b。结果是一个 32 位整数类型。使用 get_local 访问局部变量,使用 i32.add 执行添加操作。

执行时添加 2 个数字的堆栈表示如下 -

堆

步骤 1 - get_local $a 指令的执行中,第一个参数即 $a 被压入堆栈。

步骤 2中- 在执行 get_local $b 指令期间,第二个参数(即 $b)被压入堆栈。

步骤 3中- i32.add 的执行将从堆栈中弹出元素并将结果推回堆栈。栈中最后保留的值是函数$add 的结果。

WebAssembly - 安装

在本章中,将学习如何安装 Emscripten SDK 来编译 C/C++。Emscripten 是一个低级虚拟机 (LLVM),它采用 C/C++ 生成的字节码并将其编译为可以在浏览器内轻松执行的 JavaScript。

要将C/C++编译为WebAssembly,我们需要首先安装Emscripten sdk。

安装Emscripten sdk

安装 Emscripten sdk 的步骤如下 -

步骤 1 - 克隆 emsdk 存储库: git clone https://github.com/emscripten-core/emsdk.git

E:\wa>git clone https://github.com/emscripten-core/emsdk.git 
Cloning into 'emsdk'... 
remote: Enumerating objects: 14, done. 
remote: Counting objects: 100% (14/14), done. 
remote: Compressing objects: 100% (12/12), done. 
remote: Total 1823 (delta 4), reused 4 (delta 2), pack-reused 1809 receiving obje 
cts: 99% (1819/1823), 924.01 KiB | 257.00 KiB/s 
Receiving objects: 100% (1823/1823), 1.01 MiB | 257.00 KiB/s, done. 
Resolving deltas: 100% (1152/1152), done.

步骤 2 - 进入目录 emsdk。

cd emsdk

步骤 3 - 对于 Windows:执行以下命令。

emsdk install latest

对于 Linux,此命令将需要一些时间来安装必要的工具,如 java、python 等。请遵循下面提到的代码 -

./emsdk install latest

步骤 4 - 要激活最新的 SDK,请在终端中执行以下命令。

对于 Windows,执行以下命令 -

emsdk activate latest

对于 linux,执行下面提到的命令 -

./emsdk activate latest

步骤 5 - 要激活 PATH 和其他环境变量,请在终端中运行以下命令。

对于 Windows,执行命令 -

emsdk_env.bat

对于 Linux,执行以下命令 -

source ./emsdk_env.sh

我们已经完成 emsdk 的安装,现在可以编译 C 或 C++ 代码。C/C++的编译将在接下来的章节中完成。

要编译任何 C 或 C++ 代码,请执行以下命令 -

emcc source.c or source.cpp -s WASM=1 -o source.html

输出将为您提供 source.html 文件、source.js 和 source.wasm 文件。js 将具有获取 source.wasm 的 api,当您在浏览器中点击 source.html 时,您可以看到输出。

要获取 wasm 文件,您可以使用以下命令。此命令只会为您提供 source.wasm 文件。

emcc source.c or source.cpp -s STANDALONE_WASM

WebAssembly - 编译为 WASM 的工具

本章将讨论一些在使用 WebAssembly 时非常有用的易于使用的工具。让我们首先了解 WebAssembly.studio 工具。

WebAssembly.studio

该工具允许您将 C、Rust、Wat 编译为 Wasm 等。

WebAssembly工作室

首先,您可以单击 Empty C Project、Empty Rust Project、Empty Wat Project 将 C 和 Rust 编译为 WASM。5.

空C项目

它具有“构建”、“运行”来构建代码并检查输出。下载按钮允许您下载.wasm文件,该文件可用于在浏览器内进行测试。该工具对于编译 C 和 Rust 代码并检查输出非常有帮助。

WebAssembly 浏览器

WebAssembly Explorer 允许您编译 C 和 C++ 代码。请参阅链接https://mbebenita.github.io/WasmExplorer/了解更多详细信息。单击链接后出现的屏幕如下所示 -

WebAssembly 浏览器

您可以选择 C ​​和 C++ 版本。C 或 C++ 的源代码写在这里 -

编译按钮

一旦单击“编译”按钮,它就会在下面的块中提供 WebAssembly 文本格式 (WAT) 和 Firefox x86 汇编代码 -

瓦特

您可以下载.wasm代码以在浏览器中测试它。

WASMFiddle

Wasmfiddle帮助您将 C 代码编译为 WebAssembly 并测试输出。单击链接https://wasdk.github.io/WasmFiddle/ 后,您将看到以下页面 -

WASM 小提琴

单击“构建”来编译代码。您可以通过点击 Wat 和 Wasm 下载 Wat 和 Wasm 代码。要测试输出,请单击“运行”按钮。

WASM 至 WAT

当您输入 WebAssembly 文本格式时,工具wat2wasm将为您提供 wasm 代码。您可以单击链接https://web assembly.github.io/wabt/demo/wat2wasm/进行演示,将出现的屏幕如下所示 -

WASM 至 WAT

您可以使用上传按钮上传.wasm,文本区域将显示文本格式。

WASM 转 WASM

当您输入 WebAssembly 文本格式时,工具 wat2wasm 将为您提供 wasm 代码。您可以单击链接https://web assembly.github.io/wabt/demo/wat2wasm/进行演示,将出现的屏幕如下所示 -

WASM 转 WASM

这个工具非常有用,因为它有助于测试输出。您可以输入 WAT 代码并查看 .wasm 代码,还可以执行该代码以查看输出。

WebAssembly - 程序结构

WebAssembly,也称为 WASM,是二进制格式的低级代码,旨在以最有效的方式在浏览器内执行。WebAssembly 代码的结构如下:

  • 价值观
  • 类型
  • 指示

现在让我们详细了解它们。

价值观

WebAssembly 中的值旨在存储复杂的数据,例如文本、字符串和向量。WebAssembly 支持以下内容 -

  • 字节
  • 整数
  • 浮点
  • 名称

字节

字节是 WebAssembly 支持的最简单的值形式。该值采用十六进制格式。

例如

表示为b 的字节也可以采用自然数 n,其中 n <256。

byte ::= 0x00| .... |0xFF

整数

在 WebAssembly 中,支持的整数如下所示 -

  • i32:32 位整数
  • i64:64 位整数

浮点

在 WebAssembly 中支持的浮点数如下 -

  • f32:32位浮点
  • f64:64 位浮点

名称

名称是字符序列,具有由 Unicode 定义的标量值,可以从此处给出的链接http://www.unicode.org/versions/Unicode12.1.0/获得。

类型

WebAssembly 中的实体被分类为类型。支持的类型如下所示 -

  • 值类型
  • 结果类型
  • 功能类型
  • 限制
  • 内存类型
  • 表格类型
  • 全局类型
  • 外部类型

让我们一一研究一下。

值类型

WebAssembly 支持的值类型如下:

  • i32:32 位整数
  • i64:64 位整数
  • f32:32位浮点
  • f64:64 位浮点
valtype ::= i32|i64|f32|f64

结果类型

括号内写入的值将被执行并存储在结果类型内。结果类型是执行由值组成的代码块的输出。

resulttype::=[valtype?]

功能类型

函数类型将接受参数向量返回结果向量。

functype::=[vec(valtype)]--> [vec(valtype)]

限制

限制是与内存和表类型相关的存储范围。

limits ::= {min u32, max u32}

内存类型

存储器类型涉及线性存储器和大小范围。

memtype ::= limits

表格类型

表类型按分配给它的元素类型进行分类。

tabletype ::= limits elemtype
elemtype ::= funcref

表类型取决于分配给它的最小和最大大小的限制。

全局类型

全局类型保存具有值的全局变量,可以更改或保持不变。

globaltype ::= mut valtype
mut ::= const|var

外部类型

外部类型处理导入和外部值。

externtype ::= func functype | table tabletype | mem memtype | global globaltype

指示

WebAssembly 代码是遵循堆栈机器模型的指令序列。由于 WebAssembly 遵循堆栈机器模型,因此指令被推送到堆栈上。

例如,函数的参数值从堆栈中弹出,并将结果推回到堆栈上。最终,栈中只有一个值,那就是结果。

一些常用的指令如下 -

  • 数字指令
  • 变量指令

数字指令

数字指令是对数值执行的运算。

例如
nn, mm ::= 32|64
ibinop ::= add|sub|mul|div_sx|rem_sx|and|or|xor
irelop ::= eq | ne | lt_sx | gt_sx | le_sx | ge_sx
frelop ::= eq | ne | lt | gt | le | ge

变量指令

变量指令是关于访问局部变量和全局变量的。

例如

访问局部变量 -

get_local $a
get_local $b

设置局部变量-

set_local $a
set_local $b

访问全局变量-

get_global $a
get_global $b

设置全局变量-

set_global $a
set_global $b

WebAssembly - JavaScript

本章将列出 WebAssembly 和 Javascript 之间的比较。

Javascript 是一种我们在浏览器中经常使用的语言。现在,随着 WebAssembly 的发布,我们也可以在浏览器中使用 WebAssembly。

WebAssembly 出现的原因并不是要取代 javascript,而是为了处理某些 javascript 难以处理的事情。

例如

使用 javascript 很难完成图像识别、CAD 应用程序、实时视频增强、VR 和增强现实、音乐应用程序、科学可视化和模拟、游戏、图像/视频编辑等任务。

使用C/C++、Rust等高级语言,现在可以将其编译为WebAssembly,很容易完成上述任务。WebAssembly 生成易于在浏览器内执行的二进制代码。

这里是 Javascript 和 WebAssembly 之间的比较列表。

参数 JavaScript 网络组装

编码

您可以轻松地用 Javascript 编写代码。编写的代码是人类可读的并保存为 .js。在浏览器内部使用时,您需要使用 <script> 标签。

代码可以在 WebAssembly 中以文本格式编写,并保存为 .wat。以.wat 格式编写代码很困难。最好从其他高级语言编译代码,而不是从头开始编写 .wat。

您无法在浏览器内执行 .wat 文件,必须使用可用的编译器或在线工具将其转换为 .wasm。

执行

用 javascript 编写的代码在浏览器中使用时必须进行下载、解析、编译和优化。

我们的 .wasm 中的 WebAssembly 代码已编译为二进制格式。

内存管理

JavaScript 在创建变量时分配内存,在不使用时释放内存并将其添加到垃圾回收中。

WebAssembly 中的内存是保存数据的数组缓冲区。您可以使用 Javascript API WebAssembly.memory() 分配内存。

WebAssembly 内存以数组格式存储,即易于理解和执行的平面内存模型。

WebAssembly 中内存模型的缺点是 -

  • 复杂的计算需要时间。

  • WebAssembly不支持垃圾回收,不允许重用内存,造成内存浪费。

加载时间和性能

对于 javascript,当在浏览器内部调用时,必须下载并解析 javascript 文件。随后,解析器将源代码转换为 JavaScript 引擎在浏览器中执行代码的字节码。

Javascript 引擎非常强大,因此,与 WebAssembly 相比,Javascript 的加载时间和性能都非常快。

WebAssembly 最重要的目标是比 JavaScript 更快。由高级语言生成的 Wasm 代码尺寸更小,因此加载时间更快。

但是,像 GO 这样的语言,当编译为 wasm 时,会为一小段代码生成一个大文件。

WebAssembly 的设计方式使其编译速度更快,并且可以在所有主要浏览器上运行。与 JavaScript 相比,WebAssembly 在性能方面仍然需要进行大量改进。

调试

JavaScript 是人类可读的并且可以轻松调试。在浏览器内向 JavaScript 代码添加断点可以让您轻松调试代码。

WebAssembly 提供文本格式的代码,可读,但仍然很难调试。Firefox 允许您在浏览器中查看 .wat 格式的 wasm 代码。

您无法在 .wat 中添加断点,这将在将来提供。

浏览器支持

JavaScript 在所有浏览器中都能正常运行。

所有主流 Web 浏览器都支持 WebAssembly。

WebAssembly - JavaScript API

在本章中,我们将了解如何使用 JavaScript WebAssembly API 加载 wasm 代码并在浏览器中执行它们。

以下是一些重要的 API,我们将在整个教程中使用它们来执行 wasm 代码。

  • fetch() 浏览器 API
  • WebAssembly.compile
  • WebAssembly.实例
  • WebAssembly.实例化
  • WebAssembly.instantiateStreaming

在我们讨论 WebAssembly javascript API 之前,为了测试 API 和输出,我们将使用以下 C 程序以及使用 wasm explorer 从该 c 程序生成的 .wasm 代码。

C 程序的示例如下 -

#include<stdio.h>
int square(int n) { 
   return n*n; 
}

我们将使用 WASM 浏览器来获取 wasm 代码 -

WASM代码

下载 WASM 代码并使用它来测试 API。

fetch() 浏览器 API

fetch() API 旨在加载 .wasm 网络资源。

<script>
   var result = fetch("findsquare.wasm");
   console.log(result);
</script>

它返回一个承诺,如下所示 -

fetch() 浏览器 API

您还可以使用 XMLHttpRequest 方法来获取 wasm 网络资源。

WebAssembly.compile()

API 的职责是编译从 .wasm 获取的模块详细信息。

句法

语法如下 -

WebAssembly.compile(buffer);

参数

Buffer - .wasm 中的代码必须转换为类型化数组或数组缓冲区,然后才能作为输入进行编译。

返回值

它将返回一个包含已编译模块的承诺。

例子

让我们看一个示例,它使用 webAssembly.compile() 将输出作为编译模块提供。

<script> 
   fetch("findsquare.wasm") .then(bytes => bytes.arrayBuffer()) 
   .then(mod => {
      var compiledmod = WebAssembly.compile(mod);
      compiledmod.then(test=> {
         console.log(test); 
      })
   })
</script>

输出

在浏览器中检查时,console.log 将为您提供已编译的模块详细信息 -

Web汇编编译

该模块有一个带有导入、导出和自定义部分的构造函数对象。让我们看看下一个 API,以获取已编译模块的更多详细信息。

WebAssembly.实例

使用 WebAssembly.instance,API 将为您提供已编译模块的可执行实例,可以进一步执行该实例以获得输出。

句法

语法如下 -

new WebAssembly.Instance(compiled module)

返回值

返回值将是一个带有可以执行的导出函数数组的对象。

例子

<script> 
   fetch("findsquare.wasm") 
      .then(bytes => bytes.arrayBuffer())
      .then(mod => WebAssembly.compile(mod)).then(module => {
         let instance = new WebAssembly.Instance(module); 
         console.log(instance); 
      })
</script>

输出

输出将为我们提供一个导出函数数组,如下所示 -

WebAssembly 实例

您可以看到我们从编译的 C 代码中获得的 square 函数。

要执行平方函数,您可以执行以下操作 -

<script>
   fetch("findsquare.wasm") 
   .then(bytes => bytes.arrayBuffer()) 
   .then(mod => WebAssembly.compile(mod)) 
   .then(module => { 
      let instance = new WebAssembly.Instance(module);
      console.log(instance.exports.square(15));
   })
</script>

输出将是 -

225

WebAssembly.实例化

该 API 负责一起编译和实例化模块。

句法

语法如下 -

WebAssembly.instantiate(arraybuffer, importObject)

参数

arraybuffer - .wasm 中的代码必须先转换为类型化数组或 arraybuffer,然后再作为输入进行实例化。

importObject - 导入对象必须具有内存的详细信息,要在模块内使用的导入函数。它可以是一个空的模块对象,以防没有什么可共享的。

返回值

它将返回一个承诺,其中包含模块和实例详细信息。

例子

<script type="text/javascript">
   const importObj = {
      module: {}
   };
   fetch("findsquare.wasm")
      .then(bytes => bytes.arrayBuffer())
      .then(module => WebAssembly.instantiate(module, importObj)) 
      .then(finalcode => { 
         console.log(finalcode); console.log(finalcode.instance.exports.square(25)); 
      }); 
</script>

输出

当您执行代码时,您将得到下面提到的输出。

WebAssembly 实例化

WebAssembly.instantiateStreaming

该 API 负责根据给定的 .wasm 代码编译和实例化 WebAssembly 模块。

句法

语法如下 -

WebAssembly.instantiateStreaming(wasmcode, importObject);

参数

wasmcode - 来自 fetch 或任何其他提供 wasm 代码并返回承诺的 API 的响应。

importObject - 导入对象必须具有内存的详细信息,要在模块内使用的导入函数。如果没有什么可共享的,它可以是一个空模块对象。

返回值

它将返回一个承诺,其中包含模块和实例详细信息。

例子

下面讨论一个例子 -

<script type="text/javascript">     
   const importObj = { 
      module: {} 
   };
   WebAssembly.instantiateStreaming(fetch("findsquare.wasm"), importObj).then(obj => {
      console.log(obj); 
   }); 
</script>

当您在浏览器中测试它时,您会看到一个错误 -

错误

为了使其在服务器端工作,您必须添加 mime 类型 application/wasm 或使用 WebAssembly.instantiate(arraybuffer, importObject)。

WebAssembly - 在 Firefox 中调试 WASM

目前所有可用的最新浏览器(例如 Chrome、Firefox)都添加了 WebAssembly 支持。Firefox 54+ 版本为您提供了调试 wasm 代码的特殊功能。

为此,请在调用 wasm 的 Firefox 浏览器中执行代码。例如,考虑以下用于求数字平方的 C 代码。

C 程序的示例如下 -

#include<stdio.h>
int square(int n) {
   return n*n;
}

我们将利用 WASM 浏览器来获取 wasm 代码 -

浏览器中的输出

下载 WASM 代码并使用它在浏览器中查看输出。

加载 wasm 的 html 文件如下 -

!doctype html> 
<html>
   <head>
      <meta charset="utf-8"> 
      <title>WebAssembly Square function</title> 
      <style> 
         div { 
            font-size : 30px; text-align : center; color:orange; 
         } 
      </style> 
   </head> 
   <body> 
      <div id="textcontent"></div> 
      <script> 
         let square; 
         fetch("findsquare.wasm").then(bytes => bytes.arrayBuffer()) 
            .then(mod => WebAssembly.compile(mod)) 
            .then(module => {return new WebAssembly.Instance(module) }) 
            .then(instance => {  
            square = instance.exports.square(13);
            console.log("The square of 13 = " +square);           
            document.getElementById("textcontent").innerHTML = "The square of 13 = " +square; 
         }); 
      </script> 
   </body> 
</html>

打开 Firefox 浏览器并加载上述 html 文件并打开调试器工具。

调试工具

您应该在调试器工具中看到 wasm:// 条目。单击 wasm://,它会显示转换为 .wat 格式的 wasm 代码,如上所示。

您可以查看导出函数的代码,如果出现任何问题,可以调试代码。Firefox 还打算添加断点,以便您可以调试代码并检查执行流程。

WebAssembly - “你好世界”

在本章中,我们将用 C 语言编写一个简单的程序并将其转换为 .wasm 并在浏览器中执行相同的程序以获取文本“Hello World”。

将使用 wasm 资源管理器工具将 C 程序转换为 .wasm 并在我们的 .html 文件中使用 .wasm。

https://mbebenita.github.io/WasmExplorer/上提供的 Wasm 浏览器工具如下所示 -

C代码

我们将使用的 C 代码如下 -

#include <stdio.h>
char *c_hello() {
   return "Hello World"; 
}

使用 C 代码更新 wasm 资源管理器中的第一个块,如下所示 -

更新第一个块

单击 COMPILE 按钮编译为 WASM 和 WAT 以及 Firefox x86 Web Assembly,如下所示 -

编译

使用下载获取 .wasm 文件并将其另存为firstprog.wasm

创建一个名为firstprog.html的.html文件,如下所示 -

<!doctype html>
<html>
   <head>
      <meta charset="utf-8"> 
      <title>WebAssembly Hello World</title> 
   </head> 
   <body>
      <div id="textcontent"></div>     
      <script type="text/javascript"> 
         //Your code from webassembly here
      </script> 
   </body>
</html>

现在让我们使用firstprog.wasm从C函数c_hello()读取helloworld。

步骤1

使用 fetch() api 读取firstprog.wasm 代码。

第2步

.wasm 代码必须使用ArrayBuffer转换为 arraybuffer 。ArrayBuffer 对象将返回一个固定长度的二进制数据缓冲区。

到目前为止的代码如下 -

<script type="text/javascript"> 
   fetch("firstprog.wasm") .then(bytes => bytes.arrayBuffer()) 
</script>

步骤3

必须使用WebAssembly.compile(buffer)函数将 ArrayBuffer 中的字节编译到模块中。

代码如下所示 -

<script type="text/javascript">
   fetch("firstprog.wasm")
   .then(bytes => bytes.arrayBuffer())
   .then(mod => WebAssembly.compile(mod))
</script>

步骤4

要获取模块,我们必须调用 web assembly.instance 构造函数,如下所示 -

<script type="text/javascript">     
   fetch("firstprog.wasm") 
   .then(bytes => bytes.arrayBuffer())
   .then(mod => WebAssembly.compile(mod))
   .then(module => {return new WebAssembly.Instance(module) }) 
</script>

步骤5

现在让我们控制台实例以在浏览器中查看详细信息。

<script type="text/javascript"> 
   fetch("firstprog.wasm") .then(bytes => bytes.arrayBuffer()) 
   .then(mod => WebAssembly.compile(mod)) .then(module => {
      return new WebAssembly.Instance(module) 
   }) 
   .then(instance => {
      console.log(instance);
   }); 
</script>

console.log 详细信息如下所示 -

控制台日志

为了从函数 c_hello() 中获取字符串“Hello World”,我们需要在 javascript 中添加一些代码。

首先,获取内存缓冲区详细信息,如下所示 -

let buffer = instance.exports.memory.buffer;;

缓冲区值必须转换为类型化数组,以便我们可以从中读取值。缓冲区中有字符串 Hello World。

要转换为类型化,请调用构造函数 Uint8Array ,如下所示 -

let buffer = new Uint8Array(instance.exports.memory.buffer);

现在,我们可以在 for 循环中从缓冲区读取值。

现在让我们通过调用我们编写的函数来开始读取缓冲区,如下所示 -

let test = instance.exports.c_hello();

现在,测试变量有了读取字符串的起点。WebAssembly 没有任何字符串值,所有内容都存储为整数。

因此,当我们从缓冲区读取值时,它们将是一个整数值,我们需要使用 JavaScript 中的 fromCharCode() 将其转换为字符串。

代码如下 -

let mytext = ""; 
for (let i=test; buffer[i]; i++){ 
   mytext += String.fromCharCode(buffer[i]);
}

现在,当您控制台 mytext 时,您应该看到字符串“Hello World”。

例子

完整代码如下 -

<!doctype html> 
<html> 
   <head> 
      <meta charset="utf-8"> 
      <title>WebAssembly Add Function</title>
      <style>
         div { 
            font-size : 30px; text-align : center; color:orange; 
         } 
      </style>
   </head>
   <body>
      <div id="textcontent"></div>
      <script> 
         fetch("firstprog.wasm")
         .then(bytes => bytes.arrayBuffer())
         .then(mod => WebAssembly.compile(mod))
         .then(module => {return new WebAssembly.Instance(module)})
         .then(instance => {   
            console.log(instance); 
            let buffer = new Uint8Array(instance.exports.memory.buffer); 
            let test = instance.exports.c_hello(); 
            let mytext = ""; 
            for (let i=test; buffer[i]; i++) {
               mytext += String.fromCharCode(buffer[i]);
            }
            console.log(mytext); document.getElementById("textcontent").innerHTML = mytext; 
         });
      </script>
   </body>
</html>

我们添加了一个div,并将内容添加到div中,因此字符串显示在浏览器上。

输出

输出如下 -

你好世界

WebAssembly - 模块

我们已经了解了如何从 c /c++ 代码获取 .wasm 文件。在本章中,我们将把 wasm 转换为 WebAssembly 模块,并在浏览器中执行相同的模块。

让我们使用 C++ 阶乘代码,如下所示 -

int fact(int n) {
   if ((n==0)||(n==1))
      return 1;
   else
      return n*fact(n-1);
}

打开 Wasm Explorer,可从https://mbebenita.github.io/WasmExplorer/ 获取,如下所示 -

C++ 阶乘函数

第一列具有 C++ 阶乘函数,第二列具有 WebAssembly 文本格式,最后一列具有 x86 汇编代码。

WebAssembly 文本格式 -

(module
   (table 0 anyfunc)
   (memory $0 1)
   (export "memory" (memory $0))
   (export "_Z4facti" (func $_Z4facti))
   (func $_Z4facti (; 0 ;) (param $0 i32) (result i32)
      (local $1 i32)
      (set_local $1
         (i32.const 1)
      )
      (block $label$0
         (br_if $label$0
            (i32.eq
               (i32.or
                  (get_local $0)
                  (i32.const 1)
               )
               (i32.const 1)
            )
         )
         (set_local $1
            (i32.const 1)
         )
         (loop $label$1
            (set_local $1
               (i32.mul
                  (get_local $0)
                  (get_local $1)
               )
            )
            (br_if $label$1
               (i32.ne
                  (i32.or
                     (tee_local $0
                        (i32.add
                           (get_local $0)
                           (i32.const -1)
                        )
                     )
                     (i32.const 1)
                  )
                  (i32.const 1)
               )
            )
         )
      )
      (get_local $1)
   )
)

C++ 函数事实已以 WebAssembly 文本格式导出为“ _Z4facti ”。

单击下载按钮下载 wasm 代码并将文件保存为 Factorial.wasm。

阶乘 WASM

现在要将 .wasm 代码转换为模块,我们必须执行以下操作 -

步骤1

使用 ArrayBuffer 将 .wasm 转换为 arraybuffer ArrayBuffer 对象将返回一个固定长度的二进制数据缓冲区。

第2步

必须使用WebAssembly.compile(buffer)函数将 ArrayBuffer 中的字节编译到模块中。

WebAssembly.compile ()函数根据给定的字节编译并返回 WebAssembly.Module。

这里是步骤 1 和 2 中讨论的 Javascript 代码。

<script type="text/javascript">
   let factorial;
   fetch("factorial.wasm")
      .then(bytes => bytes.arrayBuffer())
      .then(mod => WebAssembly.compile(mod))
      .then(module => {return new WebAssembly.Instance(module) })
      .then(instance => {
      
      factorial = instance.exports._Z4facti;
      console.log('Test the output in Brower Console by using factorial(n)');
   });
</script>

代码说明

  • Javascript浏览器API fetch用于获取factorial.wasm的内容。

  • 使用 arrayBuffer() 将内容转换为字节。

  • 该模块是通过调用 WebAssembly.compile(mod) 从字节创建的。

  • 模块的实例是使用 new 创建的

    WebAssembly.Instance(模块)

  • 使用 WebAssembly.Module.exports() 将阶乘函数导出 _Z4facti 分配给变量阶乘。

例子

这里是 module.html 以及 javascript 代码 -

模块.html

<!doctype html>
<html>
   <head>
      <meta charset="utf-8">
      <title>WebAssembly Module</title>
   </head>
   <body>
      <script>
      let factorial;
      fetch("factorial.wasm")
      .then(bytes => bytes.arrayBuffer())
      .then(mod => WebAssembly.compile(mod))
      .then(module => {return new WebAssembly.Instance(module) })
      .then(instance => {
         factorial = instance.exports._Z4facti;
         console.log('Test the output in Browser Console by using factorial(n)');
      });
      </script>
   </body>
</html>

输出

在浏览器中执行 module.html 以查看输出 -

执行模块 HTML

WebAssembly - 验证

在本章中,我们将讨论 web assembly.validate() 函数,该函数将验证 .wasm 输出。当我们编译 C、C++ 或 Rust 代码时,.wasm 可用。

您可以使用以下工具来获取 wasm 代码。

句法

语法如下 -

WebAssembly.validate(bufferSource);

参数

bufferSource - bufferSource 具有来自 C、C++ 或 Rust 程序的二进制代码。它采用 typedarray 或 ArrayBuffer 的形式。

返回值

如果 .wasm 代码有效,该函数将返回 true,否则返回 false。

让我们尝试一个例子。转到Wasm fiddler(位于https://wasdk.github.io/WasmFiddle/),输入您选择的 C 代码并下载 wasm 代码。

缓冲源

红色标记的块是 C 代码。单击中心的“构建”按钮来执行代码。

证实

单击 Wasm 按钮下载 .wasm 代码。将 .wasm 保存在您的末尾,让我们使用它进行验证。

例子

例如:validate.html

<!doctype html>
<html>
   <head> 
      <meta charset="utf-8">
      <title>Testing WASM validate()</title>
   </head>
   <body>
      <script> 
         fetch('program.wasm').then(res => res.arrayBuffer() ).then(function(testbytes) {
         var valid = WebAssembly.validate(testbytes); 
            if (valid) {
               console.log("Valid Wasm Bytes!"); 
            } else {
               console.log("Invalid Wasm Code!"); 
            }
         }); 
      </script> 
   </body>
</html>

我已在 wamp 服务器中托管了上述 .html 文件以及下载的 .wasm 文件。这是在浏览器中测试时的输出。

输出

输出如下 -

提及

WebAssembly - 文本格式

WebAssembly 的代码采用二进制格式,称为 WASM。您还可以在 WebAssembly 中获取文本格式,它称为 WAT(WebAssembly 文本格式)。作为开发人员,您不应该在 WebAssembly 中编写代码,相反,您必须将 C、C++ 和 Rust 等高级语言编译为 WebAssembly。

瓦特代码

让我们逐步编写WAT代码。

步骤 1 - WAT 的起点是声明模块。

(module)

步骤 2 - 现在让我们以函数的形式向其添加一些功能。

该函数声明如下 -

(func <parameters/result> <local variables> <function body>)

该函数以 func 关键字开头,后跟参数或结果。

参数/结果

参数和返回值作为结果。

wasm 支持的参数可以是以下类型 -

  • i32:32 位整数
  • i64:64 位整数
  • f32:32 位浮点数
  • f64:64 位浮点数

函数的参数编写如下 -

  • (参数 i32)
  • (参数 i64)
  • (参数 f32)
  • (参数 f64)

结果将写成如下 -

  • (结果 i32)
  • (结果 i64)
  • (结果 f32)
  • (结果 f64)

带有参数和返回值的函数定义如下 -

(func (param i32) (param i32) (result i64) <function body>)

局部变量

局部变量是您的函数中需要的变量。该函数的局部值将定义如下 -

(func (param i32) (param i32) (local i32) (result i64) <function body>)

函数体

函数体是要执行的逻辑。最终的程序将如下所示 -

(module (func (param i32) (param i32) (local i32) (result i64) <function body>) )

步骤 3 - 读取和设置参数和局部变量。

要读取参数和局部变量,请使用get_localset_local命令。

例子

(module 
   (func (param i32) (param i32) (local i32) (result i64) get_local 0 
      get_local 1 
      get_local 2 
   ) 
)

根据函数签名,

  • get_local 0将给出参数 i32

  • get_local 1将给出下一个参数param i32

  • get_local 2将给出本地值 i32

您也可以在参数之前使用名称,并在名称前加上美元符号作为前缀,而不是使用 0、1、2 等数值来引用参数和局部变量。

以下示例显示如何将名称与参数和局部变量一起使用。

例子

(module 
   (func 
      (param $a i32) 
      (param $b i32) 
      (local $c i32) 
      (result i64) get_local $a get_local $b get_local $c 
   ) 
)

步骤 4 - 函数体中的指令和执行。

wasm 中的执行遵循堆栈策略。执行的指令被一条一条地送入堆栈。例如,指令 get_local $a 将推送值,它在堆栈上读取。

像i32.add这样添加的指令将从堆栈中弹出元素。

(func (param $a i32) (param $b i32) 
   get_local $a 
   get_local $b 
   i32.add
)

i32.add的指令是($a+$b)。i32.add 的最终值将被推送到堆栈上,并将其分配给结果。

如果函数签名声明了结果,则执行结束时堆栈中应该有一个值。如果没有结果参数,则堆栈最后必须为空。

因此,最终代码和函数体如下 -

(module 
   (func (param $a i32) (param $b i32) (result i32) 
      get_local $a
      get_local $b 
      i32.add
   )
)

步骤 5 - 调用该函数。

函数体的最终代码如步骤 4 所示。现在,要调用该函数,我们需要导出它。

要导出函数,可以使用 0,1 等索引值来完成,但是,我们也可以给出名称。该名称将以 $ 为前缀,并将其添加在 func 关键字之后。

例子

(module 
   (func $add (param $a i32) (param $b i32) (result i32) 
      get_local $a 
      get_local $b i32.add
   ) 
)

必须使用 export 关键字导出函数 $add ,如下所示 -

(module 
   (func $add 
      (param $a i32) 
      (param $b i32) 
      (result i32) 
      get_local $a get_local $b i32.add
   ) 
   (export "add" (func $add))
)

要在浏览器中测试上述代码,您必须将其转换为二进制形式(.wasm)。请参阅下一章,了解如何将.WAT 转换为 .WASM。

WebAssembly - 将 WAT 转换为 WASM

在上一章中,我们了解了如何以.wat(即 WebAssembly 文本格式)编写代码。WebAssembly 文本格式不能直接在浏览器中工作,您需要将其转换为二进制格式,即 WASM 才能在浏览器中工作。

WASM 转 WASM

让我们将 .WAT 转换为 .WASM。

我们要使用的代码如下 -

(module 
   (func $add (param $a i32) (param $b i32) (result i32) 
      get_local $a 
      get_local $b 
      i32.add
   ) 
   (export "add" (func $add)) 
)

现在,转到WebAssembly Studio ,可从https://webassemble.studio/获取。

当您点击链接时,您应该看到类似这样的内容 -

将 WAT 转换为 WASM

单击“Empty Wat”项目,然后单击底部的“创建”按钮。

空笏计划

它将带您到一个空项目,如下所示 -

空项目

单击 main.wat 并将现有代码替换为您的代码,然后单击“保存”按钮。

现有代码

保存后,单击构建以转换为 .wasm -

转换为 WASM

如果构建成功,您应该看到创建的 .wasm 文件,如下所示 -

WASM文件

找到 main.wasm 文件并在 .html 文件中使用它来查看输出,如下所示。

例如 - add.html

<!doctype html>
<html>
   <head>
      <meta charset="utf-8">
      <title>WebAssembly Add Function</title>
   </head>
   <body>
      <script> 
         let sum; 
         fetch("main.wasm")
            .then(bytes => bytes.arrayBuffer()) 
            .then(mod => WebAssembly.compile(mod)) .then(module => {
            
            return new WebAssembly.Instance(module) 
         })
         .then(instance => {
            sum = instance.exports.add(10,40); 
            console.log("The sum of 10 and 40 = " +sum); 
         }); 
      </script>
   </body>
</html>

函数 add 被导出,如代码所示。传递的参数是 2 个整数值 10 和 40,它返回它们的总和。

输出

输出显示在浏览器中。

显示

WebAssembly - 动态链接

动态链接是两个或多个模块在运行时链接在一起的过程。

为了演示动态链接的工作原理,我们将使用 C 程序并使用 Ecmascript sdk 将其编译为 wasm。

所以这里我们有 -

测试1.c

int test1(){ 
   return 100; 
}

测试2.c

int test2(){ 
   return 200; 
}

主程序

#include <stdio.h>

int test1(); 
int test2();
int main() { 
   int result = test1() + test2(); 
   return result; 
}

在 main.c 代码中,它使用了 test1() 和 test2(),它们在 test1.c 和 test2.c 中定义。让我们检查一下如何在 WebAssembly 中链接这些模块。

编译上述代码的命令如下:使用SIDE_MODULE =1进行动态链接,如命令所示。

emcc test1.c test2.c main.c -s SIDE_MODULE=1 -o maintest.wasm

使用 WasmtoWat(可在https://webassemble.github.io/wabt/demo/wasm2wat/获取)将获得 maintest.wasm 的 WebAssembly 文本格式。

(module 
   (type $t0 (func (result i32))) (type $t1 (func)) 
   (type $t2 (func (param i32))) (type $t3 (func (param i32 i32) (result i32))) 
   (import "env" "stackSave" (func $env.stackSave (type $t0))) 
   (import "env" "stackRestore" (func $env.stackRestore (type $t2))) 
   (import "env" "__memory_base" (global $env.__memory_base i32)) 
   (import "env" "__table_base" (global $env.__table_base i32)) 
   (import "env" "memory" (memory $env.memory 0)) 
   (import "env" "table" (table $env.table 0 funcref)) 
   (func $f2 (type $t1) 
      (call $__wasm_apply_relocs)
   )
   (func $__wasm_apply_relocs (export "__wasm_apply_relocs") (type $t1)) 
   (func $test1 (export "test1") (type $t0) (result i32) 
      (local $l0 i32) 
      (local.set $l0 
         (i32.const 100)
      )
      (return 
         (local.get $l0)
      )
   )
   (func $test2 (export "test2") (type $t0) (result i32) 
      (local $l0 i32) 
      (local.set $l0 
         (i32.const 200)) 
      (return 
         (local.get $l0)
      )
   ) 
   (func $__original_main 
      (export "__original_main") 
      (type $t0) 
      (result i32) 
      (local $l0 i32) 
      (local $l1 i32) 
      (local $l2 i32) 
      (local $l3 i32) 
      (local $l4 i32) 
      (local $l5 i32) 
      (local $l6 i32) 
      (local $l7 i32) 
      (local $l8 i32) 
      (local $l9 i32) 
      (local.set $l0(call $env.stackSave))
      (local.set $l1 (i32.const 16))
      (local.set $l2 (i32.sub (local.get $l0) (local.get $l1)))
      (call $env.stackRestore (local.get $l2) ) (local.set $l3(i32.const 0)) 
      (i32.store offset=12 (local.get $l2) (local.get $l3)) 
      (local.set $l4 (call $test1)) 
      (local.set $l5 (call $test2)) 
      (local.set $l6 (i32.add (local.get $l4) (local.get $l5))) 
      (i32.store offset=8 (local.get $l2) (local.get $l6)) 
      (local.set $l7 (i32.load offset=8 (local.get $l2))) 
      (local.set $l8 (i32.const 16)) 
      (local.set $l9 (i32.add (local.get $l2) (local.get $l8))) 
      (call $env.stackRestore (local.get $l9)) (return(local.get $l7))
   )
   (func $main 
      (export "main") 
      (type $t3) 
      (param $p0 i32) 
      (param $p1 i32) 
      (result i32) 
      (local $l2 i32) 
      (local.set $l2 
      (call $__original_main)) 
      (return (local.get $l2))
   ) 
   (func $__post_instantiate (export "__post_instantiate") (type $t1) (call $f2)) 
   (global $__dso_handle (export "__dso_handle") i32 (i32.const 0))
)

WebAssembly 文本格式有一些定义的导入,如下所示 -

(import "env" "stackSave" (func $env.stackSave (type $t0)))       
(import "env" "stackRestore" (func $env.stackRestore (type $t2))) 
(import "env" "__memory_base" (global $env.__memory_base i32)) 
(import "env" "__table_base" (global $env.__table_base i32)) 
(import "env" "memory" (memory $env.memory 0)) 
(import "env" "table" (table $env.table 0 funcref))

这是在 emcc(emscripten sdk) 编译代码时添加的,它处理 WebAssembly 中的内存管理。

处理导入和导出

现在要查看输出,我们必须定义您可以在 .wat 代码中看到的导入 -

(import "env" "stackSave" (func $env.stackSave (type $t0)))       
(import "env" "stackRestore" (func $env.stackRestore (type $t2))) 
(import "env" "__memory_base" (global $env.__memory_base i32)) 
(import "env" "__table_base" (global $env.__table_base i32)) 
(import "env" "memory" (memory $env.memory 0)) 
(import "env" "table" (table $env.table 0 funcref))

上述术语解释如下 -

  • env.stackSave - 用于堆栈管理,这是由 emscripten 编译代码提供的功能。

  • env.stackRestore - 它用于堆栈管理,这是由 emscripten 编译代码提供的功能。

  • env.__memory_base - 它是一个不可变的 i32 全局偏移量,在 env.memory 中使用并为 wasm 模块保留。模块可以在其数据段的初始化程序中使用此全局变量,以便将它们加载到正确的地址。

  • env.__table_base - 它是一个不可变的 i32 全局偏移量,在 env.table 中使用并为 wasm 模块保留。模块可以在其表元素段的初始化程序中使用此全局变量,以便将它们加载到正确的偏移量处。

  • env.memory - 这将包含 wasm 模块之间共享所需的内存详细信息。

  • env.table - 这将包含 wasm 模块之间需要共享的表详细信息。

导入必须在 javascript 中定义如下 -

var wasmMemory = new WebAssembly.Memory({'initial': 256,'maximum': 65536}); 
const importObj = { 
   env: {
      stackSave: n => 2, stackRestore: n => 3, //abortStackOverflow: () => {
         throw new Error('overflow'); 
      }, 
      table: new WebAssembly.Table({ 
         initial: 0, maximum: 65536, element: 'anyfunc' 
      }), __table_base: 0,
      memory: wasmMemory, __memory_base: 256 
   } 
};

例子

以下是使用 WebAssembly.instantiate 内的 importObj 的 javascript 代码。

<!DOCTYPE html> 
<html>
   <h