WebAssembly标准入门(txt+pdf+epub+mobi电子书下载)


发布时间:2020-05-21 05:44:45

点击下载

作者:柴树杉,丁尔男

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

WebAssembly标准入门

WebAssembly标准入门试读:

内容提要

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、测试、前端、网络技术等。

异步社区

微信服务号  第0章 WebAssembly诞生背景一切可编译为WebAssembly的,终将被编译为WebAssembly。——Ending

WebAssembly是一种新兴的网页虚拟机标准,它的设计目标包括:高可移植性、高安全性、高效率(包括载入效率和运行效率)、尽可能小的程序体积。0.1 JavaScript简史

只有了解了过去才能理解现在,只有理解了现在才可能掌握未来的发展趋势。JavaScript语言因为互联网而生,紧随着浏览器的出现而问世。JavaScript语言是Brendan Eich为网景(Netscape)公司的浏览器设计的脚本语言,据说前后只花了10天的时间就设计成型。为了借当时的明星语言Java的东风,这门新语言被命名为JavaScript。其实Java语言和JavaScript语言就像是雷锋和雷峰塔一样没有什么关系。

JavaScript语言从诞生开始就是严肃程序员鄙视的对象:语言设计垃圾、运行比蜗牛还慢、它只是给不懂编程的人用的玩具等。当然出现这些观点也有一定的客观因素:JavaScript运行确实够慢,语言也没有经过严谨的设计,甚至没有很多高级语言标配的块作用域特性。

但是到了2005年,Ajax(Asynchronous JavaScript and XML)方法横空出世,JavaScript终于开始火爆。据说是Jesse James Garrett发明了这个词汇。谷歌当时发布的Google Maps项目大量采用该方法标志着Ajax开始流行。Ajax几乎成了新一代网站的标准做法,并且促成了Web 2.0时代的来临。作为Ajax核心部分的JavaScript语言突然变得异常重要。

然后是2008年,谷歌公司为Chrome浏览器而开发的V8即时编译器引擎的诞生彻底改变了JavaScript低能儿的形象。V8引擎下的JavaScript语言突然成了地球上最快的脚本语言!在很多场景下的性能已经和C/C++程序在一个数量级(作为参考,JavaScript比Python要快10~100倍或更多)。

JavaScript终于手握Ajax和V8两大神器,此后真的是飞速发展。2009年,Ryan Dahl创建Node.js项目,JavaScript开始进军服务器领域。2013年,Facebook公司发布React项目,2015年发布React Native项目。目前JavaScript语言已经开始颠覆iOS和Android等手机应用的开发。

回顾整个互联网技术的发展历程,可以发现在Web发展历程中出现过各种各样的技术,例如,号称跨平台的Java Applet、仅支持IE浏览器的ActiveX控件、曾经差点称霸浏览器的Flash等。但是,在所有的脚本语言中只有JavaScript语言顽强地活了下来,而且有席卷整个软件领域的趋势。

JavaScript语言被历史选中并不完全是偶然的,偶然之中也有着必然的因素。它的优点同样不可替代。● 简单易用,不用专门学习就可以使用。● 运行系统极其稳定,想写出一个让JavaScript崩溃的程序真是一

个挑战。● 紧抱HTML标准,站在Ajax、WebSocket、WebGL、

WebWorker、SIMD.js等前沿技术的肩膀之上。

因为Web是一个开放的生态,所以如果一个技术太严谨注定就不会流行,XHTML就是一个活生生的反面教材。超强容错是所有Web技术流行的一个必备条件。JavaScript刚好足够简单和稳定、有着超强的容错能力、语言本身也有着极强的表达力。同时,Ajax、WebSocket、WebGL、WebWorker等标准的诞生也为JavaScript提供了更广阔的应用领域。0.2 asm.js的尝试

JavaScript是弱类型语言,由于其变量类型不固定,使用变量前需要先判断其类型,这样无疑增加了运算的复杂度,降低了执行效能。谋智公司的工程师创建了Emscripten项目,尝试通过LLVM工具链将C/C++语言编写的程序转译为JavaScript代码,在此过程中创建了JavaScript子集asm.js,asm.js仅包含可以预判变量类型的数值运算,有效地避免了JavaScript弱类型变量语法带来的执行效能低下的问题。根据测试,针对asm.js优化的引擎执行速度和C/C++原生应用在一个数量级。

图0-1给出了ams.js优化的处理流程,其中上一条分支是经过高度优化的执行分支。

图0-1

