Skip to main content

第一章、简介

本章将介绍webAssembly。从某种意义上说,它是过去几十年网络发展的顶峰。要理解这一切需要了解相当多的历史。如果你不喜欢历史和论述,你可以跳过这一章直接跳到 第 2 章,但我希望你不要。我认为了解为什么这项技术如此重要以及它来自哪里很重要。

WebAssembly 提供什么

一个工程师最伟大的技能之一是评估新技术能带来什么能力。如北卡罗来纳大学的 Fred Brooks 博士提醒我们的那样,没有“银弹”,一切都有权衡。新技术通常不会消除复杂性,而只是将其转移到其他地方。因此,当某些东西确实改变了某些事情的可能性,或者推动我们朝着积极的方向开展工作时,它值得我们关注,我们应该尝试找出原因。

在尝试理解新事物的含义时,我通常首先尝试确定其背后的动机,了解老旧方案的不足之处。之前发生了什么,它如何影响我们了解这项新技术。就像在艺术和音乐中一样,我们不断地从其他地方借用好的想法,所以要真正理解为什么 WebAssembly 值得我们关注以及它提供了什么,我们必须首先看看它之前发生了什么以及它如何让事情变得不同。

在介绍 WebAssembly 的论文中,作者指出,其动机是为了满足现代 Web 交付软件的需求,而 JavaScript 本身无法做到这一点。他要求软件具备:

  • Safe
  • fast
  • portable
  • compact

在这样一个愿景中,WebAssembly is centered at the intersection of software development, the web, its history and how it delivers functionality in a geographically-distributed space。随着时间的推移,这个想法已经大大超出了这个起点,想象一个无处不在、安全、高性能的计算平台,它几乎涉及我们作为技术人员的职业生活的方方面面。WebAssembly 将影响客户端 Web 开发、桌面和企业应用程序、服务器端功能、游戏、教育、云计算、移动平台、物联网 (IoT) 、serverless和微服务等领域。我希望在本书中让你相信这一点。

我们的部署平台比以往任何时候都更加多样化,因此我们需要code和application两方面的可移植性。

一个通用的指令集或字节码可以使算法在各种环境中工作,因为我们只需要将逻辑步骤映射到它们在特定机器架构上的表达方式上面就可以了。程序员使用API编程,例如 OpenGL 、POSIX 或 Win32 ,因为它们提供了打开文件、生成子进程或在屏幕上绘制内容的功能。它们很方便,减少了开发人员需要编写的代码量,但它们依赖于lib来提供这些功能。如果 API 在目标环境中不可用,则应用程序将不会运行。这是 Microsoft 能够利用其在操作系统市场中的优势,在应用程序套件领域占据主导地位的方式之一。另一方面,开放的标准可以更容易地将软件移植到不同的环境中。

我们构建软件的另一个问题是,不同的主机具有不同的硬件功能(内核数量、是否存在GPU)或安全限制(是否可以打开文件或可以发送/接收网络流量)。软件通常通过使用功能测试方法来确定应用程序可以利用哪些资源来适应可用的内容,但这通常会使业务功能复杂化。我们根本负担不起不断为多个平台重写软件的时间和金钱。我们需要更好的重用策略。我们需要一种灵活性,而不是这种通过修改代码来支持其运行其他平台上。为不同的主机环境制作不同的代码会增加其复杂性并使测试和部署策略复杂化。

目前为止,开源软件的价值主张很明确。我们倾向于使用其他开发人员编写的有价值的、可重用的组件来满足我们自己的需求。然而,并非所有可用的代码都是值得信赖的。当我们使用从网上下载的软件时,我们也将自己暴露在了软件依赖链的攻击之下。网络钓鱼攻击、数据泄露、恶意软件和勒索软件,让我们在风险、业务影响、成本等方面都变得脆弱。

到目前为止,JavaScript 一直是解决其中一些问题的唯一方法。当它在沙盒环境中运行时,它为我们提供了某种安全性。它无处不在且便于携带。引擎变得更快了。生态系统爆发增长。但是,一旦您离开基于浏览器的保护范围,我们仍然有安全问题。作为客户端运行的 JavaScript 代码与在服务器上运行的 JavaScript 代码之间存在差异。单线程设计使长时间运行或高度并发的任务复杂化。由于它起源于一种动态语言,因此有一些和其他编程语言类似的优化,即使是最快和最现代的 JavaScript 运行时,这些优化也将继续不可用。

