悄悄掀起 WebAssembly 的神秘面纱

2019-05-02 作者:计算机教程   |   浏览(57)

悄悄掀起 WebAssembly 的神秘面纱

2018/09/05 · JavaScript · webassembly

原文出处: WebAssembly   

www.2003.com 1

前端开发人员想必对现代浏览器都已经非常熟悉了吧?HTML5,CSS4,JavaScript ES6,这些已经在现代浏览器中慢慢普及的技术为前端开发带来了极大的便利。

得益于 JIT(Just-in-time)技术,JavaScript 的运行速度比原来快了 10 倍,这也是 JavaScript 被运用得越来越广泛的原因之一。但是,这是极限了吗?

随着浏览器技术的发展,Web 游戏眼看着又要“卷土重来”了,不过这一次不是基于 Flash 的游戏,而是充分利用了现代 HTML5 技术实现。JavaScript 成为了 Web 游戏的开发语言,但是对于游戏这样需要大量运算的程序来说,即便是有 JIT 加持,JavaScript 的性能还是不能满足人类贪婪的欲望。

简介

JS于1995年问世,设计的初衷不是为了执行起来快。直到08年性能大战中,许多浏览器引入了即时编译 JIT(just-in-time编译器),JavaScript 代码的运行渐渐变快。正是由于这些 JIT 的引入,使得 JavaScript 的性能达到了一个转折点,JS 代码执行速度快了 20 – 50倍。

JIT 是使 JavaScript 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 JavaScript 应用的性能提升很多倍。

www.2003.com 2

随着性能的提升,JavaScript 可以应用到以前根本没有想到过的领域,比如用于后端开发的 Node.js。性能的提升使得 JavaScript 的应用范围得到很大的扩展。

JavaScript的无类型是JavaScript引擎的性能瓶颈之一,在过去几年,我们看到越来越多的项目问世,它们试图通过开发编译程序,将其他语言代码转化为 JavaScript,以此让开发者克服 JavaScript 自身存在的一些短板。其中一些项目专注于给编程语言增加新的功能,比如微软的 TypeScript 和 Google 的 Dart,【设计一门新的强类型语言并强制开发者进行类型指定】或是加快 JavaScript 的执行速度,例如 Mozilla 的 asm.js 项目和Google的PNaCI【给现有的JavaScript加上变量类型】。

现在通过 WebAssembly,我们很有可能正处于第二个拐点。

www.2003.com 3

什么是webAssembly?

WebAssembly是一种新的适合于编译到Web的,可移植的,大小和加载时间高效的格式,是一种新的字节码格式。它的缩写是”.wasm”,.wasm 为文件名后缀,是一种新的底层安全的“二进制”语法。它被定义为“精简、加载时间短的格式和执行模型”,并且被设计为Web 多编程语言目标文件格式。

这意味着浏览器端的性能会得到极大提升,它也使得我们能够实现一个底层构建模块的集合.

webAssembly的优势

webassembly相较于asm.js的优势主要是涉及到性能方面。根据WebAssembly FAQ的描述:在移动设备上,对于很大的代码库,asm.js仅仅解析就需要花费20-40秒,而实验显示WebAssembly的加载速度比asm.js快了20倍,这主要是因为相比解析 asm.js 代码,JavaScript 引擎破译二进制格式的速度要快得多。

主流的浏览器目前均支持webAssembly。

Safari 支持 WebAssembly的第一个版本是11 Edge 支持 WebAssembly的第一个版本是16 Firefox 支持 WebAssembly的第一个版本是 52 chrome 支持 WebAssembly的第一个版本是 57

使用WebAssembly,我们可以在浏览器中运行一些高性能、低级别的编程语言,可用它将大型的C和C 代码库比如游戏、物理引擎甚至是桌面应用程序导入Web平台。

JavaScript 在浏览器中是怎么跑起来的?

对于现在的计算机来说,它们只能读懂“机器语言”,而人类的大脑能力有限,直接编写机器语言难度有点大,为了能让人更方便地编写程序,人类发明了大量的“高级编程语言”,JavaScript 就属于其中特殊的一种。