因为增加了类型信息,所以asm.js代码采用定制优化的AOT(Ahead Of Time)编译器,生成机器指令执行。如果中途发现语法错误或违反类型标记的情况出现,则回退到传统的JavaScript引擎解析执行。

asm.js中只有有符号整数、无符号整数和浮点数这几种类型。图0-2展示了asm.js中几种数值类型之间的关系。

图0-2

而字符串等其他类型则必须在TypedArray中提供,同时通过类似指针的技术访问TypedArray中的字符串数据。

asm.js的高明之处就是通过JavaScript已有的语法和语义为变量增加了类型信息:var x = 9527;var i = a | 0; // int32var u = a >>> 0; // uint32var f = +a; // float64

以上代码中a|0表示一个整数,而a >>> 0表示一个无符号整数,+a表示一个浮点数。即使不支持asm.js的引擎也可以产生正确的结果。

然后通过ArrayBuffer来模拟真正的内存:var buffer = new ArrayBuffer(1024*1024*8);var HEAP8 = new Int8Array(buffer);

基于HEAP8模拟的内存,就可以采用类似C语言风格计算的字符串长度。而C语言的工作模型和冯·诺伊曼计算机体系结构是高度适配的,这也是C语言应用具有较高性能的原因。下面是asm.js实现的strlen函数:function strlen(s) { var p = s|0; while(HEAP8[p]|0 != 0) { p = (p+1)|0 } return (p-s)|0;}

