+
80
-

Emscripten如何将cc++代码编译成为WebAssembly?

Emscripten如何将cc++代码编译成为WebAssembly?


网友回复

+
0
-

c语言编译

首先,我们来编译一个 C 语言的 Hello World,创建一个 hello.c 文件:

#include <stdio.h> int main() { printf("Hello World!\n"); return 0; } 编译成可执行文件 代码就是输出了一句 Hello World! ,使用 clang 或 gcc 或很多工具都可以把这段代码编译成可执行的二进制,找不到命令的话,可以在网上找教程配置一下。以 clang 为例:

clang hello.c -O3 -o out/hello

-O3 表示了优化级别, 生成的可执行文件是 hello ,但是这个文件只能在特定平台上执行,在 windows 上编译出来的文件没办法跑在 mac 上(不绝对),在 32 位系统编译出来的文件无法跑在 64 位系统上。 然而如果把它编译成 wasm 就可以跨平台分发了,这也是 wasm 的一大优势。只需要编译一次,同一个 wasm 包,可以运行在浏览器中、Node.js 中、各种独立的 runtime 里,但是要求目标平台具备执行 wasm 包的能力,而且符合规范。 c编译成WebAssembly 程序

WebAssembly 的编译和运行流程

在编译 WebAssembly 之前先了解一下它基本的编译和运行流程,想要以何种方式运行 wasm 的包,决定了以何种方式来编译它。 目前来看,大部分使用 WebAssembly 的例子都是运行在浏览器中的,有一部分运行在 Node.js 里,和 JS 的渊源很深,因为在标准里定义了一套 JS API 来编译、实例化 wasm 文件,这部分 API 已经被 JS 引擎实现了,功能已经稳定可用。因此,wasm 最常见的是搭配 js 一起使用,这种场景下用 Emscripten⑤可以搞定,它在编译 wasm 包的同时也会生成一份 js "glue" 代码,把 wasm 包的初始化接口导入导出都封装在 js 里了,使用时引入这个 js 文件即可。 Emscripten 也支持编译成独立的 wasm 包(不含 JS),但是想要运行这个 wasm 包需要宿主环境给它注入很多基础的 API,而且这些 API 是非标准的。如果想在 JS 环境里运行独立 wasm 包的话,要用 JS 实现这些 API。 其实 WebAssembly 本质上和 JS 无关,完全可以运行在独立的沙箱环境里,通过标准化的 API (wasi ⑥) 来调用系统能力。现在已经有不少 wasm 的独立运行时了,如 Wasmtime⑦ 和 wasm-micro-runtime ⑧,它们都可以加载并独立执行 wasm 文件,并且实现了一致的 wasi 接口。

关于 wasi,推荐阅读《Standardizing WASI: A system interface to run WebAssembly outside the web》 https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/

800_auto

如上图所示,面对自己的 C/C++ 代码,想要把它运行在浏览器或 Node.js 中,就使用 Emscripten 把它编译成 wasm + js 文件;想要把它运行在独立的运行时里,就使用 wasi-sdk⑨进行编译,生成单独的 wasm 包。(此结论简单粗暴,为了方便理解,并不严谨)

使用 Emscripten 编译