此外,添加 JavaScript 依赖项太容易了,而没有意识到传递性引入了多少包袱和风险。不花时间仔细考虑这些决定的开发人员最终都会阻碍上游软件测试、部署和使用等各个方面。一旦所有这些脚本都通过网络传输,就必须加载和验证这些脚本中的每一个。这会使一切变得迟缓。当一个依赖包被修改或删除时,它有可能破坏大量已部署的软件。

在过去的几十年里,我们经历了几种试图解决这些问题的工具、语言、平台和框架,但 WebAssembly 是我们第一次把它做好。它的设计师并没有试图过度指定任何东西。他们从过去学习,拥抱web,applying problem space thinking to what is ultimately a hard and multi-dimensional problem。在我们进一步深入研究之前,让我们先看看对这项令人兴奋的新技术的formative influences 。

web的历史

WebAssembly 社区中流传着一个笑话,称 WebAssembly “既不是 Web 也不是Assembly ”。虽然这在某些层面上是正确的,但这个名字足以暗示它所提供的东西:它是一个带有一系列指令集的目标平台,鉴于 WebAssembly 模块经常通过网络交付,名称中包含“Web”一词是合理的。

“传统软件开发”和“Web 开发”之间的主要区别之一是:一旦您拥有可用的浏览器,后者实际上不需要安装。在交付成本和面对错误和功能请求时快速周转新版本的能力方面,这是一个巨大的游戏规则改变者。在互联网和网络等其他跨平台技术生态系统中,它还可以更轻松地支持多个硬件和软件环境。

万维网的发明者 Tim Berners-Lee 曾在欧洲核研究组织 (CERN) ,在那里他提交了一份提案,旨在实现 CERN 更大的研究目标,将文件、图像和数据相互关联。尽管事后看来影响很明显,但在被要求采取行动之前,他不得不在内部多次宣传自己的想法。作为一个组织,CERN 由世界各地的数十个研究机构代表,这些机构向科学家提供自己的计算机、应用程序和数据。没有谁能强迫每个人使用相同的操作系统或平台,因此他意识到需要一种技术解决方案来解决问题。

在 Web 出现之前,有诸如 Archie 、Gopher和 WAIS 之类的服务,但他设想了一个更加用户友好的平台(html)。他还借用了标准通用标记语言 (SGML)的想法来作为超文本标记语言 (HTML) 的基础。

这些设计的结果(html)很快成为向全世界提供信息、文档和最终应用程序的主要机制。通过定义标准交换,它不需要和各个利益相关者就特定技术或平台达成一致。这包括如何发出请求以及响应返回的内容。任何理解标准的软件都可以与同样理解标准的任何其他软件进行通信。这给了我们选择的自由和独立的发展能力。

JavaScript 的起源

Web 的交互模型称为超文本传输协议 (HTTP)。虽然它是一个简单有效的模型,易于实现,但很快就被认为不足以完成交互式现代应用程序的任务了(因为不断往返服务端的固有延迟)。能够将代码发送到浏览器的想法一直很引人注目。如果它在交互的用户端运行,则并非每个用户活动都需要返回到服务器。这将使 Web 应用程序更具交互性、响应性和使用乐趣。然而,如何实现这一点并不完全清楚。哪种编程语言最有意义?我们如何平衡表达能力与浅层学习曲线,以便更多的人可以参与到发展过程中?哪些语言的表现优于其他语言,我们将如何保护客户端的敏感资源免受恶意软件的侵害?

浏览器领域的大部分创新最初是由 Netscape Communications Corp. 推动的。信不信由你,Netscape 浏览器最初是一款付费软件,但他们更大的兴趣在于销售服务器端软件。通过扩展客户端的可能性,他们可以创建和销售更强大、更有利可图的服务器功能。

当时,Java 刚开始是作为一种用于消费设备的嵌入式语言,但还没有取得多少成功的记录。它是在虚拟平台上运行的 C++ 的简化版本,因此本质上是跨平台的。作为一个旨在运行通过网络下载的软件的环境,它通过语言设计、沙盒容器和细粒度的权限模型内置了安全性。

