赵工的个人空间


专业技术部分转网页计算转业余爱好部分


  网站建设

首页 > 专业技术 > 网站建设 > WebAssembly初步
WebAssembly初步
  1. WebAssembly基本知识:
  2. WebAssembly汇编语言:
  3. JavaScript中的WebAssembly对象:
  4. WebAssembly有关的一些JavaScript用法:

WebAssembly是Web上的汇编语言,实际上是一个虚拟机,包含了低级汇编语言和对应的虚拟机体系结构,优点是文件小、加载快、执行效率高,可以实现更复杂的逻辑。WebAssembly是在Mozilla公司的Emscripten项目基础上发展起来的,随后获得各大主流浏览器的支持,2018年7月WebAssembly 1.0标准正式发布。
现有的Web开发技术,如JavaScript,执行效率和解决复杂问题的能力不足,WebAssembly的编译功能恰恰能弥补这些不足,其标准是在Mozilla、Google、Microsoft、Apple等厂商的支持推进下诞生的,包括Chrome、Firefox、Safari、Opera、Edge在内的大部分主流浏览器均已支持WebAssembly,前景为人看好。WebAssembly属于Web的前端技术,具有很强的移植性,可以使用C/C++、Go、Rust、Kotlin、C#等开发语言来写代码,然后编译为WebAssembly,并在Web上执行,这样可以将用其他语言编写的程序移植到Web上,丰富其内容。

一、 WebAssembly基本知识:

WebAssembly可以在主流浏览器及Node.js环境下运行,但加载运行的是二进制格式的文件代码,扩展名使用.wasm。但二进制代码很难阅读和编辑,一般是使用称为S-表达式的文本格式文件,扩展名为.wat,这种文件相当于二进制代码的汇编语言。把.wat格式的汇编语言文本文件编译为.wasm格式的二进制代码,然后才能在浏览器中加载运行,编译工具有很多,一般可以使用VSCode的WebAssembly插件来转换,这种插件可以检查.wat文件中的语法错误并高亮显示,使用比较方便。Linux环境下有wabt工具集,使用命令行来操作,相对比较繁琐一些,或者以后也会出现更方便的工具软件。

1. WebAssembly的使用:

WebAssembly程序从开发到运行于网页中一般可以分为以下几个阶段:
1) 使用.wat文本文件或其他语言(如C++、Go、Rust等)编写程序,通过特定的软件工具编译为WebAssembly二进制文件.wasm。
2) 在网页中使用fetch或XMLHttpRequest等获取.wasm文件。
3) 将.wasm编译转换为模块,并进行合法性检查。
4) 初始化导入对象,创建模块的实例。
5) 执行实例的导出函数,完成所需操作。
其中的2)到5)过程为WebAssembly程序的运行阶段,使用JavaScript操作,示例代码:
<script>
fetch('mywasm.wasm').then(response=>
  response.arrayBuffer()
).then(bytes=>
  WebAssembly.instantiate(bytes)
).then(result=>
  console.log(result.instance.exports.showAnswer())
);
</script>
浏览器普遍提供了WebAssembly的调试环境,可以使用开发面板调试,比如在chrome中使用F12调出开发面板,可以在函数体中设断点、查看局部变量/全局变量的值、查看调用栈和当前函数栈等。

2. WebAssembly的虚拟机体系结构:

WebAssembly在运行时由全局类型数组、全局函数数组、全局变量数组、全局表格对象、全局内存对象、运行时栈、局部变量数组组成,操作某个对象都是通过其索引来完成,目前版本中的索引都是32位整数。
为了与调用WebAssembly的JavaScript程序进行交互,WebAssembly使用了模块、内存、表格、实例等概念。其中模块是已编译的.wasm文件的二进制对象;内存是一段连续的空间,由JavaScript的ArrayBuffer对象实现,可以使用load、store等低级指令按照地址读写其中的数据,JavaScript与WebAssembly交换数据可以通过内存;表格用于存储函数引用;实例用于指代一个模块及其运行时的姿态,包括内存、表格以及导入对象等,模块只有实例化后才能被调用。
导入/导出对象是很重要的功能,通过导入对象的导入函数WebAssembly可以调用JavaScript的方法,而导出对象的导出函数则提供JavaScript使用的接口。目前,一个实例只能拥有一个内存对象和一个表格对象,而内存对象和表格对象都可以通过导入/导出对象被多个实例共有,类似于.dll动态链接库的模式协同工作。
比如,可以将来自JavaScript的js.print对象导入为函数并命名为js_print:
(import "js" "print" (func $js_print (param i32 i32)))
该函数有两个参数,类型均为32位整数,前一个参数为要打印的字符串在内存中的起始地址,后一个参数为字符串的长度,为字节数

3. LEB128编码:

WebAssembly二进制格式中使用LEB128编码表示整数和字符串长度信息。LEB128是Little Endian Base 128的缩写,是一种基于小端表示的128位可变长度编码格式。一个字节如果用于表达LEB128的数,则需要使用7位,而剩余的一位用于表达是否终结的标志位,标志位为1表示编码还没有结束,为0则表示编码已经结束。对一个32位整数,LEB128编码后的数据长度最小为一个字节,最多为5个字节,对于小于128的数字只需要1个字节,对于大于228的整数则需要5个字节。
对小数据的编码更友好是大多数可变长度编码的共有特性,对于64位整数LEB128也使用同样规则。LEB128还分为无符号整数和有符号整数,有符号数的LEB128编码是将其二进制数取反后加1而得到补码,然后作为无符号数进行LEB128编码,正数的补码还是自身。
对于无符号数进行LEB128编码,首先写为二进制格式,左端填充一定数目的0使位数为7的倍数,然后7位一组进行分组;将每一组左边填充一位变为8位,填充规则为左边第一组填充0,其他填充1;将填充后的每一组二进制数转换为十六进制数;最后将各个十六进制数逆序排列,数位最低的在前面。如624485转为LEB128编码为E58E26。

4. WebAssembly的二进制格式:

WebAssembly是通过二进制格式.wasm文件来被浏览器调用的,可以将WebAssembly的.wat文本文件格式转换为二进制格式,也可以将其他语言的程序输出为二进制格式。

WebAssembly的二进制文件的最外层结构为头部和段数据,其中头部标识有8个字节,前4个字节为0061736d,对应字符串为\0asm,后4个字节为当前版本,版本1为01000000。
后面部分为段数据,一般由多个段组成,WebAssembly为不同的段分配了唯一段ID:

0 自定义段Custom 1 类型段Type 2 导入段Import 3 函数段Function
4 表格段Table 5 内存段Memory 6 全局段Global 7 导出段Export
8 开始段Start 9 元素段Elem 10 代码段Code 11 数据段Data
二进制文件中的段与.wat文本文件中的关键字有对应关系,只有自定义段和代码段没有对应关键字,其中代码段保存函数代码,自定义段用于保存调试符号等与运行无关的信息。每种ID对应的段最多出现一次。在段数据中,每个段的ID后面紧跟的是此段的数据长度。
WebAssembly的二进制格式较复杂,应用中一般是浏览器来加载分析,使用者只需要简单了解。如果是使用其他语言,如C++、Go、Rust等编写程序,可能需要用来调试排错。

二、 WebAssembly汇编语言:

浏览器中使用WebAssembly,虽然加载调用的是二进制文件,但编程一般是使用.wat文本文件或其他高级语言,然后使用工具软件转换为对应二进制文件。使用者最需要了解的是.wat文本文件的规范,也称为WebAssembly汇编语言。

1. S表达式:

WebAssembly文本格式文件是以S-表达式表示的。S-表达式是用于描述树状结构的简单文本格式,其中树的每个节点均被一对圆括号包围,节点可以包含子节点。
1)最简单的WebAssembly模块:
(module)
这表示根节点为module的树,尽管module下没有其他节点,仍然合法。
2)S-表达式格式:
WebAssembly文本格式中,左括号后紧跟节点类型,随后是由分隔符(空格、换行等)分隔的属性和子节点列表。示例:
(module
  (func)
  (memory 1)
)
此模块包括一个空函数以及属性为1的memory。
3)WebAssembly的节点类型:

节点 类型 节点 类型
module WebAssembly根节点 type 函数签名
memory Memory内存 data Memory初始值
table Table表格 elem Table元素初始值
import 导入对象 export 导出对象
global 全局变量 local 局部变量
func 函数 param 函数参数
result 函数返回值 start 开始函数

2. 数据类型:

WebAssembly有4种数据类型:

  • ·i32:32位整数
  • ·i64:64位整数
  • ·f32:32位浮点数,IEEE754标准
  • ·f64:64位浮点数,IEEE754标准
WebAssembly采用的是数据类型后置的表达方式,如:
(param i32) ;;定义了i32类型的参数
(result f64) ;;定义了f64类型的返回值
WebAssembly并不区分有符号整数/无符号整数,但一些操作需要区分,WebAssembly提供了有符号/无符号指令以区分需要的操作。一般来说,无符号操作指令后缀为_u,而有符号操作指令后缀为_s。WebAssembly是强类型语言,不支持隐式类型转换。

3. 函数定义:

WebAssembly模块的可执行代码位于函数中。函数的一般结构为:
(func <函数签名> <局部变量表> <函数体>)

1)函数签名:
函数签名表明了函数的参数及返回值,由一系列param节点(参数列表)和result节点(返回值)组成。示例:
(func (param i32) (param f32) (result f64)...)
其中,(param i32) (param f32) (result f64)为函数签名,表示函数有两个输入参数,第1个为i32类型,第2个为f32类型;该函数的返回值为f64类型。WebAssembly为强类型语言,其参数和返回值必须显式注明数据类型。WebAssembly目前只支持单返回值,即其中最多只有1个result节点,如果没有result节点则说明函数没有返回值。
2)局部变量表:
局部变量表由一系列local节点组成,示例:
(func (result f64) (local i32) (local f32)...)
代码中(local i32) (local f32)定义了两个局部变量,数据类型分别为i32和f32。局部变量只能在函数内部使用。
3)函数体:
函数体是WebAssembly汇编指令的线性列表。示例:
(func (export "hello")
  i32.const 0   ;;pass offset 0 to js_print
  i32.const 13  ;;pass length 13 to js_print
  call $js_print
)
4)函数别名:
WebAssembly是通过函数索引(即函数在模块中定义的顺序)来标识的。示例:
(module
  (func ...)  ;;func[0]
  (func ...)  ;;func[1]
)
但使用索引调用函数不直观,很容易出错,因此WebAssembly文本格式允许给函数命名,方法是在func后加一个以$开头的名字。示例:
(func $add...)
这样就定义了一个名字为$add的函数。在文本格式文件.wat被转换为二进制格式.wasm时,别名会被替换为索引值。
WebAssembly中,函数只能在模块module下定义,而不能在函数内部嵌套定义,即所有函数都是全局函数。

