WebAssembly试玩

一.What?

WebAssembly or wasm is a new portable, size- and load-time-efficient format suitable for compilation to the web.

一种可移植,体积小且加载迅速的(二进制)格式,适用于编译到Web

主要目标是在Web环境支持高性能应用。但设计上不依赖Web特性,也不针对Web特性提供功能,也可以用在其它环境

简单理解,就是定义了一种编译目标格式,能在支持该格式的任何环境获得接近原生的执行性能。相当于允许扩展native模块,在苛求性能的场景,用其它更合适的语言(比如C++)来实现,再提前编译到WebAssembly形式,就能获得媲美native的性能体验

其设计目标分2方面:

  • 快速,安全和可移植的语义

    • 快速:以接近原生代码的性能执行,并利用所有现代硬件通用的功能

    • 安全:代码经过验证并在内存安全的沙盒环境中执行,防止数据损坏或安全违规

    • 定义良好:充分且精确地定义合法程序及其行为,以一种容易推断非正式与正式的方式

    • 独立于硬件:可在所有现代架构,台式机或移动设备以及嵌入式系统上进行编译

    • 独立于语言:不偏向任何特定语言,编程模型或对象模型

    • 独立于平台:可以嵌入到浏览器中,作为stand-alone VM运行,或者集成到其他环境中

    • 开放:程序能够以简单通用的方式与他们的环境交互

  • 高效、可移植的表示

    • 小巧:具有比典型文本或原生代码格式体积更小的二进制格式,能够快速传输

    • 模块化:程序可以拆分成较小的部分,可以单独传输,缓存和使用

    • 高效:可以在单趟(遍历)中快速对其进行解码,验证和编译,等同于实时(JIT)或提前(AOT)编译

    • 流式:允许在拿到所有数据之前,尽早开始解码、验证和编译

    • 可并行:允许将解码、验证和编译拆分成多个独立的并行任务

    • 可移植:对现代硬件上不受广泛支持的架构不做假设

由主流浏览器(Chrome, Edge, Firefox, and WebKit)合力推动其标准化进程:

WebAssembly is currently being designed as an open standard by a W3C Community Group that includes representatives from all major browsers.

P.S.这个事情由浏览器厂商牵头做(他们4个站在一起搞事情,很值得期待),只是顺便建立开放标准(不止面向Web环境),动力源自想要进一步提升JS运行时性能,在V8引入JIT之后,想要进一步提升性能已经不太可能了,因为面临JS语言特性方面的限制(比如解释型,弱类型)。Web能力越来越强大,客户端JS越来越重,进一步提升JS执行性能的需求仍在,所以才有了WebAssembly的釜底抽薪

二.wasm与wast

我们知道WebAssembly定义了一种二进制格式,这种格式就是wasm,例如:

0061 736d 0100 0000 0187 8080 8000 0160
027f 7f01 7f03 8280 8080 0001 0004 8480
8080 0001 7000 0005 8380 8080 0001 0001
0681 8080 8000 0007 9080 8080 0002 066d
656d 6f72 7902 0003 6763 6400 000a ab80
8080 0001 a580 8080 0001 017f 0240 2000
450d 0003 4020 0120 0022 026f 2100 2002
2101 2000 0d00 0b20 020f 0b20 010b 

这串十六进制数对应的C代码是:

// 辗转相除法求最大公约数
int gcd(int m, int n) {
    if (m == 0) return n;
    return gcd(n % m, m);
}

wasm的可读性等于0,为了缓解这个问题,就定义了一种可读性好一些的文本格式,叫wast

(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "gcd" (func $gcd))
 (func $gcd (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (block $label$0
   (br_if $label$0
    (i32.eqz
     (get_local $0)
    )
   )
   (loop $label$1
    (set_local $0
     (i32.rem_s
      (get_local $1)
      (tee_local $2
       (get_local $0)
      )
     )
    )
    (set_local $1
     (get_local $2)
    )
    (br_if $label$1
     (get_local $0)
    )
   )
   (return
    (get_local $2)
   )
  )
  (get_local $1)
 )
)

括号有点Lisp风格,但至少是可读的,例如:

// 导出了两个东西,分别叫`memory`和`gcd`
(export "memory" (memory $0))
(export "gcd" (func $gcd))
// 函数签名,接受2个int32类型参数,返回int32类型值
(func $gcd (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
// 函数体...就不猜了

P.S.wast与wasm能够互相转换,详细见WABT: The WebAssembly Binary Toolkit

另外,在浏览器的Source面板能够看到另一种文本指令:

func (param i32 i32) (result i32)
(local i32)
  block
    get_local 0
    i32.eqz
    br_if 0
    loop
      get_local 1
      get_local 0
      tee_local 2
      i32.rem_s
      set_local 0
      get_local 2
      set_local 1
      get_local 0
      br_if 0
    end
    get_local 2
    return
  end
  get_local 1
end

与wast长得很像,不知道有没有名字,或者也属于wast?这个是浏览器根据wasm转换出来的

三.试玩环境

环境要求:

  • C/C++编译环境Emscripten

  • 支持WebAssembly的浏览器(最新的Chrome默认支持)

在线环境

有无伤试玩环境:WebAssembly Explorer

COMPILEDOWNLOAD就能得到wasm,简直好用

注意,默认是C++环境,想用C的话,左侧选择C99或C89,否则函数名会被编坏,例如C++11的wast:

(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "_Z3gcdii" (func $_Z3gcdii))
 (func $_Z3gcdii (; 0 ;) (param $0 i32) (param $1 i32) (result i32)
  (local $2 i32)
  (block $label$0
   (br_if $label$0
    (i32.eqz
     (get_local $0)
    )
   )
   (loop $label$1
    (set_local $0
     (i32.rem_s
      (get_local $1)
      (tee_local $2
       (get_local $0)
      )
     )
    )
    (set_local $1
     (get_local $2)
    )
    (br_if $label$1
     (get_local $0)
    )
   )
   (return
    (get_local $2)
   )
  )
  (get_local $1)
 )
)

函数名被编成_Z3gcdii了,猜测是命名空间之类的东西在作怪,C++不太熟,乖乖用C

P.S.除了C/C++,其它语言也可以玩WebAssembly,比如Rust

本地环境

  1. 下载平台SDK

  2. 按照安装步骤来做

不出意外的话,到这里就装好了,可以emcc -v试一下:

INFO:root:(Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.37.22
clang version 4.0.0  (emscripten 1.37.22 : 1.37.22)
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: D:\emsdk-portable-64bit\clang\e1.37.22_64bit
INFO:root:(Emscripten: Running sanity checks)

在Windows环境可能会遇到一个DLL缺失(MSVCP140.dll)的报错,可以手动安装需要的C++环境,具体见MSVCP140.dll not found · Issue #5605 · kripken/emscripten

然后可以编一个试试(把之前的C代码保存成文件gcd.c):

emcc ./c/gcd.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o ./output/gcd.wasm

P.S.更多用法见Emscripten Tutorial

得到的gcd.wasm内容如下:

0061 736d 0100 0000 000c 0664 796c 696e
6b80 80c0 0200 010a 0260 027f 7f01 7f60
0000 0241 0403 656e 760a 6d65 6d6f 7279
4261 7365 037f 0003 656e 7606 6d65 6d6f
7279 0200 8002 0365 6e76 0574 6162 6c65
0170 0000 0365 6e76 0974 6162 6c65 4261
7365 037f 0003 0403 0001 0106 0b02 7f01
4100 0b7f 0141 000b 072b 0312 5f5f 706f
7374 5f69 6e73 7461 6e74 6961 7465 0002
0b72 756e 506f 7374 5365 7473 0001 045f
6763 6400 0009 0100 0a40 0327 0101 7f20
0004 4003 4020 0120 006f 2202 0440 2000
2101 2002 2100 0c01 0b0b 0520 0121 000b
2000 0b03 0001 0b12 0023 0024 0223 0241
8080 c002 6a24 0310 010b 

注意,方法名默认会被添上下划线(_)前缀,本例中导出的方法名为_gcd,具体见Interacting with code

The keys passed into mergeInto generate functions that are prefixed by _. In other words myfunc: function() {}, becomes function _myfunc() {}, as all C methods in emscripten have a _ prefix. Keys starting with $ have the $ stripped and no underscore added.

在JS中使用模块接口应该加上下划线(不知道有没有配置项能去掉它)

四.试玩

WebAssembly.compile(new Uint8Array(`
    0061 736d 0100 0000 0187 8080 8000 0160
    027f 7f01 7f03 8280 8080 0001 0004 8480
    8080 0001 7000 0005 8380 8080 0001 0001
    0681 8080 8000 0007 9080 8080 0002 066d
    656d 6f72 7902 0003 6763 6400 000a ab80
    8080 0001 a580 8080 0001 017f 0240 2000
    450d 0003 4020 0120 0022 026f 2100 2002
    2101 2000 0d00 0b20 020f 0b20 010b 
    `.match(/\S{2}/g).map(s => parseInt(s, 16))
)).then(module => {
    const instance = new WebAssembly.Instance(module);
    console.log(instance.exports);
    const { gcd } = instance.exports;
    console.log('gcd(328, 648)', gcd(328, 648));
});

其中十六进制串来自在线试玩,与最初的wasm示例内容一致。把这些东西粘到Chrome的Console执行就可以了,一切正常的话,会得到报错

VM40:1 Uncaught (in promise) CompileError: WasmCompile: Wasm code generation disallowed in this context

这是因为默认的CSP(内容安全策略)限制,很容易解决,开隐身模式(Ctrl/CMD + Shift + N)即可

会得到输出:

{memory: Memory, gcd: ƒ}
gcd(328, 648) 8

第一行是加载我们的WebAssembly得到的模块导出内容,包括一个内存对象和gcd方法,第二行输出就是调用高性能模块计算出的最大公约数

WebAssembly.compile等相关API可以参考:

另外,本地编译得到的版本要求imports env(而且函数名被添了下划线_前缀):

WebAssembly.compile(new Uint8Array(`
    0061 736d 0100 0000 000c 0664 796c 696e
    6b80 80c0 0200 010a 0260 027f 7f01 7f60
    0000 0241 0403 656e 760a 6d65 6d6f 7279
    4261 7365 037f 0003 656e 7606 6d65 6d6f
    7279 0200 8002 0365 6e76 0574 6162 6c65
    0170 0000 0365 6e76 0974 6162 6c65 4261
    7365 037f 0003 0403 0001 0106 0b02 7f01
    4100 0b7f 0141 000b 072b 0312 5f5f 706f
    7374 5f69 6e73 7461 6e74 6961 7465 0002
    0b72 756e 506f 7374 5365 7473 0001 045f
    6763 6400 0009 0100 0a40 0327 0101 7f20
    0004 4003 4020 0120 006f 2202 0440 2000
    2101 2002 2100 0c01 0b0b 0520 0121 000b
    2000 0b03 0001 0b12 0023 0024 0223 0241
    8080 c002 6a24 0310 010b 
    `.match(/\S{2}/g).map(s => parseInt(s, 16))
)).then(module => {
    let imports = {
        env: {
            memoryBase: 0,
            memory: new WebAssembly.Memory({ initial: 256 }),
            tableBase: 0,
            table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
        }
    };
    const instance = new WebAssembly.Instance(module, imports);
    console.log(instance.exports);
    // 注意下划线前缀
    const { _gcd } = instance.exports;
    console.log('gcd(328, 648)', _gcd(328, 648));
});

可以得到类似输出:

{__post_instantiate: ƒ, runPostSets: ƒ, _gcd: ƒ}
gcd(328, 648) 8

应该是Emscripten默认添了一些无关紧要的东西,功能上与我们的简版是等价的

五.优缺点及应用场景

优势

  • 代码体积很小

    300k左右(压缩后)JavaScript 逻辑改用WebAssembly重写后,体积仅有90k左右

    但使用WebAssembly需要引入一个50k-100k的JavaScript类库作为基础设施

  • 安全性稍有提升

    虽然源码对应的WebAssembly文本指令仍然毫无遮掩,但逆向成本高了一些

  • 性能提升

    理论上WebAssembly拥有接近native的执行性能,因为跳过了解释环节,并且文件体积在传输方面也有优势

    当然,前提是在业务代码量很大,且要求极致性能的场景,在benchmark等重复执行的场景,JIT并不比AOT慢多少

缺点

目前能力有限

  • 仅支持几种基本数据类型(i32 / i64 / f32 / f64 / i8 / i16)

  • 无法直接访问DOM和其它Web API

  • 无法控制GC

应用场景

WebAssembly为浏览器定义了一种标准可执行二进制格式,这样更多的开发者都能通过统一的编译机制参与进来,共建繁荣的Web生态,愿景是美好的,但面临一些实际问题

首先WebAssembly的初衷是“在Web环境支持高性能应用”,为了突破性能瓶颈,那么可能的应用场景是:

  • 视频解码

  • 图像处理

  • 3D/WebVR/AR可视化

  • 渲染引擎

  • 物理引擎

  • 压缩/加密算法

  • …等运算量比较大的场景

当然,些支持将来也可能会都内置到浏览器里,而不用通过“扩展插件”之类的方式来做。但WebAssembly的真正意义是提供了一种允许自行扩展高性能“native”模块的能力,毕竟等浏览器提供,再等到兼容性可接受可能需要相当长的一段时间,而有了这种能力之后,不用再苦苦等待市场主流浏览器都支持某个原生特性了,自己动手就搞定了,而且不存在兼容性差异。反过来,可能涌现出一批受欢迎的社区模块,并逐步被吸纳作为浏览器原生支持,生态回馈Web环境

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code