为什么说是特殊的一种呢?由于计算机并不认识“高级编程语言”写出来的东西,所以大部分“高级编程语言”在写好以后都需要经过一个叫做“编译”的过程,将“高级编程语言”翻译成“机器语言”,然后交给计算机来运行。但是,JavaScript 不一样,它没有“编译”的过程,那么机器是怎么认识这种语言的呢?

实际上,JavaScript 与其他一部分脚本语言采用的是一种“边解释边运行”的姿势来运行的,将代码一点一点地翻译给计算机。

那么,JavaScript 的“解释”与其他语言的“编译”有什么区别呢?不都是翻译成“机器语言”吗?简单来讲,“编译”类似于“全文翻译”,就是代码编写好后,一次性将所有代码全部编译成“机器语言”,然后直接交给计算机;而“解释”则类似于“实时翻译”,代码写好后不会翻译,运行到哪,翻译到哪。

“解释”和“编译”两种方法各有利弊。使用“解释”的方法,程序编写好后就可以直接运行了,而使用“编译”的方法,则需要先花费一段时间等待整个代码编译完成后才可以执行。这样一看似乎是“解释”的方法更快,但是如果一段代码要执行多次,使用“解释”的方法,程序每次运行时都需要重新“解释”一遍,而“编译”的方法则不需要了。这样一看,“编译”的整体效率似乎更高,因为它永远只翻译一次,而“解释”是运行一次翻译一次。并且,“编译”由于是一开始就对整个代码进行的,所以可以对代码进行针对性的优化。

JavaScript 是使用“解释”的方案来运行的,这就造成了它的效率低下,因为代码每运行一次都要翻译一次,如果一个函数被循环调用了 10 次、100 次,这个执行效率可想而知。

好在聪明的人类发明了 JIT(Just-in-time)技术,它综合了“解释”与“编译”的优点,它的原理实际上就是在“解释”运行的同时进行跟踪,如果某一段代码执行了多次,就会对这一段代码进行编译优化,这样,如果后续再运行到这一段代码,则不用再解释了。

JIT 似乎是一个好东西,但是,对于 JavaScript 这种动态数据类型的语言来说,要实现一个完美的 JIT 非常难。为什么呢?因为 JavaScript 中的很多东西都是在运行的时候才能确定的。比如我写了一行代码:const sum = (a, b, c) => a b c;,这是一个使用 ES6 语法编写的 JavaScript 箭头函数,可以直接放在浏览器的控制台下运行,这将声明一个叫做 sum 的函数。然后我们可以直接调用它,比如:console.log(sum(1, 2, 3)),任何一个合格的前端开发人员都能很快得口算出答案,这将输出一个数字 6。但是,如果我们这样调用呢:console.log(sum('1', 2, 3)),第一个参数变成了一个字符串,这在 JavaScript 中是完全允许的,但是这时得到的结果就完全不同了,这会导致一个字符串和两个数字进行连接,得到 "123"。这样一来,针对这一个函数的优化就变得非常困难了。

虽说 JavaScript 自身的“特性”为 JIT 的实现带来了一些困难,但是不得不说 JIT 还是为 JavaScript 带来了非常可观的性能提升。

系统">开发前准备工作(MAC系统)

1.安装 cmake brew install cmake

2.安装 pyhton brew insatll python

3.安装 Emscripten (调整下电脑的休眠时间,不要让电脑进入休眠,安装时间较长)

安装步骤如下:

git clone https://github.com/juj/emsdk.git

cd emsdk

./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit

./emsdk activate --global --build=Release sdk-incoming

    -64bit binaryen-master-64bit

执行 source ./emsdk_env.sh,并将shell中的内容添加到环境变量中(~/.bash_profile):

执行: source ~/.bash_profile

4.安装 WABT(将.wast文件转成 .wasm文件)

git clone https://github.com/WebAssembly/wabt.git

cd wabt

make install gcc-release

5.浏览器设置

Chrome: 打开 chrome://flags/#enable-webassembly,选择 enable。

Firefox: 打开 about:config 将 javascript.options.wasm 设置为 true。

如果浏览器太旧,请更新浏览器,或者安装激进版浏览器来体验新技术。

6.一个本地web服务器.

