This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,290 @@
<audio id="audio" title="01 | 基础篇:学习此课程你需要了解哪些基础知识?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fe/97/fe4625e50322eeff76a8dea40898d397.mp3"></audio>
你好,我是于航。
在我们正式进入到 WebAssembly 的学习之前,为了帮助你更好地理解课程内容,我为你准备了一节基础课。
在这一节基础课中,我将与编程语言及计算机基础相关的一些概念,按照其各自所属的领域进行了分类,供你进行本课程的预习与巩固。
这些概念大多都相互独立,因此你可以根据自己的实际情况选择性学习。在后面的课程中,我将会直接使用这些概念或术语,不再过多介绍。当然,如果你对这些知识足够熟悉,可以直接跳过这节课。
## JavaScript
接下来,我将介绍有关 JavaScript 的一些概念。其中包括 ECMAScript 语言规范中提及的一些特性,以及一些经常在 Web 应用开发中使用到的 JavaScript Web API。
### window.requestAnimationFrame
window.requestAnimationFrame 这个 Web API ,主要用来替代曾经的 window.setInterval 和 window.setTimeout 函数,以专门用于处理需要进行“动画绘制”的场景。
该方法接受一个回调函数作为参数,该回调函数将会在下一次浏览器尝试重新绘制当前帧动画时被调用。因此,我们便需要在回调函数里再次调用 window.requestAnimationFrame 函数,以确保浏览器能够正确地绘制下一帧动画。
这个 API 一个简单的用法如下所示。
```
&lt;html&gt;
&lt;head&gt;
&lt;style&gt;
div {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;script&gt;
let start = null;
let element = document.querySelector('div');
const step = (timestamp) =&gt; {
if (!start) start = timestamp;
let progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress &lt; 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
&lt;/script&gt;
&lt;/html&gt;
```
在这段代码中为了便于展示,我们直接连同 CSS 样式、HTML 标签以及 JavaScript 代码全部以“内嵌”的方式,整合到同一个 HTML 文件中。
页面元素部分,我们使用
在 JavaScript 代码部分,我们首先通过 “document.querySelector” 的方式获取到了该矩形对应的 DOM 元素对象。并编写了一个用于绘制动画的函数 “step”。
在这个函数定义的最后,我们调用了 window.requestAnimationFrame 方法,来触发对动画下一帧的绘制过程。由此便构成了一个间接递归,动画便可以持续不断地绘制下去,直到 “progress &lt; 2000” 这个条件不再成立。
对于这段动画的实际播放效果,你可以参考下面这张动图
<img src="https://static001.geekbang.org/resource/image/69/b5/69672c8f0944ebb2ed89bbyy7eef66b5.gif" alt="">
此时,整个矩形也被移动到了距离页面最左侧边界 200 像素的位置。这里你可以思考一下,整个动画从开始到结束一共持续了多长时间呢?
### Performance API
相信单从名字上,你就能够猜测出这个 Web API 的主要功能了。没错,借助于 Performance API我们可以非常方便地获得当前网页中与性能相关的一些信息。比如其中最常用的一个应用场景 —— “测量一段 JavaScript 代码的执行时间”。
我们可以使用名为 Performance.now 的 API 来达到这个目的。一段示例代码如下所示。
```
let start = performance.now();
for (let i = 0; i &lt; 10e7; ++i) {}
// Time Span: 97.4949998781085 ms.
console.log(`Time Span: ${performance.now() - start} ms`);
```
这段代码十分简单。首先我们调用 performance.now(),来获得当前时刻距离 “time origin” 所经过的毫秒数。这里你可以把 “time origin” 简单理解为当前页面创建的那个时刻。
然后我们执行了一千万次的空循环结构,主要用于模拟耗时的待测量 JavaScript 逻辑。在代码的最后,我们通过 “performance.now() - start” 便可以得到,当前时刻与上一次在 start 处所测量的时刻,两者相差的时间间隔。这段时间便是一千万次空循环结构所消耗的时间。
### TypedArray
顾名思义TypedArray 便是指“带有类型的数组”,我们一般简称其为“类型数组”。
我们都知道,在默认情况下,出现在 JavaScript 代码中的所有数字值,都是以“双精度浮点”的格式进行存储的。
也就是说,假设我们有如下所示的一个普通 JavaScript 数组,对于数组内部的每一个元素,我们都可以重新将其赋值为双精度浮点类型所能表示值范围内的,任意一个值。
你可以试着将该数组的第一个元素的值设置为 “Number.MAX_VALUE”。该值表示在 JavaScript 中所能表示的最大数值,在我本机上的结果为 “1.7976931348623157e+308”。
```
let arr = [1, 2, 3, 4];
```
而 TypedArray 则不同于传统的 JavaScript 数组。TypedArray 为内部的元素指定了具体的数据类型,比如 Int8 表示的 8 位有符号整型数值、Float32 表示的 32 位单精度浮点数值,以及 Uint32 表示的 32 位无符号整型数值等等。
TypedArray 实际上构建于底层的“二进制数据缓冲区”,在 JavaScript 中可以由 ArrayBuffer 对象来生成。ArrayBuffer 描述了一个字节数组,用于表示通用的、固定长度的原始二进制数据缓冲区。
由于 ArrayBuffer 中的数据是以“字节”为单位进行表示的,因此我们无法直接通过 ArrayBuffer 对象来操作其内部的数据,而是要通过 TypedArray 以某个固定的“类型视图”,按照某个具体的“数据单位量度”来操作其内部数据。
如下代码所示,我们可以通过几种常见的方式来使用 TypedArray。
```
const DEFAULT_INDEX = 0;
// Way one:
const int8Arr = new Int8Array(10);
int8Arr[DEFAULT_INDEX] = 16;
console.log(int8Arr); // Int8Array [16, 0, 0, 0, 0, 0, 0, 0, 0, 0].
// Way two:
const int32Arr = new Int32Array(new ArrayBuffer(16));
int32Arr.set([1, 2, 3], 0);
console.log(int32Arr); // Int32Array [1, 2, 3, 0].
```
这里我列出了两种 TypedArray 的使用方式。第一种,我们可以直接通过相应类型的 TypedArray 构造函数来构造一个类型数组。比如这里我们使用的 Int8Array其构造函数的参数为该数组可以容纳的元素个数。然后我们修改了数组中第一个元素的值并将整个数组的内容“打印”了出来。
第二种使用方式其实与第一种十分类似,唯一的不同是我们选用了另一种 TypedArray 的构造函数类型。该构造函数接受一个 ArrayBuffer 对象作为其参数,生成的 TypedArray 数组将会以该 ArrayBuffer 对象作为其底层的二进制数据缓冲区。
但需要注意的是,由于 ArrayBuffer 的构造函数其参数指定了该 ArrayBuffer 所能够存放的单字节数量,因此在“转换到”对应的 TypedArray 时,一定要确保 ArrayBuffer 的大小是 TypedArray 元素类型所对应字节大小的整数倍。
另一个需要关注的点是,在方法二中,我们使用了 TypedArray.prototype.set 方法将一个普通 JavaScript 数组中的元素,存放到了刚刚生成的,名为 int32Arr 的类型数组中。
该方法接受两个参数,第一个参数为将要进行数据读取的 JavaScript 普通数组;第二个参数为将要存放在类型数组中的元素偏移位置。这里我们指定了第二个参数为 0因此会从 int32Arr 的第一个元素位置开始存放。
## C/C++
在这个部分中,我将介绍有关 C/C++ 语言的一些概念。其中包括在编写 C/C++ 代码时可以使用到的特殊语法结构,以及在编译 C/C++ 源代码时的特殊编译器行为和选项。
### extern “C” {}
通常我们在编译一段 C++ 源代码时,由于 C++ 天生支持的“函数重载”特性,因此需要一种能够在最终生成的可执行文件中,区别出源代码中定义的同名函数的机制。编译器通常会使用名为 “Name Mangling” 的机制来解决这个问题。
Name Mangling 会将 C++ 源代码中的函数名,在编译时进行一定的变换。这样,重载的同名函数便可以在可执行文件中被区分开。一般的实现方式通常是将函数名所对应函数的实际函数签名,以某种形式拼接在原有的函数名中。举个例子,假设我们有如下这段 C++ 代码。
```
int add(int x, int y) {
return x + y;
}
int main(int argc, char** argv) {
int x = add(0, 1);
std::cout &lt;&lt; x;
return 0;
}
```
经过编译,我们可以使用诸如 readelf / objdump / nm 等命令行工具,来查看生成的可执行文件其内部的符号列表。然后你会发现我们在源代码中定义的那个函数 “add”名称在经过 Name Mangling 处理后变成了 “_Z3addii”。
而 “extern “C” {}” 这个特殊的语法结构便可以解决这个问题。我们按照以下方式改写上述代码。
```
#include &lt;iostream&gt;
extern &quot;C&quot; {
int add(int x, int y) {
return x + y;
}
}
int main(int argc, char** argv) {
int x = add(0, 1);
std::cout &lt;&lt; x;
return 0;
}
```
在经过编译后,以同样的方式查看编译器生成的可执行文件内的符号信息,你会发现我们在源代码中定义的函数 “add” 其名称被保留了下来。
之所以会产生这样效果是由于在这个特殊的结构中C++ 编译器会强制以 C 语言的语法规则,来编译放置在这个作用域内的所有 C++ 源代码。而在 C 语言的规范中,没有“函数重载”这类特性,因此也不会对函数名进行 “Name Mangling” 的处理。
### DCEDead Code Elimination
在编译器理论中DCE 是一种编译优化技术,将其翻译成中文即“死码消除(没有业界统一的中文叫法)”。从名字上你可以理解为,通过 DCE 这种技术,编译器可以将源代码中没有使用到的代码从最后的目标产物中移除,以便优化其最终大小及执行效率。
但实际上 DCE 会更进一步,它消除的是那些对程序最后运行结果没有任何影响的代码,而不仅仅是没有用到的代码。
同样的,我们来举个例子。比如对于下面这段 C/C++ 代码,编译器会怎样进行 DCE 优化呢?
```
int foo() {
int a = 24;
int b = 25; // 没有被使用到的变量 b
int c;
c = a &lt;&lt; 2; // 变量值无关乎外部输入;
return c;
b = 24;
return 0;
}
```
一般来说,我们可以得到如下与汇编代码等价的 C/C++ 代码优化结果。
```
int foo() {
return 96;
}
```
这里你可以按照我在代码中给出的注释信息,来尝试思考一下,编译器是如何优化我们之前那段 C/C++ 代码的,相信这一定不会难住你。
### -O0 / -O1 / -O2 等优化编译选项
在诸如 Clang / GCC 等编译器中,我们通常可以为编译器指定一些有关编译优化的标记,以让编译器可以通过使用不同等级的优化策略来优化目标代码的生成。而诸如 -O0 / -O1 / -O2 一直到 -Os 与 -O4 等选项,便是这些优化标记中的一部分。
在通常情况下,编译器会使用 “-O0” 来作为默认的编译优化等级。在该等级下,编译器一般不会进行任何优化,因此可以在最大程度上降低编译时间,保留最多的调式性信息。此模式一般用于对应用程序进行调试,亦可作为默认的本地开发时编译选项。
相反,诸如 “-O3” 与 “-O4” 等标记,一般用于对“生产版本”进行深入的优化。所谓“生产版本”是指即将发布给用户使用的二进制版本。对于这些版本,我们需要使用较高的优化等级,以尽量提升可执行程序的运行性能。
在这些编译优化等级下,编译器会启用多种优化策略来优化输入代码。相对的,这些选项通常也会提升编译时间,并且使得编译结果难以进行调试。所以实际上,不同的优化编译选项,其实对应着不同的使用场景。
## 计算机基础知识
在这个部分中,我将给你介绍几个计算机基础知识中的常见概念。
### 原码、反码和补码
我们知道,在计算机科学中,数字一共有三种表示方式,即“原码”、“反码”和“补码”。但实际上,计算机在存储数字值时会采用“补码”的形式。由于浮点数通常会采用 IEEE-754 标准进行编码,因此这里我们不讨论浮点数的补码形式,仅讨论整数。
这里我以有符号数 “-10” 为例,来给你介绍一下它从原码到反码,最后再到补码的具体转换过程。
首先,对于原码来说,其最高位会被用来当做符号位,该位为 “0” 表示正数“1” 则表示负数。假设这里我们使用一个 1 字节8 位)大小的 “signed char” 有符号整数类型变量来存储该数字,则 “-10” 所对应的原码如下。
```
1000 1010
```
而要将原码转换为对应的反码,我们需要把上述二进制数字的最高位符号位保持不变,而将其他位取反。也就是把 “1” 变 “0”“0” 变 “1”。得到的反码如下所示。
```
1111 0101
```
最后,为了将反码再转换为补码,我们只需要为上述二进制数字再加上“一”即可。
```
1111 0110
```
对于无符号数而言,由于它没有符号位,因此变量对应的所有数据位都可以用来存放它的值。并且它的原码、反码以及补码三种形式均完全相同。也就是说,无符号数的反码和补码,与其原码保持一致。
### ACLAccess Control List
ACL 翻译成中文即“访问控制列表“,它负责告诉计算机操作系统,每一个用户对特定的系统对象(比如某个文件)具有哪些访问权限。在 ACL 中,每一个条目都包含有权限相关的主体与相应可以执行的操作。在类 Unix 系统中,最为直观的一个 ACL 的体现便是 “ls -l” 命令的输出结果,如下图所示。
<img src="https://static001.geekbang.org/resource/image/c8/8a/c87096a1286628b3ff2b00e25ef4ab8a.png" alt="">
在这张图中“ls -l” 命令打印出了当前位置下的所有文件与文件夹信息。附带的,还有针对每一个文件或文件夹的权限及所有者信息。比如以 “rwx” 形式表示的,针对不同种类用户分配的,对于这些文件或文件夹所能够执行的操作信息(可读、可写、可执行)。以及文件或文件夹所有者的名字及其所在组的信息。总而言之,这便是 ACL 在 Unix 中的一类直观的表现形式。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在本节基础课中,我主要给你介绍了三部分内容。这些内容分别涉及 JavaScript 语言和相关 API 的概念及用法、C/C++ 相关的一些语言及编译时特性,以及其他的一些计算机基础知识。
其中JavaScript 方面,我们介绍了专用于制作 JavaScript 动画的 window.requestAnimationFrame API 的简单用法,以及用于测量网页性能数据的 Performance API 的简单用法。
C/C++ 方面,我们主要介绍了 “extern “C” {}” 结构的基本用法,该结构可用于“停用” Name Mangling 机制。定义在该结构内的函数在经过编译后,其名称不会被改变。
除此之外DCE 作为一种编译器常用的优化技术,将会帮助我们在最终输出的二进制文件内,移除对源代码功能没有影响的代码部分,以优化可执行文件的性能。而 “-O0”、“-O1” 与 “-O2” 等优化编译选项,则将会影响 DCE 的具体“功效”。
最后在计算机基础知识方面,我们介绍了 “原码、反码、补码”,以及 ACL 的概念。前者主要通过不同的形式来表示计算机中的“数字”,当实际存储时,计算机会采用“补码”的形式。而 ACL 通常是计算机权限控制系统的一个重要组成部分,它代表了一系列通过“访问控制列表“来管理系统权限的模式。
希望这节基础课能够为你在接下来的 WebAssembly 学习之旅中,提供一些帮助。
## 课后思考
最后,我们来做一个思考题吧。
这个问题引申自我们在本节课中介绍的“原码”、“反码”以及“补码”的概念。你知道在计算机中,有符号数之间的减法操作(比如 “10 - 3”是如何进行运算的吗
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="02 | 历史篇:为什么会有 WebAssembly 这样一门技术?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/b3/2cccd686797c69c29ed4756b4dec83b3.mp3"></audio>
你好,我是于航。
在开始真正学习 Wasm 这门技术之前,我想先来问你一个问题:你有没有思考过,在 Web 技术的历史发展长河中,为什么会出现 Wasm 这样一门技术?现有的这些 Web 技术,又存在着哪些无法解决的问题?
要知道,所有新兴技术的诞生都一定有它存在的意义,或者要去解决的问题。比如 jQuery 之于浏览器的兼容性、Vue.js / React.js 之于 Web 应用的构建模式。
虽然用前端框架和库来类比 Wasm 不算十分合适但我想阐述的是Wasm 的出现也并非偶然。在这节课的内容中,我们就来一起看看 Wasm 诞生背后的那些故事。相信在学习完本课程后,你会对 Wasm 有了一些新的了解。而这些了解有时可能比一项技术本身更加重要。
## JavaScript 的发展和困境
1995 年末Brendan Eich 仅用了 10 天时间便发明出了 JavaScript 编程语言而在随后的二十多年中JavaScript 已经成为了不可动摇的,用于开发 Web 前端应用的必备编程语言之一。
不仅如此,随着后来诸如 React Native、Electron 以及 Vue.js 等各类框架的不断涌现JavaScript 曾经一度成为 GitHub 语言排行榜的年度冠军。JavaScript 也因此被广泛应用到了各行各业、各个领域的各类项目中。
虽说 JavaScript 的应用场景如此广泛,但也会有它自己的烦恼。下面我们就从 Web 应用层面以及 JavaScript 语言本身,来看看它究竟在愁些什么。
### Web 应用规模的急速增长
随着移动互联网的发展和各种形式经济活动的不断展开,运行在浏览器中的各类 Web 应用,它们的体积与复杂性随着时间的推移在不断发展。为了能够在浏览器中高效运行这些不断“变大”的 Web 应用,浏览器厂商们也在不断地寻求着各种“黑科技”来优化浏览器的性能。
但与日益庞大和复杂化的 Web 应用相比,浏览器对自身性能的优化可谓是举步维艰。不难预见,当“复杂化”与“性能优化”的速度之比不断变大时,迟早有一天,浏览器会再也无法支撑起这些庞大 Web 应用的运行。
据相关数据统计,截止 2019 年底,全世界一共有约 16 亿个可索引网页,而其中的 95% 都在使用 JavaScript。在这些网页中大约有 45% 的网页创建于最近 5 年。而 2015 年ECMAScript 2015 (ES6) 诞生,也标志着 JavaScript 开始进入了标准一年一更新的节奏中。
现代的大多数网页,都会使用较新的 JavaScript 语法标准进行开发,然后在发布时使用诸如 Babel 等工具,将这些新的 JavaScript 语法转换为对应的 ES5 旧版本语法,来兼容旧版本浏览器。但这样做,产生的各类 Polyfill 代码,会极大地增加整个 Web 应用的体积。
同时,在 Web 应用的实际运行过程中,大量的 JavaScript 代码也会降低应用的整体运行效率。Twitter 曾尝试直接以 ES6+ 版本代码的形式,来发布整个 Web 应用。通过这种方式所减少的 Polyfill Bundle 文件的大小,竟然可以达到应用所使用的全部 JavaScript 代码的 83%。
### JavaScript 的弱类型之殇
除了上面我们讲到的,浏览器性能优化与 Web 应用规模日益增大这两者行进速度的“不协调”所可能带来的问题之外JavaScript 语言本身也有着其自身的“弱点”。而由于这些“弱点”所带来的妥协,使得浏览器在面对庞大的 Web 应用时,也会显得力不从心。
可以说JavaScript 是一个“动态类型”的编程语言。在实际编码过程中我们不需要为每一个变量指定对应类型。变量具体类型的推导过程会被推迟到代码的实际运行时再进行。JavaScript 这种动态类型语言所独有的特性,在某种程度上相较于静态类型语言而言,会带来额外的运行时性能开销。
下面我们来一起想象一下, JavaScript 引擎在执行表达式 “x + y” 时的具体流程。这里 x 与 y 分别是在一段 JavaScript 代码中定义的两个变量,当引擎执行到 “x + y” 时,对于运算符 “+” 来说,位于其左右两侧的操作数可以是 JavaScript 中任何有效类型的组合,比如 “{} + []”、“[] + null”、“1 + 2” 等等。因此,引擎在对 “+” 运算符表达式进行求值时,会根据 ECMAScript 标准中规定的 “+” 运算符的语义,来对表达式进行求值。
通过下图你可以看到, 在 ECMAScript 标准中定义的,“+” 运算符的运行时求值流程实际上十分复杂和繁琐。这也是相对于静态语言来说JavaScript 很少能够进行优化的地方。
在现代的 JavaScript 引擎中,尽管可以使用诸如 JIT 等技术来提高代码的执行效率,但在实际使用中,如果代码执行没有遵守 JIT 优化路径中特定 Guard 的要求,“去优化”的过程,也同样会影响引擎的整体执行效率。而这些影响都是由于 JavaScript 的“动态性”导致的。
<img src="https://static001.geekbang.org/resource/image/e4/47/e40df57ac5cc30613170717291fd8b47.png" alt="" title="图片来自 ECMAScript@2020 官方标准文档
">
## 最初的尝试 —— NaCl 与 PNaCl
JavaScript 的发展困境在逐渐显现,人们对 Web 性能的担忧也在与日俱增人们永远没有停下优化的脚步。NaCl 是由 Google 在 2011 年于 Chrome 浏览器中发布的一项技术,该技术旨在提供一个沙盒环境,可以让基于 C/C++ 语言编写的 Native 应用安全地运行在浏览器中。NaCl 的全称 “Native Client” 也暗示了这一点。
如下图所示,一个标准 NaCl 应用的组成结构,与普通的 JavaScript Web 应用十分类似。NaCl 模块作为应用的一部分主要用来进行复杂的数据处理和运算JavaScript 则负责处理应用与外部用户的交互逻辑。NaCl 实例与 JavaScript 代码之间可以通过“订阅/发布”模型,来互相传递消息。
<img src="https://static001.geekbang.org/resource/image/b9/cb/b95925e8a831ec3266727fedfc2175cb.png" alt="" title="图片来自 Chrome 官方相关文档
">
理想虽好,但现实却存在着很多问题。通常,一个 NaCl 模块文件需要在开发者本地进行编译然后才能够在浏览器中使用。而本地编译的模块文件通常仅含有架构相关architecture-dependent的代码因此没有办法直接在其他类型的系统中使用。
一个完整的 NaCl 应用在分发时需要提供支持多个架构平台X86_32 / X86_64 / ARM 等)的模块文件。浏览器在实际使用时,会根据当前系统的具体架构类型,来动态地选择,对应合适的模块文件进行使用。
不仅如此,由于 NaCl 模块“平台依赖”的特殊性,因此 NaCl 模块进行分发的过程,仅能够在 Chrome Web Store 中进行。 另一方面,如果你想要将已经存在的 C/C++ 代码库编译至 NaCl并在浏览器中使用你还需要通过名为 Pepper 的库来对这些代码进行重写。
Pepper 提供了很多包装类型,以及用于和浏览器进行交互的 API比如 “PP_Bool” 等。这些 API 和特殊类型可以便于整合传统 C/C++ 代码与 Web 浏览器的沙盒环境。
鉴于 NaCl 存在的“平台依赖”问题Google 在后期又推出了名为 PNaCl 的技术。这里名字中多出来的 “P” 代表着 “Portable”也就是“可移植”的意思。
PNaCl 采用了不一样的生命周期,参考下图我们可以看到,相较于 NaCl 模块直接包含有平台架构相关的代码PNaCl 将源 C/C++ 代码编译到一种中间代码。这些中间代码会在浏览器实际加载这个 PNaCl 模块时,再被转换为对应的平台相关代码。因此,对于 PNaCl 模块而言,分发的过程变得更加简单,且不用担心移植性的问题。
<img src="https://static001.geekbang.org/resource/image/09/46/09ef8002f3411732fc1ac513814ab646.png" alt="" title="图片来自 Chrome 官方相关文档">
不过,即使是对于 PNaCl 这类“可移植性”已经不再成为问题的技术而言,它们的面前还有很多“大山”难以逾越。比如:“需要使用 Pepper 重写 C/C++ 代码,标准较为封闭、仅 Chrome 浏览器支持”等等。
总而言之,无论是 NaCl 还是 PNaCl它们都已经成为过去。现在如果你再次回到 NaCl / PNaCl 在 Google 的官方文档网站你会发现如下这样一段声明。Wasm 将会作为新一代的技术,接替并继续传承 Google 赋予给 NaCl / PNaCl 的使命。
<img src="https://static001.geekbang.org/resource/image/e0/0c/e0c11b3ec2da8890a096509992cc1f0c.png" alt="" title="图片来自 Chrome 官方相关文档">
## Wasm 的前身 —— ASM.js
除了 NaCl 与 PNaCl另一个不可不提的技术便是 Mozilla 于 2013 提出的 ASM.js。同前两者一样ASM.js 的设计目标也是为了能够在 JavaScript 语言之外,为“构建更高性能的 Web 应用”这个目标,提供另外一种实现的可能。
“ASM.js 是 JavaScript 的一个严格子集。它是一种可用于编译器的目标语言,低层次且高效。该目标语言有效地为内存不安全语言(如 C/C++),描述了一个沙盒虚拟机运行环境。静态和动态验证相结合的方式,使得 JavaScript 引擎能够使用 AOT 等优化编译策略来验证 ASM.js 代码”。这是 Mozilla 官方给出的关于 “ASM.js 是什么?” 这个问题的解答。
乍一看这段解释,可能会有点抽象和复杂。但实际上,我们只需要知道两件事情。
第一ASM.js 是 JavaScript 的严格子集。这也就意味着,对于一段 ASM.js 代码JavaScript 引擎可以将它视作普通的 JavaScript 代码来执行,这便保障了 ASM.js 在旧版本浏览器上的可移植性。
第二ASM.js 使用了 “Annotation注解” 的方式来标记代码中包括:函数参数、局部/全局变量,以及函数返回值在内的各类值的实际类型。
当 JavaScript 引擎满足一定条件后,便会通过 AOT 静态编译的方式,将这些被 Annotation 标记的 ASM.js 代码,编译成对应的机器码并加以保存。当 JavaScript 引擎再次执行(甚至在第一次执行)这段 ASM.js 代码时,便会直接使用先前已经存储好的机器码版本。因此,引擎的性能会得到大幅的提升。
对于一段标准 ASM.js 代码的具体组成形式,你可以参考下面给出的这段代码,以便有一个更加直观的印象。
```
function asm (stdin, foreign, heap) {
&quot;use asm&quot;;
function add (x, y) {
x = x|0; // 变量 x 存储了 int 类型值;
y = y|0; // 变量 y 存储了 int 类型值;
var addend = 1.0, sum = 0.0; // 变量 addend 和 sum 默认存放了&quot;双精度浮点&quot;类型值;
sum = sum + x + y;
return +sum; // 函数返回值为&quot;双精度浮点&quot;类型;
}
return { add: add };
}
```
在这段 JavaScript 代码中,最为重要的是函数 “asm” 在其函数体定义开头处使用的 “use asm” 指令。这个指令将会在代码执行过程中“告诉” JavaScript 引擎,当前这个函数体内的代码可以按照 ASM.js 代码,来进行相应的优化和处理。
实际上,上述这样的一个 JavaScript 函数,便定义了一个标准的 ASM.js 模块。模块内部可以通过 return 的方式,导出包含有若干内联方法的对象。这些方法可以在外部的 JavaScript 代码中进行调用。
在上述 asm 模块内定义的内联函数 add 中,我们在其开头的前两行代码通过 “x|0” 和 “y|0” 的方式,分别对变量 x 与 y 的值类型进行了标记。而这种方式便是我们之前提到的 ASM.js 所使用的 Annotation。
当 JavaScript 引擎在编译这段 ASM.js 代码时,便会将这里的变量 x 与 y 的类型视为 int 整型。同样的,还有我们对函数返回值的处理 “+sum”。通过这样的 Annotation引擎会将变量 sum 的值视为双精度浮点类型。类似的ASM.js 在标准中还规定了其他的诸多 Annotation 形式,可以将变量值标记为不同的类型,甚至对值类型进行转换。
为了确保上述的这样一个 JavaScript 函数,能够被当做一个标准的 ASM.js 模块进行必要的优化处理JavaScript 引擎通常会在实际编译加载这些模块前,进行很多必要的检查验证工作。
因此,并不是说只要为函数添加了 “use asm” 指令,并且为使用到的变量添加 Annotation 之后JavaScript 引擎就会通过 AOT 的方式来优化代码的执行。所以这也是为什么我们先前提到的ASM.js 通常被作为一种可用于编译器的,低层次且高效的目标语言,而不是用于手写。
## 从过去到未来
时间来到2015年5月。Chrome 团队的 Ben 正在为 V8 设计一种新的 Prototype原型而另一位团队成员 Rosbery ,正在为这种 Prototype 设计对应的字节码格式。实际上,这个 Prototype 和对应的字节码格式,便是如今 Wasm 所分别对应的 WAT 可读文本格式与二进制字节码格式。在当时的谷歌内部,这两部分暂时被称为 ml-proto 与 v8-native-prototype。
随着 V8 团队对 ml-proto 与 v8-native-prototype 的不断修改和优化,它们最终便成为了 Wasm 早期标准的一部分。与此同期出现的,还有一个名为 “sexpr-wasm” 的内部工具 ,在当时这个工具用于对这两种格式进行相互转换。随着 Wasm 的标准化,它也同样成为了 Wasm 常用调试工具的一部分,这也就是我们所熟知的 —— WABT。
Chrome V8 团队作为参与过 PNaCL 与 ASM.js 这两个标准制定的团队,在设计和实现 Wasm 时也同样参考了很多从这两种技术中总结下来的优缺点。而这些经验也将会帮助 Wasm 做好准备,避开那些曾经走过的坑。最后,这些经验使得 Wasm 能够以一种更好的方式,展现在人们的面前。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
实际上在 Wasm 真正出现之前,人们就已经开始尝试探索各类新型技术以赋予 Web 应用更高的运行效率。
从 NaCl、PNaCl 到 ASM.js它们主要有三点共同特征<br>
1.源码中都使用了类型明确的变量;<br>
2.应用都拥有独立的运行时环境,并且与原有的 JavaScript 运行时环境分离;<br>
3.支持将原有的 C/C++ 应用通过某种方式转换到基于这些技术的实现,并可以直接运行在 Web 浏览器中。
Wasm 这项技术的设计与实现,离不开从这些“前辈”们身上学习到的经验。从表面上来看,互联网技术迭代飞快。但实际上,当稍微深入和总结之后,你就会发现其实它们都有着基本相同的,想要去解决的目标问题,比如对于性能的执著要求。以及十分类似的技术解决方案,比如尽最大可能去确定那些能够确定、不会发生变化的部分(比如类型),然后再以此为基础进行优化。 Wasm 也不例外。
## 课后思考
最后,我们来做一个思考题吧。
你觉得就目前的 Web 技术领域而言,存在着哪些困境?或者说需要去解决和优化的地方?
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="开篇词 | 我们为什么要了解 WebAssembly" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/bc/7e2b888f6a84d53542fe34a35304b1bc.mp3"></audio>
你好,我是于航,欢迎来到我的 WebAssembly 入门课!
提起 WebAssembly你可能对它的印象只是一个看上去比较“抽象”的新技术。它不像一个新推出的框架或者库功能定位十分清晰并且可以直观地通过代码来展示。
如果别人问我:“什么是 WebAssembly”从一个十分严谨的角度来看我可能会回答说**“WebAssembly是基于栈式虚拟机的虚拟二进制指令集V-ISA它被设计为高级编程语言的可移植编译目标”。**
不知道你听到这样一段对 “WebAssembly是什么” 这个问题的解答之后,会有怎样的感想。可能大多数人的第一直觉会感到疑惑,这跟它名字中的 “Web” 一词有什么关系?还有人可能会觉得,所谓指令集是属于 CPU 那一层次的,跟我的实际工作应该没有什么关系吧。
没错,所有这些真实的顾虑和疑惑,实际上都成为了阻碍 WebAssembly 这项技术被社区开发者广泛接纳的“绊脚石”。
**那为何我想要开设这样一门课程,来专门给你讲解这门技术呢?为什么我会如此痴迷这门技术?它到底有什么值得我们开发者去学习和使用的呢?**
## 我和 WebAssembly 那些事儿
说起来WebAssembly为了方便书写后面我们简称为 Wasm这门新技术自 2015 年诞生至今,已经过去了将近 5 年的时间。而自 2017 年我开始接触这门技术以来便一直不断地在为它投入热情。2018年我出版了国内第一本介绍该技术的书籍《深入浅出 WebAssembly》。
期间,我也为 Emscripten 编译工具链贡献了部分代码。2019 年底,为了能够更进一步地为 Wasm 生态做出贡献,我开始研发自己的轻量级 Wasm 虚拟机 — TWVM旨在打造一个具有高性能、高可移植性以及更加轻量的 Wasm 虚拟机。并同时能够基于此,来针对国内的众多本地化场景进行一些试验。
我认真回想了下,我对 Wasm 的热爱,其实始源于 2017 年末参与的一次 Wasm 线上研讨会。
这个研讨会是由 WCGWebAssembly Community Group定期举办的。会议会召集 WCG 成员一起讨论后期 WebAssembly 标准的制定方向和实现细节。每一次研讨会,都会由 WCG 主席亲自进行主持。
让我印象最为深刻的,不是每一次线上会议的参与人数有多少,也不是讨论有多激烈,更不是会议开了多长时间,而是在每次会议结束后,都会有完整的讨论记录存档。
这些会议产出的结果更会以“肉眼可见”的速度被快速地采纳、标准化,最后被 V8 团队“率先”实现在 Chrome 浏览器中。而早期的快速实现则可以让 Wasm 在初期以“小步快跑”的方式接触前线的开发者,在不断地迭代中快速“试错”。同时这也为其后续的社区推广起到了积极的作用。
<img src="https://static001.geekbang.org/resource/image/c8/b3/c874b47705ba9dd70ae2412c30b146b3.png" alt="" title="当时某次视频研讨会的截图">
其实,一个团队能够有多少凝聚力、在某个事情上能够有多少投入和产出,从这整个过程中便可以窥见。而被注入如此多精力的事情,也一定会充满着激情与活力。这也是我下定决心,想要把自己的大部分精力都投入到 Wasm 这门技术的一个起点。
随着 Wasm 这门技术的不断发展,我也开始不断地参加各种社区和公司举办的技术沙龙,来向更多人布道 Wasm 这门技术。
2018年为了能够让 Wasm 被更多人所了解,我开始写书。写书的过程其实特别消耗精力,尤其是当你要写一本国内外都没有人写过的书时,你没有任何可以参考的资料。每当深夜写稿没有灵感、烦躁、郁闷的时候,我就会想起自己对 Wasm 的热爱,会想让更多人知道这门技术,应用这门技术。也正是这份热情与执着带我挺过了那段最难熬的日子。
写书其实是我想去普及 Wasm 这门技术的一个新起点。因为我在写书之前就发现,虽然大家或多或少都听说过 Wasm但是一些对于 Wasm 的常见认知错误,逐渐让 Wasm 在社区的发展方现出现了偏差。而从现阶段来看,网上关于 Wasm 的中文文章大多以企业或个人的实践介绍为主,对于想要完整理解 Wasm 及其相关生态来说可能还不够全面,而官网的文档和规范又显然不适合直接作为“入门教材”。
在这个互联网时代,大家似乎对出版的纸质读物已然没有了太多兴趣,我从 2018 年末出书到现在Wasm 一直在不断地更新发展,我对 Wasm 也有了很多新的想法和理解。同样的,我也一直在寻找一个更加适合初学者去了解、运用 Wasm 的方式,去普及 Wasm 这门技术。最后发现专栏的形式会更活泼,也更通俗易懂一些。
在正式为你介绍 Wasm 这项技术之前,我想先带你看一张前端技术的生命发展周期图。
<img src="https://static001.geekbang.org/resource/image/5f/99/5f373bb83e36ac2d05ca246b87916799.png" alt="">
从图里你能够很清楚地看到Wasm 正在逐渐从一个“创新者”变成一个“早期采用者”。从论文中的思想理论走向生产实践的前沿。甚至从它的起源地出发,从 Web 平台逐渐走向各类场景。
实际上Web 前端正变得越来越开放。如今,我们不仅能够直接使用 HTML、JavaScript 来编写各类跨端应用程序,**Wasm的出现更能够让我们直接在 Web平台上使用那些业界已存在许久的众多优秀的C/C++代码库**。
除此之外,**Wasm还能让Web 应用具有更高的性能,甚至让 Web 应用能够与原生应用展开竞争**。不仅如此,走出 WebWASI 的出现更是为 Wasm 提供了更大的舞台。Wasm 有着非常巨大的潜在影响力,而且现在已经初露锋芒。
让我们来一块看看自 2015 年 Wasm 一路走来,这期间都经历了哪些重要的发展节点。
>
2015 年 4 月WebAssembly Community Group 成立;
>
2015 年 6 月WebAssembly 第一次以 WCG 的官方名义向外界公布;
>
2016 年 8 月WebAssembly 开始进入了漫长的 “Browser Preview” 阶段;
>
2017 年 2 月WebAssembly 官方 LOGO 在 Github 上的众多讨论中被最终确定;
>
同年同月一个历史性的阶段四大浏览器FireFox、Chrome、Edge、WebKit在 WebAssembly 的 MVP最小可用版本标准实现上达成共识这意味着 WebAssembly 在其 MVP 标准上的 “Brower Preview” 阶段已经结束;
>
2017 年 8 月W3C WebAssembly Working Group 成立,意味着 WebAssembly 正式成为 W3C 众多技术标准中的一员。
自此之后,还有更多令人激动的“历史性节点”不断出现,比如 WASI 概念的提出和标准化、Byte Alliance 组织的成立、第一届 WebAssembly Summit 全球峰会的成功召开等等而其中值得一提的是2019 年 12 月W3C 正式宣布Wasm 将成为除现有的 HTML、CSS 以及 JavaScript 之外的第四种W3C 官方推荐在 Web 平台上使用的“语言”。
所有上面提到的这些事情,无不都在见证着 Wasm 的不断发展,我相信你也能够体会到 Wasm 在社区推广如此困难的情况下,其相关生态还能够做到这种程度,这背后其实就是核心团队的一次“赌注”。因为大家都坚定地相信,**在未来的 35 年里Wasm一定能够被逐渐广泛地应用到包括 Web 在内的各个平台和业务领域中**。
## 学习 Wasm我们到底要学什么
那么对于这样一门抽象的技术,我们应该以怎样的方式去了解它呢?在学习本课程前,我希望你已经拥有了一定的 C/C++ 编码基础,以及 Web 前端JavaScript / CSS / HTML编码基础。如果你还没有相关经验或者相关的知识忘得差不多了****也不用担心,我为你准备了一节基础课,会带你学习、回顾一些在课程中涉及到的,相关领域的基础知识。
首先,在本课程中,我们不会介绍以下内容:
- 每一个 Wasm 虚拟指令的语义和用法;
- 虚拟机实现相关的内容,比如“如何构建一个 Wasm 虚拟机”。
我们将会介绍的内容:
<img src="https://static001.geekbang.org/resource/image/57/cc/57f4bb9ca32af0c1aed9c9b9a6f6ebcc.png" alt="">
- Wasm 的历史和起源;
- Wasm 采用的计算模型和体系结构;
- Wasm 模块的内部结构;
- Wasm 相关的实战和应用。
总而言之,我们不会介绍与虚拟机实现相关的、过于细节的一些内容。而是会从整体来看,把 下面这些问题给你一一解释清楚。比如Wasm 是什么?它能做什么?别人已经做了什么?它是怎么来的?它未来会如何发展?……
为了能够从整体的角度直观地了解整个 Wasm 生态的全貌,我特意做了如下这张知识地图。你可以通过它来了解有关 Wasm 的任意一个方面的内容。相信随着时间的推移,这张图上的分支会变得越来越多,体系会变得越来越庞大。期待未来的 Wasm 能够在各个分支领域内“开花结果”。
<img src="https://static001.geekbang.org/resource/image/8f/bc/8f41f7e3d643d6597dc7fd8b5eae02bc.png" alt="">
## 学习 Wasm我们应该怎么学
为了能够把课程内容更加直观地展示出来,我把课程整体分为了三大模块。每一个模块下都由若干个子问题组成。由于 Wasm 整个知识体系的内容非常庞大,且较为碎片化,因此,带着一个个问题来了解 Wasm 可能会是一种更加合适的学习方式。
第一个模块是**核心原理模块**。在核心原理模块中,我将会给你介绍与 “Wasm 是什么?” 这个问题有关的几个方面的知识点。我会从最基本的 Stack Machine 计算模型开始介绍,逐渐带你从外部体系深入到 Wasm 的二进制世界。
在这里,我会给你介绍 Wasm 使用的二进制编码方式和基本数据类型、模块内部的组织结构以及 Wasm 的可读文本格式 — WAT。最后我会介绍与 Wasm 紧密相关的另一项技术 — WASI。以及 Wasm 与 Web 平台的互操作性 — Wasm MVP 标准下的 JavaScript API 与 Web API。
第二个模块是**应用模块**。在应用模块里,我将首先为你解答目前被讨论最多的一个问题 —— “我们是否能够使用 Wasm 这门技术来优化现代的 Web 前端框架?”。然后我将给你介绍,目前业界有哪些已经投入到生产环境中使用的 Wasm 案例。以及在这些案例中,各个公司都是如何借助 Wasm 技术来优化其产品体验的。
接下来,我将介绍 Wasm 在物联网、多媒体以及云技术等方面的一些,业界已经正在进行的尝试。然后,我们将会一起浏览目前业界最优秀的一些 Wasm 底层基础设施,这些基础设施都有着其各自不同的特点与使用场景,为构建稳定安全的 Wasm 应用提供了强大的底层支持。
最后,我将为你讲解另外一个不得不聊的话题 — Wasm 与 LLVM。借助于 LLVM我们将能够为“任意”的、基于 LLVM 构建的编程语言支持,将 Wasm 设置为其最后的编译目标。然后,我们将再次回到 Wasm 标准,来看看正在行进中的 Post-MVP 标准中有哪些“新鲜事”。
第三个模块是**实战模块**。在这个模块中,我将手把手带你一步一步地完成一个线上多媒体 Web 应用。在这个应用中,我们将会使用 Wasm 来优化多媒体资源的处理过程,同时我们将介绍与 Wasm 有关的一些 API 以及编译工具链,在实际项目中的使用方式。
而在这之后,我们也将讨论如何调试和分析一个 Wasm 应用。最后,我们将会介绍 WASI 及其相关生态的基本应用方式,并尝试在目前最流行的 Wasm 运行时中使用它们。
这里,我想和你分享一下,我从 20172020 年这将近三年的时间里,所亲眼见到或实际接触过的一些 Wasm 应用场景。
<img src="https://static001.geekbang.org/resource/image/7e/1a/7eabe73793237fb3b17e60a3a109921a.jpg" alt="">
除此之外你还要知道目前已经有多达几十种编程语言C/C++、Rust、Go、Kotlin 等)的代码,可以在相关基础设施的帮助下被编译为 Wasm 二进制格式Wasm 已经逐渐成为编程语言不可或缺的一个重要的编译目标。
上面我分享的这些实际应用场景,还仅仅是依赖于 Wasm 的 MVP 版本标准所提供的功能特性实现的。相信随着 Post-MVP 标准的逐渐确定和相应基础设施的完善,以及 WASI 标准的进一步发展Wasm 的可用领域将会变得更加广泛。
Docker 的创始人 Solomon Hykes 曾在 Twitter 上说过这样一段话:
<img src="https://static001.geekbang.org/resource/image/61/d1/614ca982294ede0a24d725a11965efd1.png" alt="">
翻译过来的大致意思是:
>
如果 WASM 和 WASI 早在 2008 年就存在,那么我们就不需要创建 Docker。可见 Wasm 是多么重要。服务器上的 WebAssembly 将会是“计算”的未来模式。而现在的问题是缺少标准化的系统接口。希望 WASI 能够胜任这项工作!
不用我多说,相信你能够体会到 Wasm 出现的意义,以及它在未来巨大的可能性。作为一项关注于底层的变革性技术,我相信 Wasm 能够催生出更多建立于它之上的新型技术解决方案。可能在未来某一天, Wasm 将会成为每一个互联网工程师的必备技能。
以上,就是这门课我想分享给你的全部内容了。总的来说,我希望通过这门课你能够弄清以下三个问题:**Wasm 究竟是什么Wasm 可以怎么用Wasm 的现在和以后能够给我们带来什么?**
我虽然无法用短短十几篇文章,把整个 Wasm 生态的所有内容全部都概括到。但我希望你能够保持一颗持续学习和乐于探索的心,带着这样的信念去了解和挖掘新兴技术,相信你能够收获到与他人不一样的宝藏。
Wasm 核心团队当初在 GitHub 上的官方 Organization 中,以这样一句话描述了他们的所在位置 —— “The Web!”。**Wasm 虽然起源于 Web但如今它已经不止于 Web。**那就让我们来一起探索 Wasm 带来的,那个拥有无限可能的世界吧!