好了,我又想起公众号密码了 - -
2017年,WebAssembly(wasm)推出了第一个版本。2019年12月5日,wasm被W3C推荐,与HTML,CSS,JavaScript一起,成为了Web开发的第四种语言。
wasm已经不算是新技术了。由于今年的工作中会涉及到wasm,故需要进行较为系统的学习。本公众号也会不定期更新相关文章。
WebAssembly 是什么
WebAssembly是一种新的编码方式,具有二进制格式,是一种低级的类汇编语言,可以在现代的浏览器中运行。wasm在设计之初,就是为了提供比js更快速的编译和执行。开发者可以使用自己熟悉的语言(比如c/c++,golang等)编写代码,最终编译为wasm,在浏览器中运行。wasm被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。wasm的出现,使得之前只运行JavaScript的虚拟机现在可以加载和运行两种类型的代码——JavaScript和WebAssembly。当前,四大浏览器(FireFox,Chrome,Edge,Safari)均已支持了wasm。
wasm有以下几个关键的概念(摘抄自MDN):
- 模块:表示一个已经被浏览器编译为可执行机器码的WebAssembly二进制代码。一个模块是无状态的,并且像一个二进制大对象(Blob)一样能够被缓存到IndexedDB中或者在windows和workers之间进行共享(通过postMessage() 函数)。一个模块能够像一个ES2015的模块一样声明导入和导出。
- 内存:ArrayBuffer,大小可变。本质上是连续的字节数组,WebAssembly的低级内存存取指令可以对它进行读写操作。
- 表格:带类型数组,大小可变。表格中的项存储了不能作为原始字节存储在内存里的对象的引用(为了安全和可移植性的原因)。
- 实例:一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的ES2015模块。
wasm有两种格式,一种二进制代码格式,文件后缀为.wasm,一种文本格式,后缀名为:.wat。
Wasm代码实例
说了这么多,下面直接来看一个实例。由于之前写过一段时间go,对go稍微熟悉点,所以这里以go为例。
简单的一段代码:
package mainimport ( "fmt")func add(a int, b int) int { return a + b;}func main() { c := add(1, 2); fmt.Println(c);}
通过go的命令行工具,
GOARCH=wasm GOOS=js go build -o test.wasm
将其转为了.wasm文件后,长这个样子:
以上是wasm的二进制格式表示。
如何在浏览器中使用wasm
浏览器提供了一个 WebAssembly
对象来聚合所有wasm相关的功能。
我们怎么运行上面例子中的wasm代码呢?通过上述编译方法,go在编译成wasm后,想要在浏览器中运行,需要一段"jsbridge"代码,也就是胶水代码。通过下面的命令,导出go的胶水代码
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" wasm_exec.js
然后在页面中引用就可以了。
<html> <!-- 引用胶水代码 --> <script src="wasm_exec.js"></script> <script> const go = new Go(); fetch('test.wasm') .then(rsp => { // wasm二进制代码转换成arrayBuffer return rsp.arrayBuffer(); }) .then(bytes => { // 实例化wasm模块 return WebAssembly.instantiate(bytes, go.importObject) }).then(result => { // 拿到模块实例,直接执行 const instance = result.instance; go.run(instance) });</script></html>
在浏览器的控制台中,我们就可以看到go代码的执行结果了:
那么,如果想要在浏览器内,通过js调用go代码中的方法怎么办呢?
首先需要先修改go代码:
package mainimport ( "fmt" "syscall/js" // 引入必要的依赖) func add(a int, b int) int { return a + b;} // 将add函数包装一层func jsAdd(this js.Value, args []js.Value) interface{} { return js.ValueOf(add(args[0].Int(),args[1].Int()))} func main() { c := add(1, 2); fmt.Println(c); // 在 main 函数中,创建了信道(chan) done,阻塞主协程(goroutine)。jsAdd 如果在 JavaScript 中被调用,会开启一个新的子协程执行。 done := make(chan int, 0) // 将 jsAdd 函数注册到浏览器环境中 js.Global().Set("jsAdd", js.FuncOf(jsAdd)) <-done}
重新编译成wasm后,在页面中看下效果:
wasm的性能如何
wasm不仅有二进制代码格式,也有文本格式(后缀为:wat)。文本格式主要是为了方便人类阅读和编辑。我上网找了一个计算斐波拉契数列的wat文本格式代码,通过斐波拉契数列来看一下wasm的性能。wat代码如下:
(module (export "fib" (func $fib)) (func $fib (param $n i32) (result i32) (if (i32.lt_s (get_local $n) (i32.const 2) ) (return (i32.const 1) ) ) (return (i32.add (call $fib (i32.sub (get_local $n) (i32.const 2) ) ) (call $fib (i32.sub (get_local $n) (i32.const 1) ) ) ) ) ))
将其编译成wasm后,放入浏览器中运行。
我们来看一下斐波拉契数列计算到40时,两者的耗时:
可以看到,wasm版本的耗时比js版本的耗时少了 20% 多。wasm确实有性能优势。
稍微总结一下
经过一些阅读与实践,大概了解了wasm是什么以及最简单的使用方法。本文没有深入到wasm的一些细节点,不过不难体会到,wasm在性能上确实会有优势,并且wasm可以作为其他高级语言与js的"桥梁"。图片、视频等处理算法,可以比较方便地移植到浏览器上直接运行了。
更多关于wasm的内容,后面再探索总结吧。