Emscripten,它基于 LLVM ,可以将 C/C 编译成 asm.js,使用 WASM 标志也可以直接生成 WebAssembly 二进制文件(后缀是 .wasm)

www.2003.com 4

         Emscripten

source.c   ----->  target.js



     Emscripten (with flag)

source.c   ----->  target.wasm

注:emcc 在 1.37 以上版本才支持直接生成 wasm 文件

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

www.2003.com 5

wasm-as:将WebAssembly由文本格式编译成二进制格式; wasm-dis:将二进制格式的WebAssembly反编译成文本格式; asm2wasm:将asm.js编译到WebAssembly文本格式,使用Emscripten的asm优化器; s2wasm:在LLVM中开发,由新WebAssembly后端产生的.s格式的编译器; wasm.js:包含编译为JavaScript的Binaryen组件,包括解释器、asm2wasm、S表达式解析器等。

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

wat2wasm: webAssembly文本格式转换为webAssembly二进制格式(.wast 到 .wasm) wasm2wat: 将WebAssembly二进制文件转换为可读的S表达式文本文件(.wat) wasm-objdump: print information about a wasm binary. Similiar to objdump. wasm-interp: 基于堆栈式解释器解码和运行webAssembly二进制文件 wat-desugar: parse .wat text form as supported by the spec interpreter wasm-link: simple linker for merging multiple wasm files. wasm2c: 将webAssembly二进制文件转换为C的源文件

WebAssembly

为了能让代码跑得更快,WebAssembly 出现了(并且现在主流浏览器也都开始支持了),它能够允许你预先使用“编译”的方法将代码编译好后,直接放在浏览器中运行,这一步就做得比较彻底了,不再需要 JIT 来动态得进行优化了,所有优化都可以在编译的时候直接确定。

WebAssembly 到底是什么呢?

首先,它不是直接的机器语言,因为世界上的机器太多了,它们都说着不同的语言(架构不同),所以很多情况下都是为各种不同的机器架构专门生成对应的机器代码。但是要为各种机器都生成的话,太复杂了,每种语言都要为每种架构编写一个编译器。为了简化这个过程,就有了“中间代码(Intermediate representation,IR)”,只要将所有代码都翻译成 IR,再由 IR 来统一应对各种机器架构。

实际上,WebAssembly 和 IR 差不多,就是用于充当各种机器架构翻译官的角色。WebAssembly 并不是直接的物理机器语言,而是抽象出来的一种虚拟的机器语言。从 WebAssembly 到机器语言虽说也需要一个“翻译”过程,但是在这里的“翻译”就没有太多的套路了,属于机器语言到机器语言的翻译,所以速度上已经非常接近纯机器语言了。

这里有一个 WebAssembly 官网上提供的 Demo,是使用 Unity 开发并发布为 WebAssembly 的一个小游戏:https://webassembly.org/demo/,可以去体验体验。

webAssembly的方法

.wasm 文件 与 .wat 文件

WebAssembly 是通过 *.wasm 文件进行存储的,这是编译好的二进制文件,它的体积非常的小。

在浏览器中,提供了一个全局的 window.WebAssembly 对象,可以用于实例化 WASM 模块。

www.2003.com 6

WebAssembly 是一种“虚拟机器语言”,所以它也有对应的“汇编语言”版本,也就是 *.wat 文件,这是 WebAssembly 模块的文本表示方法,采用“S-表达式(S-Expressions)”进行描述,可以直接通过工具将 *.wat 文件编译为 *.wasm 文件。熟悉 LISP 的同学可能对这种表达式语法比较熟悉。

webAssembly.validate

webAssembly.validate() 方法验证给定的二进制代码的 typed array 是否是合法的wasm module.返回布尔值。

WebAssembly.validate(bufferSource);

使用

javascript
fetch('xxx.wasm').then(response =>
response.arrayBuffer()
).then(function(bytes) {
var valid = WebAssembly.validate(bytes); //true or false
});

一个非常简单的例子

我们来看一个非常简单的例子,这个已经在 Chrome 69 Canary 和 Chrome 70 Canary 中测试通过,理论上可以在所有已经支持 WebAssembly 的浏览器中运行。(在后文中有浏览器的支持情况)

首先,我们先使用 S-表达式 编写一个十分简单的程序:

;; test.wat (module (import "env" "mem" (memory 1)) ;; 这里指定了从 env.mem 中导入一个内存对象 (func (export "get") (result i32) ;; 定义并导出一个叫做“get”的函数,这个函数拥有一个 int32 类型的返回值,没有参数 memory.size)) ;; 最终返回 memory 对象的“尺寸”(单位为“页”,目前规定 1 页 = 64 KiB = 65536 Bytes)

1
2
3
4
5
;; test.wat
(module
  (import "env" "mem" (memory 1)) ;; 这里指定了从 env.mem 中导入一个内存对象
  (func (export "get") (result i32)  ;; 定义并导出一个叫做“get”的函数,这个函数拥有一个 int32 类型的返回值,没有参数
    memory.size))  ;; 最终返回 memory 对象的“尺寸”(单位为“页”,目前规定 1 页 = 64 KiB = 65536 Bytes)

可以使用 wabt 中的 wasm2wat 工具将 wasm 文件转为使用“S-表达式”进行描述的 wat 文件。同时也可以使用 wat2wasm 工具将 wat 转为 wasm。

在 wat 文件中,双分号 ;; 开头的内容都是注释。

上面这个 wat 文件定义了一个 module,并导入了一个内存对象,然后导出了一个叫做“get”的函数,这个函数返回当前内存的“尺寸”。

在 WebAssembly 中,线性内存可以在内部直接定义然后导出,也可以从外面导入,但是最多只能拥有一个内存。这个内存的大小并不是固定的,只需要给一个初始大小 initial,后期还可以根据需要调用 grow 函数进行扩展,也可以指定最大大小 maximum(这里所有内存大小的单位都是“页”,目前规定的是 1 页 = 64 KiB = 65536 Bytes。)

上面这个 wat 文件使用 wat2wasm 编译为 wasm 后生成的文件体积非常小,只有 50 Bytes:

$ wat2wasm test.wat $ xxd test.wasm 00000000: 0061 736d 0100 0000 0105 0160 0001 7f02 .asm.......`.... 00000010: 0c01 0365 6e76 036d 656d 0200 0103 0201 ...env.mem...... 00000020: 0007 0701 0367 6574 0000 0a06 0104 003f .....get.......? 00000030: 000b ..

1
2
3
4
5
6
$ wat2wasm test.wat
$ xxd test.wasm
00000000: 0061 736d 0100 0000 0105 0160 0001 7f02  .asm.......`....
00000010: 0c01 0365 6e76 036d 656d 0200 0103 0201  ...env.mem......
00000020: 0007 0701 0367 6574 0000 0a06 0104 003f  .....get.......?
00000030: 000b                                     ..

为了让这个程序能在浏览器中运行,我们还必须使用 JavaScript 编写一段“胶水代码(glue code)”,以便这个程序能被加载到浏览器中并执行:

// main.js const file = await fetch('./test.wasm'); const memory = new window.WebAssembly.Memory({ initial: 1 }); const mod = await window.WebAssembly.instantiateStreaming(file, { env: { mem: memory, }, }); let result; result = mod.instance.exports.get(); // 调用 WebAssembly 模块导出的 get 函数 console.log(result); // 1 memory.grow(2); result = mod.instance.exports.get(); // 调用 WebAssembly 模块导出的 get 函数 console.log(result); // 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.js
 
const file = await fetch('./test.wasm');
const memory = new window.WebAssembly.Memory({ initial: 1 });
const mod = await window.WebAssembly.instantiateStreaming(file, {
  env: {
    mem: memory,
  },
});
let result;
result = mod.instance.exports.get();  // 调用 WebAssembly 模块导出的 get 函数
console.log(result);  // 1
memory.grow(2);
result = mod.instance.exports.get();  // 调用 WebAssembly 模块导出的 get 函数
console.log(result);  // 3
 

这里我使用了现代浏览器都已经支持的 ES6 语法,首先,使用浏览器原生提供的 fetch 函数加载我们编译好的 test.wasm 文件。注意,这里根据规范,HTTP 响应的 Content-Type 中指定的 MIME 类型必须为 application/wasm

