初探WebAssembly
1.前言
参加完2018年上海的QCon大会,想到了会议中来自Microsoft的朱力旻大佬讲的WebAssembly,感触颇深。
我之前完全没有了解过WebAssembly,之前没有了解的原因也很简单,没有什么实际的应用场景,但工欲善其事,必先利其器。
抱着这样的想法,便开始入坑WebAssembly。
2.Why WebAssembly
2.1 javascript的背景
JavaScript这门由Brendan Eich花了10天创造出来的语言,在如今收到了广泛的应用,同时也因为其缺陷遭受到了业界的一些诟病。的确,这门被仓促的创造出来的语言有很多缺陷。毕竟JavaScript结合了函数式编程和面向对象编程,在JavaScript之前的参考极少。
并且JavaScript发展过于迅速,没有多少时间去调整标准。比如JavaScript的变量是一个动态变量。上一秒这个变量可以是Array,下一秒就变成了一个Number。
JavaScript的语法太过于灵活,这样导致JavaScript在开发大型应用上成为了瓶颈,并且类似于CAD这种网格模型对于性能要求特别高的情况下,JavaScript根本无法胜任。
2.2 社区的补救
例如,微软给出了TypeScript这个解决方案,TypeScript为JavaScript添加了静态类型检查,提升了代码的健壮性。但是最终TypeScript还是要编译成JavaScript来运行。
于是在2013年,为了推动Web性能的发展。WebAssembly的前身asm.js诞生了。下面给个例子。
function asmJS() {
'use asm';
let myInt = 0 | 0;
let myDouble = +1.1;
}
传统的JavaScript的动态变量需要在代码执行的时候,编译器才知道当前变量的类型。我们都知道"use strict",这是JavaScript的严格模式。
通过添加代码 "use strict"来使用严格模式。在严格模式下的JavaScript代码会通过抛出错误的方式来代替原来的静默错误。并且在某些情况下, 严格模式下的代码运行效率会高于"sloppy mode"下的代码。
而上述代码中的"use asm",就是告诉引擎,下面的代码是asm.js的代码。当引擎看到"0 | 0",就会在这行代码运行之前知道,这是一个整形的数据,而不会再去编译一次。而看到"+1.1"就会知道这是一个浮点型的数据。asm.js是JavaScript的一个子集。
并且将JavaScript的动态变量变成了静态变量,代码在运行时,有接近原生的性能。
可能到这里疑问就来了,既然asm.js在运行时已经有了接近native的性能。为什么还会出现WebAssembly这个技术。那是asm.js并不能解决所有的问题。
2.3 asm.js并不能解决所有的问题
为什么说asm.js不能解决所有的问题?拿Microsoft Edge的Javascript引擎举个例子。
上面这张图是ChakraCore引擎的结构。Chakra是Microsoft Edge浏览器开源的Chakra Javascript引擎的核心部分。我们在浏览器中运行的Javascript代码首先会经过编译,解析成AST(AKA抽象语法树),想实操的可以去这里试试。
asm.js及时让其运行性能接近了native,但是仍然需要经过parser这一步。而这一步是整个过程中最费时的一步。这就给asm.js造成了一个瓶颈。
2.4 WebAssembly横空出世
于是2015年,WebAssembly出现了。WebAssembly是一个可移植、体积小、加载快并且即兼容Web的汇编格式。
与asm.js类似的是,WebAssembly拥有静态类型。同时也是编译目标,并且可移植。而不同于asm.js,WebAssembly是汇编格式,代码量小,起步相对较快。而且语法上完全脱离JavaScript。这是WebAssembly官方给出的一个demo,可以去体验一下。
3 WebAssembly快速入门
WebAssembly为什么说是编译的目标。是因为WebAssembly可以将C和C++编译成WebAssembly,用的工具是Emscripten,是一个预编译工具。要进行WebAssembly的编译,需要安装git、cmake、python
假设你是在mac上进行操作的。
3.1 安装
安装WebAssembly工具链。
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --build=Release sdk-incoming-64bit binaryen-master-64bit
可能会等的有点久,并且可能会出现cmake没有安装的报错信息。可以使用homebrew安装cmake后,再执行安装操作。
3.2 设置环境变量
在emsdk目录下,运行下列命令。就可以在当前的终端窗口中运行emcc命令了。
source ./emsdk_env.sh --build=Release
3.3 编写C或者C++文件
新建一个C文件,代码如下。
#include <stdio.h>
long factorial(int num) {
if (num <= 0) return 1;
else {
return num * factorial(num - 1);
}
}
int main () {
int num = factorial(10);
printf("The Result: %d \n", num);
}
3.4 利用Emscripten编译代码
然后在相同目录下执行emcc的编译命令,该命令会将C或者C++文件编译成wasm模块。
emcc hello.c -s WASM=1 -o hello.html
其中,hello.c为源文件,-o hello.html是这个命令的输出文件,-s WASM指定输出的格式为wasm,并且版本为1。执行这个文件后,会在目录下生成如下数据。
.
├── hello.c
├── hello.html
├── hello.js
└── hello.wasm
hello.wasm就是经过编译后的C代码的二进制文件。而hello.js则是C语言与JavaScript相互转化的中间代码。执行下面的代码就可以在localhost的8080端口看到我们的C模块在前端的调用和展示。
emrun --no_browser --port 8080 .
然后就可以在浏览器中看到如下的输出。
我们编写的C模块已经可以在浏览器中正常的运行了。这说明我们的WebAssembly编译成功了。
4 在前端项目中使用Wasm
可以参考这篇文章,如何在React项目中直接使用WebAssembly,在这篇文章的项目中可以看到完整的从wasm文件中提取出函数的function,以及清晰的用C实现的函数和用JavaScript实现的函数的运行性能对比。