随着WebAssembly技术的发展,asm.js的竞争力已经逐渐被削弱了,所以Emscripten进一步与Binaryen结合,形成基于LLVM构建的WebAssembly后端。现如今,我们已经可以通过Emscripten工具链直接构建Wasm应用了。
有兴趣了解WebAssembly与asm.js的可以参考以下资料:
阮一峰《asm.js和Emscripten入门教程》
胡子大哈翻译的《WebAssembly系列(一) 生动形象地介绍WebAssembly》
在Emscripten环境下,编译目标可分为asm.js与WebAssembly两种,前者生成一个js文件,后者则生成wasm文件以及对应的JS胶水代码。因为WebAssmbly是二进制格式的,体积小、执行率高,在兼容性允许的条件下,一般建议试用WebAssembly作为编译目标。
使用emcc编译C/C++代码的主要流程如下:
C/C++代码首先通过Clang编译成LLVM Bitcode,然后再根据不同的编译目标编译为asm.js或WebAssembly。
由于内部调用Clang,因此emcc支持绝大多数的Clang选项,比如-O、-g、-s OPTIONS=VALUE(这里是根据自己需求设置,比如编译为asm.js需要设置-s WASM=0)等。除此之外,为了适应Web环境,还增加了一些特有的选项,如–pre-js YOURFILE、–post-js YOURFILE等。
emcc所有的选项列表可以通过以下指令查询:
emcc --help
首先我们编写一个简单的demo,保存为hello.c/hello.cc:
#include
int main() {
printf("Hello World!");
}
因为emsdk不是安装在系统上的,所以编译操作都需要在emsdk文件夹下进行。建议在emsdk下新建一个文件夹,存放需要编译的文件或项目。
cd YourFolder
emcc hello.c -o hello.html
完成之后会在当前文件夹下生成hello.html、hello.js、hello.wasm三个文件。打开hello.html,可以看到默认的页面中显示“Hello World!”。如果想自定义html页面,可以只生成asm.js或者WebAssembly,然后在Web项目中引入使用即可。
Emscripten是默认生成wasm文件的,如果想使用asm.js,需要设置WASM项为0。
emcc hello.c -s WASM=0 -o hello.js
编译之后只生成hello.js。但当编译目标为asm.js时,未对齐的内存读写可能会出现异常。关于Emscripten内存对齐的问题可以参考以下资料:《C/C++面向WebAssembly编程》。
执行以下命令
emcc hello.c -o hello_wasm.js
编译之后生成hello.js与hello.wasm两个文件。其中hello.wasm是Web项目中使用的WebAssembly模块,而hello.js是对应的JS胶水代码,实现了将wasm文件引入,并导出对应的函数等。
在Web项目中引入asm.js或WebAssembly,C/C++代码中的main函数在加载之后会立即运行。Emscripten除了可以导出main函数,还可以导出其他C/C++函数,但是为了避免在编译时被优化器删除普通的C/C++函数,所以需要在函数前加EMSCRIPTEN_KEEPALIVE标识,提前告知编译器保留当前函数。为了方便导出,提供以下函数导出宏:
#ifndef EM_PORT_API
# if defined(__EMSCRIPTEN__)
# include
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
# else
# define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
# endif
# else
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype
# else
# define EM_PORT_API(rettype) rettype
# endif
# endif
#endif
修改之前的demo,保存为say.c,代码如下:
// 此处加上EM_PORT_API宏
#include
EM_PORT_API(void) say_hello() {
printf("Hello World!");
}
打开编译器,执行以下命令:
emcc say.c -o say.js
这里编译目标是WebAssembly,在Web项目中可以使用以下两种方式引入使用:
<html>
<head>
<meta charset="utf-8">
<title>Say Hellotitle>
head>
<body>
<script>
Module = {}
Module.onRuntimeInitialized = function() {
Module._say_hello();
}
script>
<script src="say.js">script>
body>
html>
在Module初始化前,向Module中注入一个名为onRuntimeInitialized的方法,Emscirpten中的Runtime就绪之后,就会回调这个方法。这里使用回调的原因是因为Emscripten生成的JS胶水代码中,是使用异步加载wasm文件的,所以需要通过该方法确认Runtime准备就绪后,才执行say_hello函数。
可以观察到,如果编译时生成了JS胶水代码,那么Module中对应的C/C++导出函数,在原函数名的基础上增加了下划线“_”前缀。
注意:JS胶水代码需要与wasm文件放在同一目录下。
使用require引入使用前,需要在JS胶水代码底部增加:
module.exports = Module;
在JS引入:
var MyModule = require("say.js")
MyModule.onRuntimeInitialized = function() {
MyModule._say_hello();
}