接下来,我们 new 了一个 WebAssembly.Memory 对象,通过这个对象,可以实现 JavaScript 与 WebAssembly 之间互通数据。

再接下来,我们使用了 WebAssembly.instantiateStreaming 来实例化加载的 WebAssembly 模块,这里第一个参数是一个 Readable Stream,第二个参数是 importObject,用于指定导入 WebAssembly 的结构。因为上面的 wat 代码中指定了要从 env.mem 导入一个内存对象,所以这里就得要将我们 new 出来的内存对象放到 env.mem 中。

WebAssembly 还提供了一个 instantiate 函数,这个函数的第一个参数可以提供一个 ArrayBuffer 或是 TypedArray。但是这个函数是不推荐使用的,具体原因做过流量代理转发的同学可能会比较清楚,这里就不具体解释了。

最后,我们就可以调用 WebAssembly 导出的函数 get 了,首先输出的内容为 memoryinitial 的值。然后我们调用了 memory.grow 方法来增长 memory 的尺寸,最后输出的内容就是增长后内存的大小 1 2 = 3

webAssembly.Module

WebAssembly.Module() 构造函数可以用来同步编译给定的 WebAssembly 二进制代码。不过,获取 Module 对象的主要方法是通过异步编译函数,如 WebAssembly.compile(),或者是通过 IndexedDB 读取 Module 对象.

var myInstance = new WebAssembly.Instance(module, importObject);

module: 需要被实例化的webAssembly module importObject: 需要导入的变量

一个 WebAssembly 与 JavaScript 数据互通交互的例子

在 WebAssembly 中有一块内存,这块内存可以是内部定义的,也可以是从外面导入的,如果是内部定义的,则可以通过 export 进行导出。JavaScript 在拿到这块“内存”后,是拥有完全操作的权利的。JavaScript 使用 DataViewMemory 对象进行包装后,就可以使用 DataView 下面的函数对内存对象进行读取或写入操作。

这里是一个简单的例子:

;; example.wat (module (import "env" "mem" (memory 1)) (import "js" "log" (func $log (param i32))) (func (export "example") i32.const 0 i64.const 8022916924116329800 i64.store (i32.store (i32.const 8) (i32.const 560229490)) (call $log (i32.const 0))))

1
2
3
4
5
6
7
8
9
10
;; example.wat
(module
  (import "env" "mem" (memory 1))
  (import "js" "log" (func $log (param i32)))
  (func (export "example")
    i32.const 0
    i64.const 8022916924116329800
    i64.store
    (i32.store (i32.const 8) (i32.const 560229490))
    (call $log (i32.const 0))))

这个代码首先从 env.mem 导入一个内存对象作为默认内存,这和前面的例子是一样的。

然后从 js.log 导入一个函数,这个函数拥有一个 32 位整型的参数,不需要返回值,在 wat 内部被命名为“$log”,这个名字只存在于 wat 文件中,在编译为 wasm 后就不存在了,只存储一个偏移地址。

后面定义了一个函数,并导出为“example”函数。在 WebAssembly 中,函数里的内容都是在栈上的。

首先,使用 i32.const 0 在栈内压入一个 32 位整型常数 0,然后使用 i64.const 8022916924116329800 在栈内压入一个 64 位整型常数 8022916924116329800,之后调用 i64.store 指令,这个指令将会将栈顶部第一个位置的一个 64 位整数存储到栈顶部第二个位置指定的“内存地址”开始的连续 8 个字节空间中。

TL; DR; 简而言之,就是在内存的第 0 个位置开始的连续 8 个字节的空间里,存入一个 64 位整型数字 8022916924116329800。这个数字转为 16 进制表示为:0x 6f 57 20 6f 6c 6c 65 48,但是由于 WebAssembly 中规定字节序是使用“小端序(Little-Endian Byte Order)”来存储数据,所以,在内存中第 0 个位置存储的是 0x48,第 1 个位置存储的是 0x65……所以,最终存储的实际上是 0x 48 65 6c 6c 6f 20 57 6f,对应着 ASCII 码为:“Hello Wo”。

然后,后面的一句指令 (i32.store (i32.const 8) (i32.const 560229490)) 的格式是上面三条指令的“S-表达式”形式,只不过这里换成了 i32.store 来存储一个 32 位整型常数 560229490 到 8 号“内存地址”开始的连续 4 个字节空间中。

