0x01 前言
WebAssembly
本质是一种二进制指令格式(Binary Instruction Format),即是一种编译目标,该技术成功地使得浏览器有办法将沉重且耗时的JS代码变成了拥有Native性能的二进制,毋庸置疑,它是成功的。
当WebAssembly
脱离于浏览器后,其沙盒、高效、规范化、可移植性 等特性使其成为独立VM变得可能,本文也将讨论起WebAssembly
技术作为独立VM的优势以及其在Tengine
中的应用。
0x02 简介
WebAssembly
标准目前由Mozilla领导,Google、Microsoft、Apple等众多大公司参与制定,但是是何缘故能让这4大巨头站在一块、他们最原始的动机又是什么,或许我们已经不得而知,Brendan Eich(JS的发明者、WebAssembly
的主要推动者)也承认了最开始使用了私有github来协调各大巨头共同确定目标并且推动他们为这项技术买单。
普遍认为,这4大厂代表了4大浏览器平台,在面对日益增加的前端业务逻辑对计算机计算资源的消耗,迫切需要一种新的技术,该技术首先需要解决JS性能问题,即它是高效的;其次需要安全性,即它是沙盒的;接着是需要可移植性,即能被所有浏览器接受。
0x03 例子
首先我们先来通过一个例子来直观感受下WebAssembly
机器环境准备
准备源文件
#include <stdio.h>
int main()
{
printf("in wasm world\n");
return 1;
}
编译
chenjiayuadeMacBook-Pro:emsdk mrpre$ emcc targt.c -o target.html
chenjiayuadeMacBook-Pro:emsdk mrpre$
chenjiayuadeMacBook-Pro:emsdk mrpre$ ls -l target*
-rw-r--r-- 1 mrpre staff 80 8 13 10:21 target.c
-rw-r--r-- 1 mrpre staff 102676 8 13 10:22 target.html
-rw-r--r-- 1 mrpre staff 102935 8 13 10:22 target.js
-rw-r--r-- 1 mrpre staff 42765 8 13 10:22 target.wasm
我们看到,生成了3个文件,target.js 是胶水文件,用来调用target.wasm,其被本身用来被JSEngine加载。
使用node运行
chenjiayuadeMacBook-Pro:emsdk mrpre$ node target.js
in wasm world
chenjiayuadeMacBook-Pro:emsdk mrpre$
貌似还不直观,那就使用浏览器运行
先在当前目录中启动一个简单的httpserver用以浏览器访问
python -m SimpleHTTPServer 9000
浏览器访问http://127.0.0.1:9000/target.html
展示页面的其他元素是emcc帮助生成的我们并不关心,这里我们关心浏览器控制台console输出的结果,console里面输出的正是我们C代码printf打印的数据,可以推测,printf的功能由console.log实现。
0x04 安全性
WebAssembly
的安全性归根结底,是其沙盒的特性。
文件安全
你可以在 上面例子中 尝试 open
一个xxx文件,你会发现,你没办法打开,除非编译时显示的加上"--embed-file xxx",该编译选项将文件内容全部放在target.js中,target.wasm在被执行时,所有的文件操作也均在内存上执行,在WebAssembly
所有读写操作均在内存上进行。
内存安全
先来看下WebAssembly
是如何管理内存的。
通常,WebAssembly
的内存需要Native环境申请且提供给.wasm,即Native在实例化.wasm文件时需要显示将一块属于Native环境的内存给.wasm使用,这带来2个好处。
(1)Native和WebAssembly
内存共享
(2)防止内存泄露
(1)内存共享
先来看如何共享字符串。
1、首先,有一块内存10字节的内存,由Native环境申请,且给WebAssembly
使用,并且告诉WebAssembly
地址是14~23,WebAssembly
会将其映射为0~10。
2、WebAssembly
将字符串"Hello"写入0~4
3、WebAssembly
告诉Native"Hello"的起始地址,当前例子是0。
4、Native获得0,知道其映射在Native环境的地址是14,所有从14开始读取字符串
乍一听,反倒觉得这不安全,该特性岂不会让WebAssembly
内存的错误操作污染到Native
的环境?
实际上,当执行WebAssembly
的VM/Engine尝试操作内存的时候,都会判断内存的边界,当Vm/Engine发现地址越界时会抛出异常:
(2)防止内存泄露
这个也很容易理解,对于Native环境,其传给WebAssembly
的内存对于Native而言就是一个pool,当调用玩且释放WebAssembly
时也可以随即释放该pool;对于有GC的语言,这个pool在Native环境被创建时就已被GC跟踪,会自动释放内存。
0x05 性能
WebAssembly
通常被认为是高性能的,我们来看看究竟如何。
WebAssembly VS JS
JS执行流程
WebAssembly
执行流程(附带和上图的对比)
总结WebAssembly
比JS更快的原因
(1)WebAssembly
的变量类型不是JS的动态类型,所以编译器无需在运行时才编译。
(3)因为WebAssembly
变量是静态的,编译器无需生成多份代码。
(4)LLVM已经在编译C文件时进行了优化。
其实说到底,核心问题就是JS是动态类型语言。
WebAssembly为什么快
首先需要了解的是,编译器通常分为Front End 和 Back End,Front End 用于语法分析,然后生成IR(Intermediate representation),Front End用于生成IR对应的机器码。下图是WebAssembly从源文件到可执行机器码的整个流程。
将其分为2部分,前半部分在Server端完成,编译成wasm二进制。后半部分由VM/Engine完成,将wasm二进制编译成对应机器的字节码。
所以当一个浏览器或者VM/Engine获得WebAssembly
二进制文件时,并非解析格式后直接执行,而是需要使用Back End将其编译成机器码。
当有人都在说WebAssembly
代码有Native运行速度时,往往都忽略了使用Back End编译成机器码的耗时。
开源项目Wasmer项目使用了3个Back End,通常需要在编译耗时以及对应生成的机器码执行效率间进行取舍。
另一个针对嵌入式环境的开源库Intel WAMR,它不包含Back End,直接人肉解析WebAssembly
二进制的代码段中的指令,然后实现这些指令对应的功能,这种运行方式和解释型语言没多大区别,或许是因为嵌入式环境资源限制导致的无法加载通常体积较大的Back End。
0x06 WASI
从上面的介绍可以看到,所有的概念和例子的语境都没有离开浏览器,正在将WebAssembly
技术带离浏览器奔向更大应用场景的是WASI规范,它定义了一系列底层(特别是和系统资源相关)的操作。
上文提及,WebAssembly
是沙盒的,这对于浏览器而言很关键,但是当它脱离浏览器后,作为独立VM,和Native环境打交道就不可避免。让这些个接口规范化程度直接决定了其跨平台性。
先贴一张WASI的终极目标示意图
用大家都熟悉的话总结就是 "Write One,Run Everywhere",同一个编译目标能在不同平台、机器上运行。
和Emscripten的区别
上文提到的emcc就是Emscripten项目的一员,从本文开头的例子中,貌似emcc也能执行open
read
等操作,那为何还需要定义WASI呢?一个新的规范的出现必定是为了解决当前的某些问题,首先来看下Emscripten的问题。read
函数被emcc翻译成了__syscall3(3, args)
,即VM/Engine需要实现一个名字叫__syscall3
的函数,且函数read
的多个参数将被保存在WebAssembly
线性空间中,集成在参数 args 中,这被认为type unsafe
WASI将这些POSIX函数重新定义,如下图所示,无论哪个平台的VM/Engine,只需要实现和自身平台相关的__wasi_fd_read
函数给WebAssembly
用即可,这样在编写WebAssembly
调用read
函数时就无需关心自身将会运行在哪个平台。
WASI例子
这里使用的不再使用上文例子中的emcc
,而是使用高版本Clang,为了避免系统环境无法支持高版本Clang的情况,这里使用官方推荐的wasi-sdk-6来编译生成WASI。
下载工具链
下载好wasi-sdk-6后解压,例如我的解压路径是./code/wasi-sdk-6.0
,可以使用如下命令编译C源码
编写C文件
$cat 1.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
char buf[1024];
int main()
{
int fd = open("1.c", O_RDONLY);
if (fd < 0) {
printf("can't load file 1.c\n");
return -1;
}
read(fd, buf, sizeof(buf));
printf("Read file:\n%s",buf);
return 0;
}
编译
./code/wasi-sdk-6.0/opt/wasi-sdk/bin/clang --target=wasm32-wasi --sysroot=./code/wasi-sdk-6.0/opt/wasi-sdk/share/sysroot 1.c -o 1.wasm
运行WASI
这里我们使用Wasmer的CLI来运行WASI wasmer run 1.wasm --mapdir ./:./
如果顺利的话,他将打印出当前目录下1.c
文件的内容。
0x07 Tengine+WebAssembly
Tengine作为基于Nginx的Web服务器,开源至今已有8年,在集团内几乎所有应用前都部署了Tengine作为反向代理,同时Tengine本身作为集团统一接入产品,每天处理着集团大多数入口流量。
Tengine
在WebAssembly
领域也做了些尝试,目前使用了Wasmer/Wavm 作为自己底层的runtime(编译Tengine时选择具体runtime)
下图是Tengine使用WebAssembly
的框架(蓝色是为了支持WebAssembly而新增加的功能)
由于语言的限制,Wasmer c-api
和Wavm c-api
是分别针对两种不同的VM提供的C的api接口,虽然两个VM都自带c-api,但是其功能都无法满足Tengine需求,所以重新使用C++和Rust编写了各自的库对应的c-api。
Tengine C-API
实现了 加载WebAssembly
、实例化WebAssembly
、调用WebAssembly
函数等操作,对于熟悉编写Nginx C模块的人来说,可以在任意地方调用这些函数来加载和运行WebAssembly
。
同时在WebAssembly
代码里可以调用诸如ngx_wasm_callhost_get_headers
、ngx_wasm_callhost_get_var
等函数获取当前HTTP请求的相关信息来给WebAssembly
处理。
Wasm util
实际上是 类似 一些 Lua的指令,这里Tengine暂时实现了类似content_by_lua_file
的content_by_wasm_file
指令用于调试功能。
体验
虽王婆卖瓜,但童叟无欺。这里提供了一个HTTP接口(运气好的话当你看到这篇文章的时候这个接口还在),你可以POST一个WebAssembly
文件上来,Tengine帮你运行且将结果作为HTTP response body反吐给你!
(手下留情别传太大的,日常机器资源有限)
准备C源码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <malloc.h>
#include <time.h>
#ifdef __EMSCRIPTEN__
#define __IMPORT(name)
#else
#define __IMPORT(name) __attribute__((__import_module__("env"), __import_name__(#name)))
#endif
int ngx_wasm_callhost_say(char *str, int len) __IMPORT(_ngx_wasm_callhost_say);
int main(void) {
int max, len;
char time[65], *p;
struct timespec tp;
max = 100 + sizeof(time);
p = malloc(max);
if (!p) {
ngx_wasm_callhost_say("malloc fail", sizeof("malloc fail") - 1);
return 0;
}
clock_gettime(CLOCK_REALTIME, &tp);
len = snprintf(p ,max, "in host time %ld", tp.tv_sec);
ngx_wasm_callhost_say(p, len);
free(p);
return 0;
}
编译
emcc 1.c -o 1.wasm -s ERROR_ON_UNDEFINED_SYMBOLS=0
或者
./code/wasi-sdk-6.0/opt/wasi-sdk/bin/clang --target=wasm32-wasi --sysroot=./code/wasi-sdk-6.0/opt/wasi-sdk/share/sysroot 1.c -o 1.wasm -Wl,--allow-undefined
发送至Tengine
curl http://11.164.24.251:8088/runwasm -H "Transfer-Encoding: chunked" --data-binary "@./1.wasm"
正常情况下返回的内容是WebAssembly
代码中ngx_wasm_callhost_say
传入的内容。
这是我自己测试的结果
[shangxu.cjy@tengine-daily011164024251.na62sqa /home/shangxu.cjy/code/wasmtengine/alibaba-tengine]
$curl http://11.164.24.251:8088/runwasm -H "Transfer-Encoding: chunked" --data-binary "@./1.wasm"
in host time 1565876453
展望
社区
WebAssembly
技术还处于初中期阶段,特别是脱离于浏览器环境后作为独立VM/Engine,相关的定义和规范缺失,各开源实现都未能跟上。
在Tengine尝试加载这两个WebAssembly
VM时,碰到了各种VM自身的问题,包括 当 WebAssembly
异常(空指针等),整个VM也就crash了,同时 也出现过更新emcc编译器后,编译出来的wasm文件无法被VM运行的情况。 期望 WebAssembly
尽早被完善。
Tengine + 多语言
WebAssembly
是二进制的格式的规范,理论上只要能被编译成LLVM IR的语言都能被转换成WebAssembly
,这意味着理论上,Tengine可以使用WebAssembly
作为多语言的容器,无论哪种语言都能作为Tengine模块开发且运行速度接近Native。或许在不久得将来,Tengine能够作为底层的网络容器发挥其特有的异步优势,而上层业务代码可以使用Go/Rust/C/C++甚至是JAVA,发挥不同语言的特性。
Tengine + 安全
WebAssembly
是沙盒的,这意味着如果WebAssembly
文件内容出现异常不会连累宿主环境(Native),特别适合运行一些危险的逻辑。
Reference
所有参考资料都以超链接方式展示在原文中。