- A+
起源
WebAssembly 起源于 Mozilla 员工的一个业余项目。2010年,在 Mozilla 从事 Android Firefox 开发的 Alon Zakai,为了把他以前开发的游戏引擎移植到浏览器上运行,利用业余时间开发了一款名叫 Emscripten 的编译器,可以把 C++ 代码通过 LLVM IR 编译成 JavaScript 代码。
到了 2011 年底,Emscripten 甚至能够成功编译 Python 和 Doom 等大型 C++ 项目,Mozilla 此时觉得这个项目很有前途,于是成立团队并邀请 Alon 全职开发这个项目。2013 年 Alon 和其他成员一起提出了 asm.js 规范,asm.js 是 JavaScript 语言的一个严格子集,试图通过“减少动态特性”和”添加类型提示“的方式帮助浏览器提升 JavaScript 优化空间。相较于完整的 JavaScript 语言,裁剪后的 asm.js 更靠近底层,更适合作为编译器目标语言。
asm.js 只提供两种数据类型:32位带符号整数,64位带符号浮点数,其他数据类型比如字符串、布尔值或者对象,asm.js 一概不提供,它们都是以数值的形式存在,保存在内存中,通过 TypedArray 调用。类型的声明也有固定写法:变量 | 0
表示整数,+变量
表示浮点数。例如下面一段代码:
function MyAsmModule() { "use asm"; // 告诉浏览器这是个 asm.js 模块 function add(x, y) { x = x | 0; // 变量 | 0 表示整数 y = y | 0; return (x + y) | 0; } return { add: add }; }
支持 asm.js 的引擎提前识别出了类型,可以进行激进的 JIT(即时编译)优化,甚至是 AOT(事先编译)编译,大幅提升性能。不支持 asm.js 按普通 JavaScript 代码执行也不会影响运行结果。
但是 asm.js 的缺点也很明显,那就是“底层”得不够彻底,例如代码仍然是文本格式;代码编写仍然受 JavaScript 语法限制;浏览器仍然需要完成解析脚本、解释执行、收集性能指标、JIT 编译等一系列步骤。如果采用像 Java 类文件那样的二进制格式,不仅能缩小文件体积,减少网络传输时间和解析时间,还能选用更接近机器的字节码,这样 AOT/JIT 编译器实现起来会更轻松,效果也更好。
与此同时,Google 的 Chrome 团队也在试图解决 JavaScript 性能问题,但方向有所不同。Chrome 给出的解决方案是 NaCl(Google Native Client)和 PNaCl(Portable NaCl)。通过 NaCl/PNaC1,Chrome 浏览器可以在沙箱环境中直接执行本地代码。
asm.js 和 NaCl/PNaC1 技术各有优缺点,二者可以取长补短。Mozilla 和 Google也看到了这一点,所以从 2013 年开始,两个团队就经常交流和合作。后来他们决定结合两个项目的长处,合作开发一种基于字节码的技术。到了 2015 年,“WebAssembly” 确定为正式名称并对外公开,W3C 成立了 WASM 社区小组(成员包括Chrome、Edge、Firefox 和 WebKit),致力于推动 WASM 技术的发展。
2016 年 Rust 1.14发布,开始支持 WASM。
2017 年 Google 决定放弃 PNaCl 技术;四大浏览器 Chrome、Edge、Safari、Firefox 更新版本开始支持 WASM。
2018 年 Go 1.11 发布,开始支持 WASM。
2019 年 Emscripten 更新为默认使用 LLVM 编译为 WASM 代码,停止对 asm.js 的支持;WebAssembly 成为万维网联盟(W3C)的推荐标准,与 HTML,CSS 和 JavaScript 一起成为 Web 的第四种语言。
简介
官方给出的定义:WebAssembly / WASM 是基于栈式虚拟机的二进制指令集,可以作为编程语言的编译目标,能够部署在 Web 客户端和服务端的应用中。
WebAssembly 具有如下特性:
- 是一种底层类汇编语言,能够在所有当代桌面浏览器及很多移动浏览器上以接近本地的速度运行。
- 文件设计得很紧凑,因此可以快速传输和下载。这些文件的设计方式也使得它们可以快速解析和初始化。
- 被设计为编译目标,让 C++、Rust 和其他语言编写的代码现在可以在 Web 上运行。
也就是说 WebAssembly 可以使得以各种语言编写的代码都可以以接近原生的速度在浏览器中运行。
WebAssembly 也被设计为与 JavaScript 共存并协同工作,相对于 JavaScript(包括 asm.js)解决了如下几个问题:
- 性能提升。由于 WebAssembly是一种底层类汇编语言,代码是静态类型,浏览器执行时可以直接将其编译成机器码去大幅提高性能;并且由于 WebAssembly 是字节码形式,文件体积也很小,便于网络快速传输,浏览器厂商甚至引入了“流编译”技术,让文件可以边下载边编译,下载完毕即可进行初始化。
- 融合不同语言。之前想在 Web 上执行其他语言,只能把其他语言转成 JavaScript 语言,但这个过程并不容易,而且会带来执行性能上的大幅降低;而 WebAssembly 从设计之初就定位为编译目标语言,让其他语言可以轻松转成 WebAssembly 语言代码,不仅不用担心性能(虽然仍会有一定损失),也让代码复用变得简单。
- 加强代码安全。对 JavaScript 代码进行保护通常只能使用混淆来大幅降低代码可读性,但是在一些工具的帮助下只要多花费一些时间仍然可读。但是转译而来的 WASM 代码则完全不具有可读性,即使通过 wasm2c 等工具进行反编译,依然比分析 JS 代码要难度大很多(当然并不会达到完全的代码安全,但增加逆向难度会使其风险大大降低)。
不过 WebAssembly 并不是纯浏览器平台的技术,犹如 JavaScript 与 Node.js,如今它也有自己的 Runtime,在浏览器之外的云原生、区块链、安全等系统应用领域都有诸多应用。
编译
C / C++ 通过 Emscripten 编译:
emcc hello.c -o hello.wasm
Rust 通过 Cargo 编译:
cargo build --target wasm32-example --release
还可以进一步压缩体积:
wasm-gc target/wasm32-example/release/hello.wasm
Golang 内置编译:
GOARCH=wasm GOOS=js go build -o hello.wasm main.go
运行
在 JavaScript 运行
为了在 JavaScript 中运行 WebAssembly,在编译/实例化之前,你首先需要把模块放入内存,比如通过 XMLHttpRequest 或 Fetch,模块将会被初始化为带类型数组。
使用 Fetch 的例子:
fetch('module.wasm').then(response => response.arrayBuffer() ).then(bytes => WebAssembly.instantiate(bytes, importObject) ).then(results => { result.instance.exports });
上述方式是先创建一个包含你的 WebAssembly 模块二进制代码的 ArrayBuffer,然后使用 WebAssembly.instantiate()
编译它。
你也可以使用 WebAssembly.instantiateStreaming()
,该方法直接从原始字节码中直接获取,编译和实例化模块,无需转换为 ArrayBuffer:
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject) .then(result => { result.instance.exports });
WebAssembly 计划未来会支持 <script type='module'>
和 ES6 的 import 语句这种形式直接加载运行。
在浏览器之外运行
Wasm 社区提供了很多 Runtime 容器,让 WASM 可以在浏览器之外的系统上执行,并且运行环境是沙箱化的。
目前比较流行的 Runtime:
- wasmtime:既可以作为一个CLI,也可以被嵌入到其他应用系统中,如 IoT 或者云原生
- WebAssembly Micro Runtime:更偏向于芯片场景的虚拟机,如它的名字所示,体积非常小,起步速度只要 100 微秒,内存耗费最低只需 100KB
- wasmer:特点是支持在更多的编程语言运行 WASM 实例,并有自己的包管理平台 Wapm
- WasmEdge:之前名为 SSVM,对云原生、边缘和去中心化应用有针对性优化
底层概念
模块
WebAssembly 程序的主要单元称为模块(Module),这个术语既用来表示代码的二进制版本,也表示浏览器中的编译后版本。
一个大型 WebAssembly 应用往往由多个子模块组成,每个模块都拥有自己的独立数据资源,因此子模块无法篡改其他模块的数据;另外每个模块所能使用的权限由最上层的调用者指定,因此第三方子模块无法在上层模块不感知的情况下越权调用,这种权限管理类似于 Android 开发需要预先声明所有依赖的权限一样。
当其他高级语言编译成 WebAssembly 后,会成为了一个模块二进制文件,文件名是以 .wasm
后缀结尾,文件内容开头是 8 字节的用于描述的模块头:
0000000: 0061 736d ; WASM_BINARY_MAGIC 0000004: 0d00 0000 ; WASM_BINARY_VERSION
前4 字节被称为“魔数(Magic Number)”,对应