4. 变量:

WebAssembly中的变量分为局部变量和全局变量,主要区别是作用域不同。

1)局部变量:
(func $f1 (param i32) (param f32) (result i64)
  (local f64)
  ;;do sth.
)
上例中,定义了一个名为$f1的函数,有两个类型分别为i32和f32的参数,返回值为i64类型,同时有一个类型为f64的局部变量。在函数中,使用get_local和set_local来读写参数和局部变量,示例:
(func $f1 (param i32) (param f32) (result i64)
  (local f64)
  get_local 0      ;;get i32
  get_local 1      ;;get f32
  get_local 2      ;;get f64
  ;;do sth.
)
其中,get_local 0指令将得到第1个参数,get_local 1指令将得到第2个参数,get_local 2指令将得到局部变量。因此,函数参数与局部变量的区别在于:参数的初值是在调用函数时传入的,而局部变量是在函数内部中定义的。对函数体来说,函数参数与局部变量是等价的,都是函数内部的局部变量,WebAssembly按照变量声明的顺序赋予了递增索引0、1、2、...,而get_local n指令的作用是读取第n个索引对应的局部变量的值并将其压入栈中。相对地,set_local n的功能则是将栈顶的数据弹出并存入第n个索引对应的局部变量中。
WebAssembly中,整型局部变量的初始值为0,浮点型局部变量的初始值为+0。
2)变量别名:
WebAssembly允许为局部变量命名。示例:
(func $f1 (param $p0 i32) (param $p1 f32) (result i64)
  (local $l0 f64)
  get_local $p0      ;;get i32
  get_local $p1      ;;get f32
  get_local $l0      ;;get f64
  ;;do sth.
)
上述代码与使用索引方式是等同的。
WebAssembly中,不仅可以为函数、局部变量命名,全局变量、表格、内存等都可以命名,命名方法都是在节点类型后写入以$开头的字符串名。
3)全局变量:
全局变量的作用域是整个module,分为可变全局变量和只读全局变量两种,可变全局变量可读可写,而只读全局变量初始化后不可改变。声明语法为:
(global <别名> <类型> <初值>)
示例:
(module
  (global (mut i32) (i32.const 42))      ;;define global[0]
  (global $pi f32 (f32.const 3.14159))      ;;define global[1] as $pi
  ;;do sth.
)
声明全局变量时,类型节点如果包含mut就表示是可变全局变量,否则为只读全局变量,而初值只能为常数表达式。
WebAssembly也是按照全局变量声明的顺序为其分配索引值,然后通过索引值进行读写,全局变量的读写使用get_global和set_global指令。如果对只读全局变量使用set_global指令,在编译时会抛出WebAssembly.CompileError错误。示例:
(module
  (global (mut i32) (i32.const 42))      ;;define global[0]
  (global $pi f32 (f32.const 3.14159))      ;;define global[1] as $pi
  (func
    get_global 0      ;;get 42
    get_global 1      ;;get 3.14159
    get_global $pi      ;;get 3.14159
    ...
  )
)
在WebAssembly中,全局变量可以在module的任意位置声明和使用,无须先声明再使用。
全局变量、局部变量都不占用内存地址空间,各自独立。

5. 栈式虚拟机:

栈是一种先入后出的数据结构,只能在其中一端执行插入和删除操作,这端称为栈顶,而对应的另一端则称栈底。栈有压入和弹出两种基本操作,压入是在栈顶添加一个元素,栈中元素的个数加1;弹出是将栈顶的元素删除,栈中的元素个数减1。在空栈中执行弹出操作是非法的,而栈的容量存在上限,在满栈中执行压入操作也非法。

1)栈式虚拟机指令:
WebAssembly不仅是一种汇编语言,也是一套虚拟机体系结构规范。WebAssembly体系中没有寄存器,操作数存放在运行时的栈上,因此是一种栈式虚拟机。除nop这类特殊指令外,WebAssembly的绝大多数指令都是在栈上执行某种操作,比如:
  ·i32.const n:在栈上压入值为n的32位整数
  ·i32.add:从栈中取出两个32位整数,计算它们的和,并将结果压入栈
  ·从栈中取出2个32位整数,比较是否相等,相等则在栈中压入1,否则压入0
2)栈式调用:
WebAssembly中,函数调用时参数传递及返回值获取都是通过栈来完成的,过程为:
  ·调用方将参数压入栈中
  ·进入函数后,初始化参数
  ·执行函数体中的指令
  ·将函数的执行结果压入栈中返回
  ·调用方从栈中获取函数的返回值
由于函数调用经常是嵌套的,因此同一时刻栈中会保存调用链上多个函数的信息,每个未返回的函数占用栈式的一段独立的连续区域,这段区域被称为栈帧。栈帧是栈的逻辑片段,调用函数时栈帧被压入栈中,函数返回时栈帧被弹出栈。多级调用时,栈中将保存与调用序列一致的多个栈帧。
进入函数时,从逻辑上来说获得了一个独享的栈。这个栈初始是空的,随着函数体中指令的执行,数据不断入栈出栈。WebAssembly验证规则会执行严格检查以保证栈帧匹配,如果函数声明了i32类型的返回值,则函数体执行完毕后,栈上必须包含且仅包含一个i32类型的值;如果函数没有返回值,函数体执行完毕后,栈必须是空的。WebAssembly会对所有指令的数据类型执行检查,保证数据类型的匹配。
3)静态检查:
严格的栈式虚拟机设计简化了指令架构,增强了可移植性和安全性,因此WebAssembly系统在函数体的任意位置,栈中元素的个数及数据类型都是可以准确预估的,无须运行即可对WebAssembly代码进行操作数数量、操作数类型、函数签名等进行核验,这种非运行时的合法性检查称为静态检查。将文本格式的.wat文件编译为二进制的.wasm文件时就会执行静态检查并输出出错的位置及原因。

6. 函数调用:

WebAssembly函数调用有直接调用和间接调用两种方式。

1)直接调用:
直接调用使用call指令,语法为:
call n
其中,参数n是欲调用的函数的索引或函数名。指令中n必须为常数,也就是调用关系是固定的。
2)间接调用:
间接调用允许使用变量来选择被调用的函数,是通过表格table和call_indirect指令协同完成,表格中保存了一系列函数的引用,call_indirect通过函数在表格中的索引来调用它,语法:
call_indirect (type n)
其中,参数n为被调用函数的函数签名索引或函数签名别名。示例:
(module
  (table 2 anyfunc)      ;;define table
  (elem (i32.const 0) $plus13 $plus42)      ;;set $plus13,$plus42 to table
  (type $type_0 (func (param i32) (result i32)))     ;;define func signatures
  (func $plus13 (param $i i32) (result i32)
     i32.const 13
     get_local $i
     i32.add
  )
  (func $plus42 (param $i i32) (result i32)
     i32.const 42
     get_local $i
     i32.add
  )
  (func (export "call_by_index") (param $id i32) (param $input i32) (result i32)
     get_local $input      ;;push param into stack
     get_local $id      ;;push function id into stack
     call_indirect (type $type_0)      ;;call table: id
  )
)
其中,(table 2 anyfunc)声明了容量为2的表格,(elem (i32.const 0) $plus13 $plus42)从偏移0处开始在表格中依次存入了函数$plus13和$plus42的引用,其中(i32.const 0)表示开始存放的偏移为0。
(type $type_0 (func (param i32) (result i32)))声明了一个名为$type_0的函数签名,该签名包含了一个i32的参数以及i32的返回值。call_indirect 指令首先从栈中弹出将要调用的函数的索引,i32 类型,然后根据(type $type_0)指定的函数签名$type_0依次弹出参数,调用函数索引指定的函数。
使用间接调用时,虽然显式地约定了函数签名,但由于调用关系是由变量控制的,有可能发生函数签名与被调用的函数不匹配的情况。为了保证栈的完整性,WebAssembly虚拟机执行间接调用时会动态检查函数签名,若不匹配将弹出WebAssembly.RuntimeError。
3)递归:
WebAssembly允许递归调用,示例:
;;recurse.wat
(module
  (func $sum (export "sum") (param $i i32) (result i32)
     (local $c i32)
     get_local $i
     i32.const 1
     i32.le_s
     if
       get_local $i
       get_local $c
     else
       get_local $i
       i32.const 1
       i32.sub
       call $sum
       get_local $i
       i32.add
       set_local $c
     end
     get_local $c
  )
)
//recurse.html
fetchAndInstantialate("recurse.wasm").then(
   function(instance){
     console.log(instance.exports.sum(10));      //55
   }
);
$sum函数递归调用自身,计算指定长度的自然数列的和。要谨慎使用递归函数,避免因为递归深度过深导致栈溢出。WebAssembly并未规定栈的容量,不同的虚拟机实现可能有不同的最大栈尺寸。

7. 内存读写:

