了解 webAssembly 原理
WebAssembly 是一种可以在浏览器上运行的二进制可执行格式文件.它将成为浏览器进化史上又一次革命.
自从浏览器问世以来,javascript 就成为浏览器上执行程序的唯一标准,越来越多的应用程序通过 javascript 开发,并运行于浏览器上;而随着浏览器上 h5 程序功能的丰富,也对浏览器提出了更多的挑战.其中一条最为重要的就是性能问题.javascript 是一种弱类型,解释性的脚本语言.它天生运行速度慢,成为了很多 h5 应用的软肋.虽然 2008 年 google V8 引入了即时编译等技术使 js 的运行速度提升了一大截,但是一些大型应用程序,比如游戏,视频编辑,压缩,算法等依然不适合运行在浏览器上.
WebAssembly 的到来解决了这个问题,并给开发基于浏览器的应用程序提供了另外的编程语言选择.2017 年三大浏览器同时增加了 WebAssembly 支持,标志着 WebAssembly 已经达到生产实用标准.
为什么 WebAssembly 比 javascript 快
回答这个问题需要洞悉浏览器执行 javascript 代码的各个环节.
浏览器加载并执行 javascript 大概可分为如下几个环节: 下载,解析,执行和优化,垃圾回收.
下载
javascript 是以纯文本格式下载的.相比,webassembly 使用二进制格式存储,结构更精简,更小.
解析
javascript 下载后,需要 js 引擎经过 tokenize, parse 两个阶段转换成 AST(abstract syntax tree),然后再转换为浏览器需要的中间字节码.由于 js 是比较高级的语言,解析 js 也相对要做更多的事情.webassembly 的格式类似于汇编语言,本来就是中间字节码,和需要运行的机器码更相近,需要简单的转换工作即可转化为 CPU 可以直接执行的机器码.
下图是一个真实运行的 webassembly(它是文本的,只是为了方便调试),可以看出它和汇编是很相似的,更易转化为机器码.
执行和优化
在执行阶段,js 普遍采用解释执行策略,相当于每一次执行 javascript 指令都要通过 js 引擎中转给 cpu.现代的 js 引擎同时采用了即时编译的策略.这需要同时运行一个 profiler,关注每个函数的调用情况.当 profiler 发现一个函数调用的比较多的时候,会把这个函数抛给编译器,为它生成一个更快的编译版本.某些情况下,参数类型会发生变化.这时,需要删除之前的编译版本,对新参数类型编译新的版本.而 webassembly 由于类汇编的结构,只需简单的编译即可转换为可直接运行在 cpu 上的机器码,执行更快.
垃圾回收
javascript 运行期间需要同时间歇的运行一个垃圾回收器,扫描堆上的垃圾,释放内存.垃圾回收器的运行又和 js 引擎的执行是互斥的,导致 js 执行间歇性的被垃圾回收器打断.webassembly 不负责垃圾回收,只能编程语言自行解决.于是不同的编程语言又有所不同.C/C++ 是手动管理内存 (malloc/free, new/delete),rust 则是基于生命周期的自动内存管理.所有这些内存管理方法都不需要间歇的全局暂停.因此性能更好.
从以上各个角度看 WebAssembly 确实比 javascript 性能高.事实上,目前阶段 WebAssembly 执行时间大概等于原生程序执行时间 X1.2.
WebAssembly 的加载与执行
wasm 是 WebAssembly 格式的浏览器可执行文件.它是二进制的,但是它并不像桌面 win32 程序一样,可以随便使用系统资源,调用操作系统 api.事实上,所有与外界相关的操作都必须由 javascript 传入.比如:要申请一段内存,必须由 javascript 申请了并传给他. 浏览器上,javascript 做不到的,它也做不到;javascript 能做到的,它能做的更快. 这个就是它的价值.
目前必须要 js 启动 WebAssembly 的加载和实例化(后面可能会有单独的加载机制).
如下函数,使用 fetchAPI 加载 wasm 文件,并实例化 wasm 模块.
importObject 即浏览器需要向 webassembly 注入的交互 api.
function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}
fetchAndInstantiate('module.wasm', importObject).then(function(instance) {
...
})
如下,是一个真实运行的 importObject 包括很多 js 函数.
注意 global.memory 就是 webassembly 程序执行用到的内存,是 js 申请的一个大的 ArrayBuffer.
学会 WebAssembly 开发
讲了这么多 WebAssembly 的优点,接下就讲下 WebAssembly 的开发.
开发 WebAssembly 并不意味着需要手写 WebAssembly 汇编程序.一个开源项目 emscripten 已经提供了 sdk 可以编译 C/C++,并输出 WebAssembly 的 wasm 文件.目前,rust 也已经支持编译到 wasm.未来所有支持编译到 LLVM 字节码的编程语言,理论上都可以输出 wasm.
安装 emscripten
下载 emscripten sdk 后,是个压缩文件,其实是 sdk 包管理器.
需要执行如下命令,完成 sdk 的安装.
现在已经有个可用的 emcc 编译器了,输入:
./emsdk update
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
emcc --version
查看编译器版本.
emsdk 安装后, emscripten 文件内是按版本号安装的 sdk 内容, 里面有很多 C/C++ 用例,可以自行研究下.
简单 demo
这个简单的 C 程序可以直接编译为 wasm.
默认情况下,emcc 只输出了一个 js(asmjs).asmjs 是 webassembly 的一个早期原型,可提供 webassembly 在旧版本浏览器上的兼容.按如下命令输出 webassembly 二进制 wasm.
#include <stdio.h>
int main() {
printf("hello, world!\n");
return 0;
}
./emcc hello_world.c
node a.out.js
. / emcc hello_world.c - s WASM = 1 - o index.html
这次编译输出了 index.html, index.js, index.wasm 三个文件.通过一个静态服务器打开 index.html,可以看到 console 里的输出.
这个 index.html 是一个调试页面.生产上加载 webassembly 一般都需要自己写 index.html,只保留 js 和 wasm 文件就够了.
以上的例子中,printf 的标准输出被定向到了浏览器的 console 里面. 系统 API 调用被换成了 js 实现. 事实上很多 libc 里面的函数被 emscripten 实现成了浏览器上的兼容方案,从而更好的和浏览器结合.
环境
所有编程语言都要和它的运行环境打交道,否则除了把 cpu 跑满,没什么实用价值.跑在浏览器上的 webassembly 则是通过和 js 相互调用发挥它的作用.
Emscripten sdk 提供了很多 API 与 js 运行环境/浏览器交互.定义在其中两个头文件中:
emscripten.h: 中定义了一些基础功能相关 API,包括调用 js,文件读写,网络请求等,这些 API 在 node 中也可以用.
html5.h 中定义了浏览器中与 DOM 相关的各种操作,包括 DOM,事件,设备相关等.
下面,抽出一些关键的 API 讲下 webassembly 是如何与浏览器协同工作的.
调用 js
EM_ASM 宏,让 webassembly 可以直接调用 js.
EM_ASM(alert('hai'); alert('bai'));
如果需要从 js 获取执行结果,可以用 EM_ASM_INT, EM_ASM_DOUBLE 两个版本分别获取 int 和 double 类型的数值.
如果需要传递字符串给 js,可以传递一个字符串起始的指针给 js.由于 js 可以访问整个 wasm 程序的内存区域, js 用这个指针就可以从内存读出字符串.Module 对象上的 UTF8ToString(ptr), UTF16ToString(ptr), UTF32ToString(ptr),
int x = EM_ASM_INT({
return $0 + 42;
}, 100);
Pointer_stringify(ptr, length)
这几个函数可获得指针处的字符串.
标准输入输出
char* sample = "This is a string";
EM_ASM_({
console.log("js got string:", Module.UTF8ToString($0));
}, sample);
标准输出我们之前看过,printf 最终被转到 Module.print,默认是 console.log 实现.
标准错误输出最终会被转到 Module.printErr,默认是 console.error 实现.
对标准输入的读取在浏览器上变成了一个 prompt 框.体验不好,尽量不要读.
显示
Emscripten 支持两种 GUI 展示方法.
DOM: wasm 是可以调用 js 的,而 js 又可以操作 DOM.因此,wasm 可以通过 js 操作 DOM,创建程序的 GUI.
Webgl Canvas: 除了 DOM,emscripten 还可以提供了 opengl es 的浏览器实现.通过操作一个 Webgl Canvas,把显示内容画在 Canvas 上.
事件循环
C++ GUI 程序一般都有个事件循环,其实就是个死循环,反复获取并处理 GUI 层面上的各种事件.这样程序不会跑完 main 函数直接退出.webassembly 程序跑在浏览器上,而浏览器本来就是事件驱动,已经有了一个事件循环.假如不改动直接上浏览器,就会卡死浏览器的 GUI 进程.因此 webassembly 程序需要由浏览器控制事件循环.
emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop)
函数接受一个函数的指针后,浏览器会根据 fps 按时调用传入的函数.
存储
#include <stdio.h>
#include <emscripten.h>
int frame = 0;
void main_loop(void) {
printf("frame: %d\n", frame);
frame++;
}
int main(void) {
emscripten_set_main_loop(main_loop, 0, 1);
return 0;
}
浏览器隔离了程序直接操作存储的权限,因而 webapp 是安全的,但很多 C 代码都有同步操作文件的 API,如 open, write, close.为了兼容,emscripten 实现了一个内存文件系统,可以通过全局对象 FS 访问.
下图,是 FS 对象下的函数.
.
另外,emcc 还提供了 --preload-file 参数,在 webassembly 程序加载的过程中,预加载文件放到虚拟文件系统中.
wasm 中的文件虽然是内存的,但是支持通过 indexDB 持久化.
如下 js,mount 一个 indexdb 的文件夹到 / data 目录,然后 FS.syncfs 把 indexdb 中的文件同步到内存.
接下来,所有,/data 目录下的读写,都在内存中的同步读写.当程序关闭的时候,需要调用
FS.mkdir('/data');
FS.mount(IDBFS, {}, '/data');
FS.syncfs(true, function (err) {
});
FS.syncfs(false, function(err){})
把内存中的文件反方向同步回 indexdb.
库
emsdk 提供了一些常用的 C++ 库的 webassembly 兼容版本.用 emcc --show-ports 命令显示.如果要用 SDL2,需要给 emcc 加入选项 - s USE_SDL=2,链接 SDL2 库.
目前,emcc 内置支持这些库.
如果所需要的库没在列表里,需要先用 emsdk 编译所需要的库(可能涉及到库的改动).再编译并链接,输出最终目标.emcc 不支持动态链接.
$ emcc --show-ports
Available ports:
zlib (USE_ZLIB=1; zlib license)
libpng (USE_LIBPNG=1; zlib license)
SDL2 (USE_SDL=2; zlib license)
SDL2_image (USE_SDL_IMAGE=2; zlib license)
ogg (USE_OGG=1; zlib license)
vorbis (USE_VORBIS=1; zlib license)
bullet (USE_BULLET=1; zlib license)
freetype (USE_FREETYPE=1; freetype license)
SDL2_ttf (USE_SDL_TTF=2; zlib license)
SDL2_net (zlib license)
Binaryen (Apache 2.0 license)
cocos2d
展望
目前, webassembly 已经完成 MVP 最小功能版本开发,有非常注目的性能.可以遇见,未来将有更多 h5 app / 游戏通过 webassembly 获得更好的体验.使用 C/C++/rust 进行 webapp 开发, 混合编程,也会有很多不错的探索.
未来 h5 能否通过 webassembly 撼动原生的大门,让我们拭目以待.
来源: https://segmentfault.com/a/1190000012798495