目的:本次尝试的主要目的是希望尝试把一个大型的项目编译到 Wasm 中,这里研究的是 Wasm Runtime 的性能损耗,所以编译的结果也是 Wasm Standalone 版本
适合人群:对于 Wasm 有一定的了解,并且想做类似的实验的同学们
在本文中,笔者选用的大型项目是 nginx,一个用 C 写的代理服务器软件。本文的主要目的是将 nginx 编译到 Webassembly 中。
源码编译 nginx
wget [<https://nginx.org/download/nginx-1.17.6.tar.gz>](<https://nginx.org/download/nginx-1.17.6.tar.gz>)
tar -xf [nginx-1.17.6.tar.gz](<https://nginx.org/download/nginx-1.17.6.tar.gz>)
cd nginx-1.17.6
./configure --builddir=objs
make
./objs/nginx -v
-
编译 nginx 的命令中:
- --builddir 指定在哪个目录下进行编译,在这里指定了 objs 文件夹作为编译目录
-
然后只需要使用 make && make install 即可以在这个目录下编译出 nginx ,编译成功
使用 Emscipten 编译 nginx
-
关于 Emscripten 的使用整理如下,详细信息可参看文档,不再进行赘述
- Emscripten 主页:emscripten.org/
- Emscripten 对于编译大型项目的支持:emscripten.org/docs/compil…
- Emscripten 对于编译 Wasm Standalone 的支持:github.com/emscripten-…
-
Emscripten 中对于编译大型项目提供了非常便利的脚本,只需要两步即可进行编译
- 将 emconfigure 添加到上述 configure 脚本之前
- 调用 emmake make 进行编译
-
笔者首先参照了这个项目:github.com/wapm-packag… ,这个项目声称自己编译出了可以运行的 nginx,但是从 issue 来看,没有开发者复现成功,而且也没有维护者进行回复,但项目中的 build.sh 文件可以c参照一下
emconfigure ./configure --builddir=objs_wasm --with-cc-opt="-Wno-sign-compare" --without-http_rewrite_module
# 进入 objs_wams/Makefile 修改 -o objs_wasm/nginx 为 -o objs_wasm/nginx.wasm
emmake make
-
上述configure脚本的解释:
- --builddir 指定在哪个目录下进行编译,在这里指定了 objs_wasm 文件夹
- --with-cc-opt 是为了避免编译时的一个错误,在这里通过禁止提示这个 Warning 的方式通过编译
- 后面两个 without 用来防止 nginx 进行报错
-
由于我们希望编译一个 wasm standalone 的文件,因此在最后的链接过程中,我们需要手动修改 Makefile,将目标文件转换为 .wasm 的格式。Emscripten 默认是生成 JS + Wasm 的版本,而我们通过更改后缀的方式只生成 WASM 文件
emcc ... -o output.wasm
omits generating either JavaScript or HTML launcher file, and produces a single Wasm file built in standalone mode as if the-sSTANDALONE_WASM
settting had been used. -
最后使用 emmake 工具进行 make 即可
make 失败的解释
-
如果我们走完了上一步,会发现 make 并不能成功,报错如下:
error: undefined symbol: initgroups (referenced by top-level compiled C/C++ code) warning: Link with `-sLLD_REPORT_UNDEFINED` to get more information on undefined symbols warning: To disable errors for undefined symbols use `-sERROR_ON_UNDEFINED_SYMBOLS=0` warning: _initgroups may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library error: undefined symbol: sigsuspend (referenced by top-level compiled C/C++ code) warning: _sigsuspend may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library Error: Aborting compilation due to previous errors emcc: error: '/home/oem/wasm/emsdk/node/14.18.2_64bit/bin/node /home/oem/wasm/emsdk/upstream/emscripten/src/compiler.js /tmp/tmpgkotg_d1.json' failed (returned 1)
-
对于 undefined symbol 的解释是这样子的:initgroups 和 sigsuspend 这两个函数是 nginx 代码中被使用到的,然而 emscripten 在编译的过程中并不能将其转换为 wasm 代码,或者是将其转换为 wasm 代码 + js 代码的组合。我们分开看这两个函数:
- initgroups 函数:一个示例,但我也其实没太看懂。但是从中可以了解到这个函数是跟 linux 中的权限管理有关,强依赖于 /etc/group 文件。因此,这个函数应该是与 Linux 系统强耦合的。而我们也应该想到,这样的函数放到 wasm 环境中,浏览器端是无法理解的,而 WASI 接口我也没有找到相应的 “组权限” 的概念。因此,Emscripten 认为自己无法处理这个函数,因此报错 undefined symbol
- 同样的, sigsuspend 函数我也没太看懂,但是应该是跟信号相关的。同样,wasm 程序目前还没有什么信号机制,这个函数也没有被处理,在这里有一些相关的讨论: github.com/emscripten-…
-
我们的一个 work around 是将 nginx 中的这些两个函数的使用给注释掉了,虽然肯定会影响一些程序逻辑,但是我们先这样做了一个尝试,使其可以通过编译。注释掉之后可以发现
emmake make
成功了。
runtime 运行失败的解释
-
我们下载了 wasmedge 来运行 nginx.wasm,结果依然是报错,报错信息如下:
➜ nginx-1.17.6 wasmedge ./objs_wasm/nginx.wasm [2023-03-02 22:50:11.899] [error] instantiation failed: unknown import, Code: 0x62 [2023-03-02 22:50:11.899] [error] When linking module: "env" , function name: "getaddrinfo" [2023-03-02 22:50:11.899] [error] At AST node: import description [2023-03-02 22:50:11.899] [error] This may be the import of host environment like JavaScript or Golang. Please check that you've registered the necessary host modules from the host programming language. [2023-03-02 22:50:11.899] [error] At AST node: import section [2023-03-02 22:50:11.899] [error] At AST node: module
-
ps:wasm3 貌似可以运行 nginx -v 成功,但是其余功能无法运行。这可能是由于 wasmedge 使用了 AOT 编译,而 wasm3 是纯解释器模式因而懒加载而。
-
这个日志中的信息需要我们解析一下 wasm 得到。我们将 wasm 转换为 wat 文件可以发现,wasm import 了非常多的函数,截取部分如下
(import "env" "getaddrinfo" (func (;0;) (type 5))) (import "env" "exit" (func (;1;) (type 3))) (import "env" "strftime" (func (;2;) (type 5))) (import "env" "abort" (func (;3;) (type 10))) (import "env" "sigsuspend" (func (;4;) (type 1))) (import "env" "initgroups" (func (;5;) (type 2))) (import "wasi_snapshot_preview1" "environ_sizes_get" (func (;6;) (type 2))) (import "wasi_snapshot_preview1" "environ_get" (func (;7;) (type 2)))
其中,出错的即是
(import "env" "getaddrinfo" (func (;0;) (type 5)))
这个函数的引入,那么这个函数是从哪里来的呢? -
如果你曾经使用 Emscripten 将 C 编译为 JS + Wasm 文件,那么你应该对 env 这个module 不陌生,我们可以不修改 Makefile,获得 JS + wasm 输出看看。生成的 JS 文件中有如下的片段:
// 在实例化 wasm module 的时候传入了 info 对象作为 import object,即引入 info 对象到 wasm 中 return getBinaryPromise().then(function(binary) { return WebAssembly.instantiate(binary, info); }).then(function (instance) { .... // info 对象有两个 module,其中一个就叫做 env,另一个叫做 wasi_snapshot_preview1,这两个 module 都传入了 asmLibraryArg 包含的函数 var info = { 'env': asmLibraryArg, 'wasi_snapshot_preview1': asmLibraryArg, }; // asmLibraryArg中就有 getaddrinfo 的定义,同样也有 environ_sizes_get 的定义 var asmLibraryArg = { ... "environ_sizes_get": _environ_sizes_get, "getaddrinfo": _getaddrinfo, ... };
-
因此,我们可以得知,runtime 运行的原因是 wasm 需要引入 env module 中名为 getaddrinfo 的函数。但由于env module 这个东西是 emscripten 在产生 js 文件时由 js 进行 import 的, wasmedge 这种 runtime 并不认识 env module,因此会报出运行时错误。
-
对于 env 和 wasi_snapshot_preview1 这两个引入 module,我是这样理解的:
- wasi_snapshot_preview1 是 WASI 的当前版本,Emscirpten 为了兼容 WASI 接口,将很多 API 的命名改为了 WASI 接口的规范,所有的函数在这里可以找到。
- 但由于 WASI 接口的很多功能模块都还在提案流程中,因此并不是所有的 syscall (或者一部分标准库)都可以被实现为 WASI 接口定义的函数。因此,Emscripten 也只能保留无法兼容 WASI 函数的部分在 env module 中,这就导致了 包含这些函数的 wasm 无法被 runtime 运行。
-
Emscipten 的文档中也有相关的表述 《As Much As Possible》
Aside from APIs that require JS or Web APIs, though, we try to use WASI APIs as much as possible so that the wasm can run in wasi-supporting runtimes.
究竟如何编译 nginx 到 wasm 中呢?
-
我们提了一个相关 issue 到 wasm 中,得到的回复是这样的:
If you want to do that you would need to either remove the dependency of your program on symbols like
getaddrinfo
or somehow implement those symbols in the runtime. -
因此,方向有两个
- 改造 wasm 的 runtime,让他们提供 env 里面的函数。但这个是不现实的,因为 Emscripten 是将 C/C++ 编译为 wasm 的编译器,而 runtime 需要兼容各个语言的编译器编译的 wasm 字节码,不应该只为 Emscripten 的 API 做兼容。而由于之前提及过的原因, Emscripten 也无法将类似于 getaddrinfo 函数编译成一个大家都认可的格式。因此,在 WASI 规范进行统一之前,runtime 和 Emscipten 都无法做出合适的改动。
- 另一个就是改造 nginx,将 nginx 中所有 Emscripten 无法编译的函数都给换掉,换成可编译的,或者去掉这部分功能。但显然这个工作量比较庞大,因为这要求你对 nginx 的代码比较熟悉,并且对 Emscripten 的能力范围也比较熟悉才行。
-
在社区中找到相关问题的蛛丝马迹:
-
wasmer 的创始人曾经干过这个事情:
So we started mapping almost all the syscalls that the Emscripten Nginx version needed to run properly. However, this path was quite challenging as it required us to transform back and forth structs from WebAssembly to the inner implementation (libc).
-
stackoverflow 上有一些相关的讨论:
It's worth acknowledging that this would be very difficult. nginx is full of system calls that you would need to patch out, and modifying the control flow so majorly would require intimate farmiliary with nginx internals.
-
因此,本次 nginx 转换实验以失败告终,但是我们也因此了解到了 wasm 在目前的局限性,以及可能的努力方向。