1)内存初始化:
内存可以在WebAssembly内部创建,语法:
(memory initial_size)
其中,initial_size为内存的初始容量,单位为页。
新建内存中所有字节的默认初值都是0,可以用数据段Data来为它赋自定义的值。示例:
(module
  (memory 1)
  (data (offset i32.const 0) "hello")
)
实例化时,(data (offset i32.const 0) "hello")将在偏移0处存入字符串"hello"的字节码,(offset i32.const 0)表示起始偏移为0,语句中的offset对象是可省略的,省略时默认偏移为0。
多个data段之间可以重叠,在重叠部分,后声明的值会覆盖先声明的值。示例:
(module
  (memory 1)
  (data (i32.const 0) "hello")
  (data (i32.const 4) "u")
)
无论运行在哪种系统上,WebAssembly固定使用小端序。下列语句将在偏移为12处存入32位整数0x00123456:
(data (i32.const 12) "\56\34\12\00")
在函数体中,可以使用一系列load/store指令来按数据类型读写内存。
2)读取内存:
读取内存中的数据时,需要先将内存地址(即欲读取的数据在内存中的起始偏移)压入栈中,然后调用指定类型的load指令。Load指令将地址弹出后,从内存中读取数据并压入栈上。
下列代码将从内存地址12处读入i32类型整数到栈上:
i32.const 12
i32.load
load指令允许在指令中输入立即数为寻址的额外偏移,例如:
i32.const 4
i32.load offset=8 align=4
这段代码与前面的代码是等价的,其中offset=8表示额外偏移为8字节,因此实际的有效地址仍然为4+8=12。通过设置offset的方法,可以获得的最大有效地址为2**33,但目前的标准内存的最大容量为2**32,即4GB。如果不显式声明offset,默认的offset为0。
align=4是地址对齐标签,提示虚拟机按4字节对齐来读取数据,对齐数值必须为2的整数幂,目前标准中align数值只能为1、2、4、8。从内存中读取数据时,地址对齐并不会影响执行结果,但会影响执行效率。
  · 如果要读取的数据的长度与align完全相等,且有效地址是align的整数倍,执行效率最高
  · 如果align小于要读取的数据的长度,且有效地址是align的整数倍,执行效率较低
  · 如果有效地址不是align的整数倍,执行效率最低
如果不显式地声明align值,align默认与将要读取的数据长度一致。
WebAssembly的4种数据类型分别有各自的内存读取指令与之一一对应,分别为i32.load、f32.load、i64.load和f64.load。除此之外,某些情况下还需要单独读取内存中的某些字节、字等,WebAssembly提供了以下指令:
i32.load8_s 读取1字节,并按有符号整数扩展为i32(符号位扩充到最高位,其余填充0)
i32.load8_u 读取1字节,并按无符号整数扩展为i32(高位填充0)
i32.load16_s 读取2字节,并按有符号整数扩展为i32(符号位扩充到最高位,其余填充0)
i32.load16_u 读取2字节,并按无符号整数扩展为i32(高位填充0)
i64.load8_s 读取1字节,并按有符号整数扩展为i64(符号位扩充到最高位,其余填充0)
i64.load8_u 读取1字节,并按无符号整数扩展为i64(高位填充0)
i64.load16_s 读取2字节,并按有符号整数扩展为i64(符号位扩充到最高位,其余填充0)
i64.load16_u 读取2字节,并按无符号整数扩展为i64(高位填充0)
i64.load32_s 读取4字节,并按有符号整数扩展为i64(符号位扩充到最高位,其余填充0)
i64.load32_u 读取4字节,并按无符号整数扩展为i64(高位填充0)
3)写入内存:
写入内存使用store指令。使用时,先将内存地址入栈,然后将数据入栈,调用store。
下列代码在内存偏移12处存入了i32类型的42:
i32.const 12          ;;address
i32.const 42          ;;value
i32.store
store指令也可以额外指定地址偏移和对齐值,使用方法雷同:
i32.const 4          ;;address
i32.const 42          ;;value
i32.store offset=8 align=4
除了i32.store、f32.store、i64.store和f64.store这4个基本指令外,WebAssembly提供了部分写入指令:
i32.store8 i32整数低8位写入内存(写入1字节)
i32.store16 i32整数低16位写入内存(写入2字节)
i64.store8 i64整数低8位写入内存(写入1字节)
i64.store16 i64整数低16位写入内存(写入2字节)
i64.store32 i64整数低32位写入内存(写入4字节)
4)获取内存容量及内存扩容:
内存的当前容量可以用memory.size指令获取。示例:
(func $mem_size (result i32)
   memory.size
)
使用memory.grow指令可以扩容内存,该指令从栈上弹出欲扩大的容量(i32类型),如果执行成功则将扩容前的容量压入栈,否则将-1压入栈。示例:
;;grow_size.wat
(module
  (memory 3)
  (func (export "size") (result i32)
     memory.size
  )
  (func (export "grow") (param i32) (result i32)
     get_local 0
     memory.grow
  )
)
fetchAndInstantiate('grow_size.wasm').then(
  function(instance){
    console.log(instance.exports.size()); //3
    console.log(instance.exports.grow(2)); //3
    console.log(instance.exports.size()); //5
  )
)

8. 控制流:

控制流指令指的是改变代码执行顺序,使其不再按照声明的顺序执行的一类特殊指令。前面的函数调用指令call和call_indirect也属于控制流指令,还有一些用于函数体内部的控制流指令。

1)nop和unreachable:
这是两个特殊的控制流指令:
  • ·nop:什么也不干
  • ·unreachable:意思是不应该执行到这里,常见用法是在意料之外的执行路径上设置中断,当执行到这个指令时会抛出WebAssembly.RuntimeError。
2)block指令块:
block与loop、if指令被称为结构化控制流指令,这些指令不会单独出现,而是要和end、else成对或成组出现。示例:
block
  i32.const 42
  set_local $answer
end
代码中,block和end包围起来的指令构成一个整体,称为指令块。end指令的作用是标识指令块的结尾。对block指令块,如果内部没有跳转指令(如br、br_if、br_table、return),则顺序执行指令块中的指令,然后继续执行指令块后续的指令。
指令块与无参数的函数很相似,体现在:
  • ·从逻辑上,指令块拥有自己独立的栈帧
  • ·指令块可以有返回值,当指令块执行完毕时,其栈状态必须与它声明的返回值相匹配
WebAssembly使用这种设计方式,是为了维持栈平衡。在条件分支if和循环分支loop构成的指令块中,指令块的执行是动态的,如果指令块没有独立的栈,将使合法性检查变得困难。指令块函数化的设计,在简化合法性检查的同时,保持了整个体系结构的优雅。
为指令块声明返回值的方法与函数一样,使用result属性声明。示例:
block (result i32)
  i32.const 42
end      ;;get 42 on the stack
上述指令执行完毕后,栈上增加了一个i32的值,这与调用了一个无参数且返回值为i32的函数相比,对栈的影响是一致的。WebAssembly目前不允许函数有多个返回值,这个限制同样适用于指令块。
指令块可以访问其所在函数的能访问的所有资源—局部变量、内存个表格等,也可以调用其他函数。示例:
(func $sum (param $a i32) (param $b i32) (result i32)
  get_local $a
  get_local $b
  i32.add
)
(func $sum_mul2 (param $a i32) (param $b i32) (result i32)
  block (result i32)
    get_local $a
     get_local $b
     call $sum
     i32.const 2
     i32.mul
  end
)
上述代码中,block指令块中读取了函数的局部变量,调用了$sum函数。
3)if指令块:
if指令块可以搭配else指令形成典型的if/else条件分支语句,语法:
if
<BrunchA>
else
<BrunchB>
end
if指令先从栈中弹出一个i32的值;如果该值不为0,则执行分支BrunchA;若该值为0,则执行BrunchB。示例:
(func $func1 (param $a i32) (result i32)
  get_local $a
  if (result i32)
     i32.const 13
  else
     i32.const 42
  end
)
上述代码中,如果调用$func1时输入参数不为0,则返回13,否则返回42。
4)loop指令块:
对loop指令块来说,如果内部不含跳转指令,那么loop指令块的行为与block指令块是一致的。示例:
(func $func1 (result i32)
  (local $i32)
  i32.const 12
  set_local $i
  loop
    get_local $i
     i32.const 1
     i32.add
     set_local $i
  end
  get_local $i
)
上述代码中,loop指令块只会执行一次,函数$func1的返回值是13。
5)指令块的label索引及嵌套:
指令块是可以嵌套的,示例:
if      ;;label 2
  nop
  block      ;;label 1
     nop
     loop      ;;label 0
       nop
     end      ;;end of loop-label 0
  end      ;;end of block-label 1
end      ;;end of if-label 2
代码中,每个指令块都被隐式赋予了一个label索引。大多数对象的索引是以声明顺序递增的,而label索引是由指令块的嵌套深度决定的,位于最内层的指令块索引为0,每向外一层索引加1。label也可以命名,示例:
if $L1
  nop
  block $L2
     nop
     loop $L3
       nop
     end      ;;end of $L3
  end      ;;end of $L2
end      ;;end of $L1
label的用处是作为跳转指令的跳转目标。
6)br:
跳转指令共有4条,分别为br、br_if、br_table和return。其中br为无条件跳转指令,语法:
br L
br指令的基本作用是跳出指令块。由于指令块是可以嵌套的,br指令的参数L指定了跳出的层数:如果L为0,则跳转至当前指令块的后续点;如果L为1,则跳转至当前指令块的父指令块的后续点,以此类推,L每增加1,多向外跳转一层。
block指令块和if指令块的后续点是其结尾。示例:
block (result i32)
  i32.const 13
  br 0
  drop
  i32.const 42
