WebAssembly 入门
什么是 WebAssembly?
WebAssembly(简称 Wasm)是一种基于堆栈的虚拟机的二进制指令格式。Wasm 旨在作为编程语言的可移植编译目标,支持在 Web 上部署客户端和服务器应用程序。
WebAssembly 是一种可在现代 Web 浏览器中运行的新型代码 — 它是一种低级汇编类语言,具有紧凑的二进制格式,运行时性能接近原生性能,并为 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它还可以与 JavaScript 一起运行,使两者可以协同工作。
谁在使用 WebAssembly?
- Google:利用 WebAssembly 技术让 Gmail 运行速度更快,而他们 ChromeLab 团队制作的图片压缩工具 Squoosh 也将几种常见的图片编解码器编译成 WebAssembly,实现高效的图片压缩;
- Mozilla:将 WebAssembly 发展为新一代 Web 技术;
- Unity:利用 WebAssembly 构建高性能游戏引擎;
- AutoCAD:利用 WebAssembly 实现在 Web 上运行的 CAD 应用;
- SketchUp:利用 WebAssembly 实现在 Web 上运行的 3D 建模应用;
- Figma:利用 WebAssembly 在 Web 上运行的协作设计工具;
- Dropbox:利用 WebAssembly 构建客户端加速器;
- 微软:使用 WebAssembly 加速 Edge 浏览器中的 JavaScript;
- TensorFlow.js:使用 WebAssembly 在浏览器中运行 TensorFlow 模型;
- HandBrake:使用 WebAssembly 构建视频转换器以提高性能;
- WordPress In Browser:通过 WebAssembly,你可以在浏览器上运行这个经典的 PHP CMS 应用程序,而无需任何服务器;
- 网页中的计算机博物馆 站长构建了一个 WebAssembly 应用程序,允许我们在页面上加载一些古老的操作系统,非常有趣。
- …
我如何使用它?
通过 WebAssembly - WebAssembly | MDN 文档的介绍,我们可以看到它一共有4个加载方法。其中1和2会直接返回一个生成WebAssembly实例的Promise,3和4则会返回一个 WebAssembly.Module,后者执行完之后会返回一个WebAssembly实例。从它们的名字也可以看出来:1和2是实例化的,3和4是只编译了(并未实例化)。
- WebAssembly.instantiate()
用于编译和实例化 WebAssembly 代码的主要 API,返回一个 Module 及其第一个 Instance 实例。 - WebAssembly.instantiateStreaming()
直接从流式底层源编译和实例化 WebAssembly 模块,返回 Module 及其第一个 Instance 实例。 - WebAssembly.compile()
将 WebAssembly 二进制文件编译为 WebAssembly.Module,无需实例化。 - WebAssembly.compileStreaming()
直接从流式传输底层源代码编译 WebAssembly.Module,并将其实例化为单独的步骤。
无论是从 Streaming 还是 bufferSource 初始化 Web Assembly 模块,都只是一个加载 .wasm
文件的过程,我们可以根据实际需求选择合适的加载方式。通过上述方法加载后,我们会得到一个 WebAssembly.Instance 对象。通过这个对象我们可以访问其内部的变量和函数。
一个简单的例子
通过 WebAssembly.instantiateStreaming 实例化 WebAssembly 模块
1 |
|
本例中的 .wasm 文件是从 .wat
文件编译而来的。什么是 .wat
文件?WAT - WebAssembly 文本格式。
WebAssembly 具有二进制格式和文本格式。二进制格式 (.wasm) 是一种用于基于堆栈的虚拟机的紧凑二进制指令格式,旨在成为其他高级语言(如 C、C++、Rust、C#、Go、Python 等)的可移植编译目标。文本格式 (.wat) 是一种人类可读的格式,旨在帮助开发人员查看 WebAssembly 模块的源代码。文本格式还可用于编写可编译为二进制格式的代码。
1 | (module |
我们可以通过 wabt 将 .wat
文件编译为 .wasm
二进制文件。
更多演示可在 webassembly-examples 中找到。
如果我们以另一种方式实例化它会怎么样?
WebAssembly.instantiate() 和 WebAssembly.instantiateStreaming() 的结果完全相同,区别在于它们接受的输入参数不同,使用 WebAssembly.instantiate() 其输入参数需要使用 ArrayBuffer。
1 |
|
我们如何获取 WebAssembly 模块代码?
WebAssembly 二进制文件可以从多种语言(C/C++、Rust、Assembly Script、C#……)编译而来,请参阅我想要…… - WebAssembly。
这里我以 C 代码编译 .wasm
为例。
安装编译工具 Emscripten
按照 Emscripten 文档 中的安装步骤进行操作。执行
1 | source ./emsdk_env.sh |
我们可以在我们的 shell 中使用 emcc
将 .c
文件编译为 .wasm
文件。
编写用于编译的 C 代码
我仍然使用 a+b
作为示例。此 C 文件模块仅包含一个 add 函数,该函数以两个数字作为输入并返回它们的相加之和。
1 |
|
有一个奇怪的
#include <emscripten/emscripten.h>
,它是什么?emscripten.h
提供了一些公共的 C++ API,详细信息请参阅 emscripten.h。
您可以看到我在 add 函数中添加了
EMSCRIPTEN_KEEPALIVE
宏,它告诉编译器保留它并导出它,这允许我们在使用 JavaScript 访问 WebAssembly 实例时访问它。
编译 .wasm
文件
在 shell 中执行以下命令
1 | emcc add.c -s WASM=1 -o add.html |
上述命令参数解释如下:
emcc
是 Emscripten 的命令行命令-s WASM=1
告诉 Emscripten 输出 wasm 文件,如果不指定此参数,则默认输出 asm.js-o add.html
告诉编译器生成一个名为 add.html 的 HTML 文档来运行代码,以及用于编译和实例化 wasm 的 wasm 模块和相应的 JavaScript 粘合代码,以便 wasm 可以在所使用的 web 环境中使用
运行上述命令后,你的 WebAssembly 目录中应该还会有三个文件:
- add.wasm 二进制 wasm 模块代码
- add.js 包含粘合代码的 JavaScript 文件,通过它将原生 C 函数翻译成 JavaScript/wasm 代码
- add.html 用于加载、编译和实例化 wasm 代码并在浏览器上显示 wasm 代码输出的 HTML 文件
使用 JavaScript 运行
用 http 服务器打开上面生成的add.html
文件,打开页面后使用 devtools 可以发现我们编译的 .wasm
文件已经被自动加载了,这是因为我们编译的add.js已经帮我们生成了实例化 WebAssembly 所需的相关代码。
我们可以找到instantiateAsync
函数,它是加载我们WebAssembly Module的关键。
1 | function instantiateAsync(binary, binaryFile, imports, callback) { |
在这个例子中,add.js 初始化了 WebAssembly 实例,并将其放入全局模块 Module 中,因此我们可以通过 Module.asm.add
访问 C 模块中的 add 函数。
在其他应用程序中实现 WebAssembly
通过上面的 demo,我们知道可以用 JavaScript 来初始化 WebAssembly 并使用,但是有一个问题,emcc生成的js胶水代码会创建一个名为 Module的 全局变量。但是如果我:
不想使用 Module 作为变量名,因为我可能已经有了这个名字
一开始就不想加载 WebAssembly 模块
在 Node.js 环境中不需要使用 WebAssembly 模块
想简化 js 胶水代码以实现更快的加载速度怎么办?
以下是我的答案:
自定义我们的 WebAssembly 模块名称
使用 emcc 编译时,添加-s EXPORT_NAME="CustomModuleName"
参数,自定义模块名称异步加载 WebAssembly 模块
使用 emcc 编译时,使用-o target.mjs
导出一个 ES6 JavaScript 模块,或者使用-s MODULARIZE
对其进行模块化。-s MODULARIZE
可以与-s EXPORT_NAME="CustomModuleName"
一起使用。加载 js 粘合代码后,可以使用CustomModuleName()
实例化 WebAssembly。其中,CustomModuleName
函数返回一个 Promise。当我不需要 Node.js 相关代码时
使用 emcc 编译时,添加-s ENVIRONMENT=web
,编译后的粘合 js 代码将不包含 Node.js 相关逻辑。精简 glue js 代码
使用 emcc 编译时,添加-O3
参数进行压缩,本例中 add.c 压缩效果为:add.js(10KB/53KB),add.wasm(72B/916B)。
您可以在 Emscripten FAQ 中找到更多答案。
WebAssembly + Web Worker?
当我们使用 WebAssembly 进行比较复杂的计算时,为了避免影响浏览器渲染过程,我们可以将其放入 Web Worker 中运行。
可以参考 使用 WebAssembly 和 Web Workers - SitePen。
1 |
|
1 | importScripts("add.js"); |