在各种操作系统之间移植应用程序是一项棘手的业务。Sun Microsystems 发现自己处于令人羡慕的地位,掌握着解决问题的能力和机遇。鉴于这种潜力,正在讨论将 Java 引入浏览器,但尚不清楚这笔交易会是什么样子或何时落地。

作为一种面向对象的编程 (OOP) 语言,Java 包含复杂的语言功能,例如线程和继承。Netscape 担心这对于非专业软件开发人员来说可能太难掌握,因此他们聘请 Brendan Eich 创建“浏览器方案” ,创造一种更简单、轻量级的脚本语言。Brendan 可以自由决定他想在语言中包含的内容,但也面临着尽快完成它的压力。

最初,浏览器中的 JavaScript 仅限于简单的交互,例如动态菜单、弹出对话框和响应按钮点击。相比于每次用户活动都往返于服务器进步了很多,但与当时在台式机和工作站相比,它仍然是一个玩具。

我在网络早期工作的公司创建了第一个全地球可视化环境,其中包括 TB 级的地形信息、高光谱图像和从无人机视频中提取视频帧。最初需要 Silicon Graphics 工作站,之后在几年内能够在配备消费级图形处理单元 (GPU) 的 PC 上运行。那时在网络上运行几乎是不可能的,但多亏了 WebAssembly,这不再是幻想。

客户端和服务器之间的关注点分离的好处之一是客户端可以独立于服务器而发展。虽然 Java 和 Java Enterprise 模型开始主导后端,但 JavaScript 在浏览器中发展并最终成为它的主导力量。

网络平台的演变

随着 Java applets和 JavaScript 在 Netscape 浏览器中可用,开发人员开始尝试动态页面、动画和更复杂的用户界面组件。多年来,这些仍然只是玩具应用,但这个愿景很有吸引力,不难想象它最终会走向何方。

微软认为有必要跟上步伐,但对直接支持竞争对手的技术并不太感兴趣。他们认为 Web 开发最终可能会颠覆他们的操作系统主导地位。当他们发布支持脚本的 Internet Explorer 时,他们将其称为 JScript 以避免法律问题并逆向工程 Netscape 的解释器。他们的版本支持与基于 Windows 的组件对象模型 (COM) 组件的交互,并具有其他一些变化,可以轻松地在浏览器之间编写不兼容的脚本。他们对将 JavaScript 标准化为 ECMAScript 的支持减弱了一段时间,最终导致了浏览器大战开始了。对于最终涉及美国政府针对微软的反竞争诉讼的开发人员来说,这是一个令人沮丧的时期。

随着 Netscape 的命运日渐衰落,Internet Explorer 开始主导浏览器领域,跨平台创新也逐渐消退。Java applets在某些圈子中得到了广泛使用,但它们在沙盒环境中运行,因此将它们用作驱动动态网页活动的基础会比较困难。您当然可以使用 Sun 的图形和用户界面 API 来做富有成效和有趣的事情,但它们在与 HTML 文档对象模型 (DOM) 在不同的内存空间中运行。它们不兼容并且具有不同的编程和事件模型。沙盒元素和 Web 元素之间的用户界面看起来并不相同。总的来说,这是一次完全不合适的开发体验。

其他可移植技术(例如 ActiveX)在 Microsoft Web 开发领域变得流行。Macromedia 的 Flash 变成了 Adobe 的 Flash,并流行了大约10年。然而,所有这些都存在问题。内存空间彼此隔离,安全模型没有了人们希望的那么健壮。引擎是新的并且在不断开发中,所以错误很常见。ActiveX 提供了代码签名保护,但没有沙箱,因此如果可以伪造证书,就可能发生相当可怕的攻击。

Firefox 从 Mozilla 中脱颖而出,成为从 Netscape 灰烬中脱颖而出的一个可行的竞争对手。它和谷歌的 Chrome 最终成为 Internet Explorer 的合适替代品。每个阵营都有自己的拥护者,但人们对解决它们之间的不相容性越来越感兴趣。浏览器的选择迫使每个供应商更加努力地工作,做得更好,以此作为实现技术优势和吸引市场份额的手段。