end
上述代码中,br 0直接替换到了end处,后续的drop、i32.const 42都被忽略了,因此指令块返回值为13。
但loop指令块的后续点则是其开始处。示例:
(func $func1 (result i32)
  (local $i32)
  i32.const 12
  set_local $i
  loop
    get_local $i
     i32.const 1
     i32.add
     set_local $i
     br 0
  end
  get_local $i
)
上述代码中,br 0跳转到了loop处,实际上上述loop指令是个无法结束的死循环。直观上来说,br在block指令块和if指令块中的作用类似于C语言中的break,而在loop指令块中的作用则类似于continue。
多级跳出的示例:
(func $func1 (result i32)
  (local $i32)
  i32.const 12
  set_local $i
  block      ;;label 1
     loop      ;;label 0
       get_local $i
       i32.const 1
       i32.add
       set_local $i
       br 1
     end      ;;end of loop
  end      ;;end of block, 'br 1'jump hear
  get_local $i
)
br 1向外跳出2层,跳转到了block指令块的后续点,使函数返回13。使用br时,也可以将label的别名直接作为跳转目标。示例:
(func $func1 (result i32)
  (local $i32)
  i32.const 12
  set_local $i
  block $L1      ;;label 1
     loop      ;;label 0
      get_local $i
       i32.const 1
       i32.add
       set_local $i
       br $L1
     end      ;;end of loop
  end      ;;end of $L1, '$L1'jump hear
  get_local $i
)
上述代码与前面的代码是等价的。
7)br_if:
指令语法:
br_if L
br_if执行时,会先从栈上弹出一个i32类型的值,如果该值不为0,则执行br L的操作,否则执行后续操作。示例:
(func $func1 (param $i i32) (result i32)
  block (result i32)
     i32.const 13
     get_local $i
     i32.const 5
     i32.gt_s
     br_if 0
     drop      ;;drop 13
     i32.const 42
  end
)
上述代码中,如果$i大于5,将导致br_if 0跳转至end,指令块返回预先放在栈上的13;如果$i小于等于5,br_if 0无效,后续指令将丢弃之前放在栈上的13,返回42。
8)return:
return指令用于跳出至最外层的结尾,即函数的结尾处,执行结果等同于直接返回。示例:
(func $func1 (result i32)
  block (result i32)
     block (result i32)
      block (result i32)
         i32.const 4
         return      ;;return 4
       end
       drop
       i32.const 5
     end
     drop
     i32.const 6
  end
  drop
  i32.const 7
)
上述代码中,return直接跳至函数结尾处,返回值为4。
9)br_table:
br_table指令比较复杂,语法为:
br_table L(n) L_Default
其中,L(n)是一个长度为n的label索引数组。br_table执行时,先从栈上弹出一个i32类型的值m,如果m小于n,则执行br L(m),否则执行br L_Default。示例:
;;br_table.wat
(module
  (func (export "brTable") (param $i i32) (result i32)
     block
       block
         block
           get_local $i
           br_table 2 1 0
         end
         i32.const 4
         return
       end
       i32.const 5
       return
     end
     i32.const 6
  )
)
br_table 2 1 0根据$i的值选择跳出的层数:$i等于0时跳出2层,$i等于1时跳出1层,$i大于等于2时跳出0层。跳出0、1、2层时,函数$brTable分别返回4、5、6。在JavaScript中调用:
//br_table.html
fetchAndInstantiate('br_table.wasm').then(
  function(instance){
    console.log(instance.exports. br_table (0));      //6
    console.log(instance.exports. br_table (1));      //5
    console.log(instance.exports. br_table (2));      //4
    console.log(instance.exports. br_table (3));      //4
  }
);

9. 导入和导出:

1)导出对象:
WebAssembly中可导出的对象包括内存、表格、函数、只读全局变量。若要导出某个对象,只需要在该对象的类型后加入(export "export_name")属性即可。用法:
;;exports.wat
(module
  (func (export "wasm_func") (result i32)
     i32.const 42
  )
  (memory (exports "wasm_mem") 1)
  (table (exports "wasm_table") 2 anyfunc)
  (global (exports "wasm_global_pi") f32 (f32.const 3.14159))
)
JavaScript代码为:
//exports.html
fetch ("exports.wasm").then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes)
).then(results =>
  var exports=WebAssembly.Module.exports(results.module);
  for(var e in exports){
    console.log(results.instance.exports);
    console.log(results.instance.exports.wasm_func());
    console.log(results.instance.exports.wasm_global_pi);
    console.log(typeof(results.instance.exports.wasm_global_pi));
  }
);
上述JavaScript程序将.wasm编译后,使用WebAssembly.Module.exports()方法获取了模块的导出对象信息,并输出了实例的exports属性,如下:
{name: "wasm_func", kind: "function"}
{name: "wasm_mem", kind: "memory"}
{name: "wasm_table", kind: "table"}
{name: "wasm_global_pi", kind: "global"}
{wasm_func: f, wasm_mem: Memory, wasm_table: Table, wasm_global_pi: 3.141590118408203}
wasm_func: f 0()
wasm_global_pi: 3.141590118408203
wasm_mem: Memory {}
wasm_table: Table {}
42
3.141590118408203
number
模块的导出信息只包括了导出对象的名字和类别,实际的导出对象必须通过实例的exports属性访问。
在所有的导出对象中,导出函数使用的频率最高,它是JavaScript访问WebAssembly模块提供的功能的入口。导出函数中封装了实际的WebAssembly函数,调用导出函数时,虚拟机会按照函数签名执行必要的类型转换、参数初始化,然后调用WebAssembly函数并返回调用结果。导出函数使用起来与正常的JavaScript方法一致,区别是函数体的实际执行是在WebAssembly中。
除了通过实例的exports属性获取导出函数,还可以通过表格的get()方法获取已被存入表格中的函数。示例:
;;exports_table.wat
(module
  (table $tab 2 anyfunc)      ;;define $tab, initSize=2
  (export "table" (table $tab) )      ;;export $tab as table
  (elem (i32.const 0) f32 $func1 $func0)      ;;set $func0,$func1 to table
  (func $func0 (result i32)
     i32.const 13
  )
  (func $func1 (result i32)
     i32.const 42
  )
)
//exports_table.html
var table=new WebAssembly.Table({element:'anyfunc', initial:1});
fetchAndInstantiate ("export_table.wasm",{js:{table:table}}).then(
  function(instance)
    console.log(table.get(0)); //null
    console.log(instance.exports);
    console.log('instance.exports.table.length:'+ instance.exports.table.length);
    for(var i=0;i<instance.exports.table.length;i++){
      console.log(instance.exports.table.get(i));
      console.log(instance.exports.table.get(i)());
     }
  }
);
程序运行后控制台输出:
null
{table:Table}
instance.exports.table.length:2
f 1() {[native code]}
42
f 0() {[native code]}
13
在.wat中声明导出对象时,除了在对象类型后加入export属性,还可以通过单独的export节点声明导出对象,二者是等价的。示例:
(module
  (func (result i32)
     i32.const 42
  )
  (memory 1)
  (table $t 2 anyfunc)
  (global $g0 f32 (f32.const 3.14159))
  (export "wasm_func" (func 0))
  (export "wasm_mem" (memory 0))
  (export "wasm_table" (table $t))
  (export "wasm_global" (global $g0))
)
2)导入对象:
WebAssembly中可导入的对象包括内存、表格、函数、只读全局变量。用法:
;;import.wat
(module
  (import "js" "memory" memory 1)      ;;import Memory
  (import "js" "table" (table 1 anyfunc)      ;;import Table
  (import "js" "print_i32" (func $js_print_i32 (param i32)))      ;;import Function
  (import "js" "global_pi" (global $pi f32))      ;;import Global
)
由于导入函数必须先于内部函数定义,因此习惯上导入函数对象一般在module的开始处声明。
使用WebAssembly.Module.imports()可以获取模块导入对象信息。示例:
//imports.html
fetch ("imports.wasm").then(response =>
  response.arrayBuffer()
).then(bytes =>)
  WebAssembly.compile(bytes)
).then(module =>{
      var imports=WebAssembly.Module.imports(module);
      for(var e in imports){
      console.log(imports[e]);
      }
  }
);
运行后控制台将输出:
{module: "js", name: "memory", kind: "memory"}
{module: "js", name: "table", kind: "table"}
{module: "js", name: "print_i32", kind: "function"}
{module: "js", name: "global_pi", kind: "global"}
import节点使用了两级名字空间的方式对外部导入的对象进行识别,第一级为模块名(示例中的js),第二级为对象名(示例中的memory、table等)。导入对象是在实例化时导入实例中去的,在JavaScript环境下,如果导入对象为importObj,那么(import "m" "n"...)对应的就是importObj.m.n。例如,imports.wasm模块实例化时应提供的导入对象为:
function js_print_i32(param){
  console.log(param);
}
var memory=new WebAssembly.Memory({initial:1, maximum:10});
var table=new WebAssembly.Table({element:'anyfunc', initial:2});
var importObj={js:{print_i32:js_print_i32, memory:memory, table:table, global_pi:3.14}};
fetchAndInstantiate ("imports.wasm",importObj).then(instance =>
  console.log(instance);
);
导入的作用是让WebAssembly调用外部对象。WebAssembly代码调用导入对象时,虚拟机同样执行了参数类型转换、参数和返回值的出入栈等操作,因此导入函数的调用方法与内部函数是一致的。示例:
;;import.wat
(module
  (import "js" "print_f32" (func $js_print_f32 (param f32) (result f32)))
  (import "js" "global_pi" (global $pi f32))
  (func (export "print_pi") (result f32))
     get_global $pi
     call $js_print_f32
  )
)
print_pi()函数读入了导入的只读全局变量$pi并压入栈中,然后调用了导入函数$js_print_f32,并将返回值一并返回。JavaScript代码为:
//imports.html
function js_print_f32(param){
  console.log(param);
  return param*2.0;
}
var importObj={js:{print_f32:js_print_f32, global_pi:3.14}};
fetchAndInstantiate ("imports.wasm",importObj).then(
  function(instance){
    console.log(instance.exports.print_pi());
  }
);
上述JavaScript代码将js_print_f32()方法通过importObj.js.print_f32导入了WebAssembly,其中特意将参数乘以2后返回,因此运行后控制台输出为:
3.140000104904175
6.28000020980835
内存和表格导入后的使用方法可以参加后面的JavaScript中的WebAssembly对象部分。
通过导入函数,WebAssembly可以调用外部JavaScript环境中的方法,执行读写DOM等操作。

10. start()函数:

有时希望模块在实例化后立即执行一些启动操作,可以使用start()函数。示例:
;;start.wat
(module
  (start $print_pi)
  (import "js" "print_f32" (func $js_print_f32 (param f32)))
  (func $print_pi
     f32.const 3.14
     call $js_print_f32
  )
)
代码中的start后的函数$print_pi在实例化后将自动执行。
对应的JavaScript代码:
//start.html
function js_print_f32(param){
  console.log(param);
}
var importObj={js:{print_f32:js_print_f32}};
console.log("fetchAndInstantiate()");
fetchAndInstantiate ("start.wasm",importObj).then();
代码中仅创建了start.wasm实例,没有调用实例的任何函数,但控制台输出为:
fetchAndInstantiate()
3.140000104904175
start段引用的启动函数不能包含参数,不能有返回值,否则无法通过静态检查。

