WebAssembly初探以及在Tengine中的应用

shangxu.cjy 2019-08-26

编程语言 native 函数 阿里技术协会 浏览器 Image github tengine WebAssembly wasm

0x01 前言

WebAssembly本质是一种二进制指令格式(Binary Instruction Format),即是一种编译目标,该技术成功地使得浏览器有办法将沉重且耗时的JS代码变成了拥有Native性能的二进制,毋庸置疑,它是成功的。
1*g09zv9WuuH00KfVisRPAOg.png

WebAssembly脱离于浏览器后,其沙盒、高效、规范化、可移植性 等特性使其成为独立VM变得可能,本文也将讨论起WebAssembly技术作为独立VM的优势以及其在Tengine中的应用。

0x02 简介

WebAssembly标准目前由Mozilla领导,Google、Microsoft、Apple等众多大公司参与制定,但是是何缘故能让这4大巨头站在一块、他们最原始的动机又是什么,或许我们已经不得而知,Brendan Eich(JS的发明者、WebAssembly的主要推动者)也承认了最开始使用了私有github来协调各大巨头共同确定目标并且推动他们为这项技术买单。

普遍认为,这4大厂代表了4大浏览器平台,在面对日益增加的前端业务逻辑对计算机计算资源的消耗,迫切需要一种新的技术,该技术首先需要解决JS性能问题,即它是高效的;其次需要安全性,即它是沙盒的;接着是需要可移植性,即能被所有浏览器接受。

0x03 例子

首先我们先来通过一个例子来直观感受下WebAssembly

机器环境准备

  • 升级工具链(仅集团的机器需要,因为他们的版本较低,使用OSX可以不需要这步)
  • emcc编译器

准备源文件

#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
image.png
展示页面的其他元素是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。
image.png

2、WebAssembly将字符串"Hello"写入0~4
image.png

3、WebAssembly告诉Native"Hello"的起始地址,当前例子是0。
image.png

4、Native获得0,知道其映射在Native环境的地址是14,所有从14开始读取字符串

乍一听,反倒觉得这不安全,该特性岂不会让WebAssembly内存的错误操作污染到Native的环境?
实际上,当执行WebAssembly的VM/Engine尝试操作内存的时候,都会判断内存的边界,当Vm/Engine发现地址越界时会抛出异常:
image.png

(2)防止内存泄露

这个也很容易理解,对于Native环境,其传给WebAssembly的内存对于Native而言就是一个pool,当调用玩且释放WebAssembly时也可以随即释放该pool;对于有GC的语言,这个pool在Native环境被创建时就已被GC跟踪,会自动释放内存。

0x05 性能

WebAssembly通常被认为是高性能的,我们来看看究竟如何。

WebAssembly VS JS

JS执行流程
image.png

WebAssembly执行流程(附带和上图的对比)
image.png

总结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从源文件到可执行机器码的整个流程。
image.png

将其分为2部分,前半部分在Server端完成,编译成wasm二进制。后半部分由VM/Engine完成,将wasm二进制编译成对应机器的字节码。

image.png

所以当一个浏览器或者VM/Engine获得WebAssembly二进制文件时,并非解析格式后直接执行,而是需要使用Back End将其编译成机器码。

当有人都在说WebAssembly代码有Native运行速度时,往往都忽略了使用Back End编译成机器码的耗时。

开源项目Wasmer项目使用了3个Back End,通常需要在编译耗时以及对应生成的机器码执行效率间进行取舍。
image.png

另一个针对嵌入式环境的开源库Intel WAMR,它不包含Back End,直接人肉解析WebAssembly二进制的代码段中的指令,然后实现这些指令对应的功能,这种运行方式和解释型语言没多大区别,或许是因为嵌入式环境资源限制导致的无法加载通常体积较大的Back End。

0x06 WASI

从上面的介绍可以看到,所有的概念和例子的语境都没有离开浏览器,正在将WebAssembly技术带离浏览器奔向更大应用场景的是WASI规范,它定义了一系列底层(特别是和系统资源相关)的操作。
上文提及,WebAssembly是沙盒的,这对于浏览器而言很关键,但是当它脱离浏览器后,作为独立VM,和Native环境打交道就不可避免。让这些个接口规范化程度直接决定了其跨平台性。

先贴一张WASI的终极目标示意图
image.png
用大家都熟悉的话总结就是 "Write One,Run Everywhere",同一个编译目标能在不同平台、机器上运行。

和Emscripten的区别

上文提到的emcc就是Emscripten项目的一员,从本文开头的例子中,貌似emcc也能执行open read等操作,那为何还需要定义WASI呢?一个新的规范的出现必定是为了解决当前的某些问题,首先来看下Emscripten的问题。
image.png
read函数被emcc翻译成了__syscall3(3, args),即VM/Engine需要实现一个名字叫__syscall3的函数,且函数read的多个参数将被保存在WebAssembly线性空间中,集成在参数 args 中,这被认为type unsafe

WASI将这些POSIX函数重新定义,如下图所示,无论哪个平台的VM/Engine,只需要实现和自身平台相关的__wasi_fd_read函数给WebAssembly用即可,这样在编写WebAssembly调用read函数时就无需关心自身将会运行在哪个平台。
image.png

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本身作为集团统一接入产品,每天处理着集团大多数入口流量。

TengineWebAssembly领域也做了些尝试,目前使用了Wasmer/Wavm 作为自己底层的runtime(编译Tengine时选择具体runtime)

下图是Tengine使用WebAssembly的框架(蓝色是为了支持WebAssembly而新增加的功能)
image.png

由于语言的限制,Wasmer c-apiWavm 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_headersngx_wasm_callhost_get_var等函数获取当前HTTP请求的相关信息来给WebAssembly处理。

Wasm util实际上是 类似 一些 Lua的指令,这里Tengine暂时实现了类似content_by_lua_filecontent_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

所有参考资料都以超链接方式展示在原文中。

登录 后评论
下一篇
云攻略小攻
433人浏览
2019-10-21
相关推荐
创建和使用 WebAssembly 组件
1419人浏览
2017-10-17 16:24:00
在Chrome 70中体验WebAssembly线程
374人浏览
2019-08-06 22:21:00
再见 JavaScript, 你好 WebAssembly
355人浏览
2019-08-06 23:07:07
Node.js中的WebAssembly入门
2001人浏览
2018-06-20 11:04:35
JavaScript引擎 V8 的前世今生
429人浏览
2019-08-06 23:43:07
tengine初探
1089人浏览
2017-06-02 13:52:00
BlinkOn9 - WebAssembly
1617人浏览
2018-04-26 15:49:12
0
0
0
7326