因此,JavaScript 引擎发展的速度明显加快。尽管 HTML 4 在跨浏览器和平台上使用仍然“古怪”和痛苦,但已经开始有可能隔离这些差异。这些发展和在基于标准的环境结构中工作的愿望的结合鼓励了 Jesse James Garrett 想出了一种不同的 Web 开发方法。他引入了术语 Ajax,它代表一组标准的组合:异步 JavaScript 和 XML。这个想法是让来自后端系统的数据流入前端应用程序,前端应用程序将动态响应新输入。浏览器成为了基于 Web 的客户端。

长期受苦的 HTML 5 标准化过程也在此期间开始,目的是提高浏览器之间的一致性,引入新的输入元素和元数据模型,并提供硬件加速的 2D 图形和视频元素以及其他功能。Ajax 、ECMAScript 标准为一种语言、更容易的跨浏览器支持、功能日益丰富web环境,这些东西的聚集导致web爆炸式增长。我们已经看到无数基于 JavaScript 的应用程序框架来来去去。随着开发者不断突破极限,浏览器供应商会改进他们的引擎,以进一步推动极限。

随着其他障碍和限制的消除,这种位于其核心的奇怪的小语言变得开始拖累前进步伐。随着 WebGL和 WebRTC 等技术的开发和采用,Web 平台标准不断发展。不幸的是,JavaScript 的性能限制使其不适合使用涉及低级网络、多线程代码和图形以及流视频编解码器的功能来扩展浏览器。

Web平台的发展需要 W3C 成员组织艰难地决定什么是重要的设计和构建,然后将其推广到各种浏览器实现中。然后一切都必须用 JavaScript 重新编写,或者浏览器们将行为和界面标准化,但这可能需要数年时间才能实现新的进步。

正是出于这些和其他原因,谷歌开始考虑安全、快速和便携的客户端 Web 开发的替代方法。

Native Client (NaCl)

2011 年,Google 发布了一个名为 Native Client (NaCl) 的新开源项目。这个想法是为了安全起见,在有限权限的沙箱中运行时,在浏览器中提供接近本机的代码执行速度。你可以把它想象成有点像背后有一个真正的安全模型的 ActiveX。该技术非常适合 Google 的一些更大目标,例如支持 ChromeOS 并将事物从桌面应用程序转移到网络应用程序中。

这些主要用于一些支持基于浏览器的计算密集型软件的交付,例如:

  • 游戏
  • 音频和视频编辑系统
  • 科学计算和 CAD 系统
  • simulations

最初的重点是将 C 和 C++ 作为源语言,但由于他们都基于 LLVM 编译器工具链,因此有可能支持可以生成 LLVM 中间表示 (IR)其他语言。正如您将看到的,这将是我们向 WebAssembly 过渡的一个反复出现的主题。

这里有两种形式的可分发代码。第一个是同名的 NaCl,它产生了针对特定硬件架构(例如 ARM 或 x86-64)并且只能通过 Google Play 商店分发的“nexe”模块。另一种是称为 PNaCl 的可移植形式,它将以 LLVM 的 Bitcode 格式表示,使其与目标无关。这些被称为“pexe”模块,需要在客户端的主机环境中转换为原生架构。

该技术是成功的,因为在浏览器中展示的性能与本机执行速度相差无几。通过使用软件故障隔离 (SFI) 技术,他们创造了从网络下载高性能、安全代码并在浏览器中运行的能力。一些流行的游戏,如 Quake 和 Doom 被编译成这种格式,以展示最终可能的结果。问题是需要为每个目标平台生成和维护 NaCl 二进制文件,并且只能在 Chrome 中运行。它们还在进程外空间中运行,因此它们无法直接与其他 Web API 或 JavaScript 代码交互。

虽然可以在有限权限的沙箱中运行,但它确实需要对二进制文件进行静态验证,以确保它们不会尝试直接调用操作系统服务。生成的代码必须遵循某些地址边界对齐模式,以确保它们不会违反分配的内存空间。