11. 指令折叠:

在书写函数体中的指令时,一般是按照每条指令一行的格式来书写,但也可以用S-表达式的方式书写,指令的操作数可以使用括号嵌套折叠其中。示例:
i32.const 13
get_local $x
i32.add
可以写作:
i32.add (i32.const 13) (get_local $x)
上述两种写法是等价的。指令可以嵌套折叠,折叠后的执行顺序为从内到外,从左到右。
i32.const 13
get_local $x
i32.add
i32.const 5
i32.mul
可以写作:
i32.mul (i32.add (i32.const 13) (get_local $x))(i32.const 5)
结构化流控制指令也可以折叠,折叠后不需要写end指令。示例:
block $label1 (result i32)
  i32.const 13
  get_local $x
  i32.add
end
折叠后写为:
(block $label1 (result i32) (i32.add (i32.const13) (get_local $x)))
但if指令块中,if分支必须折叠为then。示例:
if $label1 (result i32)
  i32.const 13
else
  i32.const 42
end
折叠后写为:
(if $label1 (result i32) (then (i32.const13) )(else(i32.const 42)))
过度使用指令折叠,如嵌套层次过多,会增加阅读难度。

三、JavaScript中的WebAssembly对象:

在浏览器环境中,WebAssembly程序运行在WebAssembly虚拟机上,页面可以通过一组JavaScript对象进行WebAssembly模块的编译、载入、配置、调用等操作。

1.WebAssembly对象:

WebAssembly有模块、内存、表格以及实例等关键概念,每个概念都有对象与之对应,分别为WebAssembly.Module、WebAssembly.Memory、WebAssembly.Table以及WebAssembly.Instance。
所有与WebAssembly相关的功能,都属于全局对象WebAssembly。WebAssembly除了对象,还包含一些全局方法,主要用于WebAssembly二进制模块的编译及合法性检查。WebAssembly不是一个构造函数,而是一个命名空间,与Math对象相似,使用WebAssembly相关功能时,直接调用WebAssembly.XXX()方法即可,不需要使用构造函数。

1)WebAssembly.compile()全局方法:
该方法用于将WebAssembly二进制代码.wasm编译为WebAssembly.Module。
语法 Promise <WebAssembly.Module> WebAssembly.compile(bufferSource)
参数 bufferSource为包含WebAssembly二进制代码的TypedArray或ArrayBuffer
返回值 Promise对象为编译好的模块对象
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError;
如果编译失败,将抛出WebAssembly.CompileError
示例:
fetch("show_answer.wasm").then(response =>
  response.arrayBuffer();
).then(bytes =>
  WebAssembly.compile(bytes);
).then(module =>
  console.log(module.toString());      //"object WebAssembly.Module"
);
2)WebAssembly.instantiate()全局方法:
该方法有两种重载形式,第一种用于将WebAssembly二进制代码编译为模块,并创建其第一个实例。
语法 Promise <ResultObject> WebAssembly.instantiate (bufferSource, importObject)
参数 bufferSource为包含WebAssembly二进制代码的TypedArray或ArrayBuffer
importObject可选,将被导入新创建的实例中的对象,可以包含JavaScript方法
返回值 Promise对象,包含两个属性:
module:编译好的模块对象,类型为WebAssembly.Module
instance:上述module的第一个实例,类型为WebAssembly.Instance
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError;
如果操作失败,根据失败原因将抛出WebAssembly.CompileError、WebAssembly.LinkError和WebAssembly.RuntimeError等3种异常之一
示例:
fetch("show_answer.wasm").then(response =>
  response.arrayBuffer();
).then(bytes =>
  WebAssembly.instantiate(bytes);
).then(result=>
  console.log(result.instance.exports.showAnswer());
);
而另一种重载形式用于基于已编译好的模块创建实例。
语法 Promise <WebAssembly.Instance> WebAssembly.instantiate (module, importObject)
参数 module为已编译号的模块对象,类型为WebAssembly.Module
importObject可选,将被导入新创建的实例中的对象
返回值 Promise对象,新建的对象,类型为WebAssembly.Instance
异常 如果参数类型或结构不正确,将抛出TypeError;
如果操作失败,根据失败原因将抛出WebAssembly.CompileError、WebAssembly.LinkError和WebAssembly.RuntimeError等3种异常之一
在需要为一个模块创建多个实例时,使用这种重载形式可以省去多次编译模块的开销。示例:
fetch("show_answer.wasm").then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.compile(bytes)
).then(mod =>{
  WebAssembly.instantiate(mod).then(result=>
     console.log("Instance0:", result.exports.showAnswer()));
  WebAssembly.instantiate(mod).then(result=>
     console.log("Instance1:", result.exports.showAnswer()));
  }
);
3)WebAssembly.validate()全局方法:
该方法用于校验二进制代码.wasm是否合法。
语法 var valid=WebAssembly.validate(bufferSource)
参数 bufferSource为包含WebAssembly二进制代码的TypedArray或ArrayBuffer
返回值 布尔型,合法返回true,否则返回false
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError
4)WebAssembly.compileStreaming()全局方法:
该方法与WebAssembly.compile()方法类似,用于WebAssembly二进制代码的编译,区别是此方法使用流式底层源作为输入。
语法 Promise <WebAssembly.Module> WebAssembly.compileStreaming(source)
参数 source通常为fetch()方法返回的Response对象
返回值 Promise对象为编译好的模块对象
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError;
如果编译失败,将抛出WebAssembly.CompileError
示例:
WebAssembly.compileStreaming(fetch("show_answer.wasm")).then(module =>
  console.log(module.toString());      //"object WebAssembly.Module"
);
5)WebAssembly.instantiateStreaming()全局方法:
该方法与WebAssembly.instantiate()的第一种重载形式类似,用于将WebAssembly二进制代码编译为模块,并创建其第一个实例,区别是此方法使用流式底层源作为输入。
语法 Promise <ResultObject> WebAssembly.instantiateStreaming(source, importObject)
参数 source通常为fetch()方法返回的Response对象
返回值 Promise对象,包含两个属性:
module:编译好的模块对象,类型为WebAssembly.Module
instance:上述module的第一个实例,类型为WebAssembly.Instance
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError;
如果操作失败,根据失败原因将抛出WebAssembly.CompileError、WebAssembly.LinkError和WebAssembly.RuntimeError等3种异常之一
示例:
WebAssembly.instantiateStreaming(fetch("show_answer.wasm")).then(result =>
  console.log(result.instance.exports.showAnswer));
);
注意,WebAssembly.compileStreaming()和WebAssembly.instantiateStreaming()全局方法书写较为简单,但一些浏览器尚不支持,不建议使用。

2. WebAssembly.Module对象:

与模块对应的JavaScript对象为WebAssembly.Module,它是无状态的,可以被多次实例化。将WebAssembly二进制代码编译为模块需要消耗较大计算资源,因此获取模块的主要方法为异步方法WebAssembly.compile()和WebAssembly.instantiate()。

1)WebAssembly.Module()方法:
这是WebAssembly.Module的构造器方法,用于同步编译.wasm为模块。
语法 var module = new WebAssembly.Module(bufferSource)
参数 bufferSource为包含WebAssembly的二进制代码的TypedArray或ArrayBuffer
返回值 编译好的模块对象,类型为WebAssembly.Module
异常 如果传入的bufferSource不是TypedArray或ArrayBuffer,将抛出TypeError;
如果编译失败,将抛出WebAssembly.CompileError
示例:
fetch('hello.wasm').then(response =>
  Response.arrayBuffer()
).then(bytes => {
    var module = new WebAssembly.Module(bytes);
    console.log(module.toString());
  }
);
2)WebAssembly.Module.exports()方法:
用于获取模块的导出信息。
语法 var exports = WebAssembly.Module.exports(module)
参数 module为WebAssembly.Module对象
返回值 module的导出对象信息的数组
异常 如果module不是WebAssembly.Module,抛出TypeError
示例:
fetch('hello.wasm').then(response =>
  Response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module => {
     var exports=WebAssembly.Module.exports(module);
     for(var e in exports){
       console.log(exports[e]);
     }
  }
);
执行后,控制台输出:
{name: "hello", kind:" function"}
3)WebAssembly.Module.imports()方法:
用于获取模块的导入信息。
语法 var imports = WebAssembly.Module.imports(module)
参数 module为WebAssembly.Module对象
返回值 module的导入对象信息的数组
异常 如果module不是WebAssembly.Module,抛出TypeError
示例:
fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module => {
     var imports=WebAssembly.Module.imports(module);
     for(var i in imports){
       console.log(imports[i]);
     }
  }
);
执行后,控制台输出:
{module:"js", name: "print", kind:"function"}
{module:"js", name: "mem", kind:"memory"}
4)WebAssembly.Module.customSections()方法:
此方法用于获取模块中的自定义字符段section的数据。WebAssembly的二进制规范中允许带名字的自定义段,编译器可以在生成wasm代码过程中插入符号/调试信息等数据。但目前WebAssembly文本格式wat文件并不支持自定义段。
语法 var sections = WebAssembly.Module.customSections(module, secName)
参数 module为WebAssembly.Module对象;secName为欲获取的自定义段的名字
返回值 1个数组,其中包含所有名字与secName相同的自定义段,每个段都为ArrayBuffer
异常 如果module不是WebAssembly.Module,抛出TypeError
示例:
fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module => {
     var sections=WebAssembly.Module.customSections(module, "name");
     for(var i in sections){
       console.log(sections[i]);
     }
  }
);
5)缓存Module:
在部分浏览器中,如Firefox,Module可以像Blob一样被装入IndexedDB缓存,也可以在多个Worker之间传递。示例:
//worker.html
var sub_worker=new Worker("worker.js");
sub_worker.onmessage=function(event){
  console.log(event.data);
}
fetch('show_answer.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module =>
  sub_worker.postMessage(module)
);
//worker.js
onmessage=function(event){
  WebAssembly.instantiate(event.data).then(instance =>
     postMessage(' '+instance.exports.showAnswer());
  );
}

3. WebAssembly.Instance对象:

