为什么需要 WebAssembly?
首先看一下 js 代码的执行过程:
上述是 Microsoft Edge 之前的 ChakraCore 引擎结构,目前 Microsoft Edge 的 JS 引擎已经切换为 V8 。
动态语言之踵
js代码整体的流程是:
- 拿到 js 源代码,交给 Parser,生成 AST
- ByteCode Compiler 将 AST 编译为字节码(ByteCode)
- ByteCode 进入翻译器,翻译器将字节码一行一行翻译(Interpreter)为机器码(Machine Code),然后执行
但其实我们平时写的代码有很多可以优化的地方,如多次执行同一个函数,那么可以将这个函数生成的 Machine Code 标记可优化,然后打包送到 JIT Compiler(Just-In-Time),下次再执行这个函数的时候,就不需要经过 Parser-Compiler-Interpreter 这个过程,可以直接执行这份准备好的 Machine Code,大大提高的代码的执行效率。
上述的 JIT 优化只能针对静态类型的变量,如我们要优化的函数,它只有两个参数,每个参数的类型是确定的,而 JavaScript 却是一门动态类型的语言,这也意味着,函数在执行过程中,可能类型会动态变化,参数可能变成三个,第一个参数的类型可能从对象变为数组,这就会导致 JIT 失效,需要重新进行 Parser-Compiler-Interpreter-Execuation,而 Parser-Compiler 这两步是整个代码执行过程中最耗费时间的两步,这也是为什么 JavaScript 语言背景下,Web 无法执行一些高性能应用,如大型游戏、视频剪辑等。
静态语言优化
通过上面的说明了解到,其实 JS 执行慢的一个主要原因是因为其动态语言的特性,导致 JIT 失效,所以如果我们能够为 JS 引入静态特性,那么可以保持有效的 JIT,势必会加快 JS 的执行速度,这个时候 asm.js 出现了。
asm.js 只提供两种数据类型:
- 32 位带符号整数
- 64 位带符号浮点数
其他类似如字符串、布尔值或对象都是以数值的形式保存在内存中,通过 TypedArray 调用。整数和浮点数表示如下:
ArrayBuffer对象、TypedArray视图和DataView 视图是 JavaScript 操作二进制数据的一个接口,以数组的语法处理二进制数据,统称为二进制数组。参考 ArrayBuffer[2] 。
var x = a | 0; // x 是32位整数
var y = +a; // y 是64位浮点数
而函数的写法如下:
function add(x, y) {
x = x | 0;
y = y | 0;
return (x + y) | 0;
}
上述的函数参数及返回值都需要声明类型,这里都是 32 位整数。
而且 asm.js 也不提供垃圾回收机制,内存操作都是由开发者自己控制,通过 TypedArray 直接读写内存:
var buffer = new ArrayBuffer(32768); // 申请 32 MB 内存
var HEAP8 = new Int8Array(buffer); // 每次读 1 个字节的视图 HEAP8
function compiledCode(ptr) {
HEAP[ptr] = 12;
return HEAP[ptr + 4];
}
从上可见,asm.js 是一个严格的 JavaScript 子集要求变量的类型在运行时确定且不可改变,且去除了 JavaScript 拥有的垃圾回收机制,需要开发者手动管理内存。这样 JS 引擎就可以基于 asm.js 的代码进行大量的 JIT 优化,据统计 asm.js 在浏览器里面的运行速度,大约是原生代码(机器码)的 50% 左右。
推陈出新
但是不管 asm.js 再怎么静态化,干掉一些需要耗时的上层抽象(垃圾收集等),也还是属于 JavaScript 的范畴,代码执行也需要 Parser-Compiler 这两个过程,而这两个过程也是代码执行中最耗时的。
为了极致的性能,Web 的前沿开发者们抛弃 JavaScript,创造了一门可以直接和 Machine Code 打交道的汇编语言 WebAssembly,直接干掉 Parser-Compiler,同时 WebAssembly 是一门强类型的静态语言,能够进行最大限度的 JIT 优化,使得 WebAssembly 的速度能够无限逼近 C/C++ 等原生代码。
相当于下面的过程:
WebAssembly 初探
我们可以通过一张图来直观了解 WebAssembly 在 Web 中的位置:
WebAssembly(也称为 WASM),是一种可在 Web 中运行的全新语言格式,同时兼具体积小、性能高、可移植性强等特点,在底层上类似 Web 中的 JavaScript,同时也是 W3C 承认的 Web 中的第 4 门语言。
为什么说在底层上类似 JavaScript,主要有以下几个理由:
- 和 JavaScript 在同一个层次执行:JS Engine,如 Chrome 的 V8
- 和 JavaScript 一样可以操作各种 Web API
同时 WASM 也可以运行在 Node.js 或其他 WASM Runtime 中。
WebAssembly 文本格式
实际上 WASM 是一堆可以直接执行二进制格式,但是为了易于在文本编辑器或开发者工具里面展示,WASM 也设计了一种 “中间态” 的文本格式[3],以 .wat 或 .wast 为扩展命名,然后通过 wabt[4] 等工具,将文本格式下的 WASM 转为二进制格式的可执行代码,以 .wasm 为扩展的格式。
来看一段 WASM 文本格式下的模块代码:
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i
)
)
上述代码逻辑如下:
- 首先定义了一个 WASM 模块,然后从一个 imports JS 模块导入了一个函数 imported_func ,将其命名为 $i ,接收参数 i32
- 然后导出一个名为 exported_func 的函数,可以从 Web App,如 JS 中导入这个函数使用
- 接着为参数 i32 传入 42,然后调用函数 $i
我们通过 wabt 将上述文本格式转为二进制代码:
- 将上述代码复制到一个新建的,名为 simple.wat 的文件中保存
- 使用 wabt[5] 进行编译转换
当你安装好 wabt 之后,运行如下命令进行编译:
wat2wasm simple.wat -o simple.wasm
虽然转换成了二进制,但是无法在文本编辑器中查看其内容,为了查看二进制的内容,我们可以在编译时加上 -v 选项,让内容在命令行输出:
wat2wasm simple.wat -v
输出结果如下:
可以看到,WebAssembly 其实是二进制格式的代码,即使其提供了稍为易读的文本格式,也很难真正用于实际的编码,更别提开发效率了。
将 WebAssembly 作为编程语言的一种尝试
因为上述的二进制和文本格式都不适合编码,所以不适合将 WASM 作为一门可正常开发的语言。
为了突破这个限制,AssemblyScript[6] 走到台前,AssemblyScript 是 TypeScript 的一种变体,为 JavaScript 添加了 WebAssembly 类型[7], 可以使用 Binaryen[8] 将其编译成 WebAssembly。
WebAssembly 类型大致如下:
- i32、u32、i64、v128 等
- 小整数类型:i8、u8 等
- 变量整数类型:isize、usize 等
Binaryen 会前置将 AssemblyScript 静态编译成强类型的 WebAssembly 二进制,然后才会交给 JS 引擎去执行,所以说虽然 AssemblyScript 带来了一层抽象,但是实际用于生产的代码依然是 WebAssembly,保有 WebAssembly 的性能优势。AssemblyScript 被设计的和 TypeScript 非常相似,提供了一组内建的函数可以直接操作 WebAssembly 以及编译器的特性。
内建函数:
- 静态类型检查:
- function isInteger
(value?: T): bool 等 - 实用函数:
- function sizeof
(): usize 等 - 操作 WebAssembly:
- function select
(ifTrue: T, ifFalse: T, condition: bool): T 等 - function load
(ptr: usize, immOffset?: usize): T 等 - function clz
(value: T): T 等 - 数学操作
- 内存操作
- 控制流
- SIMD
- Atomics
- Inline instructions
然后基于这套内建的函数向上构建一套标准库。
标准库:
- Globals
- Array
- ArrayBuffer
- DataView
- Date
- Error
- Map
- Math
- Number
- Set
- String
- Symbol
- TypedArray
如一个典型的 Array 的使用如下:
var arr = new Array<string>(10)
// arr[0]; // 会出错 😢
// 进行初始化
for (let i = 0; i < arr.length; ++i) {
arr[i] = ""
}
arr[0]; // 可以正确工作 😊
可以看到 AssemblyScript 在为 JavaScript 添加类似 TypeScript 那样的语法,然后在使用上需要保持和 C/C++ 等静态强类型的要求,如不初始化,进行内存分配就访问就会报错。
还有一些扩展库,如 Node.js 的 process、crypto 等,JS 的 console,还有一些和内存相关的 StaticArray、heap 等。
可以看到通过上面基础的类型、内建库、标准库和扩展库,AssemblyScript 基本上构造了 JavaScript 所拥有的的全部特性,同时 AssemblyScript 提供了类似 TypeScript 的语法,在写法上严格遵循强类型静态语言的规范。
值得一提的是,因为当前 WebAssembly 的 ES 模块规范依然在草案中,AssemblyScript 自行进行了模块的实现,例如导出一个模块:
// env.ts
export declare function doSomething(foo: i32): void { /* ... 函数体 */ }
导入一个模块:
import { doSomething } from "./env";
一个大段代码、使用类的例子:
static ONE: i32 = 1;
static add(a: i32, b: i32): i32 { return a + b + Animal.ONE; }
two: i16 = 2; // 6
instanceSub<T>(a: T, b: T): T { return a - b + <T>Animal.ONE; } // tsc does not allow this
}
export function staticOne(): i32 {
return Animal.ONE;
}
export function staticAdd(a: i32, b: i32): i32 {
return Animal.add(a, b);
}
export function instanceTwo(): i32 {
let animal = new Animal<i32>();
return animal.two;
}
export function instanceSub(a: f32, b: f32): f32 {
let animal = new Animal<f32>();
return animal.instanceSub<f32>(a, b);
}
AssemblyScript 为我们打开了一扇新的大门,可以以 TS 形式的语法,遵循静态强类型的规范进行高效编码,同时又能够便捷的操作 WebAssembly/编译器相关的 API,代码写完之后,通过 Binaryen 编译器将其编译为 WASM 二进制,然后获取到 WASM 的执行性能。
得益于 AssemblyScript 兼具灵活性与性能,目前使用 AssemblyScript 构建的应用生态已经初具繁荣,目前在区块链、构建工具、编辑器、模拟器、游戏、图形编辑工具、库、IoT、测试工具等方面都有大量使用 AssemblyScript 构建的产物:https://www.assemblyscript.org/built-with-assemblyscript.html#games
一种鬼才哲学:将 C/C++ 代码跑在浏览器
虽然 AssemblyScript 的出现极大的改善了 WebAssembly 在高效率编码方面的缺陷,但是作为一门新的编程语言,其最大的劣势就是生态、开发者与积累。
WebAssembly 的设计者显然在设计上同时考虑到了各种完善的情况,既然 WebAssembly 是一种二进制格式,那么其就可以作为其他语言的编译目标,如果能够构建一种编译器,能够将已有的、成熟的、且兼具海量的开发者和强大的生态的语言编译到 WebAssembly 使用,那么相当于可以直接复用这个语言多年的积累,并用它们来完善 WebAssembly 生态,将它们运行在 Web、Node.js 中。
幸运的是,针对 C/C++ 已经有 Emscripten[9] 这样优秀的编译器存在了。
即将 C/C++ 的代码(或者 Rust/Go 等)编译成 WASM,然后通过 JS 胶水代码将 WASM 跑在浏览器中(或 Node.js)的 runtime,如 ffmpeg 这个使用 C 编写音视频转码工具,通过 Emscripten 编译器编译到 Web 中使用,可直接在浏览器前端转码音视频。
上述的 JS “Gule” 代码是必须的,因为如果需要将 C/C++ 编译到 WASM,还能在浏览器中执行,就得实现映射到 C/C++ 相关操作的 Web API,这样才能保证执行有效,这些胶水代码目前包含一些比较流行的 C/C++ 库,如 SDL[10]、OpenGL[11]、OpenAL[12]、以及 POSIX[13] 的一部分 API。
目前使用 WebAssembly 最大的场景也是这种将 C/C++ 模块编译到 WASM 的方式,比较有名的例子有 Unreal Engine 4[14]、Unity[15] 之类的大型库或应用。
WebAssembly 会取代 JavaScript 吗?
答案是不会。
根据上面的层层阐述,实际上 WASM 的设计初衷就可以梳理为以下几点:
- 最大程度的复用现有的底层语言生态,如 C/C++ 在游戏开发、编译器设计等方面的积淀
- 在 Web、Node.js 或其他 WASM runtime 获得近乎于原生的性能,也就是可以让浏览器也能跑大型游戏、图像剪辑等应用
- 还有最大程度的兼容 Web、保证安全
- 同时在开发上(如果需要开发)易于读写和可调试,这一点 AssemblyScript 走得更远
所以从初衷出发,WebAssembly 的作用更适合下面这张图:
WASM 桥接各种系统编程语言的生态,进一步补齐了 Web 开发生态之外,还为 JS 提供性能的补充,正是 Web 发展至今所缺失的重要的一块版图。
Rust Web Framework:https://github.com/yewstack/yew
深入探索 Emscripten
地址:https://github.com/emscripten-core/emscripten 下面所有的 demo 都可以在仓库:https://code.byted.org/huangwei.fps/webassembly-demos/tree/master 找到
Emscripten 是一个开源的,跨平台的,用于将 C/C++ 编译为 WebAssembly 的编译器工具链,由 LLVM、Binaryen、Closure Compiler 和其他工具等组成。
Emscripten 的核心工具为 Emscripten Compiler Frontend(emcc),emcc 是用于替代一些原生的编译器如 gcc 或 clang,对 C/C++ 代码进行编译。
实际上为了能让几乎所有的可移植的 C/C++ 代码库能够编译为 WebAssembly,并在 Web 或 Node.js 执行,Emscripten Runtime 其实还提供了兼容 C/C++ 标准库、相关 API 到 Web/Node.js API 的映射,这份映射存在于编译之后的 JS 胶水代码中。
再看下面这张图,红色部分为 Emscripten 编译后的产物,绿色部分为 Emscripten 为保证 C/C++ 代码能够运行的一些 runtime 支持:
简单体验一下 “Hello World”
值得一提的是,WebAssembly 相关工具链的安装几乎都是以源码的形式提供,这可能和 C/C++ 生态的习惯不无关系。
为了完成简单的 C/C++ 程序运行在 Web,我们首先需要安装 Emscripten 的 SDK:
# Clone 代码仓库
git clone https: // github . com / emscripten-core / emsdk . git
# 进入仓库
cd emsdk
# 获取最新代码,如果是新 clone 的这一步可以不需要
git pull
# 安装 SDK 工具,我们安装 1.39.18,方便测试
./emsdk install 1.39.18
# 激活 SDK
./emsdk activate 1.39.18
# 将相应的环境变量加入到系统 PATH
source ./emsdk_env.sh
# 运行命令测试是否安装成功
emcc -v #
如果安装成功,上述的命令运行之后会输出如下结果:
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.39.18
clang version 11.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 613c4a87ba9bb39d1927402f4dd4c1ef1f9a02f7)
Target: x86_64-apple-darwin21.1.0
Thread model: posix
让我们准备初始代码:
mkdir -r webassembly/hello_world
cd webassembly/hello_world && touch main.c
在 main.c 中加入如下代码:
#include <stdio.h>
int main() {
printf("hello, world!\n");
return 0;
}
然后使用 emcc 来编译这段 C 代码,在命令行切换到 webassembly/hello_world 目录,运行:
emcc main.c
上述命令会输出两个文件:a.out.js 和 a.out.wasm ,后者为编译之后的 wasm 代码,前者为 JS 胶水代码,提供了 WASM 运行的 runtime。
可以使用 Node.js 进行快速测试:
node a.out.js
会输出 “hello, world!” ,我们成功将 C/C++ 代码运行在了 Node.js 环境。
接下来我们尝试一下将代码运行在 Web 环境,修改编译代码如下:
emcc main.c -o main.html
上述命令会生成三个文件:
- main.js 胶水代码
- main.wasm WASM 代码
- main.html 加载胶水代码,执行 WASM 的一些逻辑
Emscripten 生成代码有一定的规则,具体可以参考:https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-linker-output-files
如果要在浏览器打开这个 HTML,需要在本地起一个服务器,因为单纯的打开通过 file:// 协议访问时,主流浏览器不支持 XHR 请求,只有在 HTTP 服务器下,才能进行 XHR 请求,所以我们运行如下命令来打开网站:
npx serve .
打开网页,访问 localhost:3000/main.html,可以看到如下结果:
同时开发者工具里面也会有相应的打印输出:
尝试在 js 中调用 C / C++ 函数
上一小节我们初步体验了一下如何在 Web 和 Node.js 中运行 C 程序,但其实如果我们想要让复杂的 C/C++ 应用,如 Unity 运行在 Web,那我们还有很长的路要走,其中一条,就是能够在 JS 中操作 C/C++ 函数。
让我们在目录下新建 function.c 文件,添加如下代码:
#include <stdio.h>
#include <emscripten/emscripten.h>
int main() {
printf("Hello World\n");
}
EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
printf("MyFunction Called\n");
}
值得注意的是 Emscripten 默认编译的代码只会调用 main 函数,其他的代码会作为 “死代码” 在编译时被删掉,所以为了使用我们在上面定义的 myFunction ,我们需要在其定义之前加上 EMSCRIPTEN_KEEPALIVE 声明,确保在编译时不会删掉 myFunction 函数相关的代码。
我们需要导入 emscripten/emscripten.h 头文件,才能使用 EMSCRIPTEN_KEEPALIVE 声明。
同时我们还需要对编译命令做一下改进如下:
emcc function.c -o function.html -s NO_EXIT_RUNTIME=1 -s “EXTRA_EXPORTED_RUNTIME_METHODS=[‘ccall’]”
上述额外增加了两个参数:
- -s NO_EXIT_RUNTIME=1 表示在 main 函数运行完之后,程序不退出,依然保持可执行状态,方便后续可调用 myFunction 函数
- -s “EXTRA_EXPORTED_RUNTIME_METHODS=[‘ccall’]” 则表示导出一个运行时的函数 ccall ,这个函数可以在 JS 中调用 C 程序的函数
进行编译之后,我们还需要修改生成的 function.html 文件,加入我们的函数调用逻辑如下:```
<html>
<body>
<!-- 其它 HTML 内容 -->
<button class="mybutton">Run myFunction</button>
</body>
<!-- 其它 JS 引入 -->
<script>
document
.querySelector(".mybutton")
.addEventListener("click", function () {
alert("check console");
var result = Module.ccall(
"myFunction", // 需要调用的 C 函数名
null, // 函数返回类型
null, // 函数参数类型,默认是数组
null // 函数需要传入的参数,默认是数组
);
});
</script>
</html>
可以看到我们增加了一个 Button,然后增加了一段脚本,为这个 Button 注册了 click 事件,在回调函数里,我们调用了 myFunction 函数。
在命令行中运行 npx serve . 打开浏览器访问 http://localhost:3000/function.html,查看结果如下:
- 只执行 main 函数:
- 尝试点击按钮执行 myFunction 函数:
可以看到首先进行 alert 弹框展示,然后打开控制台,可以看到 myFunction 的调用结果,打印 “MyFunction Called” 。
初尝Emscripten文件系统
我们可以在 C/C++ 程序中使用 libc stdio API 如 fopen 、fclose 来访问你文件系统,但是 JS 是运行在浏览器提供的沙盒环境里,无法直接访问到本地文件系统。所以为了兼容 C/C++ 程序访问文件系统,编译为 WASM 之后依然能够正常运行,Emscripten 会在其 JS 胶水代码里面模拟一个文件系统,并提供和 libc stdio 一致的 API。
让我们重新创建一个名为 file.c 的程序,添加如下代码:
#include <stdio.h>
int main() {
FILE *file = fopen("file.txt", "rb");
if (!file) {
printf("cannot open file\n");
return 1;
}
while (!feof(file)) {
char c = fgetc(file);
if (c != EOF) {
putchar(c);
}
}
fclose (file);
return 0;
}
上述代码我们首先使用 fopen 访问 file.txt ,然后一行一行的读取文件内容,如果程序执行过程中有任何的出错,就会打印错误。
我们在目录下新建 file.txt 文件,并加入如下内容:
==
This data has been read from a file.
The file is readable as if it were at the same location in the filesystem, including directories, as in the local filesystem where you compiled the source.
==
如果我们要编译这个程序,并确保能够在 JS 中正常运行,还需要在编译时加上 preload 参数,提前将文件内容加载进 Emscripten runtime,因为在 C/C++ 等程序上访问文件都是同步操作,而 JS 是基于事件模型的异步操作,且在 Web 中只能通过 XHR 的形式去访问文件(Web Worker、Node.js 可同步访问文件),所以需要提前将文件加载好,确保在代码编译之前,文件已经准备好了,这样 C/C++ 代码可以直接访问到文件。
运行如下命令进行代码编译:
emcc file.c -o file.html -s EXIT_RUNTIME=1 --preload-file file.txt
上述添加了 -s EXIT_RUNTIME=1 ,依然是确保 main 逻辑执行完之后,程序不会退出。
然后运行我们的本地服务器,访问 http://localhost:3000/file.html,可以查看结果:
尝试编译已存在 WebP 模块并使用
通过上面三个例子,我们已经了解了基础的 C/C++ 如打印、函数调用、文件系统相关的内容如何编译为 WASM,并在 JS 中运行,这里的 JS 特指 Web 和 Node.js 环境,通过上面的例子基本上绝大部分自己写的 C/C++ 程序都可以自行编译到 WASM 使用了。
而之前我们也提到过,其实当前 WebAssembly 最大的一个应用场景,就是最大程度的复用当前已有语言的生态,如 C/C++ 生态的库,这些库通常都依赖 C 标准库、操作系统、文件系统或其他依赖,而 Emscripten 最厉害的一点就在于能够兼容绝大部分这些依赖的特性,尽管还存在一些限制,但是已经足够可用。
简单的测试
接下来我们来了解一下如何将一个现存的、比较复杂且广泛使用的 C 模块:libwebp,将其编译到 WASM 并允许到 Web。libwebp 的源码是用 C 实现的,能够在 Github[16] 上找到它,同时可以了解到它的一些 API 文档[17]。
首先准备代码,在我们的目录下运行如下命令:
git clone https://github.com/webmproject/libwebp
为了快速测试是否正确的接入了 libwebp 进行使用,我们可以编写一个简单的 C 函数,然后在里面调用 libwebp 获取版本的函数,测试版本是否可以正确获取。
我们在目录下创建 webp.c 文件,添加如下内容:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE int version() {
return WebPGetEncoderVersion();
}
上述的 WebPGetEncoderVersion 就是 libwebp 里面获取当前版本的函数,而我们是通过导入 src/webp/encode.h 头文件来获取这个函数的,为了让编译器在编译时能够找到这个头文件,我们需要在编译的时候将 libwebp 库的头文件地址告诉编译器,并将编译器需要的所有 libwebp 库下的 C 文件传给编译器。
让我们运行如下编译命令:
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
上述命令中主要做了如下工作:
- -I libwebp 将 libwebp 库的头文件地址告诉编译器
- libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c 将编译器所需的 C 文件传给编译器,这里将 dec,dsp,demux,enc,mux,utils 等目录下的所有 C 文件都传递给了编译器,避免了一个个列出所需文件的繁琐,然后让编译器去自动识别那些没有使用的文件,并将其过滤掉
- webp.c 是我们编写的 C 函数,用于调用 WebPGetEncoderVersion 获取库版本
- -O3 代表在编译时进行等级为 3 的优化,包含内联函数、去除无用代码、对代码进行各种压缩优化等
- 而 -s WASM=1 其实是默认的,就是在编译时输出 xx.out.wasm ,这里之所以会设置这个选项主要是针对那些不支持 WASM 的 runtime,可以设置 -s WASM=0 ,输出等价的 JS 代码替代 WASM
- EXTRA_EXPORTED_RUNTIME_METHODS= ‘[“cwrap”]’ 则是输出 runtime 的函数 cwrap ,类似 ccall 可以在 JS 中调用 C 函数
上述的编译输出只有 a.out.js 和 a.out.wasm ,我们还需要建一份 HTML 文档来使用输出的脚本代码,新建 webp.html ,添加如下内容:
<html>
<head></head>
<body></body>
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
值得注意的是,我们通常在 Module.onRuntimeInitialized 的回调里面去执行我们 WASM 相关的操作,因为 WASM 相关的代码从加载到可用是需要一段时间的,而 onRuntimeInitialized 的回调则是确保 WASM 相关的代码已经加载完成,达到可用状态。
接着我们可以运行 npx serve . ,然后访问 http://localhost:3000/webp.html,查看结果:
可以看到控制台打印了 66049 版本号。
libwebp 通过十六进制的 0xabc 的 abc 来表示当前版本 a.b.c ,例如 v0.6.1,则会被编码成十六进制 0x000601 ,对应的十进制为 1537。而这里为十进制 66049,转成 16 进制则为 0x010201 ,表示当前版本为 v1.2.1。
在 JavaScript 中获取图片并放入 WebAssembly 中运行
刚刚通过调用编码器的 WebPGetEncoderVersion 方法来获取版本号来证实了已经成功编译了 libwebp 库到 wasm,然后可以在 JavaScript 使用它,接下来我们将了解更加复杂的操作,如何使用 libwebp 的编码 API 来转换图片格式。
libwebp 的 encoding API 需要接收一个关于 RGB、RGBA、BGR 或 BGRA 的字节数组,幸运的是,Canvas API 有一个 CanvasRenderingContext2D.getImageData 方法,能够返回一个 Uint8ClampedArray ,这个数组包含 RGBA 格式的图片数据。
首先我们需要在 JavaScript 中编写加载图片的函数,将其写到上一步创建的 HTML 文件里:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片一致
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
现在剩下的操作则是如何将图片数据从 JavaScript 复制到 wasm,为了达成这个目的,需要在先前的 webp.c 函数里面暴露额外的方法:
- 一个为 wasm 里面的图片分配内存的方法
- 一个释放内存的方法
修改 webp.c 如下:
#include <stdlib.h> // 此头文件导入用于分配内存的 malloc 方法和释放内存的 free 方法
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
}
create_buffer 为 RGBA 的图片分配内存,RGBA 图片一个像素包含 4 个字节,所以代码中需要添加 4 * sizeof(uint8_t) ,malloc 函数返回的指针指向所分配内存的第一块内存单元地址,当这个指针返回给 JavaScript 使用时,会被当做一个简单的数字处理。当通过 cwrap 函数获取暴露给 JavaScript 的对应 C 函数时,可以使用这个指针数字找到复制图片数据的内存开始位置。
我们在 HTML 文件中添加额外的代码如下:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap("encode", "", ["number","number","number","number",]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "number", []),
};
const image = await loadImage('./image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片一致
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
可以看到上述代码除了导入之前添加的 create_buffer 和 destroy_buffer 外,还有很多用于编码文件等方面的函数,我们将在后续讲解,除此之外,代码首先加载了一份 image.jpg 的图片,然后调用 C 函数为此图片数据分配内存,并相应的拿到返回的指针传给 WebAssembly 的 Module.HEAP8 ,在内存开始位置 p,写入图片的数据,最后会释放分配的内存。
编码图片
现在图片数据已经加载进 wasm 的内存中,可以调用 libwebp 的 encoder 方法来完成编码过程了,通过查阅 WebP 的文档[18],发现可以使用 WebPEncodeRGBA 函数来完成工作。这个函数接收一个指向图片数据的指针以及它的尺寸,以及每次需要跨越的 stride 步长,这里为 4 个字节(RGBA),一个区间在 0-100 的可选的质量参数。在编码的过程中,WebPEncodeRGBA 会分配一块用于输出数据的内存,我们需要在编码完成之后调用 WebPFree 来释放这块内存。
我们打开 webp.c 文件,添加如下处理编码的代码:
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
上述 WebPEncodeRGBA 函数执行的结果为分配一块输出数据的内存以及返回内存的大小。因为 C 函数无法使用数组作为返回值(除非我们需要进行动态内存分配),所以我们使用一个全局静态数组来获取返回的结果,这可能不是很规范的 C 代码写法,同时它要求 wasm 指针为 32 比特长,但是为了简单起见我们可以暂时容忍这种做法。
现在 C 侧的相关逻辑已经编写完毕,可以在 JavaScript 侧调用编码函数,获取图片数据的指针和图片所占用的内存大小,将这份数据保存到 WASM 的缓冲中,然后释放 wasm 在处理图片时所分配的内存,让我们打开 HTML 文件完成上述描述的逻辑:
<script src="./a.out.js"></script>
<script>
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap("encode", "", ["number","number","number","number",]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
get_result_size: Module.cwrap("get_result_size", "number", []),
};
const image = await loadImage('./image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
api.destroy_buffer(p);
};
async function loadImage(src) {
// 加载图片
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// 设置 canvas 画布的大小与图片一致
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// 将图片绘制到 canvas 上
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
</script>
在上述代码中我们通过 loadImage 函数加载了一张本地的 image.jpg 图片,你需要事先准备一张图片放置在 emcc 编译器输出的目录下,也就是我们的 HTML 文件目录下使用。
注意:new Uint8Array(someBuffer) 将会在同样的内存块上创建一个新视图,而 new Uint8Array(someTypedArray) 只会复制 someTypedArray 的数据,确保使用复制的数据进行操作,不会修改原内存数据。
当你的图片比较大时,因为 wasm 不能自动扩充内存,如果默认分配的内存无法容纳 input 和 output 图片数据的内存,你可能会遇到如下报错:
但是我们例子中使用的图片比较小,所以只需要单纯的在编译时加上一个过滤参数 -s ALLOW_MEMORY_GROWTH=1 忽略这个报错信息即可:
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-s ALLOW_MEMORY_GROWTH=1`
再次运行上述命令,得到添加了编码函数的 wasm 代码和对应的 JavaScript 胶水代码,这样当我们打开 HTML 文件时,它已经能够将一份 JPG 文件编码成 WebP 的格式,为了进一步证实这个观点,我们可以将图片展示到 Web 界面上,通过修改 HTML 文件,添加如下代码:
<script>
// ...
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
// 添加到这里
const blob = new Blob([result], {type: 'image/webp'});
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img)
api.free_result(resultPointer);
api.destroy_buffer(p);
</script>
然后刷新浏览器,你应该可以看到如下界面:
通过将这个文件下载到本地,可以看到其格式转成了 WebP:
通过上述的流程我们成功编译了现有的 libwebp C 库到 wasm 使用,并将 JPG 图片转成了 WebP 格式并展示在 Web 界面上,通过 wasm 来处理计算密集型的转码操作可以大大提高网页的性能,这也是 WebAssembly 带来的主要优势之一。
如何调试 WebAssembly 代码?
WebAssembly 的原始调试方式
Chrome 开发者工具目前已经支持 WebAssembly 的调试,虽然存在一些限制,但是针对 WebAssembly 的文本格式的文件能进行单个指令的分析以及查看原始的堆栈追踪,具体见如下图:
上述的方法对于一些无其他依赖函数的 WebAssembly 模块来说可以很好的运行,因为这些模块只涉及到很小的调试范围。但是对于复杂的应用来说,如 C/C++ 编写的复杂应用,一个模块依赖其他很多模块,且源代码与编译后的 WebAssembly 的文本格式的映射有较大的区别时,上述的调试方式就不太直观了,只能靠猜的方式才能理解其中的代码运行方式,且大多数人很难以看懂复杂的汇编代码。
更加直观的调试方式
现代的 JavaScript 项目在开发时通常也会存在编译的过程,使用 ES6 进行开发,编译到 ES5 及以下的版本进行运行,这个时候如果需要调试代码,就涉及到 Source Map 的概念,source map 用于映射编译后的对应代码在源代码中的位置,source map 使得客户端的代码更具可读性、更方便调试,但是又不会对性能造成很大的影响。
而 C/C++ 到 WebAssembly 代码的编译器 Emscripten 则支持在编译时,为代码注入相关的调试信息,生成对应的 source map,然后安装 Chrome 团队编写的 C/C++ Devtools Support[20] 浏览器扩展,就可以使用 Chrome 开发者工具调试 C/C++ 代码了。
这里的原理其实就是,Emscripten 在编译时,会生成一种 DWARF 格式的调试文件,这是一种被大多数编译器使用的通用调试文件格式,而 C/C++ Devtools Support[21] 则会解析 DWARF 文件,为 Chrome Devtools 在调试时提供 source map 相关的信息,使得开发者可以在 89+ 版本以上的 Chrome Devtools 上调试 C/C++ 代码。
参考链接
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://pspdfkit.com/blog/2017/webassembly-a-new-hope/
- https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/
- https://www.sitepoint.com/understanding-asm-js/
- http://www.cmake.org/download/
- https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasm
- https://research.mozilla.org/webassembly/
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16?gi=e525b34f2c21
- https://dev.to/alfg/ffmpeg-webassembly-2cbl
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926
- https://github.com/Kagami/ffmpeg.js/
- https://qdmana.com/2021/04/20210401214625324n.html
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- http://ffmpeg.org/doxygen/4.1/examples.html
- https://github.com/alfg/ffmpeg-webassembly-example
- https://github.com/alfg/ffprobe-wasm
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926#file-ffmpeg-emscripten-build-sh
- https://emscripten.org/docs/compiling/Building-Projects.html#integrating-with-a-build-system
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16
- https://github.com/mymindstorm/setup-emsdk
- https://github.com/emscripten-core/emsdk
- https://github.com/FFmpeg/FFmpeg/blob/n4.3.1/INSTALL.md
- https://yeasy.gitbook.io/docker_practice/container/run
- Debugging WebAssembly with modern tools - Chrome Developers[29]
- https://www.infoq.com/news/2021/01/chrome-extension-debug-wasm-c/
- https://developer.chrome.com/blog/wasm-debugging-2020/
- https://lucumr.pocoo.org/2020/11/30/how-to-wasm-dwarf/
- https://v8.dev/docs/wasm-compilation-pipeline
- Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces ([bitsrc.io)](https://blog.bitsrc.io/debugging-webassembly-with-chrome-devtools-99dbad485451 “Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io “Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)”)”)
- Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium[30]
- https://zhuanlan.zhihu.com/p/68048524
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://www.jianshu.com/p/e4a75cb6f268
- https://www.cloudsavvyit.com/13696/why-webassembly-frameworks-are-the-future-of-the-web/
- https://mp.weixin.qq.com/s/LSIi2P6FKnJ0GTodaTUGKw
参考资料
[1]WebAssembly 入门:如何和有 C 项目结合使用: https://bytedance.feishu.cn/docs/doccnmiuQS1dKSWaMwUABoHkxez
[2]ArrayBuffer: https://es6.ruanyifeng.com/#docs/arraybuffer
[3]文本格式: https://webassembly.github.io/spec/core/text/index.html
[4]wabt: https://github.com/WebAssembly/wabt
[5]wabAssemblyScript: https://www.assemblyscript.org/
[7]WebAssembly 类型: https://www.assemblyscript.org/types.html#type-rules [8]Binaryen: https://github.com/WebAssembly/binaryen
[9]Emscripten: https://github.com/emscripten-core/emscripten
[10]SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[11]OpenGL: https://en.wikipedia.org/wiki/OpenGL
[12]OpenAL: https://en.wikipedia.org/wiki/OpenAL
[13]POSIX: https://en.wikipedia.org/wiki/POSIX
[14]Unreal Engine 4: https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/[15]Unity: https://blogs.unity3d.com/2018/08/15/webassembly-is-here/
[16]Github: https://github.com/webmproject/libwebp
[17]API 文档: https://developers.google.com/speed/webp/docs/api
[18]WebP 的文档: https://developers.google.com/speed/webp/docs/api#simple_encoding_api
[19]文件系统 API: https://emscripten.org/docs/api_reference/Filesystem-API.html
[20]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[21]C/C++ Devtools Support: *https://chrome.google.com/webstore/deSDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[23]complex numbers: https://en.cppreference.com/w/cpp/numeric/complex
[24]WebAssembly 命名策略: https://webassembly.github.io/spec/core/appendix/custom.html#name-section
[25]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[26]WASI: https://github.com/WebAssembly/WASI
[27]yew: https://github.com/yewstack/yew
[28]WAPM: https://wapm.io/
[29]Debugging WebAssembly with modern tools - Chrome Developers: https://developer.chrome.com/blog/wasm-debugging-2020/
[30]Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium: https://medium.com/@torch2424/making-web-assembly-even-faster-debugging-web-assembly-performance-with-assemblyscript-and-a-4d30cb6463f1
作者:涅槃快乐是金
链接:https://www.jianshu.com/p/d4b588b54d7f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
参考链接
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://pspdfkit.com/blog/2017/webassembly-a-new-hope/
- https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/
- https://www.sitepoint.com/understanding-asm-js/
- http://www.cmake.org/download/
- https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasm
- https://research.mozilla.org/webassembly/
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16?gi=e525b34f2c21
- https://dev.to/alfg/ffmpeg-webassembly-2cbl
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926
- https://github.com/Kagami/ffmpeg.js/
- https://qdmana.com/2021/04/20210401214625324n.html
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
- http://ffmpeg.org/doxygen/4.1/examples.html
- https://github.com/alfg/ffmpeg-webassembly-example
- https://github.com/alfg/ffprobe-wasm
- https://gist.github.com/rinthel/f4df3023245dd3e5a27218e8b3d79926#file-ffmpeg-emscripten-build-sh
- https://emscripten.org/docs/compiling/Building-Projects.html#integrating-with-a-build-system
- https://itnext.io/build-ffmpeg-webassembly-version-ffmpeg-js-part-2-compile-with-emscripten-4c581e8c9a16
- https://github.com/mymindstorm/setup-emsdk
- https://github.com/emscripten-core/emsdk
- https://github.com/FFmpeg/FFmpeg/blob/n4.3.1/INSTALL.md
- https://yeasy.gitbook.io/docker_practice/container/run
- Debugging WebAssembly with modern tools - Chrome Developers[29]
- https://www.infoq.com/news/2021/01/chrome-extension-debug-wasm-c/
- https://developer.chrome.com/blog/wasm-debugging-2020/
- https://lucumr.pocoo.org/2020/11/30/how-to-wasm-dwarf/
- https://v8.dev/docs/wasm-compilation-pipeline
- Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces ([bitsrc.io)](https://blog.bitsrc.io/debugging-webassembly-with-chrome-devtools-99dbad485451 “Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io “Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)”)”)
- Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium[30]
- https://zhuanlan.zhihu.com/p/68048524
- https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
- https://www.jianshu.com/p/e4a75cb6f268
- https://www.cloudsavvyit.com/13696/why-webassembly-frameworks-are-the-future-of-the-web/
- https://mp.weixin.qq.com/s/LSIi2P6FKnJ0GTodaTUGKw
参考资料
[1]WebAssembly 入门:如何和有 C 项目结合使用: https://bytedance.feishu.cn/docs/doccnmiuQS1dKSWaMwUABoHkxez
[2]ArrayBuffer: https://es6.ruanyifeng.com/#docs/arraybuffer
[3]文本格式: https://webassembly.github.io/spec/core/text/index.html
[4]wabt: https://github.com/WebAssembly/wabt
[5]wabAssemblyScript: https://www.assemblyscript.org/
[7]WebAssembly 类型: https://www.assemblyscript.org/types.html#type-rules [8]Binaryen: https://github.com/WebAssembly/binaryen
[9]Emscripten: https://github.com/emscripten-core/emscripten
[10]SDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[11]OpenGL: https://en.wikipedia.org/wiki/OpenGL
[12]OpenAL: https://en.wikipedia.org/wiki/OpenAL
[13]POSIX: https://en.wikipedia.org/wiki/POSIX
[14]Unreal Engine 4: https://blog.mozilla.org/blog/2014/03/12/mozilla-and-epic-preview-unreal-engine-4-running-in-firefox/[15]Unity: https://blogs.unity3d.com/2018/08/15/webassembly-is-here/
[16]Github: https://github.com/webmproject/libwebp
[17]API 文档: https://developers.google.com/speed/webp/docs/api
[18]WebP 的文档: https://developers.google.com/speed/webp/docs/api#simple_encoding_api
[19]文件系统 API: https://emscripten.org/docs/api_reference/Filesystem-API.html
[20]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[21]C/C++ Devtools Support: *https://chrome.google.com/webstore/deSDL: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[23]complex numbers: https://en.cppreference.com/w/cpp/numeric/complex
[24]WebAssembly 命名策略: https://webassembly.github.io/spec/core/appendix/custom.html#name-section
[25]C/C++ Devtools Support: https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
[26]WASI: https://github.com/WebAssembly/WASI
[27]yew: https://github.com/yewstack/yew
[28]WAPM: https://wapm.io/
[29]Debugging WebAssembly with modern tools - Chrome Developers: https://developer.chrome.com/blog/wasm-debugging-2020/
[30]Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium: https://medium.com/@torch2424/making-web-assembly-even-faster-debugging-web-assembly-performance-with-assemblyscript-and-a-4d30cb6463f1