如上所述,PNaCl 模块更便于移植。LLVM 基础架构可以生成 NaCl 本地代码或可移植的 Bitcode,而无需修改原始源代码。这是一个不错的结果,但是代码可移植性和应用程序可移植性之间存在差异。应用程序需要它们依赖的 API 可用才能工作。Google 提供了一个应用程序二进制接口 (ABI),称为 Pepper APIs用于低级服务,例如 3D 图形库、音频播放、文件访问(通过 IndexedDB 或 LocalStorage 模拟)等。由于 LLVM,PNaCl 模块可以在不同平台的 Chrome 中运行,但它们只能在提供合适的 Pepper API 实现的浏览器中运行。虽然 Mozilla 最初表示有兴趣这样做,但他们最终决定尝试一种不同的方法,即 asm.js。NaCl 在推动行业朝着这个方向发展方面值得高度赞扬,但它最终过于繁琐且过于特定于 Chrome,无法推动开放网络向前发展。Mozilla 的尝试在这方面更为成功,即使它没有提供与Native Client方法相同的性能水平。

asm.js

asm.js项目开始是为了将更好的游戏带到web上。这很快扩展到允许任意应用程序安全地交付到浏览器沙箱,而无需实质性地修改现有代码。

正如我们之前所讨论的,浏览器生态系统已经在推进以基于标准的跨平台方式提供 2D 和 3D 图形、音频处理、硬件加速视频等。这个想法是允许应用程序使用任何定义为从 JavaScript 调用的功能。JavaScript 引擎非常高效,并且拥有经过重大安全审计的强大沙盒环境,因此没有人愿意从头开始。真正的问题仍然是无法提前优化 JavaScript (AoT),因此可以进一步提高运行时性能。

由于它的动态特性和缺乏适当的整数支持,在将代码加载到浏览器之前,存在一些无法有效管理的性能障碍。一旦发生这种情况,虽然JIT能够很好地加快速度,但仍然存在一些固有问题,例如缓慢的边界检查。虽然 JavaScript 的整体无法提前优化,但它的一个子集可以。

具体的细节我们不再赘述,最终的结果是 asm.js 通过Emscripten 工具链 使用了基于llvm的clang前端解析器 。编译后的 C 和 C++ 代码可以提前优化,因此可以通过现有的优化通道非常快速地生成生成的指令。LLVM 代表了一种干净的模块化架构,因此可以替换其中的一部分,包括机器代码的后端生成。本质上,Emscripten 团队可以重用前两个阶段(解析和优化),然后将 JavaScript 的这个子集作为自定义后端输出。因为输出都是“ JavaScript”,所以它比 NaCl/PNaCl 方法更可移植。不幸的是,在性能上不如 Google 的方法。不过,这足以让开发人员感到惊讶。我们来看一个简单的例子。“hello world!” 似乎是一个很好的起点。

#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}

请注意,此版本的经典程序没有任何异常。如果你将它存储在一个名为hello.c的文件中,emscripten 工具链将允许你输出一个名为a.out.js的文件,它可以直接在 Node.js中运行,或者通过一些脚手架在浏览器中运行。

brian@tweezer ~/s/w/ch01> emcc hello.c 
brian@ tweezer ~/s/w/ch01>node a.out.js
Hello, world!

很酷,不是吗?

只有一个问题。

brian@tweezer ~/s/w/ch01> ls -lah a.out.js
-rw-r--r-- 1 brian staff 119K Aug 17 19:08 a.out.js

119 KB, 这是一个非常大的 hello world 程序!快速查看本机可执行文件可能会让您了解正在发生的事情。

brian@tweezer ~/s/w/ch01> clang hello.c
brian@tweezer ~/s/w/ch01> ls -lah a.out
-rwxr-xr-x 1 brian staff 48K Aug 17 19:11 a.out

为什么我们所谓的优化 JavaScript 程序比原生版本大近三倍?这不仅仅是因为作为基于文本的文件 JavaScript 更加冗长。

如果我们使用 nm 查看已编译的可执行文件中定义的符号,我们将看到该printf()函数的定义不包含在二进制文件。它被标记为“U”代表“未定义”。

brian@tweezer ~/s/w/ch01> nm -a a.out
0000000100002008 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000f50 T_main
_printf
U dyld_stub_binder

当 clang 生成可执行文件时,它留下了一个占位符引用,指向它期望由操作系统提供的函数。浏览器没有以这种方式可用的标准库,至少不是动态加载的,因此还需要提供库函数和它需要的任何东西。此外,此版本无法直接与浏览器中的控制台对话,因此需要为其提供挂钩以调用诸如浏览器功能之类的console.log()功能。为了在浏览器中工作,功能必须随应用程序一起提供,这就是它最终变得如此庞大的原因。