首先安装官方文档安装 Emscripten (https://emscripten.org/) ,安装完成后命令行环境里会有 emcc 命令,使用方式和 gcc 差不多,执行如下代码就可以生成 wasm 的包:

emcc hello.c -O3 -o out/hello-emcc.wasm

但是,上面这个命令隐含了 -s STANDALONE_WASM 的配置 ,实际上触发的是 WebAssembly Standalone build ⑩,只生成了一个 wasm 的包,需要自己写 loader 加载和执行。如果不想费这个劲,就可以使用如下命令直接生成 wasm + js 文件:

emcc hello.c -O3 -o out/hello-emcc.js

该命令除了生成 js 文件以外,还会生成同名的 hello-emcc.wasm 文件,可以使用 WABT ⑪(WebAssembly Binary Toolkit) 提供的小工具把 wasm 文件转成对等的文本格式,方便阅读。 wasm2wat out/hello-emcc.wasm -o out/hello-emcc.wat 代码比较短,但是生成出来的 wasm 文件有 2.1KB,js 文件 16KB,主要是因为 stdio.h 头文件里有很多依赖,在运行时是由 js 代码来实现的。用 wasm 做 io 本身也不是个好的用法。 最后,直接在 Node.js 环境里执行这个 js 文件就行了,可以看到控制台输出了 Hello World! 。 node out/hello-emcc.js

使用 wasi-sdk 编译

首先根据自己的系统下载相应的 wasi-sdk ,配置好环境变量之后,就可以调用其中自带的 clang 工具编译生成 wasm 文件:

clang hello.c -O3 -o out/hello-wasi.wasm

大概率跑不通…… 因为要配各种环境变量还要指定 sysroot 才行。假如你下载的是 8.0 版本,放到了个人目录之下,可以用下面这个命令编译代码,不需要配置环境变量:

/wasi-sdk-8.0/bin/clang --sysroot ~/wasi-sdk-8.0/share/wasi-sysroot hello.c -O3 -o out/hello-wasi.wasm

如果是 Mac 电脑,遇到安全提示,在【系统偏好设置】-【安全与隐私】-【通用】里,找到“允许以下位置下载的App”的配置,下方应该有提示信息,点击允许就可以了。 打出来包之后,可以用 file out/hello-wasi.wasm 命令检查一下生成的包格式对不对,有如下输出才是正确的,否则你打出来的很可能是个原生的二进制文件。

hello-wasi.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

同样,可以用 wasm2wat 工具生成可读的文本格式,方便看代码:

wasm2wat out/hello-wasi.wasm -o out/hello-wasi.wat

想要运行这个 wasm 包,需要有独立的运行时,理论上讲,所有实现了标准 wasi 接口的 runtime 都可以执行这个包。以Wasmtime (https://wasmtime.dev/)为例,安装好后,用下方命令就可以执行这个 wasm 文件,会看到控制台有 Hello World! 的输出:

wasmtime out/hello-wasi.wasm

另外,如果不想在电脑上装这些独立运行时,还有个神奇的网站(https://wasi.dev/polyfill/)可以在线运行基于 wasi 接口的 wasm 包。这个网站在浏览器环境里实现了一份 polyfill,对等实现了原生 wasm 运行时的一部分功能。把刚编译好的 hello-wasi.wasm 文件传上去,也可以看到 Hello World! 的输出。

800_auto

编译独立模块

然而在实际情况中,并不是所有包都想自执行,不一定都有 main 函数,大部分 wasm 包是想提供一些 api 供外部调用。自己打印 Hello World 没有任何意义,要和宿主环境有交互才行。 下面以斐波那契数列为例,介绍如何编译一个独立的 wasm 模块。C 语言代码如下: int fib (int n) { if (n <= 0) return 0; if (n <= 2) return 1; return fib(n - 2) + fib(n - 1); }

使用 Emscripten 编译

这次代码里没了 main 函数,只有一个 fib 函数,而 Emscripten 默认只导出 main 函数,所以在编译时加上 EXPORTED_FUNCTIONS 的配置指定导出的接口,其他同上: emcc fib.c -s EXPORTED_FUNCTIONS='["_fib"]' -O3 -o out/fib-emcc.wasm 编译 C/C++ 的时候函数名会默认加上 _ 前缀,所以导出的接口名是 _fib 而不是 fib 。 这次生成的包很小,把它转成文本格式后只有 27 行,代码如下: 可以看到这个包只导出一个了 _fib 函数,函数接受 i32 数字为参数,返回一个 i32 数字。想要在 JS 环境里运行起来这个包,需要用 js 代码来加载执行这个包,可以封装如下函数: // 编译并实例化 wasm 模块,返回导出的接口 async function loadWebAssembly (filename, env) { const filePath = path.resolve(__dirname, filename) // 读入 wasm 文件的二进制代码 const buffer = fs.readFileSync(filePath) // 将 wasm 包实例化并传入外部接口,因为没有外部依赖,不传 env 也可以的 const results = await WebAssembly.instantiate(buffer, { env: Object.assign({ '__memory_base': 0, '__table_base': 0, memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }), table: new WebAssembly.Table({ initial: 0, maximum: 128, element: 'anyfunc' }) }, env) }) // 返回实例化好之后的接口 if (results && results.instance) { return results.instance.exports } } 然后使用这个函数加载 wasm 文件: loadWebAssembly('./out/fib-emcc.wasm').then(apis => { console.log(apis._fib(13)) // 输出 233

})

完整代码:https://github.com/Hanks10100/cpp2wasm/blob/master/loader.js

使用 wasi-sdk 编译

这次 wasi-sdk 的编译选项稍微复杂了一些:

~/wasi-sdk-8.0/bin/clang --sysroot ~/wasi-sdk-8.0/share/wasi-sysroot fib.c \

-nostartfiles -fvisibility=hidden -Wl,--no-entry,--export=fib \

-O3 -o out/fib-wasi.wasm

第一行和第三行其实没变,只是加了第二行指定导出的接口并添加其他的优化编译选项,具体每个字段的含义(我也不懂)我就不解释了,感兴趣的话自行搜索吧。

生成的包也很小,转成文本格式后代码如下:

800_auto 同样是内部定义了 fib 函数并 export 出来,同时还定义了 table 和 memory 而且把 memory 导出,table 是用来存放间接调用的函数表,memory 定义了初始内存大小,这个例子里并没用到,可删掉。 运行这个包的方式和上面一样,加上 --invoke 可以指定调用的接口,可以传递参数:

wasmtime out/fib-wasi.wasm --invoke fib 7

上面的命令会输出 13 。 另外,因为这个例子没有外部依赖,所以生成的包对环境没什么要求,上面用 Emscripten 生成的 fib-emcc.wasm 这个包,也是可以用 Wasmtime 来执行的,调用方法一致:

wasmtime out/fib-emcc.wasm --invoke _fib 13

参考https://zhuanlan.zhihu.com/p/260031610

我知道答案,我要回答