书名:WebAssembly标准入门
ISBN:978-7-115-50059-5
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 柴树杉 丁尔男
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
WebAssembly是一种新兴的网页虚拟机标准,它的设计目标包括高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。本书详尽介绍了WebAssembly程序在JavaScript环境下的使用方法、WebAssembly汇编语言和二进制格式,给出了大量简单易懂的示例,同时以C/C++和Go语言开发环境为例,介绍了如何使用其他高级语言开发WebAssembly模块。
本书适合从事高性能Web前端开发、跨语言Web应用开发的技术人员学习参考,也可以作为WebAssembly 标准参考手册随时查阅。
某一天,有朋友向我推荐了一项新技术——WebAssembly。我认为这是一项值得关注的技术。
说WebAssembly是一门编程语言,但它更像一个编译器。实际上它是一个虚拟机,包含了一门低级汇编语言和对应的虚拟机体系结构,而WebAssembly这个名字从字面理解就说明了一切——Web的汇编语言。它的优点是文件小、加载快、执行效率非常高,可以实现更复杂的逻辑。
其实,我觉得出现这样的技术并不令人意外,而只是顺应了潮流,App的封闭系统必然会被新一代Web OS取代。但现有的Web开发技术,如JavaScript,前端执行效率和解决各种复杂问题的能力还不足,而WebAssembly的编译执行功能恰恰能弥补这些不足。WebAssembly标准是在谋智(Mozilla)、谷歌(Google)、微软(Microsoft)、苹果(Apple)等各大厂商的大力推进下诞生的,目前包括Chrome、Firefox、Safari、Opera、Edge在内的大部分主流浏览器均已支持WebAssembly。这使得WebAssembly前景非常好。
WebAssembly是Web前端技术,具有很强的可移植性,技术的潜在受益者不局限于传统的前端开发人员,随着技术的推进,越来越多的其他语言的开发者也将从中受益。如果开发者愿意,他们可以使用C/C++、Go、Rust、Kotlin、C#等开发语言来写代码,然后编译为WebAssembly,并在Web上执行,这是不是很酷?它能让我们很容易将用其他编程语言编写的程序移植到Web上,对于企业级应用和工业级应用都是巨大利好。
WebAssembly的应用场景也相当丰富,如Google Earth,2017年10月Google Earth开始在Firefox上运行,其中的关键就是使用了WebAssembly;再如网页游戏,WebAssembly能让HTML5游戏引擎速度大幅提高,国内一家公司使用WebAssembly后引擎效率提高了300%。
WebAssembly作为一种新兴的技术,为开发者提供了一种崭新的思路和工作方式,未来是很有可能大放光彩的,不过目前其相关的资料和社区还不够丰富,尽管已经有一些社区开始出现了相关技术文章,CSDN上也有较多的文章,但像本书这样全面系统地介绍WebAssembly技术的还不多,甚至没有。本书的两位作者都是有10多年经验的一线开发者,他们从WebAssembly概念诞生之初就开始密切关注该技术的发展,其中柴树杉是Emscripten(WebAssembly的技术前身之一)的首批实践者,丁尔男是国内首批工程化使用WebAssembly的开发者。
2018年7月,WebAssembly社区工作组发布了WebAssembly 1.0标准。现在,我在第一时间就向国内开发者介绍和推荐本书,是希望开发者能迅速地了解和学习新技术,探索新技术的价值。
蒋涛
CSDN创始人、总裁,极客帮创始合伙人
第0章回顾了WebAssembly的诞生背景。
第1章针对不熟悉JavaScript的读者,介绍本书将使用到的部分JavaScript基础知识,包括console对象、箭头函数、Promise、ArrayBuffer等。对JavaScript很熟悉的读者可以跳过本章。
第2章通过两个简单的小例子展示WebAssembly的基本用法。
第3章和第4章分别从外部和内部两个角度详细介绍WebAssembly,前者着重于相关的JavaScript对象,后者着重于WebAssembly虚拟机的内部运行机制。因为WebAssembly跨越了两种语言、两套运行时结构,所以读者阅读第3章时可能会感到不明就里——为什么多数指令中没有操作数?所谓的“栈式虚拟机”到底是什么?类似的疑问都将在第4章中得到解答。在写作本书时,我们期望读者读完第4章后复读第3章时,能有豁然开朗的感觉。
第5章介绍WebAssembly汇编的二进制格式。若想尝试自己实现WebAssembly虚拟机或者其他语言到WebAssembly的编译器,掌握二进制汇编格式是必需的。即使不开展类似的项目,通过阅读本章也可以加深对WebAssembly虚拟机架构的整体认识,厘清各种全局索引的相互关系。
第 6 章和第 7 章分别以 C/C++和 Go 语言为例,介绍如何使用高级语言来开发WebAssembly应用。WebAssembly全手工编写.wat文件实现大型模块的机会并不会很多。在实际工程中,WebAssembly作为一门类汇编语言,更多的是作为其他语言的编译目标而存在。目前C/C++、Rust、Go、Lua、Kotlin、C#均已支持WebAssembly,可以预见这一支持列表将越来越长。
附录列出了现有的200多条WebAssembly指令及其作用。
感谢蒋涛先生为本书作序,感谢所有为WebAssembly标准诞生作出努力的朋友。其中特别感谢Emscripten和asm.js的作者,没有他们的灵感,WebAssembly标准就不可能诞生。感谢WebAssembly工作组的专家,是他们的工作让我们看到了草案1.0的成果。感谢各大浏览器厂商为WebAssembly提供的支持。最后,感谢人民邮电出版社的杨海玲编辑,没有她,本书就不可能出版。谢谢大家!
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供书中的源代码,要获得以上配套资源,请在异步社区本书页面中点击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
WebAssembly,一次编写到处运行。
——yjhmelody
本章将快速展示几个小例子,借此对WebAssembly形成一个大致的印象,掌握一些基本的用法。在后续的章节将会系统、深入地学习各个技术细节。
“工欲善其事,必先利其器。”在正式开始之前,需要先准备好兼容WebAssembly的运行环境以及WebAssembly文本格式转换工具集。
常见桌面版浏览器及Node.js对WebAssembly特性的支持情况如表2-1所示,表内的数字表示浏览器版本。
表2-1
WebAssembly特性 |
Chrome |
Edge |
Firefox |
IE |
Opera |
Safari |
Node.js |
---|---|---|---|---|---|---|---|
基本支持 |
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
不支持 |
不支持 |
62 |
不支持 |
不支持 |
不支持 |
不支持 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
61 |
16 |
58 |
不支持 |
47 |
不支持 |
不支持 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
|
61 |
16 |
58 |
不支持 |
47 |
不支持 |
不支持 |
|
57 |
16 |
52 |
不支持 |
44 |
11 |
8.0.0 |
常见移动版(Android及iOS)浏览器对WebAssembly特性的支持情况如表2-2所示,表内的数字表示浏览器版本。
表2-2
WebAssembly特性 |
Webview |
Chrome |
Edge |
Firefox |
Opera |
Safari |
Samsung Internet |
---|---|---|---|---|---|---|---|
基本支持 |
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
不支持 |
不支持 |
不支持 |
62 |
未知 |
不支持 |
不支持 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
61 |
61 |
不支持 |
58 |
未知 |
不支持 |
不支持 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
|
61 |
61 |
不支持 |
58 |
未知 |
不支持 |
不支持 |
|
57 |
57 |
支持 |
52 |
未知 |
11 |
7.0 |
可见大多数现代浏览器都已支持WebAssembly,可以在列表中任选一种支持WebAssembly的浏览器来运行测试本书的例程。
当浏览器启用WebAssembly模块时,会强行启用同源及沙盒等安全策略。因此本书的WebAssembly例程需通过http网页发布后方可运行。本书的例程目录中有一个名为“py_simple_server.bat”的批处理文件,该文件用于在Windows操作系统下使用python
将当前目录发布为http服务;当然也可以使用Nginx、IIS、Apache或任意一种惯用的工具来完成该操作。
一般来说,浏览器加载并运行的WebAssembly程序是二进制格式的WebAssembly汇编代码,文件扩展名通常为.wasm。由于二进制文件难以阅读编辑,WebAssembly提供了一种基于S-表达式的文本格式,文件扩展名通常为.wat。下面是一个WebAssembly文本格式的例子:
(module
(import "console" "log" (func $log (param i32)))
(func $add (param i32 i32)
get_local 0
get_local 1
i32.add
call $log
)
(export "add" (func $add))
)
上述程序定义了一个名为$add
的函数,该函数将两个i32
类型的输入参数相加,并使用由外部JavaScript导入的log
函数将结果输出。最后该add
函数被导出,以供外部JavaScript环境调用。
WebAssembly文本格式(.wat)与WebAssembly汇编格式(.wasm)的关系类似于宏汇编代码与机器码的关系。如同.asm文件向机器码转换需要使用nasm这样的编译工具一样,.wat文件向.wasm文件的转换需要用到wabt工具集,该工具集提供了.wat与.wasm相互转换的编译器等功能。
wabt工具集可从GitHub上下载获取。按照页面说明下载编译后将获得wat2wasm
程序。在命令行下执行
wat2wasm input.wat -o output.wasm
即可将WebAssembly文本格式文件input.wat编译为WebAssembly汇编格式文件output.wasm。
使用-v
选项调用wat2wasm
可以查看汇编输出,例如将前述的代码保存为test.wat并执行:
wat2wasm test.wat -v
终端输出如图2-1所示。
图2-1
第5章介绍WebAssembly二进制格式时给出的示例代码中含有二进制的部分,除此之外其他章节的WebAssembly代码均以文本格式(.wat)提供以便于阅读。
很多语言的入门教程都始于“Hello, World!”例程,但是对于WebAssembly来说,一个完整的“Hello, World!”程序仍然过于复杂,因此,我们将从一个更简单的例子开始。本节的例程名为ShowMeTheAnswer,其中WebAssembly代码位于show_me_the_answer.wat中,如下:
(module
(func (export "showMeTheAnswer") (result i32)
i32.const 42
)
)
上述代码定义了一个返回值为42
(32位整型数)的函数,并将该函数以showMeTheAnswer
为名字导出,供JavaScript调用。
JavaScript代码位于show_me_the_answer.html中,如下:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Show me the answer</title>
</head>
<body>
<script>
fetch('show_me_the_answer.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(result =>
console.log(result.instance.exports.showMeTheAnswer()) //42
);
</script>
</body>
</html>
上述代码首先使用fetch()
函数获取WebAssembly汇编代码文件,将其转为ArrayBuffer
后使用WebAssembly.instantiate()
函数对其进行编译及初始化实例,最后调用该实例导出的函数showMeTheAnswer()
并打印结果。
将例程目录发布后,通过浏览器访问show_me_the_answer.html,浏览器控制台应输出结果42
,如图2-2所示。
图2-2
JavaScript代码运行在JavaScript虚拟机上,相对地,WebAssembly代码也运行在其特有的虚拟机上。参照2.1节内容,大部分最新的浏览器均提供了WebAssembly虚拟机,然而WebAssembly代码并非只能在浏览器中运行,Node.js 8.0之后的版本也能运行WebAssembly;更进一步来说,WebAssembly虚拟机甚至可以脱离JavaScript环境的支持。不过正如其名,WebAssembly的设计初衷是运行于网页之中,因此本书绝大部分内容均是围绕网页应用展开。
在深入WebAssembly内部运行机制之前,我们暂时将其看作一个黑盒,这个黑盒需要通过一些手段来与外部环境——调用WebAssembly的JavaScript网页程序等进行交互,这些手段可以抽象为以下几个关键概念。
模块是已被编译为可执行机器码的二进制对象,模块可以简单地与操作系统中的本地可执行程序进行类比,是无状态的(正如Windows的.exe文件是无状态的)。模块是由WebAssembly二进制汇编代码(.wasm)编译而来的。
在WebAssembly代码看来,内存是一段连续的空间,可以使用load
、store
等低级指令按照地址读写其中的数据(时刻记住WebAssembly汇编语言是一种低级语言),在网页环境下,WebAssembly内存是由JavaScript中的ArrayBuffer
对象实现的,这意味着,整个WebAssembly的内存空间对JavaScript来说是完全可见的,JavaScript和WebAssembly可以通过内存交换数据。
C/C++等语言强烈依赖于函数指针,WebAssembly汇编代码作为它们的编译目标,必须提供相应的支持。受限于WebAssembly虚拟机结构和安全性的考虑,WebAssembly引入了表格对象用于存储函数引用,后续章节将对表格
对象进行详细介绍。
用可执行程序与进程的关系进行类比,在WebAssembly中,实例用于指代一个模块及其运行时的所有状态,包括内存、表格以及导入对象等,配置这些对象并基于模块创建一个可被调用的实例的过程称为实例化。模块只有在实例化之后才能被调用——这与C++/Java中类与其实例的关系是类似的。
导入/导出对象是模块实例很重要的组成部分,模块内部的WebAssembly代码可以通过导入对象中的导入函数调用外部JavaScript环境的方法,导出对象中的导出函数是模块提供给外部JavaScript环境使用的接口。
按照目前的规范,一个实例只能拥有一个内存对象以及一个表格对象。内存对象和表格对象都可以通过导入/导出对象被多个实例共有,这意味着多个WebAssembly模块可以以.dll动态链接库的模式协同工作。
WebAssembly程序从开发到运行于网页中大致可以分为以下几个阶段。
(1)使用WebAssembly文本格式或其他语言(C++、Go、Rust等)编写程序,通过各自的工具链编译为WebAssembly汇编格式.wasm文件。
(2)在网页中使用fetch
、XMLHttpRequest
等获取.wasm文件(二进制流)。
(3)将.wasm编译为模块,编译过程中进行合法性检查。
(4)实例化。初始化导入对象,创建模块的实例。
(5)执行实例的导出函数,完成所需操作。
流程图如图2-3所示。
图2-3
第2步到第5步为WebAssembly程序的运行阶段,该阶段与JavaScript环境密切相关,第3章将系统地介绍相关知识。
WebAssembly模块在运行时由以下几部分组成,如图2-4所示。
(1)一个全局类型数组。与很多语言不同,在WebAssembly中“类型”指的并非数据类型,而是函数签名,函数签名定义了函数的参数个数/参数类型/返回值类型;某个函数签名在类型数组中的下标(或者说位置)称为类型索引。
(2)一个全局函数数组,其中容纳了所有的函数,包括导入的函数以及模块内部定义的函数,某个函数在函数数组中的下标称为函数索引。
(3)一个全局变量数组,其中容纳了所有的全局变量——包括导入的全局变量以及模块内部定义的全局变量,某个全局变量在全局变量数组中的下标称为全局变量索引。
(4)一个全局表格对象,表格也是一个数组,其中存储了元素(目前元素类型只能为函数)的引用,某个元素在表格中的下标称为元素索引。
(5)一个全局内存对象。
(6)一个运行时栈。
(7)函数执行时可以访问一个局部变量数组,其中容纳了函数所有的局部变量,某个局部变量在局部变量数组中的下标称为局部变量索引。
图2-4
在WebAssembly中,操作某个具体的对象(如读写某个全局变量/局部变量、调用某个函数等)都是通过其索引完成的。在当前版本中,所有的“索引”都是32位整型数。
本节将介绍经典的HelloWorld例程。开始之前让我们先梳理一下需要完成哪些功能。
(1)函数导入。WebAssembly虚拟机本身没有提供打印函数,因此需要将JavaScript中的字符串输出功能通过函数导入的方法导入WebAssembly中供其使用。
(2)初始化内存,并在内存中存储将要打印的字符串。
(3)函数导出,提供外部调用入口。
在WebAssembly部分,首先将来自JavaScript的js.print
对象导入为函数,并命名为js_print
:
;;hello.wat
(module
;;import js:print as js_print():
(import "js" "print" (func $js_print (param i32 i32)))
该函数有两个参数,类型均为32位整型数,第一个参数为将要打印的字符串在内存中的开始地址,第二个参数为字符串的长度(字节数)。
提示
WebAssembly文本格式中,双分号“
;;
”表示该行后续为注释,作用类似于JavaScript中的双斜杠“//”
。
接下来将来自JavaScript的js.mem
对象导入为内存:
(import "js" "mem" (memory 1)) ;;import js:mem as memory
memory
后的1表示内存的起始长度至少为1页(在WebAssembly中,1页=64 KB=65 536字节)。
接下来的data
段将字符串"你好,WASM"
写入了内存,起始地址为0:
(data (i32.const 0) "你好,WASM")
最后定义了导出函数hello()
:
(func (export "hello")
i32.const 0 ;;pass offset 0 to js_print
i32.const 13 ;;pass length 13 to js_print
call $js_print
)
)
函数hello()
先后在栈上压入了字符串的起始地址0
以及字符串的字节长度13
(每个汉字及全角标点的UTF-8编码占3字节),然后调用导入的js_print()
函数打印输出。
在JavaScript部分,首先创建内存
对象wasmMem
,初始长度为1页:
//hello.html
var wasmMem = new WebAssembly.Memory({initial:1});
接下来定义用于打印字符串的方法printStr()
:
function printStr(offset, length) {
var bytes = new Uint8Array(wasmMem.buffer, offset, length);
var string = new TextDecoder('utf8').decode(bytes);
console.log(string);
}
对应于.wat部分的定义,该方法的两个参数分别为字符串在内存中的起始地址及字节长度。从内存中获取字节流后,使用TextDecoder
将其解码为字符串并输出。
然后将上述Memory
对象wasmMem
、printStr()
方法组合成对象importObj
,导入并实例化:
var importObj = { js: { print: printStr, mem: wasmMem } };
fetch('hello.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObj)
).then(result =>
result.instance.exports.hello()
);
最后调用实例
的导出函数hello()
,在控制台输出“你好,WASM”,如图2-5所示。
图2-5
作为最主要的WebAssembly运行平台,浏览器普遍提供了WebAssembly的调试环境,当页面上包含WebAssembly模块时,可以使用开发面板对其运行进行调试。
图2-6是在Chrome中使用F12调出开发面板调试程序的截图,我们可以在WebAssembly的函数体中下断点,查看局部变量/全局变量的值,查看调用栈和当前函数栈等。
图2-6
WebAssembly文本格式(.wat)文件可以使用任何文本编辑器编辑,对于Windows用户我们推荐使用VSCode,并安装WebAssembly插件,如图2-7所示。
图2-7
该插件除.wat文件编辑器语法高亮之外,甚至还支持直接打开.wasm文件反汇编,图2-8是安装插件后打开2.4节例子的hello.wasm文件的截图。
图2-8
WebAssembly的到来,使自制虚拟机不再遥远。
——Ending
在浏览器环境中,WebAssembly程序运行在WebAssembly虚拟机之上,页面可以通过一组JavaScript对象进行WebAssembly模块的编译、载入、配置、调用等操作。本章将介绍这些JavaScript对象的使用方法。
在2.3节中,我们介绍了WebAssembly中的几个关键概念:模块、内存、表格以及实例。事实上,每个概念在JavaScript中都有对象与之一一对应,分别为WebAssembly.Module
、WebAssembly.Memory
、WebAssembly.Table
以及WebAssembly.Instance
。
所有与WebAssembly相关的功能,都属于全局对象WebAssembly
。除刚才提到的对象之外,WebAssembly
对象中还包含了一些全局方法,如之前章节中曾出现的用于执行实例化的WebAssembly.instantiate()
。
值得注意的是,与很多全局对象(如Date
)不同,WebAssembly
不是一个构造函数,而是一个命名空间,这与Math
对象相似——当我们使用WebAssembly相关功能时,直接调用WebAssembly.XXX()
,无须(也不能)使用类似于Date()
的构造函数。
后续各节在具体介绍WebAssembly相关对象时使用的例子中,包含了一些.wasm汇编模块,本章将暂不深入汇编模块的内部。WebAssembly汇编语言将在第4章中介绍。
本节将介绍全局对象WebAssembly的全局方法,这些方法主要用于WebAssembly二进制模块的编译及合法性检查。
WebAssembly.compile()
该方法用于将WebAssembly二进制代码(.wasm)编译为WebAssembly.Module
。语法为:
Promise<WebAssembly.Module>WebAssembly.compile(bufferSource);
bufferSource
:包含WebAssembly二进制代码(.wasm)的TypedArray
或ArrayBuffer
。Promise
对象,编译好的模块对象,类型为WebAssembly.Module
。bufferSource
不是TypedArray
或ArrayBuffer
,将抛出TypeError
。WebAssembly.CompileError
。fetch('show_me_the_answer.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(module =>
console.log(module.toString()) //"object WebAssembly.Module"
);
WebAssembly.instantiate()
该方法有两种重载形式。第一种用于将WebAssembly二进制代码编译为模块,并创建其第一个实例,语法为:
Promise<ResultObject>WebAssembly.instantiate(bufferSource, importObject);
bufferSource
:包含WebAssembly二进制代码(.wasm)的TypedArray
或ArrayBuffer
。importObject
:可选,将被导入新创建的实例中的对象,它可以包含JavaScript方法、WebAssembly.Memory
和WebAssembly.Table
。Promise
对象,该对象包含两个属性。
module
:编译好的模块对象,类型为WebAssembly.Module
。instance
:上述module
的第一个实例,类型为WebAssembly.Instance
。bufferSource
不是TypedArray
或ArrayBuffer
,将抛出TypeError
。Promise
会抛出下面3种异常之一:WebAssembly.CompileError
、WebAssembly.LinkError和WebAssembly.RuntimeError
。2.2.节中我们就曾经使用过这种重载形式:
fetch('show_me_the_answer.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(result =>
console.log(result.instance.exports.showMeTheAnswer()) //42
);
WebAssembly.instantiate()
的另一种重载形式用于基于已编译好的模块创建实例,语法为:
Promise<WebAssembly.Instance>WebAssembly.instantiate(module, importObject);
module
:已编译好的模块对象,类型为WebAssembly.Module
。importObject
:可选,将被导入新创建的实例中的对象。Promise
对象,新建的实例,类型为WebAssembly.Instance
。TypeError
。WebAssembly.CompileError
、WebAssembly.LinkError
和WebAssembly.RuntimeError
。在需要为一个模块创建多个实例时,使用这种重载形式可以省去多次编译模块的开销。例如:
fetch('show_me_the_answer.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(mod => {
WebAssembly.instantiate(mod).then(result =>
console.log('Instance0:', result.exports.showMeTheAnswer()));
WebAssembly.instantiate(mod).then(result =>
console.log('Instance1:', result.exports.showMeTheAnswer()));
}
);
WebAssembly.validate()
该方法用于校验WebAssembly二进制代码是否合法,语法为:
var valid = WebAssembly.validate(bufferSource);
bufferSource
:包含WebAssembly二进制代码(.wasm)的TypedArray
或ArrayBuffer
。true
,否则返回false
。bufferSource
不是TypedArray
或ArrayBuffer
,将抛出TypeError
。WebAssembly.compileStreaming()
该方法与WebAssembly.compile()
类似,用于WebAssembly二进制代码的编译,区别在于本方法使用流式底层源作为输入:
Promise<WebAssembly.Module>WebAssembly.compileStreaming(source);
参数source
通常是fetch()
方法返回的Response
对象,例如:
WebAssembly.compileStreaming(fetch('show_me_the_answer.wasm')).
then(module => console.log(module.toString()) //"object WebAssembly.Module"
);
本方法的返回值和异常与WebAssembly.compile()
相同。
WebAssembly.instantiateStreaming()
该方法与WebAssembly.instantiate()
的第一种重载形式类似,用于将WebAssembly二进制代码编译为模块,并创建其第一个实例,区别在于本方法使用流式底层源作为输入:
Promise<ResultObject>WebAssembly.instantiateStreaming(source, importObject);
参数source
通常是fetch()
方法返回的Response
对象,例如:
WebAssembly.instantiateStreaming(fetch('show_me_the_answer.wasm')).
then(result =>
console.log(result.instance.exports.showMeTheAnswer()) //42
);
本方法的返回值和异常与WebAssembly.instantiate()
的第一种重载形式相同。
提示
WebAssembly.compileStreaming()
/WebAssembly.instantiate-Streaming()
与其非流式版的孪生函数相比,虽然书写较为简单,但是对浏览器的要求较高(目前Safari全系不支持),因此,若无特殊情况,不建议使用这一对函数。
WebAssembly.Module
对象与模块对应的JavaScript对象为WebAssembly.Module
,它是无状态的,可以被多次实例化。
将WebAssembly二进制代码(.wasm)编译为模块需要消耗相当大的计算资源,因此获取模块的主要方法是上一节中讲到的异步方法WebAssembly.compile()
和WebAssembly.instantiate()
。
WebAssembly.Module()
WebAssembly.Module
的构造器方法用于同步地编译.wasm为模块:
var module = new WebAssembly.Module(bufferSource);
bufferSource
:包含WebAssembly二进制代码(.wasm)的TypedArray
或ArrayBuffer
。bufferSource
不是TypedArray
或ArrayBuffer
,将抛出TypeError
。WebAssembly.CompileError
。//constructor.html
fetch('hello.wasm').then(response =>
response.arrayBuffer()
).then(bytes => {
var module = new WebAssembly.Module(bytes);
console.log(module.toString()); //"object WebAssembly.Module"
}
);
WebAssembly.Module.exports()
该方法用于获取模块的导出信息,语法为:
var exports = WebAssembly.Module.exports(module);
module
:WebAssembly.Module
对象。module
的导出对象信息的数组。module
不是WebAssembly.Module
,抛出TypeError
。//exports.html
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"}
WebAssembly.Module.imports()
该方法用于获取模块的导入信息,语法为:
var imports = WebAssembly.Module.imports(module);
module
:WebAssembly.Module
对象。module
的导入对象信息的数组。module
不是WebAssembly.Module
,抛出TypeError
。//imports.html
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"}
WebAssembly.Module.customSections()
该方法用于获取模块中的自定义段(section)的数据。WebAssembly代码是由一系列以S-表达式描述的段嵌套而成,在WebAssembly的二进制规范中,允许包含带名字的自定义段。编译器可以利用这一特性,在生成WebAssembly二进制格式(.wasm)的过程中插入符号/调试信息等数据以利于运行时调试,遗憾的是目前WebAssembly文本格式(.wat)并不支持自定义段。
var sections = WebAssembly.Module.customSections(module, secName);
module
:WebAssembly.Module
对象。secName
:欲获取的自定义段的名字。secName
相同的自定义段,每个段均为一个ArrayBuffer
。module
不是WebAssembly.Module
,抛出TypeError
。//custom_sections.html
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]);
}
}
);
执行后,控制台输出如图3-1所示。
图3-1
Module
在部分浏览器(如Firefox)中,Module
可以像Blob
一样被装入IndexedDB
缓存,也可以在多个Worker
间传递,下面的例子展示了这种用法(Chrome/Safari不支持):
//worker.html
var sub_worker = new Worker("worker.js");
sub_worker.onmessage = function (event) {
console.log(event.data);
}
fetch('show_me_the_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.showMeTheAnswer())
);
}
WebAssembly.Instance
对象与实例对应的JavaScript对象为WebAssembly.Instance
,获取实例的主要方法是3.2节中介绍过的WebAssembly.instantiate()
方法。
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()
],都必须提供完整的导入对象。例如,我们在“你好,WASM”例子的WebAssembly代码中声明了导入内存及print()
函数,若在实例化时不提供导入对象,则实例化将失败:
//linkerror.html
fetch('hello.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(module =>
WebAssembly.instantiate(module);
//TypeError: Imports argument must be present and must be an object
);
即使在实例化时提供了导入对象,若不完整,例如,只导入内存
,而不导入js.print
方法,实例化仍然会失败:
//linkerror.html
var wasmMem = new WebAssembly.Memory({initial:1});
var importObj = { js: { /*print: printStr, */mem: wasmMem } };
fetch('hello.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.compile(bytes)
).then(module =>
WebAssembly.instantiate(module, importObj)
);
控制台输出如下:
Uncaught (in promise) LinkError: Import #0 module="js" function="print" error:
function import requires a callable
WebAssembly.Instance.prototype.exports
WebAssembly.Instance
的只读属性exports
包含了实例的所有导出函数,即实例供外部JavaScript程序调用的接口。例如,我们定义一个WebAssembly模块如下:
;;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);
console.log(instance.exports.add(21, 21)); //42
console.log(instance.exports.inc(12)); //13
}
);
控制台将输出:
{add: ƒ, inc: ƒ}
42
13
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...
})
在后续各节中,为了简化代码,方便阅读,我们将使用这一方法。
WebAssembly.Memory
对象与WebAssembly内存对应的JavaScript对象为WebAssembly.Memory
,它用于在WebAssembly程序中存储运行时数据。无论从JavaScript的角度,还是WebAssembly的角度,内存对象本质上都是一个一维数组,JavaScript和WebAssembly可以通过内存相互传递数据。较为常见的用法是:在JavaScript中创建内存对象(该对象包含了一个ArrayBuffer
,用于存储上述一维数组),模块实例化时将其通过导入对象导入WebAssembly中。一个内存对象可以导入多个实例,这使得多个实例可以通过共享一个内存对象的方式交换数据。
WebAssembly.Memory()
WebAssembly内存对象的构造器方法,语法为:
var memory = new WebAssembly.Memory(memDesc);
memDesc
:新建内存的参数,包含下述属性。
initial
:内存的初始容量,以页为单位(1页=64 KB=65 536字节)。maximum
:可选,内存的最大容量,以页为单位。TypeError
。maximum
属性,但是其值小于initial
属性,将抛出RangeError
。WebAssembly.Memory.prototype.buffer
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
get_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 = 1page
(export "memory" (memory $mem)) ;;$export $mem as "memory"
(func (export "fibonacci") (param $count i32)
(local $i i32) (local $a i32) (local $b i32)
i32.const 0
i32.const 1
...
我们在WebAssembly中定义了初始容量为1页的内存,并将其导出;同时导出了名为fibonacci()
的方法,用于根据输入的数列长度来生成斐波拉契数列。
//export_mem.htm
fetchAndInstantiate('export_mem.wasm').then(
function(instance) {
console.log(instance.exports); //{memory: Memory, fibonacci: ƒ}
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
}
);
上述JavaScript代码运行后,控制台输出如下:
{memory: Memory, fibonacci: ƒ}
Memory {}
65536
1 1 2 3 5 8 13 21 34 55
可见,在WebAssembly内部创建的内存被成功导出,调用fibonacci()
后在内存中生成的斐波拉契数列亦可正常访问。
值得注意的是,如果WebAssembly内部创建了内存,但是在实例化时又导入了内存对象,那么WebAssembly会使用内部创建的内存,而不是外部导入的内存。例如:
var memory = new WebAssembly.Memory({initial:1, maximum:10});
fetchAndInstantiate('export_mem.wasm', {js:{mem:memory}}).then(
function(instance) {
instance.exports.fibonacci(10);
var i32 = new Uint32Array(instance.exports.memory.buffer);
var s = "Internal mem:";
for (var i = 0; i < 10; i++) {
s += i32[i] + ' '
}
console.log(s); //1 1 2 3 5 8 13 21 34 55
var i32 = new Uint32Array(memory.buffer);
s = "Imported mem:";
for (var i = 0; i < 10; i++) {
s += i32[i] + ' '
}
console.log(s); //0 0 0 0 0 0 0 0 0 0
}
);
上述代码执行后,控制台将输出:
Internal mem:1 1 2 3 5 8 13 21 34 55
Imported mem:0 0 0 0 0 0 0 0 0 0
由此可见,WebAssembly优先使用内部创建的内存。
WebAssembly.Memory.prototype.grow()
该方法用于扩大内存对象的容量。语法为:
var pre_size = memory.grow(number);
number
:内存对象扩大的量,以页为单位。number
)已超过指定的最大容量,则抛出RangeError
。内存对象扩大后,其扩大前的数据将被复制到扩大后的buffer
中,例如:
//grow.html
fetchAndInstantiate('export_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); //65536
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);
i32 = new Uint32Array(instance.exports.memory.buffer);
console.log("mem size after grow():",
instance.exports.memory.buffer.byteLength); //6553600
var s = "mem content after grow():";
for (var i = 0; i < 10; i++) {
s += i32[i] + ' '
}
console.log(s); //still 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看来,数据并未丢失;在WebAssembly看来也一样:
//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 before grow():",
memory.buffer.byteLength); //655360
console.log("sum:", instance.exports.sum(0, 32)); //still 496
}
);
上述代码运行后,控制台输出:
mem size before grow(): 65536
sum: 496
mem size before grow(): 655360
sum: 496
可见在WebAssembly看来,grow()
也不会丢失数据。但值得注意的是,grow()
有可能引发内存对象的ArrayBuffer
重分配,从而导致引用它的TypedArray
失效。例如:
//grow3.html
fetchAndInstantiate('export_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);
//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
时,必须随用随创建。
如果内存对象在创建时指定了最大容量,则使用grow()
扩容时不能超过最大容量值。例如,下列代码第二次grow()
时将抛出RangeError
:
var memory = new WebAssembly.Memory({initial:1, maximum:10});
//...
memory.grow(9); //ok, current size = 10 pages
memory.grow(1); //RangeError
WebAssembly.Table
对象通过前面的介绍我们不难发现,在WebAssembly的设计思想中,与执行过程相关的代码段/栈等元素与内存是完全分离的,这与通常体系结构中代码段/数据段/堆/栈全都处于统一编址内存空间的情况完全不一样,函数地址对WebAssembly程序来说不可见,更遑论将其当作变量一样传递、修改以及调用了。然而函数指针对很多高级语言来说是必不可少的特性,如回调、C++的虚函数都依赖于它。解决这一问题的关键,就是本节将要介绍的表格对象。
表格是保存了对象引用的一维数组。目前可以保存在表格中的元素只有函数引用一种类型,随着WebAssembly的发展,将来或许有更多类型的元素(如DOM对象)能被存入其中,但到目前为止,可以说表格是专为函数指针而生。目前每个实例只能包含一个表格,因此相关的WebAssembly指令隐含的操作对象均为当前实例拥有的唯一表格。表格不占用内存地址空间,二者是相互独立的。
使用函数指针的本质行为是:通过变量(即函数地址)找到并执行函数。在WebAssembly中,当一个函数被存入表格中后,即可通过它在表格中的索引(该函数在表格中的位置,或者说数组下标)来调用它,这就间接地实现了函数指针的功能,只不过用来寻找函数的变量不是函数地址,而是它在表格中的索引。
WebAssembly为何使用这种拐弯抹角的方式来实现函数指针?最重要的原因是为了安全。倘若能通过函数的真实地址来调用它,那么WebAssembly代码的执行范围将不可控,例如,调用非法地址导致浏览器崩溃,甚至下载恶意程序后导入运行等,而在WebAssembly当前的设计框架下,保存在表格中的函数地址对WebAssembly代码不可见、无法修改,只能通过表格索引来调用,并且运行时的栈数据并不保存在内存对象中,由此彻底断绝了WebAssembly代码越界执行的可能,最糟糕的情况不过是在内存对象中产生一堆错误数据而已。
与WebAssembly表格对应的JavaScript对象为WebAssembly.Table
。
WebAssembly.Table()
表格的构造器方法,语法为:
var table = new WebAssembly.Table(tableDesc);
tableDesc
:新建表格的参数,包含下述属性。
element
:存入表格中的元素的类型,当前只能为anyfunc
,即函数引用。initial
:表格的初始容量。maximum
:可选,表格的最大容量。“表格的最大容量”指表格能容纳的函数索引的个数(即数组长度),这与内存对象的容量以页为单位不同,注意区分。
TypeError
。maximum
属性,但是其值小于initial
属性,将抛出RangeError
。WebAssembly.Table.prototype.get()
该方法用于获取表格中指定索引位置的函数引用,语法为:
var funcRef = table.get(index);
index
:欲获取的函数引用的索引。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
ƒ 1() { [native code] }
ƒ 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(f0());
console.log(table.get(1)());
}
);
运行后控制台输出如下:
42
13
提示
在上一个例子中,
func0
和func1
并未导出,而将其存入表格后,JavaScript依然可以通过表格对其进行调用。
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
)
)
//export_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
ƒ 1() { [native code] }
42
ƒ 0() { [native code] }
13
前面的例子展示的是表格中的函数被JavaScript调用,然而表格更主要的作用还是被WebAssembly调用,例如:
;;call_by_index.wat
(module
(import "js" "table" (table 2 anyfunc))
(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 id into stack
call_indirect (type $type_0) ;;call table:id
)
)
WebAssembly代码定义了两个函数,即plus13()
以及plus42()
,并将其分别存入表格的0和1处。
(type $type_0 (func (param i32) (result i32)))
定义了将要被调用的函数的签名(即函数的参数列表及返回值类型)——根据栈式虚拟机的特性,WebAssembly在执行函数调用时,调用方与被调用方需要严格匹配签名;若签名不匹配,会抛出WebAssembly.RuntimeError
。
提示
WebAssembly汇编语言的相关细节将在第4章详细介绍。
导出函数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));
console.log(instance.exports.call_by_index(1, 10));
}
);
控制台将输出:
23
52
在WebAssembly中使用call_indirect
时,如果试图调用索引范围超过表格容量的函数,将抛出WebAssembly.RuntimeError
,如在上述例子中,表格的容量为2,如果我们执行下列操作:
var table = new WebAssembly.Table({element:'anyfunc', initial:2});
fetchAndInstantiate('call_by_index.wasm', {js:{table:table}}).then(
function(instance) {
instance.exports.call_by_index(2, 10); //WebAssembly.RuntimeError
}
);
控制台输出如图3-2所示。
图3-2
与内存对象类似,一个表格也可以被导入多个实例从而被多个实例共享。接下来,我们以一个相对复杂的例子来展示多个实例共享内存和表格协同工作。在这个例子中,我们将载入两个模块并各自创建一个实例,其中一个将在内存中生成斐波拉契数列的函数,另一个将调用前者,计算数列的和并输出。
计算斐波拉契数列的模块如下:
;;fibonacii.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
)
)
上述代码定义了计算斐波拉契数列的函数fibonacci()
,并将其引用存入表格的索引0处。
求和模块如下:
;;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处的函数,后续的代码计算并返回了数列的和。
//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));
});
不出意外,上述程序按照我们设计的路径(JavaScript->sumfib.wasm.sumfib->table:0-> fibonacci.wasm.fibonacci->sumfib.wasm.sumfib->return)正确执行,并输出了斐波拉契数列前10项的和:
143
刚才的例子中都是在WebAssembly中修改表格,事实上,运行时我们还可以在JavaScript中修改它。
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);
table.set(0, results[1].exports.fibonacci);
console.log(results[0].exports.sumfib(10));
});
也就是说,fibonacci()
函数是在JavaScript中动态导入表格的。程序运行后,结果依然:
{fibonacci: ƒ}
143
表格和内存的跨实例共享,加上表格在运行时可变,使WebAssembly可以实现非常复杂的动态链接。
WebAssembly.Table.prototype.grow()
该方法用于扩大表格的容量,语法为:
preSize = table.grow(number);
number
:表格扩大的量。number
)已超过指定的最大容量,则抛出RangeError
。与内存类似,表格的grow()
方法不会丢失扩容前的数据。鉴于相关的验证方法和例子与内存高度相似,在此不再重复。
除了常规的TypeError
和RangeError
,与WebAssembly相关的异常还有另外3种,即WebAssembly.CompileError
、WebAssembly.LinkError
和WebAssembly.RuntimeError
。这3种异常正好对应WebAssembly程序生命周期的3个阶段,即编译、链接(实例化)和运行。
编译阶段的主要任务是将WebAssembly二进制代码编译为模块(如何转码取决于虚拟机实现)。在此过程中,若WebAssembly二进制代码的合法性无法通过检查,则抛出WebAssembly.CompileError
。
在链接阶段,将创建实例,并链接导入/导出对象。导致该阶段抛出WebAssembly.LinkError
异常的典型情况是导入对象不完整,导入对象中缺失了模块需要导入的函数、内存或表格。另外,在实例初始化时,可能会执行内存/表格的初始化,如2.4节的例子中使用data
段初始化内存:
(data (i32.const 0) "你好,WASM")
以及3.6节的例子中使用elem
段初始化表格:
;;import_table.wat
(module
(import "js" "table" (table 2 anyfunc))
(elem (i32.const 0) $func1 $func0) ;;set $func0,$func1 to table
...
)
在此过程中,若内存/表格的容量不足以容纳装入的数据/函数引用,也会导致WebAssembly.LinkError
。
运行时抛出WebAssembly.RuntimeError
的情况较多,常见的主要有以下几个。
(1)内存访问越界。试图读写超过内存当前容量的地址空间。
(2)调用函数时,调用方与被调用方签名不匹配。
(3)表格访问越界。试图调用/修改大于等于表格容量的索引处的函数。
在运行时产生的异常中有一种情况需要特别注意:目前JavaScript的Number
类型无法无损地表达64位整型数。这意味着虽然WebAssembly支持i64
类型的运算,但是与JavaScript对接的导出函数不能使用i64
类型作为参数或返回值,一旦在JavaScript中调用参数或返回值类型为i64
的WebAssembly函数,将抛出TypeError
。
下面给出了一些异常的例子。WebAssembly代码如下:
;;exceptions.wat
(module
(import "js" "mem" (memory 1))
(import "js" "table" (table 2 anyfunc))
(elem (i32.const 0) $f0 $f1)
(type $type_0 (func (result i32)))
(func $f0 (param i32)(result i32)
i32.const 13
)
(func $f1 (result i32)
i32.const 65540
i32.load
)
(func (export "call_by_index") (param $index i32)(result i32)
get_local $index
call_indirect (type $type_0)
)
(func (export "return_i64") (result i64)
i64.const 0
)
(func (export "param_i64") (param i64))
)
JavaScript代码如下:
//exceptions.html
var table = new WebAssembly.Table({element:'anyfunc', initial:3});
var memory = new WebAssembly.Memory({initial:1});
fetchAndInstantiate('exceptions.wasm', {js:{table:table, mem:memory}}).then(
function(instance) {
try{
instance.exports.call_by_index(0)
}
catch(err){
console.log(err);
}
try{
instance.exports.call_by_index(1)
}
catch(err){
console.log(err);
}
try{
instance.exports.call_by_index(2)
}
catch(err){
console.log(err);
}
try{
instance.exports.call_by_index(3)
}
catch(err){
console.log(err);
}
try{
instance.exports.return_i64()
}
catch(err){
console.log(err);
}
try{
instance.exports.param_i64(0)
}
catch(err){
console.log(err);
}
}
);
程序运行后,捕获异常如图3-3所示。
图3-3