这很好地突出了可移植代码和可移植应用程序之间的区别,这是本书的另一个共同主题。现在,我们可以惊叹它的工作原理,但是这本书没有被称为“asm.js:权威指南”是有原因的。这是一个非凡的垫脚石,证明了可以从各种可优化语言生成性能合理的沙盒 JavaScript 代码。JavaScript 本身也可以以超集无法实现的方式进一步优化。通过基于 LLVM 的工具链和自定义后端生成这个子集,工作量比其他方式小得多。

对于不支持 WebAssembly 标准的浏览器,asm.js 代表了一个很好的后备位置,但现在是为本书主题奠定基础的时候了。

WebAssembly 的兴起

使用 NaCl,我们找到了一种提供沙盒和性能的解决方案。使用 PNaCl,我们还发现了平台可移植性,但未发现浏览器可移植性。使用 asm.js,我们发现浏览器可移植性和沙盒,但性能水平不同。我们也仅限于处理 JavaScript,这意味着我们无法在不首先更改语言本身的情况下使用新功能(例如高效的 64 位整数)扩展平台。鉴于这是由国际标准组织管理的,这不太可能是一种快速周转的方法。

此外,JavaScript 在浏览器如何从 Web 加载和验证它方面存在某些问题。浏览器必须等到它完成下载所有引用的文件,然后才能开始验证和优化它们(而进一步的优化将需要我们等到应用程序已经运行)。鉴于我们已经说过开发人员如何用大量的可传递依赖项阻碍他们的应用程序,JavaScript 的网络传输和加载时性能是超越既定运行时问题需要克服的另一个瓶颈。

在看到这些部分解决方案的可能性之后,人们对高性能、沙盒、可移植代码产生了强烈的需求。浏览器、Web 标准和 JavaScript 环境中的各种利益相关者都认为需要一种在现有生态系统范围内工作的解决方案。为了让浏览器达到他们的水平,已经做了大量的工作。创建跨操作系统平台和浏览器实现的动态、有吸引力和交互式应用程序是完全可能的。只需稍加努力,似乎就可以将这些愿景合并为一种统一的、基于标准的方法。

2015 年正是在这种情况下,Javascript 的创造者 Brendan Eich 宣布 WebAssembly 工作已经开始。他强调了努力的几个具体原因,并将其称为“低级安全代码的二进制语法,最初类似于asm.js,但从长远来看,能够与 JS 的语义不同,以便最好地作为多种源级编程语言的通用对象级格式。”

他继续说道:“区别:零成本异常、动态链接、call/cc。是的,我们的目标是开发 Web 的多语言编程语言目标文件格式。”

至于为什么各方对此感兴趣,他给出了这样的理由:“asm.js 很棒,但是一旦引擎针对它进行优化,解析器就会成为发热点——在移动设备上将会很烫。需要传输压缩并节省带宽,但在解析之前解压缩会造成伤害。”

最后,也许公告中最令人惊讶的部分是谁将参与其中:“一个 W3C 社区组,WebAssembly CG,向所有人开放。正如你从 github 日志中看到的,WebAssembly 到目前为止是谷歌、微软、Mozilla 和其他一些人的共同努力。

很快,Apple、Adobe、AutoCAD、Unity 和 Figma 等其他公司都支持了这项工作。令人费解的是,这个几十年前就已经开始并且没有结束冲突和利己主义的愿景正在转变为一个统一的倡议,最终为我们带来一个安全、快速、可移植和网络兼容的运行时环境。

在接下来的一年左右,CG 成为 W3C 工作组 (WG),其任务是定义实际标准。他们做出了一系列决定来定义一个所有主要浏览器供应商都支持的最小可行产品 (MVP) WebAssembly 平台。此外,Node.js 社区很兴奋,因为这可以为需要用低级语言编写的 Node 应用程序部分管理本机库的苦差事提供解决方案。Node.js 应用程序不需要依赖于 Windows、Linux 和 macOS 库,而是可以拥有一个 WebAssembly 库,该库可以加载到 V8 环境中并即时转换为本地汇编代码。突然间,WebAssembly 似乎准备超越在浏览器中部署代码的目标。