上篇介绍了WebAssembly的前生今世,这篇准备写几个例子试玩一下。
首先肯定是先安装Emscripten,这是一个toolchain,可以把C/C++代码编译成asm.js和WASM字节码。内部其实分为三部分:
# 下载Emscripten源码
git clone https://github.com/juj/emsdk.git
cd emsdk
# 安装最新版本
./emsdk install latest
# 使用最新版本
./emsdk activate latest
# 添加环境变量
source ./emsdk_env.sh
写个HelloWorld:
#include
int main(int argc, char** argv) {
printf("Hello World!\n");
return 0;
}
用下面的命令编译:
注:最新版本的Emscripten默认就会生成.wasm,不需要像以前那样指定“-s WASM=1”了
emcc hello.c -o index.html
编译后的目录文件结构如下:
需要注意的是,生成的index.html直接双击打开是无法运行的,必须运行一个本地的HTTP服务器。通过下面的命令安装http-server:
npm install http-server -g
然后,在当前目录下直接运行HTTP服务器:
最后通过URL访问该网页:http://127.0.0.1:8080/index.html
当然,如果你想把网页搞得更漂亮一点,还可以自定义HTML模版,在emsdk目录中有一个最小版本的HTML模版shell_minimal.html,可以在这个基础上进行修改,然后用下面的命令编译:
emcc hello.c -o index.html --shell-file shell_minimal.html
C代码里可以通过Emscripten中的EM_ASM这个宏调用Javascript代码,注意Javascript代码要放进大括号里。我们把上面的代码改一改,让它蹦一个弹窗:
#include
#include
int main(int argc, char** argv) {
EM_ASM({ alert("Hello!"); });
return 0;
}
重新编译、运行的结果如下:
如果运行后发现代码修改没有生效,清理一下浏览器的缓存再重新加载。
Javascript也可以直接调用WebAssembly中定义的函数,需要使用Emscripten中的ccall()函数以及EMSCRIPTEN_KEEPALIVE声明(将你的函数添加到导出的函数列表中,默认只导出main())。
我们在前面的代码中增加一个自定义函数:
#include
#include
void EMSCRIPTEN_KEEPALIVE myFunction() {
printf("Good Morning!\n");
}
int main(int argc, char** argv) {
printf("Hello World!\n");
return 0;
}
编译的时候要注意,由于ccall()函数默认是不导出的,需要加上一个编译选项:
emcc hello.c -o index.html -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall"]'
然后在生成的index.html中添加一个按钮,用来调用我们的自定义函数:
最后在标签之前添加以下代码,响应按钮点击事件:
document.querySelector('.mybutton').addEventListener('click', function(){
var result = Module.ccall('myFunction', // name of C function
null, // return type
null, // argument types
null); // arguments
});
如果你觉得不过瘾,或者觉得自动生成的wasm代码不够好,可以尝试手写WebAssembly代码。
我们可以先写一个文本格式的WebAssembly模块,一般以.wat或者.wast作为后缀名,然后通过工具把它转换成.wasm文件。比如我们实现一个计算阶乘的函数,保存成fac.wat:
(func (export "fac") (param $x i32) (result i32)
(if (result i32) (i32.eq (get_local $x) (i32.const 0))
(then (i32.const 1))
(else
(i32.mul (get_local $x) (call 0 (i32.sub (get_local $x) (i32.const 1))))
)
)
)
然后我们需要使用wabt工具进行转换,首先安装wabt:
git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
make
编译完成后会在bin目录中的生成一堆工具:
我们先试试wat2wasm工具:
wat2wasm fac.wat -o fac.wasm -v
会生成fac.wasm文件,同时因为加了-v选项,会在控制台上打印出对应的WASM指令。
我们可以再通过wasm2wat把.wasm转回.wat:
wasm2wat fac.wasm -o fac-flat.wat
打开生成的代码,你会发现跟之前的代码似乎略有不同:
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
get_local 0
i32.const 0
i32.eq
if (result i32) ;; label = @1
i32.const 1
else
get_local 0
get_local 0
i32.const 1
i32.sub
call 0
i32.mul
end)
(export "fac" (func 0)))
这种格式被称为“flat”格式,是针对基于栈的虚拟机的,你可以发现它跟之前控制台上打印的指令其实是1:1对应的。如果想还原成方便我们阅读的“folded”格式,则需使用wat-desugar工具:
wat-desugar fac-flat.wat --fold -o fac-folded.wat
查看新生成的fac-folded.wat,会发现就跟我们的原始代码相差无几了。
如何验证我们生成的WASM代码是否运行正常呢?可以通过wasm-interp工具。但是这个工具似乎不能接收参数,因此需要额外写一个无参数的函数调用我们的fac()函数:
(func $fac (param $x i32) (result i32)
(if (result i32) (i32.eq (get_local $x) (i32.const 0))
(then (i32.const 1))
(else
(i32.mul (get_local $x) (call 0 (i32.sub (get_local $x) (i32.const 1))))
)
)
)
(func (export "exported_fac") (result i32)
i32.const 5
call $fac
)
先通过wat2wasm转换成fac2.wasm,然后通过wasm-interp解释执行WASM代码:
wasm-interp fac2.wasm --run-all-exports
运行结果:exported_fac() => i32:120
最后,我们再尝试一下用wasm2c工具把.wasm文件转换成C代码:
wasm2c fac.wasm -o fac.c
转换完会生成一个fac.h和一个fac.c,fac.h里导出了2个函数:
extern void WASM_RT_ADD_PREFIX(init)(void);
extern u32 (*WASM_RT_ADD_PREFIX(Z_facZ_ii))(u32);
第一个init()函数会帮我们做一些注册和初始化的工作,另外一个fac()函数就是和我们刚刚看到的flat格式的WASM代码1:1对应的C代码实现了。
首先介绍一下WebAssembly中的四大组件:
Javascript要执行WebAssembly代码,首先需要下载.wasm文件,然后调用WebAssembly.instantiate()进行编译,生成模块和实例。一般会封装成以下函数方便代码复用:
function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}
返回的result中包含了module和instance两个对象,一般直接使用instance即可。如果有多个地方需要创建实例,可以考虑把module缓存起来,后面直接实例化,可以避免重复编译。
上面的函数里有个importObject参数,可以把Javascript中的对象传递到WASM中,比如传递一个函数对象:
var importObject = {
imports: {
imported_func: function(arg) {
console.log(arg);
}
}
};
然后WASM中用下面的方式声明一下,就可以使用了:
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i))
上面的代码还导出了一个函数exported_func,会保存在instance.exports中,在Javascript中可以通过以下方式调用:
fetchAndInstantiate("XXX.wasm", importObject).then(function(instance){
instance.exports.exported_func();
})
官方有一个示例来说明如何在Javascript和WASM之间共享内存:
var i32 = new Uint32Array(results.instance.exports.mem.buffer);
for (var i = 0; i < 10; i++) {
i32[i] = i;
}
var sum = results.instance.exports.accumulate(0, 10);
console.log(sum);
首先为instance.exports.mem.buffer对象创建了一个Uint32Array视图,方便赋值。然后调用WASM模块导出的accumulate()函数:
重点是标红的那两句,首先声明导入内存对象,然后通过i32.load每次从内存中读取4个字节。
表格是用来传递带类型的对象引用的,但是目前阶段只支持传递函数对象引用。比如下面的代码导出了一个表格,包含两个函数引用:
(module
(func $thirteen (result i32) (i32.const 13))
(func $fourtytwo (result i32) (i32.const 42))
(table (export "tbl") anyfunc (elem $thirteen $fourtytwo))
)
Javascript里用下面的方式可以调用表格中的WASM函数:
var tbl = results.instance.exports.tbl;
console.log(tbl.get(0)()); // 13
console.log(tbl.get(1)()); // 42
最后推荐几个WebAssembly必备的网站,大部分问题基本都能在上面找到答案,总有一款适合你:
WebAssembly官网:http://webassembly.org.cn/docs/js/
mozilla开发者:https://developer.mozilla.org/zh-CN/docs/WebAssembly
emscripten官网:https://kripken.github.io/emscripten-site/docs/
WebAssembly开发者指南:http://www.clipcode.net/training/clipcode-webassembly-devguide.pdf
参考:
http://kripken.github.io/emscripten-site/docs
https://www.cnblogs.com/saintlas/p/5738739.html
https://developer.mozilla.org/zh-CN/docs/WebAssembly/C_to_wasm
https://github.com/webassembly/wabt
更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号: