6

1、WebAssembly工作原理

分点介绍

官方解读

它可以从各类现有的其他高级语言写的业务库编译而来,比如下文提到的bullet库,就是一种C++语言编写的刚体动力学与
碰撞检测计算的库。根据调研,还有Haskell、Go、C#的语言的一些WebAssembly编译工具或者已经编译成的WebAssembly代码库,
OK,既然是经过编译而得来,可以将WebAssembly理解为是该库的低级语言代码版本,是一种类汇编语言。

另类理解

可以把它理解成一个ES6语法写的js模块,既可以有导入又有导出,也可以没有导入只有导出。

两类文件

WebAssembly文件格式与源码阅读->.wasm文件和.wast文件

WebAssembly代码存储在.wasm文件内,这类文件是要浏览器直接执行的。
因为.wasm文件内是二进制文件,难以阅读,为了方便开发者查看,官方给出了对.wasm文件的阅读方法,
通过把.wasm文件通过工具转为.wast的文本格式,开发者可以在一定程度上理解这个.wast文件。
.wast文件是通过S-表达式(一种类似lisp语言的代码书写风格)来写成的,
至于怎么读懂S-表达式,请去看官方介绍。
.wast文件和.wasm文件的关系,他们之间的相互转化,可以通过工具wabt(https://github.com/WebAssembl...
实现。

工作流程

某高级语言写的某功能库-->emscripten编译-->.wasm文件-->结合WebAssembly JS API-->浏览器中运行
完成一部分 用js写,而后依靠浏览器解释执行,会比较消耗性能 的工作,比如视频解码,OpenGL,OpenCV等。
简单来说,加载运行wasm代码的过程如下图所示。
<div align="center">
<img src="https://github.com/cunzaizhuy...;>
</div>
详细的过程以及每个过程调用的API如下图。
<div align="center">
<img src="https://github.com/cunzaizhuy...;>
</div>

2、WebAssembly工具集

emscripten:是基于LLVM的一系列编译工具的集合,包括LLVM,clang等。下载比较费时,且易出错。
该工具集的一大作用是将c/c++编写的库编译成wasm格式的代码。
使用方式是通过命令行进行命令操作。

目前来说,WebAssembly程序的工作方式是和js程序相结合,互相调用,所以将合适的其他语言的库编译移植到web的过程,算是开发
中的相对独立的一块工作,正好emscripten工具也是命令行方式来工作。当然如果移植库需要开发者自己开发,就不算
独立,不过这脱离写前端的范畴。
真正开发时,更多的是直接拿已编译好的现成的移植代码加载到js代码中,来开发。

开发带有WebAssembly的程序需要开发者具备使用移植代码库的API使用能力或者说阅读其他语言代码的能力。

Binaryen:一套更为全面的工具链,是用C++编写成用于WebAssembly的编译器和工具链基础结构库。
WebAssembly是二进制格式(Binary Format)并且和Emscripten集成,因此该工具以Binary和Emscript-en的末
尾合并命名为Binaryen。它旨在使编译WebAssembly容易、快速、有效。

WABT工具包:支持将二进制WebAssembly格式转换为可读的文本格式。其中wasm2wast命令行工具
可以将WebAssembly二进制文件转换为可读的S表达式文本文件。而wast2wasm命令行工具则执行完全相反的过程。

3、WebAssembly API一览

一级API 二级API 描述
table() length、set()、get()、grow() 方法
memory() buffer、grow()
instantiate()
instance 属性
module 对象
compile()
validate() bool
CompileError()
LinkError()
RuntimeError()

WebAssembly.Mudule和WebAssembly.compile()

都是用来把一个wasm的arraybuffer对象编译成一个模块,前者是同步的,后者是异步的,后者使用更多
前者使用方式:new WebAssembly.Mudule(buffer);后者使用方式:WebAssembly.compile(buffer);
WebAssembly.Mudule本身也是抽象意义上的模块对象。这两种方式调用以后,返回值都是一个模块对象,该对象
有导入对象、导出对象和自定义片段(custom section)。

WebAssembly.Instance和WebAssembly.instantiate()

都是用来做实例化,前者是同步的,后者是异步的,后者使用更多
前者使用方式:new WebAssembly.Instance();后者使用方式:WebAssembly.instantiate();
前者有两个重载,一个是传入buffer和imports对象,这种调用一次性完成了编译和实例化两个步骤,
第二个重载是传模块对象和imports对象,这种调用只完成实例化步骤。
因此,实际上WebAssembly.instantiate()和WebAssembly.Instance的第二张重载调用功能上更接近。

4、一个简单Demo

一段c语言代码

int add (int x, int y) {
  return x + y;
}

int square (int x) {
  return x * x;
}

通过emcc工具编译为.wasm文件,
编译命令:

emcc input.c -s WASM=1 -s SIDE_MODULE=1 -o out.js

上述命令运行后,我们可以得到独立的Wasm文件。如果想看懂这个wasm文件,可以将其装换为wast文本格式,
使用上面介绍的工具WABT工具包https://github.com/WebAssembl...
使用这个工具需要安装
cmake自己build一下,生成相应的可执行程序,然后用命令行wasm2wast test.wasm -o test.wast就可以查看
test.wast了。下面是上面c代码生成的wasm的wast对应文件。

S-表达式形式的wast文件
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (type (;1;) (func (param i32) (result i32)))
  (type (;2;) (func))
  (import "env" "memoryBase" (global (;0;) i32))
  (import "env" "memory" (memory (;0;) 256))
  (import "env" "table" (table (;0;) 0 anyfunc))
  (import "env" "tableBase" (global (;1;) i32))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    get_local 1
    get_local 0
    i32.add)
  (func (;1;) (type 1) (param i32) (result i32)
    get_local 0
    get_local 0
    i32.mul)
  (func (;2;) (type 2)
    nop)
  (func (;3;) (type 2)
    block  ;; label = @1
      get_global 0
      set_global 2
      get_global 2
      i32.const 5242880
      i32.add
      set_global 3
      call 2
    end)
  (global (;2;) (mut i32) (i32.const 0))
  (global (;3;) (mut i32) (i32.const 0))
  (export "_add" (func 0))
  (export "__post_instantiate" (func 3))
  (export "_square" (func 1))
  (export "runPostSets" (func 2)))

