WebAssembly理论学习
一、读前热身
WebAssembly(以下简称WASM)是一项看似很新其实并不“新”的技术,早在2015年4月,WebAssembly的社区就已经成立,而同年FB推出的React Native,以及阿里的Weex等都已在跨端框架技术上取得了可见的结果,与他们不同的是,WASM作为一个W3C标准,直到2017年才由Firefox和Chrome等实现了其MVP(最小可用)版本,相对来说,是一个年轻且充满想象力的崭新技术。
这里引用MDN上官方对其的解释:WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。
Docker的创始人Solomon Hykes曾经说过:
如果 WASM 和 WASI 早在 2008 年就存在,那么我们就不需要创建 Docker。可见 Wasm 是多么重要。服务器上的 WebAssembly 将会是“计算”的未来模式。而现在的问题是缺少标准化的系统接口。希望 WASI 能够胜任这项工作!
1. WASM会替代JavaScript吗?
只从目前可见的视角来说,WASM不会替代JavaScript,但是却可以辅助解决很多JavaScript无法解决的问题
-
解除对不同浏览器环境的强依赖
如今现代前端开发对webpack、babel、polyfill的依赖都非常重,编译后动辄就是几兆几兆的大小,当一个业务非常复杂之后,不得不采用各种各样的手段来做优化。另外,浏览器的安全也是一个大问题。
-
弱类型还是强类型
尽管我们有TypeScript,然而它只能在编译阶段帮我们做好类型的校验,除此之外,如 + 运算符、null == undefined 等诡异结果也是JavaScript种种难以克服的通病。
2.关于ASM.js
ASM.js相信很多人都了解过,本身是Mozilla在2013年提出一个的非常有探索意义的技术。ASM.js在FireFox率先支持浏览器内核级别的优化,可以认为是WASM的前身技术。
ASM.js是JavaScript的子集,意思是生成的代码仍然是100% 纯JavaScript。
ASM.js仍然是被编译出来的,书写的C/C++代码经过静态编译后,会输出为一段特定的JavaScript代码,浏览器引擎在运行这段代码时,会做特别的优化,举个例子
在经过编译之后生成的JavaScript代码:
注意这里用了 i|0, 这里的意思就是告诉浏览器引擎,请用32位整数的内存空间来处理 i这个变量,类似地,也会用 +i的方式来标记为64位浮点数。
如果你对ASM.js 还有其他想快速了解的内容,可参考阮一峰的入门文档: https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
3. 关于WASM
WASM其实有点像我们的一位老朋友: Flash
曾经Flash的存在赋予了互联网极大的活力,如视频、游戏、插件等等,然而随着时间流逝,其诸多的安全漏洞,移动端的兼容缺乏等问题正在不断暴露出来,不光四大浏览器厂商即将彻底放弃支持,后爹Adobe也已经不再维护。
话是这么说,不过现在的纯JavaScript真的有100%的能力能代替ActionScript吗?
Google的Chrome V8团队早在2011年便开始调研如何把复杂的跨端能力以一种更安全更高效的方式输出到浏览器中,他们吸收了前人的经验,作为WASM可靠的领军人物在前进。在2019年12月,W3C正式宣布WASM成为第四个可在浏览器中原生运行的语言。
4. 哪些场景适用于WASM?
这里直接照搬 于航大佬 现成的总结
这些应用在下面基本都会提到,不过在描述这些应用之前,我们先来了解一下WASM的基本原理。
二、运行原理
1. WASM到底是怎么运行的
一般来说,一个可运行的程序分为两部分,即数据 + 指令,而WASM是基于堆栈机模型和虚拟指令集来实现程序的运行,堆栈机模型是常见的内存管理结构,将数据和操作符压入栈中并弹出逐次执行,而虚拟指令集(V-ISA)则可以认为是对平台无关的一系列自定义操作符(如JVM),相对的,物理指令集(ISA)则是强依赖物理系统的(如Intel的x86-64)。
我们把浏览器比作JVM虚拟机,WASM二进制编码比作Java字节码,是不是就能很快明白为什么浏览器上可以运行WASM了?
2. 剖析WASM文件结构
这里只简单的介绍一下WASM二进制文件中到底包含哪些内容,到底有什么作用。
首先在组织结构上,WASM会把特定功能或者有相关联的代码放进一个特定的区块(Section)中,而这些区块(Section)组成了程序。
-
TypeSection
存放与“类型”相关的内容,主要是“函数签名”,即返回值与参数值。
-
StartSection
在模块初始化完成后,被首先调用的函数,可以近似理解为main函数
-
GlobalSection
顾名思义,存放了程序相关的全局变量,可能是程序自定义的数据,也可能是流程相关的
-
CustomSection
可以用作将来自定义扩展功能的实现
-
ImportSection
从宿主环境中,我们可以向WASM模块导入各式各样的数据,换句话说,通过这个Section我们可以实现数据或代码的共享
-
ExportSection
同理,既然可以导入,我们也可以导出数据和方法
-
FunctionSection
这里存储的是函数类型,与TypeSection一一对应
-
CodeSection
这里存储的是函数具体代码,与FunctionSection一一对应
-
TableSection、ElementSection
Table存放了函数指针的元信息,Element则是对应Table的具体内容,关于Table的概念可以参考这篇文章:https://zhuanlan.zhihu.com/p/28222049
-
MemorySection、DataSection
同上,Memory描述了使用内存的基本情况,Data则是对应具体的实际内容
最后,还有一个小问题,我们怎么来判断一个二进制文件到底是不是wasm模块呢?
如果大家使用过base64转化过图片二进制代码的话,会注意到它是以类似 data:img/jpg;base64开头标记为base64格式的jpg图片,WASM也是采用类似的方式,以 asm这个字符串的二进制编码开头,随后跟上版本号。
3. 基本数据类型
-
无符号整数
WASM支持三种非负整数类型: uint8、uint16、uint32,后面的数字表示占用了多少个bit
-
可变长无符号整数
WASM支持三种可变长非负整数类型: varuint1, varuint7, varuint32,所谓可变长的意思是会根据具体数据大小决定使用多少bit,后面的数字表示最大可占用多少个bit
-
可变长有符号整数
同上,这里允许负数的出现,这里支持varint7, varint32, varint64三种类型
-
浮点数
这里同JS,采用IEEE-754方案,单精度为32位
三、周边生态
1. WAT 使WASM更加可读
通常来说,WASM二进制文件是不可读的,WebAssembly Text Format(WAT)是另外一种输出格式,以类文本的方式展示输出,我们可以近似的理解为与二进制等价的汇编语言,或者说就是WASM的source-map。
另外,Flat-WAT是经过优化后的WAT格式,我们可以一张图比较三者输出:
现在社区已经有成熟的转换工具,如wasm2wat,wat2wasm等,我们可以在一个名为WABT(WebAssembly Binary Toolkit)的工具集中找到。
2. WASI 操作系统接口
正如Docker创始人所说,WASI被寄予了厚望。既然WASM是运行在浏览器的沙箱环境,那么我们把它移出浏览器,是不是直接可以与操作系统打交道呢,WASI就是用来解决这个问题的。
之前提到了WASM依赖的是V-ISA指令集,即完全依赖宿主环境提供的操作指令,所以如果要在真实的操作系统上运行,必须要有一个“虚拟机”来翻译这些指令并屏蔽具体系统调用之间的差异,即需要类似JVM的WASM VM,WASI便是定义这些抽象的系统调用抽象层,其有两个最重要的特性:
1、可移植性
显然,“一次编译,到处运行”是最起码的要求
2、安全性
虚拟机完全负责权限的控制,所有系统调用由外围控制,隔离出沙箱环境,这样可以避免第三方库带来的安全问题
那么,在哪里可以买的到呢?
不要着急,我们先了解一下“字节码联盟”,这是一个成立于2019年的半官方组织,致力于实现WASM、WASI的相关标准,并不断探索WASM在浏览器外的世界。
回顾普遍存在于应用开发中的问题,第三方代码共享着主程序的所有权限,如Socket、File、内存资源访问等权限,代码的依赖图越来越复杂,安全漏洞越来越多,这点相信使用FastJson开发Java和使用NPM开发NodeJs的同学深有体会。
一个大型WASM应用往往由多个子WASM模块组成,每个模块都拥有自己的独立数据资源,因此子模块无法篡改其他模块的数据;另外每个模块所能使用的权限由最上层的调用者指定,因此第三方子模块无法在上层模块不感知的情况下越权调用,这种权限管理类似于Android开发需要预先声明所有依赖的权限一样。
下面是Github上比较流行的WASM虚拟机:
-
WASMTIME
WASMTIME是字节码联盟主推的一个WASM虚拟机,既可以作为一个CLI,也可以被嵌入到其他应用系统中,如IoT或者云原生
-
WAMR
同样是字节码联盟旗下的,更偏向于芯片场景的虚拟机,如它的名字所示,体积非常小,起步速度只要100微秒,内存耗费最低只需100KB
-
WASMER
这是独立于字节码联盟,并努力构建自己生态的社区推出的产品,特点是支持在更多的编程语言运行WASM实例,并有自己的包管理平台Wapm
-
SSVM
这是一个相对小众的运行时,对云、AI 以及区块链有针对性的优化
四、应用场景
1. 如何在浏览器中使用一个WASM模块?
一个体积巨大的二进制WASM模块是不可能放在浏览器中同步加载的,我们必须通过异步引入的方式来拉取对应的模块,不过webpack的import方法已经支持了WASM模块的快速引入,我们可以按需选择推送到CDN、或者再放在Service Worker缓存。
在拉取WASM模块的同时,浏览器可能已经开始对模块进行流式编译,将字节码编译为平台相关的代码,接着便开始模块的实例化,并导入WASM需要的宿主数据,完成实例化操作后,这个模块便可以通过JavaScript来调用了。
-
生成二进制数据
当我们fetch获取到WASM的二进制编码之后,我们需要将其转化为JavaScript二进制数组
-
编译阶段
我们可以使用单步编译的方法
或者一次性完成编译+实例化
-
实例化
你在上步可能已经发现了,实例化的时候可能需要传入importObject,这是什么东西呢?
上面有一块讲了ImportSection是可以从外界向模块内导入数据的,这里的数据主要包含
如果我们单独需要实例化,可以调用如下方法
-
流式编译
其实对于编译和实例化,WASM都提供了对应的流式处理方法
注意,为了实现流式编译,source需要是一个尚未Resolve的Promise,即fetch请求自身。
-
错误对象
在整个WASM的加载过程中,我们也需要明确不同错误对象代表不同的含义
-
内存管理
WASM一个最大的特点就是安全性,在浏览器的沙箱环境内,WASM模块与浏览器的内存完全隔离,单独管理。如果两者需要数据通信(即 importObject),那么分两种情况:
简单数据类型,直接通过 WebAssembly.Global导入到模块内
复杂数据类型,需要通过 WebAssembly.Memory导入到模块的线型内存中
-
缺憾: 无法直接访问DOM
显然,WASM如果想对DOM进行操作,不可能在模块内直接操作,必须依赖传入的JavaScript方法进行操作,而这显然避免不了高额的开销,并且开发起来也非常复杂,从这方面说,WASM是对DOM不友好的。
2. 在前端方面的应用
谈到在前端方面的应用,一个永远无法避开的问题是:WASM究竟比纯JavaScript快多少,我到底应该如何做取舍?
上面也提到了,在目前阶段的WASM的MVP标准中,我们不能直接操作DOM,甚至可以说,大部分的API都需要JavaScript API来模拟(即所谓胶水代码),举个例子来说,我们在C++中,调用了创建DIV的API createDiv,则在胶水代码中,会有一个 createDiv:document.createElement('div')的映射来执行,故此,基于胶水代码的JavaScript API并不会带来明显的效率提升,相反还会带来体积的膨胀。另外一方面,WASM和JavaScript API的频繁上下文切换的开销成本也是很大的,这完全依赖浏览器厂商对其做的优化程度。
既然有了以上弊端,那么WASM在前端方面有哪些可以探索的尝试呢?
-
重写框架的核心逻辑
当代流行的前端框架,如React、Vue,他们性能的瓶颈经常出现在频繁的虚拟DOM计算上,既然是虚拟DOM,又是计算密集型的,若用WASM来实现其计算逻辑是不是一种可行的尝试呢?事实上,React团队确实考虑过这个事情,不过一个Fiber都折腾了好几年,更别说WASM了。
-
重新创建一个框架
业界比较知名的就是使用Rust编写的Yew,我们可以使用Rust语言来做JavaScript一样的事情,本身设计理念与React非常相似,也是将虚拟DOM的计算WASM化的激进典型,遗憾的是胶水代码的存在并不能明显的缩小性能成本。
-
多媒体
WASM在多媒体的作用是非常直观的,大家知道音视频的编解码是一个计算密集型的工作,ogv.js 是由维基百科团队开发的一款在线播放ogg、webm、av1等视频的插件,WXInlinePlayer 则更加轻量级,消耗更低的CPU和内存占用
-
迁移成熟的工具
这个大家见得比较多,如Google Earth,Unity3D, CAD 等等,包括轻游戏等等,这一般是专业的团队进行迁移,同样也大大扩展了WASM的应用前景,限于篇幅不在这里展开讨论了。
3. 在系统方面的应用
WASM因为本身就是一个精简的运行时应用,在一些新的领域也进行了很多有趣的探索
-
嵌入式设备(IoT)
一个设备如果需要跑起来WASM应用,只需要有一个能解析和运行它字节码的虚拟机即可,而无需关注于具体语言,并且这个虚拟机通常是非常轻量级的,这便为嵌入式设备的扩展带来了极大的可能性,要知道流行技术之所以流行,大部分都是对开发者或者运维者友好的。
-
微内核
假如我们使用Linux系统,但其实我们真正可能只用到了它50%模块的功能,另外一半可能我们把电脑用坏了都不会用到。同理,在一个专注于提供特定应用的系统上,我们也不需要它全部的功能,一个完整的集成图形界面、多线程、网络、C标准库的WASM虚拟机执行层微内核只有468KB,系统冷启动时间和资源使用率是一个非常亮眼的数据。
-
云
Krustlet 定位是一个Kubernetes Kubelet。根据指定的 Kubernetes Toleration,Kubernetes API 能够将特定的 Pod 调度到 Krustlet 上,然后将它们运行在基于 WASI 的 Wasm 运行时上。
Embly 是一个基于WASM的Serverless框架,它只需要一个配置文件,便可以让我们在服务器上执行Rust语言生成的 Wasm 字节码(函数),并访问完成任务所需要的网络和系统资源。
五、放眼未来
上面提到目前的开发都是基于WASM MVP标准来说的,那么现在有哪些处在进行中的新进展(Post-MVP)呢?
-
多线程与原子操作
此提案提出了一种新的共享内存模型方案,允许线性内存被多个线程同时使用,并且每个线程都有自己的WASM实例和栈容器。原子操作的含义是保证对共享内存的访问不会发生数据竞争的问题
-
单指令多数据流
所谓单指令,我们可以理解为一个乘法操作,而多数据流,我们可以理解为同时对多个数进行了乘法操作,对于复杂的矩阵计算而言这会是一个很优秀的特性,然而目前并没有浏览器能实现这点
-
64位WASM
目前而言,WASM所有关于内存操作,都仅能使用32位长度的偏移地址,故其能访问的内存最多只有4GB,因此将其扩展到64位也自然是一个重要的需求
-
WASM模块化
一种比较理想的状态,是我们在代码中可以使用类似ES6 Module的方式来导入导出WASM Module。
或者使用script标签来直观告诉浏览器自动加载和试用
其他更多的提案可以在 这里 看到。
关于WebAssembly的知识就总结到这里,感谢大家的观看
References
[1] WABT(WebAssembly Binary Toolkit)的工具集: https://github.com/WebAssembly/wabt#building-using-cmake-directly-linux-and-macos
[2] WASMTIME: https://github.com/bytecodealliance/wasmtime
[3] WAMR: https://github.com/bytecodealliance/wasm-micro-runtime
[4] WASMER: https://github.com/wasmerio/wasmer
[5] SSVM: https://github.com/second-state/SSVM
[6] Yew: https://github.com/yewstack/yew
[7] ogv.js: https://github.com/brion/ogv.js
[8] WXInlinePlayer: https://github.com/ErosZy/WXInlinePlayer
[9] Google Earth:
[10] Unity3D: https://blogs.unity3d.com/2018/08/15/webassembly-is-here/
[11] CAD: https://web.autocad.com/
[12] Krustlet: https://github.com/deislabs/krustlet
[13] Embly: https://www.embly.run/
[14] 这里: https://github.com/WebAssembly
The End
作者:米兰的小铁匠