所有的asm.js代码将被组织到一个模块中:function MyasmModule(stdlib, foreign, heap) { "use asm"; var HEAP8 = new Int8Array(heap);function strlen(s) { // ... } return {strlen: strlen, };}

asm.js模块通过stdlib提供了基本的标准库,通过foreign可以传入外部定义的函数,通过heap为模块配置堆内存。最后,模块可以将asm.js实现的函数或变量导出。模块开头通过"use asm"标注内部是asm.js规格的实现,即使是旧的引擎也可以正确运行。

asm.js优越的性能让浏览器能够运行很多C/C++开发的3D游戏。同时,Lua、SQLite等C/C++开发的软件被大量编译为纯JavaScript代码,极大地丰富了JavaScript社区的生态。asm.js诸多技术细节我们就不详细展开了,感兴趣的读者可以参考它的规范文档。0.3 WebAssembly的救赎

在整个Web技术变革的过程之中,不断有技术人员尝试在浏览器中直接运行C/C++程序。自1995年起包括Netscape Plugin API(NPAPI)在内的许多知名项目相继开发。微软公司的IE浏览器甚至可以直接嵌入运行本地代码的ActiveX控件。同样,谷歌公司在2010年也开发了一项Native Clients技术(简称NaCL)。然而这些技术都太过复杂、容错性也不够强大,它们最终都未能成为行业标准。

除尝试直接运行本地代码这条路之外,也有技术人员开始另辟蹊径,将其他语言直接转译为JavaScript后运行,2006年,谷歌公司推出Google Web Toolkit(GWT)项目,提供将Java转译成JavaScript的功能,开创了将其他语言转为JavaScript的先河。之后的CoffeeScript、Dart、TypeScript等语言都是以输出JavaScript程序为最终目标。

在众多为JavaScript提速的技术中,Emscripten是与众不同的一个。它利用LLVM编译器前端编译C/C++代码,生成LLVM特有的跨平台中间语言代码,最终再将LLVM跨平台中间语言代码转译为JavaScript的asm.js子集。这带来的直接结果就是,C/C++程序经过编译后不仅可在旧的JavaScript引擎上正确运行,同时也可以被优化为机器码之后高速运行。

2015年6月谋智公司在asm.js的基础上发布了WebAssembly项目,随后谷歌、微软、苹果等各大主流的浏览器厂商均大力支持。WebAssembly不仅拥有比asm.js更高的执行效能,而且由于使用了二进制编码等一系列技术,WebAssembly编写的模块体积更小且解析速度更快。目前不仅C/C++语言编写的程序可以编译为WebAssembly模块,而且Go、Kotlin、Rust等新兴的编程语言都开始对WebAssembly提供支持。2018年7月,WebAssembly 1.0标准正式发布,这必将开辟Web开发的新纪元!  第1章  JavaScript语言基础当歌曲和传说都已经缄默的时候,只有代码还在说话。——柴树杉

本章将介绍console模块、函数和闭包、Promise的用法和二进制数组对象TypedArray的简要用法。1.1 console对象

console对象是重要的JavaScript对象,通过该对象可以实现输出打印功能。而打印输出函数正是学习每种编程语言第一个要掌握的技术。console也是我们的基本调试手段。其中console.log是最常用的打印方法,可以打印任意对象。

下面的代码向控制台输出“你好,WebAssembly!”:console.log('你好,WebAssembly!')

console是一个无须导入的内置对象,在Node.js和浏览器环境均可使用。console对象的方法主要分为简单的日志输出、assert断言、输出对象属性、调试输出以及简单的时间测量等。

需要说明的是,在不同的浏览器环境中,console对象可能扩展了很多特有的方法。表1-1给出的是Node.js和主流的浏览器提供的方法,其中log、info、warn和error分别用于输出日志信息、一般信息、警告信息和错误信息。

表1-1方 法 名概 念 描 述console.log([data]日志信息,以标准格式输出所有参数,[, ...args])并在末尾输出换行console.info([data]一般信息,以标准格式输出所有参数,[, ...args])并在末尾输出换行console.警告信息,以警告格式输出所有参数,warn([data][, ...args])并在末尾输出换行console.错误信息,以错误格式输出所有参数,error([data][, ...args])并在末尾输出换行console.断言,如果参数expr为假,则抛出assert(value[, AssertionErrormessage][, ...args])console.dir(obj[, 检查对象,输出对象所有属性options])console.开始测量时间time(label)console.结束测量时间,根据相同label的计时器timeEnd(label)统计所经过时间,用log输出结果

下面是console.log的常见用法:console.log(123)console.log(123, 'abc')console.log(123, 'abc', [4,5,6])

console.log等方法还支持类似C语言中printf函数的格式化输出,它可以支持下面这些格式。● %s:输出字符串。● %d:输出数值类型,整数或浮点数。● %i:输出整数。● %f:输出浮点数。● %j:输出JSON格式。● %%:输出百分号('%')。

下面是用格式化的方式输出整型数:const code = 502;console.error('error #%d', code);

断言对于编写健壮的程序会有很大的帮助。下面的开方函数通过前置断言确保输入的参数是正整数:function sqrt(x) { console.assert(x >= 0) return Math.sqrt(x)}

前置断言一般用于确保是合法的输入,后置断言则用于保证合法的输入产生了合法的输出。例如,在连接两个字符串之后,我们可以通过后置断言使连接后的字符串长度大于或等于之前的任何一个字符串的长度:function joinString(a, b) { let s = a + b console.assert(s.length >= a.length) console.assert(s.length >= b.length) return s}

不应该在断言中放置具有功能性逻辑的代码,下面使用断言的方式在实际应用中应该尽量避免:let i = 90console.assert(i++ < 100)console.log('i:', i)

因为最终输出的结果依赖于断言语句中i++的正常运行,所以如果最终运行的环境不支持断言操作,那么程序将产生不同的结果。1.2 函数和闭包

在高级编程语言中,函数是一个比较核心的概念。简单来说,函数就是一组语句的集合。通过将语句打包为一个函数,就可以重复使用相同的语句集合。同时,为了适应不同的场景,函数还支持输入一些动态的参数。从用户角度来说,函数是一个黑盒子,根据当前的上下文环境状态和输入参数产生输出。

我们简单看看JavaScript中如何用函数包装1到100的加法运算:function sum100() { var result = 0; for(var i = 1; i <= 100; i++) { result += i; } return result;}

function关键字表示定义一个函数。然后函数体内通过for循环来实现1到100的加法运算。sum100通过函数的方式实现了对1到100加法语句的重复利用,但是sum100的函数无法计算1到200的加法运算。

为了增加sum函数的灵活性,我们可以将要计算等差和的上界通过参数传入:function sum(n) { var result = 0; for(var i = 1; i <= n; i++) { result += i; } return result;}

现在我们就可以通过sum(100)来计算1到100的加法运算,也可以通过sum(200)来计算1到200的加法运算。

和C/C++等编译语言不同,JavaScript的函数是第一对象,可以作为表达式使用。因此可以换一种方式实现sum函数:var sum = function(n) { var result = 0; for(var i = 1; i <= n; i++) { result += i; } return result;}

上面的代码中sum更像一个变量,不过这个变量中保存的是一个函数。保存了函数的sum变量可以当作一个函数使用。

我们还可以使用箭头函数来简化函数表达式的书写:var sum = (n) => { var result = 0; for(var i = 1; i <= n; i++) { result += i; } return result;}

其中箭头=>前面的(n)对应函数参数,箭头后面的{}内容表示函数体。如果函数参数只有一个,那么可以省略小括号。类似地,如果函数体内只有一个语句也可以省略花括号。

因为箭头函数写起来比较简洁,所以经常被用于参数为函数的场景。不过需要注意的是,箭头函数中this是被绑定到创建函数时的this对象,而不是绑定到运行时上下文的this对象。

既然函数是一个表达式,那么必然会遇到在函数内又定义函数的情形:function make_sum_fn(n) { return () => { var result = 0; for(var i = 1; i <= n; i++) { result += i; } return result; }}

在make_sum_fn函数中,通过函数表达式创建了一个函数对象,最后返回了函数对象。另一个重要的变化是,内部函数没有通过参数来引用外部的n变量,而是直接跨越了内部函数引用了外部的n变量。

如果一个函数变量直接引用了函数外部的变量,那么外部的变量将被该函数捕获,而当前的函数也就成了闭包函数。在早期的JavaScript语言中,变量并没有块级作用域,因此经常通过闭包函数来控制变量的作用域。

通过返回的闭包函数,我们就可以为1到100和1到200分别构造求和函数对象:var sum100 = make_sum_fn(100);var sum200 = make_sum_fn(200);sum100();sum200();

每次make_sum_fn函数调用返回的闭包函数都是不同的,因为闭包函数每次捕获的n变量都是不同的。1.3 Promise对象

JavaScript是一个单线程的编程语言,通过异步、回调函数来处理各种事件。因此,如果要处理多个有先后顺序的事件,那么将会出现多次嵌套回调函数的情况,这也被很多开发人员称为回调地狱。

而Promise对象则是通过将代表异步操作最终完成或者失败的操作封装到了对象中。Promise本质上是一个绑定了回调的对象,不过这样可以适当缓解多层回调函数的问题。

通过构造函数可以生成Promise实例。下面代码创造了一个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回调函数。下面的makeFetchImage是基于fetchImage包装的函数:const makeFetchImage = () => {fetchImage("/static/logo.png").then(() => { console.log('done') })}makeFetchImage()

在makeFetchImage包装函数中,Promise对象的then方法只提供了resolved回调函数。因此当成功获取图像后将输出done字符串。

Promise对象虽然从一定程度上缓解了回调函数地狱的问题,但是Promise的构造函数、返回对象的then方法等地方依然要处理回调函数。因此,ES2017标准又引入了async和await关键字来简化Promise对象的处理。

await关键字只能在async定义的函数内使用。async函数会隐式地返回一个Promise对象,Promise对象的resolve值就是函数return的值。下面是用async和await关键字重新包装的makeFetchImage函数:const makeFetchImage = async () => { await fetchImage("/static/logo.png") console.log('done')}makeFetchImage()

在新的代码中,await关键字将异步等待fetchImage函数的完成。如果图像下载成功,那么后面的打印语句将继续执行。

基于async和await关键字,可以以顺序的思维方式来编写有顺序依赖关系的异步程序:asyncfunction delay(ms) { return new Promise((resole) => {setTimeout(resole, ms) })}asyncfunction main(...args) { for(const arg of args) { console.log(arg) await delay(300) }}main('A', 'B', 'C')

上述程序中,main函数依次输出参数中的每个字符串,在输出字符串之后休眠一定时间再输出下一个字符串。而用于休眠的delay函数返回的是Promise对象,main函数通过await关键字来异步等待delay函数的完成。1.4 二进制数组

二进制数组(ArrayBuffer对象、TypedArray视图)是JavaScript操作二进制数据的接口。这些对象很早就存在,但是一直不是JavaScript语言标准的一部分。在ES2015中,二进制数组已经被纳入语言规范。基于二进制数组,JavaScript也可以直接操作计算机内存,因为该抽象模型和实际的计算机硬件结构非常地相似,理论上可以优化到近似本地程序的性能。

二进制数组由3类对象组成。(1)ArrayBuffer:代表内存中一段空间,要操作该内存空间必须通过基于其创建的TypedArray或DataView进行。(2)TypedArray:Uint8Array、Float32Array等9种二进制类型数组的统称,TypedArray的底层是ArrayBuffer对象,通过它们可以读写底层的二进制数组。(3)DataView:用于处理类似C语言中结构体类型的数据,其中每个元素的类型可能并不相同。

例如,下面的代码先创建一个1024字节的ArrayBuffer,然后再分别以uint8和uint32类型处理数组的元素:let buffer = new ArrayBuffer(1024)// 转为uint8处理let u8Array = new Uint8Array(buffer, 0, 100)for(int i = 0 ; i < u8Array.length; i++) { u8Array[i] = 255}// 转为uint32处理let u32Array = new Uint32Array(buffer, 100)for(int i = 0 ; i < u32Array.length; i++) { u32Array[i] = 0xffffffff}

TypedArray对象的buffer属性返回底层的ArrayBuffer对象,为只读属性。上述代码中,因为u8Array和u32Array都是从同一个ArrayBuffer对象构造,所以下面的断言是成立的:console.assert(u8Array.buffer == buffer)console.assert(u32Array.buffer == buffer)

TypedArray对象的byteOffset属性返回当前二进制数组对象从底层的ArrayBuffer对象的第几个字节开始,byteLength返回当前二进制数组对象的内存的字节大小,它们都是只读属性。console.assert(u8Array.byteOffset == 0)console.assert(u8Array.byteLength == 100)console.assert(u32Array.byteOffset == 100)console.assert(u32Array.byteLength == buffer.byteLength-100)

TypedArray视图对应的二进制数组的每个元素的类型和大小都是一样的。但是视图可能无法直接对应复杂的结构类型,因为结构体中每个成员的内存大小可能是不同的。

我们可以为结构体的每个成员创建一个独立的TypedArray视图实现操作结构体成员的目的,不过这种方式不便于处理比较复杂的结构体。除了通过复合视图来操作结构体类的数据,还可以通过DataView视图实现同样的功能。

二进制数组用于处理图像或矩阵数据时有着较高的性能。在浏览器中,每个canvas对象底层也是对应的二进制数组。通过canvas底层的二进制数组,我们可以方便地将一个彩色图像变换为灰度图像: rgba => grayshow image

二进制数组是JavaScript在处理运算密集型应用时经常用到的特性。同时二进制数组也是网络数据交换和跨语言数据交换最有效的手段。WebAssembly模块中的内存也是一种二进制数组。  第2章 WebAssembly快速入门WebAssembly,一次编写到处运行。——yjhmelody

本章将快速展示几个小例子,借此对WebAssembly形成一个大致的印象,掌握一些基本的用法。在后续的章节将会系统、深入地学习各个技术细节。2.1 准备工作“工欲善其事,必先利其器。”在正式开始之前,需要先准备好兼容WebAssembly的运行环境以及WebAssembly文本格式转换工具集。2.1.1 WebAssembly兼容性

常见桌面版浏览器及Node.js对WebAssembly特性的支持情况如表2-1所示,表内的数字表示浏览器版本。

表2-1WebAssembly特ChromEdgeFirefoxIEOperaSafariNode.性ejs不8.基本支持57165244110.0支持CompileErro不8.5716524411r0.0支持不不不不不不Global62支持支持支持支持支持支持不8.Instance57165244110.0支持不8.LinkError57165244110.0支持不8.Memory5716524411支持0.0不8.Module5716524411支持0.0RuntimeErr不8.5716524411or支持0.0不8.Table5716524411支持0.0不8.compile5716524411支持0.0compileStre不不不61165847aming支持支持支持不8.instantiate5716524411支持0.0instantiateSt不不不61165847reaming支持支持支持不8.validate5716524411支持0.0

常见移动版(Android及iOS)浏览器对WebAssembly特性的支持情况如表2-2所示,表内的数字表示浏览器版本。

表2-2WebAssemblyWebviChromEdgeFirefoxOpeSafariSamsung 特性eweraInternet未基本支持5757支持52117.0知CompileErr未5757支持52117.0or知不支不支不支未不支Global62不支持持持持知持未Instance5757支持52117.0知未LinkError5757支持52117.0知未Memory5757支持52117.0知未Module5757支持52117.0知RuntimeEr未5757支持52117.0ror知未Table5757支持52117.0知未compile5757支持52117.0知compileStr不支未不支616158不支持eaming持知持未instantiate5757支持52117.0知instantiate不支未不支616158不支持Streaming持知持未validate5757支持52117.0知

可见大多数现代浏览器都已支持WebAssembly,可以在列表中任选一种支持WebAssembly的浏览器来运行测试本书的例程。

当浏览器启用WebAssembly模块时,会强行启用同源及沙盒等安全策略。因此本书的WebAssembly例程需通过http网页发布后方可运行。本书的例程目录中有一个名为“py_simple_server.bat”的批处理文件,该文件用于在Windows操作系统下使用python将当前目录发布为http服务;当然也可以使用Nginx、IIS、Apache或任意一种惯用的工具来完成该操作。2.1.2 WebAssembly文本格式与wabt工具集

一般来说,浏览器加载并运行的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)提供以便于阅读。2.2 首个例程

很多语言的入门教程都始于“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中,如下: Show me the answer

上述代码首先使用fetch()函数获取WebAssembly汇编代码文件,将其转为ArrayBuffer后使用WebAssembly.instantiate()函数对其进行编译及初始化实例,最后调用该实例导出的函数showMeTheAnswer()并打印结果。

将例程目录发布后,通过浏览器访问show_me_the_answer.html,浏览器控制台应输出结果42,如图2-2所示。

图2-22.3 WebAssembly概览

JavaScript代码运行在JavaScript虚拟机上,相对地,WebAssembly代码也运行在其特有的虚拟机上。参照2.1节内容,大部分最新的浏览器均提供了WebAssembly虚拟机,然而WebAssembly代码并非只能在浏览器中运行,Node.js 8.0之后的版本也能运行WebAssembly;更进一步来说,WebAssembly虚拟机甚至可以脱离JavaScript环境的支持。不过正如其名,WebAssembly的设计初衷是运行于网页之中,因此本书绝大部分内容均是围绕网页应用展开。2.3.1 WebAssembly中的关键概念

在深入WebAssembly内部运行机制之前,我们暂时将其看作一个黑盒,这个黑盒需要通过一些手段来与外部环境——调用WebAssembly的JavaScript网页程序等进行交互,这些手段可以抽象为以下几个关键概念。1.模块

模块是已被编译为可执行机器码的二进制对象,模块可以简单地与操作系统中的本地可执行程序进行类比,是无状态的(正如Windows的.exe文件是无状态的)。模块是由WebAssembly二进制汇编代码(.wasm)编译而来的。2.内存

在WebAssembly代码看来,内存是一段连续的空间,可以使用load、store等低级指令按照地址读写其中的数据(时刻记住WebAssembly汇编语言是一种低级语言),在网页环境下,WebAssembly内存是由JavaScript中的ArrayBuffer对象实现的,这意味着,整个WebAssembly的内存空间对JavaScript来说是完全可见的,JavaScript和WebAssembly可以通过内存交换数据。3.表格

C/C++等语言强烈依赖于函数指针,WebAssembly汇编代码作为它们的编译目标,必须提供相应的支持。受限于WebAssembly虚拟机结构和安全性的考虑,WebAssembly引入了表格对象用于存储函数引用,后续章节将对表格对象进行详细介绍。4.实例

用可执行程序与进程的关系进行类比,在WebAssembly中,实例用于指代一个模块及其运行时的所有状态,包括内存、表格以及导入对象等,配置这些对象并基于模块创建一个可被调用的实例的过程称为实例化。模块只有在实例化之后才能被调用——这与C++/Java中类与其实例的关系是类似的。

导入/导出对象是模块实例很重要的组成部分,模块内部的WebAssembly代码可以通过导入对象中的导入函数调用外部JavaScript环境的方法,导出对象中的导出函数是模块提供给外部JavaScript环境使用的接口。

按照目前的规范,一个实例只能拥有一个内存对象以及一个表格对象。内存对象和表格对象都可以通过导入/导出对象被多个实例共有,这意味着多个WebAssembly模块可以以.dll动态链接库的模式协同工作。2.3.2 WebAssembly程序生命周期

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章将系统地介绍相关知识。2.3.3 WebAssembly虚拟机体系结构

WebAssembly模块在运行时由以下几部分组成,如图2-4所示。(1)一个全局类型数组。与很多语言不同,在WebAssembly中“类型”指的并非数据类型,而是函数签名,函数签名定义了函数的参数个数/参数类型/返回值类型;某个函数签名在类型数组中的下标(或者说位置)称为类型索引。(2)一个全局函数数组,其中容纳了所有的函数,包括导入的函数以及模块内部定义的函数,某个函数在函数数组中的下标称为函数索引。(3)一个全局变量数组,其中容纳了所有的全局变量——包括导入的全局变量以及模块内部定义的全局变量,某个全局变量在全局变量数组中的下标称为全局变量索引。(4)一个全局表格对象,表格也是一个数组,其中存储了元素(目前元素类型只能为函数)的引用,某个元素在表格中的下标称为元素索引。(5)一个全局内存对象。(6)一个运行时栈。(7)函数执行时可以访问一个局部变量数组,其中容纳了函数所有的局部变量,某个局部变量在局部变量数组中的下标称为局部变量索引。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载