得到wasm文件后,就可以使用js加载该模块,实例化该模块,运行该模块中的函数。

<script>

    function loadWebAssembly (path, imports = {}) {
        return fetch(path)
            .then(response => response.arrayBuffer())
            .then(buffer => WebAssembly.compile(buffer))
            .then(module => {
                imports.env = imports.env || {}

                // 开辟内存空间
                imports.env.memoryBase = imports.env.memoryBase || 0
                if (!imports.env.memory) {
                    imports.env.memory = new WebAssembly.Memory({ initial: 256 })
                }

                // 创建变量映射表
                imports.env.tableBase = imports.env.tableBase || 0
                if (!imports.env.table) {
                    // 在 MVP 版本中 element 只能是 "anyfunc"
                    imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
                }
                // 创建 WebAssembly 实例
                return new WebAssembly.Instance(module, imports)
            })
    }

    loadWebAssembly('./math.wasm',imports)
        .then(instance => {
            //const { add, square } = instance.exports;
            const add = instance.exports._add;
            const square = instance.exports._square;
            // ...

            console.log(add(5,5));
            console.log(square(add(5,5)));
        });
</script>

如上,通过js调用这两个c语言方法,浏览器运行,控制台打印出正确结果。

5、基于WebAssembly模块库ammo.js的Demo

Demo介绍

基于three.js构建了三维场景,场景中有一个图片纹理拼成的ground地面,和两个THREE.Mesh()方法创建的
球体,这两个球体在地面上一左一右有固定的位置。

然后使用ammo构建了一个刚体动力学环境,这是一个有重力、考虑物体惯性等的物理环境,在这个环境中创建了
一个球体(界面中不可见),给该球体设置了一些刚体动力学的参数,如平移、旋转等,设置完这些参数再使用相反的
API获取这些参数,然后把这些参数赋给three.js创建的第二个球体(图1中右边那个),一秒后重新渲染threejs场景,该球体
则获得了一个平移的参数,移动到相应的(本例中是更靠右)的位置。

图1 使用ammo库前

图2 调用ammo相关代码后

Demo源代码地址

https://github.com/cunzaizhuy...

如需测试使用,请注意替换掉以下两行

<script src="../../builds/ammo.js"></script>
<script src="../js/three/three.min.js"></script>

本Demo参考链接

(1)Bullet类库API http://bulletphysics.org/Bull...

(2)Ammo库地址 https://github.com/kripken/am...

6、WebAssembly资源推荐

(1)英文官网 http://webassembly.org/

(2)中文官网 http://webassembly.org.cn/

(3)MDN网址 https://developer.mozilla.org...

(4)资料齐全 https://github.com/mbasso/awe...

(5)一篇讲解详细的博客 https://segmentfault.com/a/11...

(6)一篇讲解详细的博客 https://segmentfault.com/a/11...

(7)有编译工具链简单介绍 http://geek.csdn.net/news/det...

(0)本篇博客中的一些资源 https://github.com/cunzaizhuy...


飞叶_前端
1.4k 声望139 粉丝

Wasm和emscripten技术交流群:939206522