实际上这一句指令的写法写成上面三句的语法是完全等效的:

i32.const 8 i32.const 560229490 i32.store

1
2
3
i32.const 8
i32.const 560229490
i32.store

类似的,这里是在内存的第 8 个位置开始的连续 4 个字节的空间里,存入一个 32 位整型数字 560229490。这个数字转为 16 进制表示位:0x 21 64 6c 72,同样采用“小端序”来存储,所以存储的实际上是 0x 72 6c 64 21,对应着 ASCII 码为:“rld!“。

所以,最终,内存中前 12 个字节中的数据为 0x 48 65 6c 6c 6f 20 57 6f 72 6c 64 21,连起来就是对应着 ASCII 码:“Hello World!“。

将这个 wat 编译为 wasm 后,文件大小为 95 Bytes:

$ wat2wasm example.wat $ xxd example.wasm 00000000: 0061 736d 0100 0000 0108 0260 017f 0060 .asm.......`...` 00000010: 0000 0215 0203 656e 7603 6d65 6d02 0001 ......env.mem... 00000020: 026a 7303 6c6f 6700 0003 0201 0107 0b01 .js.log......... 00000030: 0765 7861 6d70 6c65 0001 0a23 0121 0041 .example...#.!.A 00000040: 0042 c8ca b1e3 f68d c8ab ef00 3703 0041 .B..........7..A 00000050: 0841 f2d8 918b 0236 0200 4100 1000 0b .A.....6..A....

1
2
3
4
5
6
7
8
$ wat2wasm example.wat
$ xxd example.wasm
00000000: 0061 736d 0100 0000 0108 0260 017f 0060  .asm.......`...`
00000010: 0000 0215 0203 656e 7603 6d65 6d02 0001  ......env.mem...
00000020: 026a 7303 6c6f 6700 0003 0201 0107 0b01  .js.log.........
00000030: 0765 7861 6d70 6c65 0001 0a23 0121 0041  .example...#.!.A
00000040: 0042 c8ca b1e3 f68d c8ab ef00 3703 0041  .B..........7..A
00000050: 0841 f2d8 918b 0236 0200 4100 1000 0b    .A.....6..A....

接下来,还是使用 JavaScript 编写“胶水代码”:

JavaScript

// example.js const file = await fetch('./example.wasm'); const memory = new window.WebAssembly.Memory({ initial: 1 }); const dv = new DataView(memory); const log = offset => { let length = 0; let end = offset; while(end < dv.byteLength && dv.getUint8(end) > 0) { length; end; } if (length === 0) { console.log(''); return; } const buf = new ArrayBuffer(length); const bufDv = new DataView(buf); for (let i = 0, p = offset; p < end; i, p) { bufDv.setUint8(i, dv.getUint8(p)); } const result = new TextDecoder('utf-8').decode(buf); console.log(result); }; const mod = await window.WebAssembly.instantiateStreaming(file, { env: { mem: memory, }, js: { log }, }); mod.instance.exports.example(); // 调用 WebAssembly 模块导出的 example 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// example.js
 
const file = await fetch('./example.wasm');
const memory = new window.WebAssembly.Memory({ initial: 1 });
const dv = new DataView(memory);
const log = offset => {
  let length = 0;
  let end = offset;
  while(end < dv.byteLength && dv.getUint8(end) > 0) {
     length;
     end;
  }
  if (length === 0) {
    console.log('');
    return;
  }
  const buf = new ArrayBuffer(length);
  const bufDv = new DataView(buf);
  for (let i = 0, p = offset; p < end; i, p) {
    bufDv.setUint8(i, dv.getUint8(p));
  }
  const result = new TextDecoder('utf-8').decode(buf);
  console.log(result);
};
const mod = await window.WebAssembly.instantiateStreaming(file, {
  env: {
    mem: memory,
  },
  js: { log },
});
mod.instance.exports.example();  // 调用 WebAssembly 模块导出的 example 函数

这里,使用 DataViewmemory 进行了一次包装,这样就可以方便地对内存对象进行读写操作了。