与实例对应的JavaScript对象为WebAssembly.Instance,获取实例的主要方法为WebAssembly.instantiate()。

1)WebAssembly.Instance():
这是实例的构造器方法,用于同步方式创建模块的实例。
语法 var instance = new WebAssembly.Instance(module, importObject)
参数 module为用于创建实例的模块;importObject可选,是新建实例的导入对象,可以包含JavaScript方法、WebAssembly.Memory、WebAssembly.Table和WebAssembly全局变量对象
返回值 新创建的实例
异常 如果传入参数的类型不正确,抛出TypeError;如果链接失败,将抛出WebAssembly.LinkError
如果Module中声明了导入对象,无论采用哪种方法实例化(WebAssembly.Instance()、WebAssembly.instantiate()、WebAssembly.instantiateStreaming())都必须提供完整的导入对象。若在实例化时不提供导入对象,实例化将失败。示例:
fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module =>
  WebAssembly.Instance(module);      //TypeError:
);
TypeError的提示为:Import argument must be present and must be an object
即使在实例化时提供了导入对象,若不完整,实例化仍然会失败。示例:
var wasmMem=new WebAssembly.Memory({initial:1});
var importObj={js:{/*print:printStr, /men:wasmMem}};
fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module =>
  WebAssembly.instance(module, importObj );
);
控制台会输出:Uncaught (in promise) LinkError: Import #0 module="js" function="print" error: function import requires a callable
因代码中只导入了内存,而没有导入js.print方法。
2)WebAssembly.Instance.prototype.exports属性:
WebAssembly.Instance的只读属性exports包含了实例的所有导出函数,即实例供外部JavaScript程序调用的接口。示例:
;;test.wat
(module
  (func (export "add") (param $i1 i32) (param $i2 i32) (result i32))
     get_local $i1
     get_local $i2
     i32.add
  )
  (func (export "inc") (param $i1 i32) (result i32))
     get_local $i1
     i32.const 1
     i32.add
  )
)
代码中导出了两个函数add()和inc(),分别执行加法和加1操作。执行JavaScript程序:
//exports.html
fetch('test.wasm').then(response =>
  response.arrayBuffer()
).then(bytes => {
  WebAssembly.compile(bytes);
).then(module =>
  WebAssembly.instantiate(module);
).then(instance =>{
     console.log(instance.exports);      //{add:f, inc:f}
     console.log(instance.exports.add(21,21));      //42
     console.log(instance.exports.inc(12));      //13
  }
);
3)创建WebAssembly.Instance的简洁方法:
因为在创建实例过程中,fetch()和compile()等操作会重复出现,定义一个fetchAndInstantiate()方法:
function fetchAndInstantiate(url, importObject){
  return fetch(url).then(response =>
     response.arrayBuffer()
  ).then(bytes =>
     WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
     results.instance
  );
}
这样,只需要一行代码即可完成实例的创建。示例:
fetchAndInstantiate("test.wasm", importObject).then(function(instance){
  //do sth. with instance...
});

4. WebAssembly.Memory对象:

与内存对应的JavaScript对象为WebAssembly.Memory,用于在程序中存储运行数据。内存对象本质上是一个一维数组,JavaScript和WebAssembly可通过内存相互传递数据。常见的用法是:在JavaScript中创建内存对象,该对象包含一个ArrayBuffer用于存储一维数组,模块实例化时将其通过导入对象导入WebAssembly中。一个内存对象可以导入多个实例,这使得多个实例可以通过共享一个内存对象的方式交换数据。

1)WebAssembly.Memory():
WebAssembly内存对象的构造器方法。
语法 var memory =new WebAssembly.Memory(memDesc)
参数 memDesc为新建内存的参数,包含下述属性:
initial为内存的初始容量,以页为单位,1页为64kB,65536字节
maximum可选,为内存的最大容量,以页为单位
返回值 新创建的内存对象
异常 如果传入参数的类型不正确,抛出TypeError;如果传入的参数包含maximum属性,但是其值小于initial属性,将抛出RangeError
2)WebAssembly.Memory.prototype.buffer属性:
WebAssembly.Memory的属性buffer用于访问内存对象的ArrayBuffer。示例:
//sum.html
var memory = new WebAssembly.Memory({initial:1, maximum:10});
fetchAndInstantiate("sum.wasm", {js:{mem:memory}}).then(
  function(instance){
     var i32=new Uint32Array(memory.buffer);
     for(var i=0;i<32;i++){
       i32[i]=i;
     }
     console.log(instance.exports.sum(0, 32));      //496
  }
);
上述代码创建了初始容量为1页的内存对象并导入实例,从内存的起始处开始依次填入了32个32位整型数,然后调用实例导出的sum()方法:
;;sum.wat
(module
  (import "js" "mem" (memory 1))
  (func (export "sum") (param $offset i32) (param $count i32) (result i32))
     (local $end i32) (local $re i32)
     get_local $offset
     get_local $count
     i32.const 4
     i32.mul
     i32.add
    set_local $end
    block
       loop
         get_local $offset
         get_local $end
         i32.eq
         br_if 1
         get_local $re
         get_local $ offset
         i32.load ;;load i32 from memory: offset
         i32.add
         set_local $re
         set_local $offset
         i32.const 4
         i32.add
         set_local $offset
         br 0
       end
    end
    get_local $re
  )
)
sum()按照输入参数,从内存中依次取出整型数,计算总和并返回。
上例是在JavaScript中创建内存导入WebAssembly,反向操作也是可以的,即在WebAssembly中创建内存,导出到JavaScript。示例:
;;export_mem.wat
(module
  (memory $mem 1)      ;;define $mem, initSize=1 page
  (export "memory" (memory $mem))      ;;$export $mem as "memory"
  (func (export "fibonacci") (param $count i32) (result i32)
     (local $i i32) (local $a i32) (local $b i32)
     i32.const 0
     i32.const 1
     ......
  )
)
代码中在WebAssembly中定义了初始容量为1页的内存,并将其导出;同时导出了名为fibonacci()的方法,用于根据输入的数列长度来生成斐波拉契数列。JavaScript代码为:
// export_mem.html
fetchAndInstantiate("export_mem.wasm").then(
  function(instance){
     console.log(instance.exports);      //{memory:Memory, fibonacci:f}
     console.log(instance.exports.memory);      //Memory{}
     console.log(instance.exports.memory.buffer.byteLength);      //65536
     instance.exports.fibonacci(10);
     var i32=new Uint32Array(instance.exports.memory.buffer);
     var s='';
     for(var i=0;i<10;i++){
       s+=i32[i]+' ';
     }
     console.log(s);      //1 1 2 3 5 8 13 21 34 55
  }
);
如果WebAssembly内部创建了内存,但是实例化时又导入了内存对象,那么WebAssembly会使用内部创建的内存,而不是外部导入的内存。
3)WebAssembly.Memory.prototype.grow()方法:
该方法用于扩大内存对象的容量。
语法 var pre_size = memory.grow(number)
参数 number为内存对象扩大的量,以页为单位
返回值 内存对象扩大前的容量,以页为单位
异常 如果内存对象构造时指定了最大的量,且要扩至的容量已超过指定的最大容量,则抛出RangeError
内存扩大后,其扩大前的数据将复制到扩大后的buffer中。示例:
//grow.html
fetchAndInstantiate("exports_mem.wasm").then(
  function(instance){
     instance.exports.fibonacci(10);
     var i32=new Uint32Array(instance.exports.memory.buffer);
     console.log("mem size before grow():", instance.exports.memory.buffer.byteLength);
     var s="mem content before grow():";
     for(var i=0;i<10;i++){
       s+=i32[i]+' ';
     }
     console.log(s);      //1 1 2 3 5 8 13 21 34 55
    instance.exports.memory.grow(99);
     var i32=new Uint32Array(instance.exports.memory.buffer);
     console.log("mem size after grow():", instance.exports.memory.buffer.byteLength);
     var s="mem content after grow():";
     for(var i=0;i<10;i++){
       s+=i32[i]+' ';
     }
     console.log(s);      //1 1 2 3 5 8 13 21 34 55
  }
);
上述程序执行后,控制台输出:
mem size before grow(): 65536
mem content before grow():1 1 2 3 5 8 13 21 34 55
mem size after grow(): 6553600
mem content after grow():1 1 2 3 5 8 13 21 34 55
可见,在JavaScript看来,数据并未丢失。
//grow2.html
var memory=new WebAssembly.Memory({initial:1, maximum:10});
fetchAndInstantiate("sum.wasm", {js:{mem:memory}}).then(
  function(instance){
     var i32=new Uint32Array(memory.buffer);
     for(var i=0;i<32;i++){
       i32[i]=i;
     }
     console.log("mem size before grow():", memory.buffer.byteLength);     //65536
     console.log("sum:", instance.exports.sum(0,32));     //496
     memory.grow(9);
    console.log("mem size after grow():", memory.buffer.byteLength);     //655360
    console.log("sum:", instance.exports.sum(0,32));     //496
  }
);
上述程序执行后,控制台输出:
mem size before grow(): 65536
sum:496
mem size after grow(): 655360
sum:496
可见,在WebAssembly看来,数据也未丢失。但grow()有可能引发内存对象的ArrayBuffer重分配,从而导致引用它的TypedArray失效。示例:
//grow3.html
fetchAndInstantiate("exports_mem.wasm").then(
  function(instance){
     instance.exports.fibonacci(10);
     var i32=new Uint32Array(instance.exports.memory.buffer);
     var s="i32 content before grow():";
     for(var i=0;i<10;i++){
       s+=i32[i]+' ';
     }
     console.log(s); //1 1 2 3 5 8 13 21 34 55
    instance.exports.memory.grow(99);
     //var i32=new Uint32Array(instance.exports.memory.buffer);
     var s=" i32 content after grow():";
     for(var i=0;i<10;i++){
       s+=i32[i]+' ';
     }
     console.log(s); //undefined...
  }
);
代码中注释掉了i32=new Uint32Array(instance.exports.memory.buffer),由于grow()后Memory.buffer重分配,导致之前创建的i32失效,控制台输出:
i32 content before grow():1 1 2 3 5 8 13 21 34 55
i32 content after grow(): undefined undefined undefined undefined undefined undefined undefined undefined undefined undefined
因此通过TypedArray读写Memory.buffer时,必须随用随创建。

5. WebAssembly.Table对象:

在WebAssembly的设计思想中,与执行过程相关的代码段/栈等元素与内存是完全分离的,这与通常体系结构中代码段/数据段/堆/栈全都处于统一编址内存空间的情况完全不同,函数地址对WebAssembly程序来说不可见,更不能将其当作变量来传递、修改及调用。而函数指针是很多高级语言必不可少的特性,回调、虚函数等都依赖于此,因此有了表格Table。
表格是保存了对象引用的一维数组,目前可以保存在表格中的元素只有函数引用,目前每个实例只能包含一个表格,相关的WebAssembly指令隐含的操作对象均为当前实例拥有的唯一表格。表格不占用内存地址空间,二者是相互独立的。
使用函数指针的本质行为是通过变量(即函数地址)找到并执行函数。在WebAssembly中,当一个函数被存入表格中后,即可通过它在表格中的索引来调用它,这就间接实现了函数指针的功能,用来寻找函数的变量是在表格中的索引。WebAssembly使用这种方式实现函数指针主要是为了安全,避免WebAssembly代码的执行范围不可控,如调用非法地址导致浏览器崩溃,或下载恶意程序导入运行等。在WebAssembly当前设计框架下,保存在表格中的函数地址对WebAssembly代码不可见,也无法修改,而只能通过表格索引来调用,并且运行时的栈数据并不保存在内存对象中,因此彻底断绝了WebAssembly代码越界执行的可能,最坏的情况不过是在内存对象中产生一堆错误数据。
与WebAssembly表格对应的JavaScript对象为WebAssembly.Table。

1)WebAssembly.Table():
表格的构造器方法。
语法 var table =new WebAssembly.Table(tableDesc)
参数 tableDesc为新建表格的参数,包含以下属性:
element:存入表格中的元素类型,当前只能为anyfunc,即函数引用
initial:表格的初始容量
maximum可选,为表格的最大容量,指表格中能容纳的函数索引的个数
返回值 新创建的表格对象
异常 如果传入参数的类型不正确,将抛出TypeError;如果传入的参数包含maximum属性,但是值小于initial属性,将抛出RangeError。
2)WebAssembly.Table.prototype.get()方法:
该方法用于获取指定索引位置的函数引用。
语法 var funcRef = table.get(index)
参数 index为欲获取的函数引用的索引
返回值 WebAssembly函数的引用
异常 如果index大于等于表格当前的容量,抛出RangeError
示例:
//import_table.html
var table =new WebAssembly.Table({element:'anyfunc', initial:2});
console.log(table);
console.log(table.get(0));
console.log(table.get(1));
fetchAndInstantiate("import_table.wasm",{js:{table:table}}).then(
  function(instance){
     console.log(table.get(0));
     console.log(table.get(1));
  }
);
;;import_table.wat
(module
  (import "js" "table" {table 2 anyfunc})
  (elem (i32.const 0) $func1 $func0) ;;set $func0 $func1 to table
  (func $func0 (result i32)
     i32.const 13
  )
  (func $func1 (result i32)
     i32.const 42
  )
)
在JavaScript中创建了初始容量为2的表格并为其导入实例,在WebAssembly的elem段将func1、func0存入表格中。上述程序执行后控制台输出:
Table {}
null
null
f 1() {[native code]}
f 0() {[native code]}
table.get()的返回值类型与Instance导出函数是一样的,意味着可以调用它。修改为:
// import_table2.html
var table =new WebAssembly.Table({element:'anyfunc', initial:2}) ;
fetchAndInstantiate("import_table.wasm",{js:{table:table}}).then(
  function(instance){
     var f0=table.get(0);
     console.log(table.get(0));
     console.log(table.get(1));
  }
);
示例中func0和func1并未导出,而是将其存入表格,JavaScript可以通过表格对其进行调用。
3)WebAssembly.Table.prototype.length属性:
length属性用于获取表格的当前容量。
表格既可以在JavaScript中创建后导入WebAssembly,也可以在WebAssembly中创建后导出到JavaScript,并且优先使用模块内部创建的表格。示例:
;;export_table.wat
(module
  (table $tab 2 anyfunc)      ;;define $tab initSize=2
  (export "table" (table $tab))      ;;export $tab as table
  (elem (i32.const 0) $func1 $func0)      ;;set $func0 $func1 to table
  (func $func0 (result i32)
     i32.const 13
  )
  (func $func1 (result i32)
     i32.const 42
  )
)
// import_table.html
var table =new WebAssembly.Table({element:'anyfunc', initial:1}) ;
fetchAndInstantiate("import_table.wasm",{js:{table:table}}).then(
  function(instance){
     console.log(table.get(0));      //null
     console.log(instance.exports);
     console.log("instance.exports.table.length:"+ instance.exports.table.length);
     for(var i=0;i<instance.exports.table.length;i++){
       console.log(instance.exports.table.get(i));
       console.log(instance.exports.table.get(i) ());
    }
  }
);
程序执行后控制台输出为:
null
{table: Table}
instance.exports.table.length: 2
f 1() {[native code]}
42
f 0() {[native code]}
13
4)在WebAssembly内部使用表格:
表格中的函数可以被JavaScript调用,但更主要的作用还是被WebAssembly调用。示例:
;;call_by_index.wat
(module
  (import "js" "table" (table 2 anyfunc))
  (elem (i32.const 0) $func13 $func42)      ;;set $func13 $func42 to table
  (type $type_0 (func (param $i i32) (result i32)))      ;;define func signatures
  (func $func13 (param $i i32) (result i32)
     i32.const 13
     get_local $i
     i32.add
  )
  (func $func42 (param $i i32) (result i32)
     i32.const 42
     get_local $i
     i32.add
  )
  (func (export "call_by_index") (param $id i32) (param $input i32) (result i32)
     get_local $input      ;;push param into stack
     get_local $id      ;;push id into stack
     call_indirect (type $type_0)      ;;call table:id
  )
)
上述代码中定义了2个函数plus13和plus42,并将其分别存入表格的0和1处。(type $type_0 (func (param $i i32) (result i32)))语句定义了将要被调用的函数的签名,即函数的参数列表及返回值类型。WebAssembly在执行函数调用时,调用方与被调用方需要严格匹配签名,若签名不匹配会抛出WebAssembly.RuntimeError错误。
导出函数call_by_index()调用表格中的第$id个函数并返回。在JavaScript中调用方法为:
//call_by_index.html
var table =new WebAssembly.Table({element:'anyfunc', initial:2}) ;
fetchAndInstantiate("call_by_index.wasm",{js:{table:table}}).then(
  function(instance){
     console.log(instance.exports.call_by_index(0,10));      //23
     console.log(instance.exports.call_by_index(1,10));      //52
  }
);
在WebAssembly中使用call_indirect时,如果试图调用索引范围超过表格容量的函数,将抛出WebAssembly.RuntimeError错误。
5)多个实例通过共享表格及内存协同工作:
表格也可以被导入多个实例从而被多个实例共享。示例中将载入两个模块并各自创建一个实例,其中一个将在内存中生成斐波拉契数列的函数,另一个将调用前者,计算数列的和并输出。计算斐波拉契数列的模块为:
;;fibonacci.wat
(module
  (import "js" "mem" (memory 1))
  (import "js" "table" (table 1 anyfunc))
  (elem (i32.const 0) $fibonacci) ;;set $fibonacci to table:0
  (func $fibonacci (param $count i32)
     (local $i i32) (local $a i32) (local $b i32)
     i32.const 0
     i32.const 1
     i32.store
     i32.const 4
     i32.const 1
     i32.store
     i32.const 1
     set_local $a
     i32.const 1
     set_local $b
     i32.const 8
     set_local $i
     get_local $count
     i32.const 4
     i32.mul
     set_local $count
     block
       loop
         get_local $i
         get_local $count
         i32.ge_s
         br_if 1
         get_local $a
         get_local $b
         i32.add
         set_local $b
         get_local $b
         get_local $a
         i32.sub
         set_local $a
         get_local $i
         get_local $b
         i32.store
         get_local $i
         i32.const 4
         i32.add
         set_local $i
         br 0
       end
    end
  )
)
求和模块为:
;;sumfib.wat
(module
  (import "js" "mem" (memory 1))
  (import "js" "table" (table 1 anyfunc))
  (type $type_0 (func (param i32)))      ;;define func signatures
  (func (export "sumfib") (param $count i32) (result i32)
     (local $offset i32) (local $end i32) (local $re i32)
     ;;call table:element 0:
     (call_indirect (type $type_0) (get_local $count) (i32.const 0))
     i32.const 0
     set_local $offset
     get_local $count
     i32.const 4
     i32.mul
     set_local $end
     block
       loop
         get_local $offset
         get_local $end
         i32.eq
         br_if 1
         get_local $re
         get_local $offset
         i32.load      ;;load i32 from memory:offset
         i32.add
         set_local $re
         get_local $offset
         i32.const 4
         i32.add
         set_local $offset
         br 0
       end
    end
    get_local $re
  )
)
代码中(call_indirect (type $type_0) (get_local $count) (i32.const 0))按照函数签名将参数$count压入了栈,然后通过call_indirect调用了表格中索引0处的函数,后续的代码计算并返回了数列的和。JavaScript代码为:
//sum_fibonacci.html
var memory =new WebAssembly.Memory({initial:1, maximum:10}) ;
var table =new WebAssembly.Table({element:'anyfunc', initial:2}) ;
Promise.all([
  fetchAndInstantiate("sumfib.wasm",{js:{table:table,mem:memory}}),
  fetchAndInstantiate("fibonacci.wasm",{js:{table:table,mem:memory}})
]).then(function(results){
  console.log(results[0].exports.sumfib(10));      //143
});
示例中都是在WebAssembly中修改表格,实际上运行时也可以在JavaScript中修改。
6)WebAssembly.Table.prototype.set()方法:
该方法用于将一个WebAssembly引用存入表格的指定索引处。
语法 table.set(index, value)
参数 index为表格索引;value为函数引用,函数指的是实例导出的函数或保存在表格中的函数
返回值
异常 如果index大于等于表格当前的容量,抛出RangeError;如果value为null,或者不是合法的函数引用,抛出TypeError
前面的斐波拉契数列求和示例可修改为:
;;fibonacci2.wat
(module
  (import "js" "mem" (memory 1))
  (import "js" "table" (table 1 anyfunc))
  (func (export "fibonacci") (param $count i32)
    ......
//sum_fibonacci2.html
var memory =new WebAssembly.Memory({initial:1, maximum:10}) ;
var table =new WebAssembly.Table({element:'anyfunc', initial:2}) ;
Promise.all([
  fetchAndInstantiate("sumfib.wasm",{js:{table:table,mem:memory}}),
  fetchAndInstantiate("fibonacci2.wasm",{js:{table:table,mem:memory}})
]).then(function(results){
  console.log(results[1].exports);      //{ fibonacci : f}
  table.set(0, results[1].exports.fibonacci);
  console.log(results[0].exports.sumfib(10));      //143
});
代码中,fibonacci()函数是在JavaScript中动态导入表格的。
表格和内存的跨实例共享,加上表格在运行时可变,使WebAssembly可以实现非常复杂的动态链接。
7)WebAssembly.Table.prototype.grow()方法:
该方法用于扩大表格的容量。
语法 table.set(index, value)
参数 index为表格索引;value为函数引用,函数指的是实例导出的函数或保存在表格中的函数
返回值
异常 如果index大于等于表格当前的容量,抛出RangeError;如果value为null,或者不是合法的函数引用,抛出TypeError
表格的grow()方法也不会丢失扩容前的数据。

6. 错误类型:

除了常规的TypeError和RangeError,WebAssembly的异常还有WebAssembly.CompileError、WebAssembly.LinkError、WebAssembly.RuntimeError,这3种异常对应于WebAssembly程序的生命周期的3个阶段,即编译、链接(实例化)和运行。
编译阶段的主要任务是将WebAssembly二进制代码编译为模块,转码方式取决于虚拟机的实现。在此过程中,若WebAssembly二进制代码的合法性无法通过检查,则抛出WebAssembly.CompileError。
在链接阶段,将创建实例,并链接导入/导出对象。导致该阶段抛出WebAssembly.LinkError 异常的典型情况是导入对象的不完整,导入对象中缺失了模块需要导入的函数、内存或表格。另外,在实例初始化时,可能会执行内存/表格的初始化,在此过程中若内存/表格的容量不足以容纳装入的数据/函数引用,也会导致WebAssembly.LinkError。
运行时抛出WebAssembly.RuntimeError的情况较多,常见的主要有:

  • 内存访问越界,即试图读写超过内存当前容量的地址空间
  • 调用函数时,调用方与被调用方签名不匹配
  • 表格访问越界,即试图调用/修改大于等于表格容量的索引处的函数
在运行时有一种产生异常的情况,是目前JavaScript的Number类型无法无损地表达64位整型数。虽然WebAssembly支持i64类型的运算,但是与JavaScript对接的导出函数不能使用i64类型作为参数或返回值,一旦在JavaScript中调用参数或返回值类型为i64的WebAssembly函数,将抛出TypeError。

附录:WebAssembly示例中的一些JavaScript用法:

1. console对象:

通过console对象可以实现输出信息功能。console对象是无需导入的内置对象,主要用于输出对象属性、调试、简单时间测量、简单日志输出、assert断言等场合。不同的浏览器中,console对象可能扩展了多种方法,主流的方法有:

console.log([data][, ...args]) 日志信息,以标准格式输出所有参数,末尾换行
console.info([data][, ...args]) 一般信息,以标准格式输出所有参数,末尾换行
console.warn([data][, ...args]) 警告信息,以警告格式输出所有参数,末尾换行
console.error([data][, ...args]) 错误信息,以错误格式输出所有参数,末尾换行
console.assert(value[, message][, ...args]) 断言,如果参数为假则抛出AssertionError
console.dir(obj[, options]) 检查对象,输出对象的所有属性
console.time(label) 开始测量时间
console.timeEnd(label) 结束测量时间,根据label的计时器统计经过时间
1)格式化输出符:
console.log等方法还支持类似C语言中printf函数的格式化输出:
%s 输出字符串 %d 输出数值类型,整数或浮点数 %i 输出整数
%f 输出浮点数 %j 输出JSON格式 %% 输出百分号
示例:
const code=502;
console log('error #%d', code);
2)断言:
断言对于编写健壮代码很有帮助,如开方前置断言确保输入参数为正整数:
function sqrt(x){
  console.assert(x>=0);
  return Math.sqrt(x);
}
前置断言一般用于确保是合法输入,而后置断言则用于保证产生了合法的输出。示例:
function joinStr(a,b){
  let s=a+b;
  console.assert(s.length>=a.length);
  console.assert(s.length>=b.length);
  return s;
}
代码中的断言使连接后的字符串长度大于或等于之前的任何一个字符串的长度。不应在断言中放置具有功能性逻辑的代码,如i++,以免在不同环境出现不同的结果。

2. 函数的表达式使用和箭头函数:

函数就是一组语句的集合,便于重复使用,并支持输入一些动态参数。示例:
function sum(n){
  var result=0;
  for(var i=0; i<=n; i++){
     result+=i;
  }
  return result;
}
这是一个计算从0到输出上界数值n之间整数的连加的函数。可以使用另外一种方式:
var sum=function (n){
  var result=0;
  for(var i=0; i<=n; i++){
     result+=i;
  }
  return result;
}
上述代码中sum更像一个变量,但变量中保存的是一个函数,保存了函数的sum变量可以当作函数来使用,也称为函数表达式。
可以使用箭头函数来简化函数表达式的书写:
var sum=(n) => {
  var result=0;
  for(var i=0; i<=n; i++){
     result+=i;
  }
  return result;
}
其中,箭头=>前面的(n)对应函数参数,箭头后面的{}内容表示函数体。如果函数的参数只有一个可以省略小括号,而函数体只有一个语句也可以省略花括号。注意,箭头函数中的this是被绑定到创建函数时的this对象上。

3. Promise对象:

JavaScript是单线程的编程语言,通过异步、回调函数来处理各种事件。如果要处理多个有先后顺序的事件,将会出现多次嵌套回调的情况,被开发人员称为回调地狱。
Promise对象是通过将代表异步操作最终完成或者失败的操作封装到了对象中,其本质上是一个绑定了回调的对象,可以适当缓解多层回调函数的问题。示例:
function fetchImage(path){
  return new Promise((resolve, reject) => {
     const m= new Image();
     m.onload=() => {resolve(image)}
     m.onerror=() => {reject(new Error(path))}
     m.src=path;
  })
}
代码中,fetchImage()返回的是一个Promise对象,Promise对象的构造函数的参数是一个函数,有resolve和恶reject两个参数,分别表示操作成功或失败。内部加载一个图像,加载成功调用resolve函数,失败调用reject函数报错。
而返回Promise对象的then方法可以分别指定resolved和rejected回调函数:
const makeFetchImage=() => {
  fetchImage("/static/logo.png").then(() => {
     console.log('done');
  })
);
makeFetchImage();
上述代码中,Promise对象的then方法只提供了resolved回调函数,因此成功获取图像后将输出done字符。
Promise的构造函数、返回对象的then方法等地方依然要处理回调函数,因此ES2017标准又引入了async和await关键字来简化Promise的处理:
await关键字只能在async定义的函数内使用。async函数会隐式地返回一个Promise对象,其resolve值就是函数的return值。这样改写的上述函数为:
const makeFetchImage=async () => {
  await fetchImage("/static/logo.png");
  console.log('done');
);
makeFetchImage();
这样,await关键字将异步等待fetchImage函数的完成,如果图像 下载成功,那么后面的输出语句将继续执行。
基于async和await关键字,可以以顺序方式来编写有顺序依赖关系的异步程序。示例:
async function delay(ms){
  return new Promise((resole) =>{
     setTimeout(resole, ms);
  });
}
async function main(...args){
  for(const arg of args){
     console.log(arg);
     await delay(300);
  });
}
main('A', 'B', 'C');
上述代码中,main函数依次输出参数中的每一个字符串,在输出字符串之后休眠一定时间再输出下一个字符串。用于休眠的delay函数返回的是Promise对象,main函数通过await关键字来异步等等delay的完成。

4. 二进制数组:

二进制数组ArrayBuffer、TypedArray是JavaScript操作二进制数据的接口,在ES2015标准中已纳入语言规范。基于二进制数组,JavaScript可以直接操作二进制内存。
二进制数组由3类对象组成:

  • ·ArrayBuffer:代表内存中的一段空间,要操作该内存空间要通过基于其创建的TypedArray或DataView进行
  • ·TypedArray:是Uint8Array、Float32Array等9种二进制类型数组的统称,其底层是ArrayBuffer对象,通过它们可以读写底层的二进制数组
  • ·DataView:用于处理类似C语言中结构体类型的数据,其中每个元素的类型可能并不相同
二进制数组是JavaScript处理运算密集型应用时经常用到的,也是网络数据交换和跨语言数据交换中的有效手段。WebAssembly模块中的内存也是一种二进制数组。
使用方法示例:
let buffer=new ArrayBuffer(1024);
// Uint8Array
let u8Array=new Uint8Array(buffer, 0, 100);
for(int i=0; i<u8Array.length; i++){
  u8Array[i]=255;
}
// Uint32Array
let u32Array=new Uint32Array(buffer, 100);
for(int i=0; i<u32Array.length; i++){
  u32Array[i]=0xffffffff;
}
TypedArray对象的buffer属性返回底层的ArrayBuffer对象,为只读属性;byteOffset属性返回当前二进制数组对象从底层的ArrayBuffer对象的第几个字节开始,为只读属性;byteLength属性返回当前二进制属性对象的内存字节大小,也是只读属性。
TypedArray视图对应的二进制数组的每个元素的类型和大小都是一样的,不便于对应每个成员的内存大小可能不同的结构体。

Copyright@dwenzhao.cn All Rights Reserved  备案号:粤ICP备15026949号
联系邮箱:dwenzhao@163.com QQ:1608288659