WebAssembly初体验

栏目: 后端 · 前端 · 发布时间: 5年前

内容简介:上篇介绍了WebAssembly的前生今世,这篇准备写几个例子试玩一下。首先肯定是先安装Emscripten,这是一个toolchain,可以把C/C++代码编译成asm.js和WASM字节码。内部其实分为三部分:写个HelloWorld:

上篇介绍了WebAssembly的前生今世,这篇准备写几个例子试玩一下。

1.安装Emscripten

首先肯定是先安装Emscripten,这是一个toolchain,可以把C/C++代码编译成asm.js和WASM字节码。内部其实分为三部分:

  • 调用编译器前端Clang把C/C++代码编译成LLVM字节码
  • 调用编译器后端Fastcomp把LLVM字节码编译成asm.js
  • 调用Binaryen的asm2wasm把asm.js转换成WASM字节码
    WebAssembly初体验 Emscripten的具体安装步骤如下:
# 下载Emscripten源码
git clone https://github.com/juj/emsdk.git
cd emsdk

# 安装最新版本
./emsdk install latest

# 使用最新版本
./emsdk activate latest

# 添加环境变量
source ./emsdk_env.sh

2.HelloWorld

写个HelloWorld:

#include <stdio.h>

int main(int argc, char** argv) {
    printf("Hello World!\n");
    return 0;
}

用下面的命令编译:

注:最新版本的Emscripten默认就会生成.wasm,不需要像以前那样指定“-s WASM=1”了

emcc hello.c -o index.html

编译后的目录文件结构如下:

WebAssembly初体验

需要注意的是,生成的index.html直接双击打开是无法运行的,必须运行一个本地的HTTP服务器。通过下面的命令安装http-server:

npm install http-server -g

然后,在当前目录下直接运行HTTP服务器:

WebAssembly初体验

最后通过URL访问该网页: http://127.0.0.1:8080/index.html

WebAssembly初体验

当然,如果你想把网页搞得更漂亮一点,还可以自定义HTML模版,在emsdk目录中有一个最小版本的HTML模版shell_minimal.html,可以在这个基础上进行修改,然后用下面的命令编译:

emcc hello.c -o index.html --shell-file shell_minimal.html

3.C代码调用Javascript

C代码里可以通过Emscripten中的 EM_ASM 这个宏调用Javascript代码,注意Javascript代码要放进大括号里。我们把上面的代码改一改,让它蹦一个弹窗:

#include <stdio.h>
#include <emscripten.h>

int main(int argc, char** argv) {
    EM_ASM({ alert("Hello!"); });
    return 0;
}

重新编译、运行的结果如下:

WebAssembly初体验

如果运行后发现代码修改没有生效,清理一下浏览器的缓存再重新加载。

4.Javascript调用C代码

Javascript也可以直接调用WebAssembly中定义的函数,需要使用Emscripten中的 ccall()函数 以及 EMSCRIPTEN_KEEPALIVE声明 (将你的函数添加到导出的函数列表中,默认只导出main())。

我们在前面的代码中增加一个自定义函数:

#include <stdio.h>
#include <emscripten.h>

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中添加一个按钮,用来调用我们的自定义函数:

<button class="mybutton">Run myFunction</button>

最后在</script>标签之前添加以下代码,响应按钮点击事件:

document.querySelector('.mybutton').addEventListener('click', function(){
  var result = Module.ccall('myFunction', // name of C function 
                             null, // return type
                             null, // argument types
                             null); // arguments
});

最终运行结果:

WebAssembly初体验

5.手写WebAssembly代码

如果你觉得不过瘾,或者觉得自动生成的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:把.wat转换成.wasm格式
  • wasm2wat:把.wasm转换成.wat格式
  • wasm-objdump:类似于objdump,查看wasm汇编指令
  • wasm-interp:一个基于栈的解释器,可以用来做执行验证
  • wasm2c:把.wasm转成C代码
  • wat-desugar:“去糖”工具,通过wasm2c生成的C代码是跟汇编代码1:1对应的,比较难懂,可通过该工具转换成优化之前的代码结构

我们先试试 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代码实现了。

6.Javascript和WebAssembly互相调用

首先介绍一下WebAssembly中的四大组件:

  • 模块(module):已经被编译为可执行机器码的WebAssembly二进制代码
  • 实例(instance):模块的一个实例化对象,一个模块可以有多个实例
  • 内存(Memory):一个可变大小的ArrayBuffer,无具体类型
  • 表格(Table):一个可变大小的包含引用类型(如函数)的带类型数组

模块 & 实例

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()函数:

WebAssembly初体验

重点是标红的那两句,首先声明导入内存对象,然后通过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必备的网站,大部分问题基本都能在上面找到答案,总有一款适合你:

参考:

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

或关注飞久微信公众号:
WebAssembly初体验

以上所述就是小编给大家介绍的《WebAssembly初体验》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

第三次工业革命

第三次工业革命

[美] 杰里米•里夫金(Jeremy Rifkin) / 张体伟 / 中信出版社 / 2012-5 / 45.00元

第一次工业革命使19世纪的世界发生了翻天覆地的变化 第二次工业革命为20世纪的人们开创了新世界 第三次工业革命同样也将在21世纪从根本上改变人们的生活和工作 在这本书中,作者为我们描绘了一个宏伟的蓝图:数亿计的人们将在自己家里、办公室里、工厂里生产出自己的绿色能源,并在“能源互联网”上与大家分享,这就好像现在我们在网上发布、分享消息一样。能源民主化将从根本上重塑人际关系,它将影响......一起来看看 《第三次工业革命》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具