1. 介绍
WebAssembly是一个可移植、体积小、加载快并且兼容 Web 的全新格式
浏览器内:
WebAssembly是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范
WASM 1.0(MVP版本)目前已经被Chrome、Safari、FireFox、Edge所支持
浏览器外:
由Mozilla、fastly、inter、Red hat组成的字节码联盟,该联盟旨在通过协作实施标准和提出新标准,以完善 WebAssembly 在浏览器之外的生态
CanIUse
开发语言支持
- .Net
- AssemblyScript
- Astro
- Brainfuck
- C
- C#
- C++
- Clean
- D
- Elixir
- F#
- Faust
- Forest
- Forth
- Go
- Grain
- Haskell
- Java
- JavaScript
- Julia
- Idris
- Kotlin/Native
- Kou
- Lobster
- Lua
- Lys
- Nim
- Ocaml
- Perl
- PHP
- Plorth
- Poetry
- Python
- Prolog
- Ruby
- Rust
- Scheme
- Scopes
- Speedy.js
Unmaintained - Swift
- Turboscript
Unmaintained - TypeScript
- Wah
- Walt
- Wam
- Wracket
Unmaintained - Xlang
- Zig
具体可以查看 Awesome WebAssembly Languages
- 理论上所有基于LLVM架构的高级语言都可以编译到WASM,只不过一些扩展语言会附带一个巨大的runtime。
- 官方主推C/C++作为官方开发语言。
- 前端则推荐AssemblyScript,开发语言是基于TypeScript,是前端开发的最佳选择:https://docs.assemblyscript.org/
2. 工具链
前世今生
ASM.js
介绍工具链之前先介绍ASM.js,它是Mozilla提出的一个基于JS的语法标准,是JS的子集,也就是说所有用ASM.js写的程序都是合法的JS程序,JS语言与ASM.js的关系有点类似C与C++的关系,因此,不支持ASM.js的浏览器或JS引擎也可以无误地执行ASM.js的代码。
使用"use asm"标识代码由ASM.js编译器解析执行,一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。据称,asm.js 在浏览器里的运行速度,大约是原生代码的50%左右。
function asmModule(stdlib, foreign, buffer) {
"use asm"; // 标识下面一段使用asm.js编译器解析执行
function calc(add1, add2) {
const num1 = add1|0 // xxx | 0 标识这是一个整型变量
const num2 = add2|0 // 类似的还有 +add1 代表一个双精度浮点型
return num1 + num2;
}
return {
calc: calc,
}
}
const asm = asmModule();
console.log(asm.calc(1, 2))
// 3
但ASM.js自2014年发布至今已近鲜为人知了。
LLVM
Low Level Virtual Machine。是一套编译器框架,定义了一种IR中间描述语言,当需要支持一种新语言只需要实现一个编译器前端,当需要支持一种新设备,只需要实现一个编译器后端。
传统编译器架构:
LLVM编译器架构:
ASM.js的诞生为C/C++等强类型语言提供了一种可以直接无痛地跨平台到Web端运行的可能,而最大程度的保留了原生应用所具有的高性能。
Emscripten工具链由此而生,借助LLVM编译器架构从C/C++编译到ASM.js。
编译工具
Emscripten
- emcc 编译器前端 C/C++ => LLVM-IR中间比特码
- Fastcomp 编译器后端 LLVM-IR => ASM.js
Binaryen
基于ASM.js已经没落,所以Emscripten和Binaryen相互结合成为WASM的编译器平台
目前编译WASM有常用两种方式:
方式一:
方式二:
安装:
$ git clone https://github.com/emscripten-core/emsdk.git
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest
3. 一个小例子
C++
first.cpp
#include <iostream>
#include <emscripten.h>
using namespace std;
extern "C" int EMSCRIPTEN_KEEPALIVE add(int num1, int num2) {
return num1 + num2;
}
编译:
emcc first.cpp -o index.html
编译结果如果输出html文件的话默认会生成一个胶水js文件和一个html调试文件:
ps: 胶水文件连接了一些C++常用类库的实现比如控制台、网络等在浏览器端的实现,如果是纯计算类的wasm应用则不需要胶水文件。
运行index.html
控制台输入:
AssemblyScript
我们再看看AssemblyScript同样代码实现:
export function add(a: i32, b: i32): i32 {
return a + b;
}
创建index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<script>
fetch('./build/optimized.wasm')
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, {
env: {
abort: function() { throw Error("abort called"); }
}
}))
.then(module => {
var exps = module.instance.exports;
console.log(exps.add(1, 4))
});
</script>
</head>
<body>
</body>
</html>
调试
如何调试一个Wasm模块?
- 在编译时执行可调试性级别(-g4)生成sourcemap文件
- 添加--source-map-base "http://localhost:8888/"参数,手动指定Wasm的sourcemap文件域名
- 浏览器调试
emcc first.cpp -g4 -o index.html --source-map-base "http://localhost:8888/"
4. API
#WebAssembly.compile(bufferSource)
接受一段二进制格式的WASM模板,编译并返回一个对应于模块的WebAssembly.Module对象,但并不会对模块进行实例化操作。
#WebAssembly.instantiate([module|bufferSource], importObject)
接受WebAssembly.Module对象并实例化一个有状态的WebAssembly.Instance对象
#WebAssembly.compileStreaming(fetch('xxx.wasm'))
接受一端数据流来流式编译WebAssembly.Module对象(ps.为啥可以流式编译?)
#WebAssembly.instantiateStreaming(fetch('xxx.wasm'))
接受一端数据流来流式编译WebAssembly.Instance对象
#WebAssembly.Memory()
标识一个可以在WASM和JS中共享的内存段,其内部结构是一个ArrayBuffer形式的二进制缓冲区,WASM和JS可以直接操作这个缓冲区数据,完成数据共享传递。
#WebAssembly.Table()
与WebAssembly.Memory类似,同样可以共享内存段,但表中数据需要符合特定WASM数据类型,主要定位是存储一些无法被前端用户直接读取和修改的符合WASM引擎可信的特征数据,目的是为了安全性。
5. 性能
为什么WebAssembly比JS快?
先来大概了解下js的编译执行过程:
- 源码经过编译器前端parser变成AST语法树
- AST语法树交由编译器后端Ignition解释器生成“比特码(Bytecode)”数据结构
- 部分代码由Ignition自身直接执行
- 另一部分推测可优化的代码交由TurboFan生成器,生成优化机器码,再交由Ignition执行
Javascript Compiler Pipeline:
摘自《深入浅出WebAssembly》
Chrome V8编译器链路流程图:
摘自《深入浅出WebAssembly》
那么WASM编译在其中的什么环节?
V8引擎在处理WASM时省略了大量Pipeline环节,引擎不需要对WASM的二进制代码进行优化、也不需要生成大量占用内存的AST结构信息。只需要把这些模块加载到内存中,直接经过V8链路末端的编译器后端,生成机器码便可以被浏览器执行。
另外WebAssembly是静态语言,不需要在运行时去编译和优化,在生成WebAssembly Module时已经经过C++编译优化。
这也是为什么WASM有着近乎原生执行性能的原因。
摘自《深入浅出WebAssembly》
借用一张tfjs的人脸识别算法的性能对比:
6. 安全性
WebAssembly描述了一个内存安全的沙箱执行环境,可以在现有JavaScript虚拟机中机型实现。当在浏览器中运行一个Wasm模块时,Wasm将遵循浏览器中与Web应用一致的同源策略来保证其安全性。即使在非浏览器环境中WASI标准也提供了基于能力的安全模型,wasm能够访问的资源必须在调用执行时明确的显式提供。
沙盒
wasm模块运行在沙盒环境中,在沙盒中所有使用的系统API,都必须通过Imports导入到wasm执行环境中。
内存模型
wasm无法创建内存,只能由native环境创建好内存块并传递给wasm,wasm修改这块显式定义的内存后和native共享内存块。
Nanoprocess进程模型
一个大型Wasm模块可能同时包含多个相互依赖的Wasm模块,每个模块实例都拥有自己独立的数据、资源和权限控制,恶意模块完全依赖于上层提供的数据和权限来执行,无法越权。
7. WASI
WASI(WebAssembly System Interface)是WebAssembly的一个重要概念,是WebAssembly的系统接口标准定义。
当我们的WebAssembly模块只是做一些算法或者其他的计算类操作时,并不需要WASI,但当我们在WebAssembly中调用一些比如socket、操作一些文件、打开一个系统进程就要涉及到系统接口的调用了。
而WASI的原理其实很简单,它只是定义了一些系统API名称,runtime环境只需要实现这些API即可在wasm模块中操作系统接口了,在实例化wasm模块时通过importObject传入。(类似nodejs)
wasm模块的系统接口调用完全依赖于外部runtime提供了哪些系统接口,比如我们的wasm模块只会有网络操作,那么runtime就不需要提供文件访问API的定义,这样wasm的安全就可以得到保障。
以下是部分API定义:
目前实现了WASI并且可以跑wasm的runtime有:lucet/wasmtime/wasmer/wavm/wasm3,其中wasmer热度最高,wasmtime则是官方版本。
利用wasm的轻量、高效、安全的特点可以做哪些事情:
WASI x 容器
大佬在K8S中运行WASM容器应用
https://www.cnblogs.com/alisystemsoftware/p/12461134.html
WASI x Serverless
对于Serverless来说当请求到来时需要一个冷启动过程,docker image的启动代价是比较大的,如果使用wasm可以实现毫秒级启动和极低的资源消耗。另外Serverless的两个函数之间调用是需要走通讯协议或者本地网络的,但wasm模块之间是可以相互直接调用的。
Cloudfalre厂商已经实现了一套基于WebAssembly的应用沙箱 https://blog.cloudflare.com/cloud-computing-without-containers/?spm=ata.13261165.0.0.43266baeTsIywH,和虚拟机相比WASM只需要启动一个runtime就可以跑n多模块实例。
阿里云CDN团队的EdgeRoutine(边缘可编程CDN)https://help.aliyun.com/document_detail/154866.html
WASI x 智能合约
借助wasm的轻量、高效、安全,还可以在区块链场景下做哪些:
https://zhuanlan.zhihu.com/p/80157870
8. 展望
目前来说WebAssembly仍属于初期阶段,包括WASI也有很多不完善的地方,距离大规模使用还设有一定距离,但随着在web浏览器端应用场景的越来越多(AI、游戏等),未来WASM和社区的的不断的完善(多线程、dom操作等),在不就的未来将会大方光彩。