然后,这里在 JavaScript 中实现了一个 log 函数,函数接受一个参数(这个参数在上面的 wat 中指定了是整数型)。下面的实现首先是确定输出的字符串长度(字符串通常以 '' 结尾),然后将字符串复制到一个长度合适的 ArrayBuffer 中,然后使用浏览器中的 TextDecoder 类对其进行字符串解码,就得到了原始字符串。

最后,将 log 函数放入 importObject 的 js.log 中,实例化 WebAssembly 模块,最后调用导出的 example 函数,就可以看到打印的 Hello World

www.2003.com 7

通过 WebAssembly,我们可以将很多其他语言编写的类库直接封装到浏览器中运行,比如 Google Developers 就给了一个使用 WebAssembly 加载一个使用 C 语言编写的 WebP 图片编码库,将一张 jpg 格式的图片转换为 webp 格式并显示出来的例子:https://developers.google.com/web/updates/2018/03/emscripting-a-c-library

这个例子使用 Emscripten 工具对 C 语言代码进行编译,这个工具在安装的时候需要到 GitHub、亚马逊 S3 等服务器下载文件,在国内这神奇的网络环境下速度异常缓慢,总共几十兆的文件可能挂机一天都下不完。可以尝试修改 emsdk 文件(Python),增加代理配置(但是效果不明显),或是在下载的过程中会提示下载链接和存放路径,使用其他工具下载后放到指定地方,重新安装会自动跳过已经下载的文件。

webAssembly.instantiate

Promise WebAssembly.instantiate(module, importObject);

WebAssembly 的现状与未来

目前 WebAssembly 的二进制格式版本已经确定,未来的改进也都将以兼容的形式进行更新,这表示 WebAssembly 已经进入现代标准了。

www.2003.com 8

现在的 WebAssembly 还并不完美,虽说已经有使用 WebAssembly 开发的 Web 游戏出现了,但是还有很多不完美的地方。

比如,现在的 WebAssembly 还必须配合“JavaScript glue code”来使用,也就是必须使用 JavaScript 来 fetch WebAssembly 的文件,然后调用 window.WebAssembly.instantiatewindow.WebAssembly.instantiateStreaming 等函数进行实例化。部分情况下还需要 JavaScript 来管理堆栈。官方推荐的编译工具 Emscripten 虽然使用了各种黑科技来缩小编译后生成的代码的数量,但是最终生成的 JavaScript Glue Code 文件还是至少有 15K。

未来,WebAssembly 将可能直接通过 HTML 标签进行引用,比如:<script src="./wa.wasm"></script>;或者可以通过 JavaScript ES6 模块的方式引用,比如:import xxx from './wa.wasm';

线程的支持,异常处理,垃圾收集,尾调用优化等,都已经加入 WebAssembly 的计划列表中了。

webAssembly.Memory

当 WebAssembly 模块被实例化时,它需要一个 memory 对象。你可以创建一个新的WebAssembly.Memory并传递该对象。如果没有创建 memory 对象,在模块实例化的时候将会自动创建,并且传递给实例。

var myMemory = new WebAssembly.Memory(memoryDescriptor);

memoryDescriptor (object)

initial maximum 可选

小结

WebAssembly 的出现,使得前端不再只能使用 JavaScript 进行开发了,C、C 、Go 等等都可以为浏览器前端贡献代码。

这里我使用 wat 文件来编写的两个例子仅供参考,实际上在生产环境不大可能直接使用 wat 来进行开发,而是会使用 C、C 、Go 等语言编写模块,然后发布为 WebAssembly。

WebAssembly 的出现不是要取代 JavaScript,而是与 JavaScript 相辅相成,为前端开发带来一种新的选择。将计算密集型的部分交给 WebAssembly 来处理,让浏览器发挥出最大的性能!

www.2003.com, 1 赞 收藏 评论

www.2003.com 9

webAssembly.Table

var myTable = new WebAssembly.Table(tableDescriptor);

tableDescriptor (object)

element,当前只支持一个值。 ‘anyfunc’ initial, WebAssembly Table的初始元素数 maximum(可选), 允许的最大元素数

本文由www.2003.com发布于计算机教程,转载请注明出处:悄悄掀起 WebAssembly 的神秘面纱

关键词: