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,232 @@
<audio id="audio" title="09 | WebAssembly 能够为 Web 前端框架赋能吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6e/e0/6ebe1d367ba670d5e204fa96f0596ce0.mp3"></audio>
你好,我是于航。
相信现在你已经知道“WebAssembly” 是由 “Web” 与 “Assembly” 两个单词组成的。前面的 “Web” 代指 Web 平台;后面的 “Assembly” 在我们所熟悉的编程语言体系中,可以理解为“汇编”。
通常来说,汇编语言给人的第一感觉便是“底层,外加高性能”。而这,也正是第一次听说 Wasm 这门技术的开发者们的第一感受。
说到 Web 开发,那我们不得不提到层出不穷的 Web 前端开发框架。以 React、Vue.js 及 Angular 为代表的三大框架的出现,使得 Web 前端应用的开发模式,自 2013 年后便开始逐渐从“旧时代”的 jQuery、Prototype.js 走向了以 “MVVM” 框架为主的“新时代”。
既然我们说 Wasm 起源于 Web并且它的出现会给未来的 Web 应用开发模式,带来一系列变化。那么,对于这些现阶段在我们日常工作中承担“主力”角色的 Web 前端框架来说Wasm 会给它们带来怎样的变化呢?未来的 Web 前端开发框架会以怎样的方式与 Wasm 紧密融合呢?
相信这些问题,是每一个 Web 前端开发同学在接触 Wasm 这项技术之后,都会存在的疑问。今天,我们就来看一看,在如今的 Wasm MVP 标准下,对于这些基于 JavaScript 编写的现代 Web 前端框架我们能够做些什么。
## 几种方案的思考
在上一章的“核心原理篇”中,我们从不同的角度讲解了 Wasm 究竟是什么。那这里我们还是用最精简的方式来概括一下“Wasm 是一种基于堆栈式虚拟机的二进制指令集,它被设计成为编程语言的可移植编译目标。借助 Web 平台提供的相关接口,我们可以在 Web 浏览器中高效地调用从 Wasm 模块中导出的函数”。
那我们就根据 Wasm 现阶段所具有的这些能力,来讨论一下 Wasm 对现代 Web 前端开发框架可以产生怎样的影响。我将会根据 Wasm 与框架之间的可能融合程度,来从不同的角度加以讨论。相应的,我们可以总结出如下四种方案:
- 使用 Wasm 完全重写现有框架
- 使用 Wasm 重写现有框架的核心逻辑
- 使用 Wasm 配合框架增强应用的部分功能
- 使用其他语言构建 Web 前端框架
接下来,我将依次和你讨论上面的这四种情况。
### 使用 Wasm 完全重写现有框架
在这个方案下,我们将使用 Wasm 完全重写现有的 Web 前端框架。而这就需要我们通过 JavaScript 之外的诸如 C/C++、Rust 等第三方静态类型语言,将框架的逻辑全部重写。先不谈能否实现,我们先来看看在这种方案下,前端框架的使用方式会发生怎样的改变。
在此之前Web 前端框架的使用方式可以通过如下图来大致描述。
<img src="https://static001.geekbang.org/resource/image/e9/98/e958f8a141040208c2428d6723d99b98.png" alt="">
你可以看到除去样式文件CSS以外我们的 Web 应用程序仅由“框架代码”和“应用程序代码”两部分组成。这两部分代码全部由 JavaScript 语言进行编写。HTML 文件负责将这些 JavaScript 代码整合在一起,并确保在页面加载时执行它们。
当我们将 Web 前端框架使用 Wasm 完全重写后,事情又会变成另外一幅景象。此时的 Web 应用组成结构将如下图所示。
<img src="https://static001.geekbang.org/resource/image/12/5f/12398e8a5a72f2c89ebf1bcb6c667f5f.png" alt="">
除了使用 JavaScript 编写的“应用程序代码”,以及经过编译生成的 Wasm 字节码格式的框架代码以外,我们的项目中还会多出来一部分用作 “Glue Code”胶水代码的 JavaScript 代码。那这部分 Glue Code 主要用来做什么呢?这就要从现阶段的 Wasm 标准与 Web 浏览器的可交互性开始说起了。
#### 无法剥离的 JavaScript 代码
在现阶段 Wasm 的 MVP 标准中,我们需要通过各类 JavaScript API 与 Web API 来在 Web 平台上与 Wasm 代码(模块)进行交互。这些 API 均只能够通过 JavaScript 代码来进行调用。而所有这些需要与 Wasm 模块直接进行的交互(互操作),都是由包含有 API 调用的 Glue Code 代码完成的。
恰巧在目前 Wasm 的 MVP 标准中,我们也同样无法直接在 Wasm 字节码中操作 HTML 页面上的 DOM 元素。因此,对于这部分 Web 框架最核心的功能,便也是需要通过借助 Glue Code 调用 Web API 来帮助我们完成的。
为了达到这个目的,我们需要将 DOM 操作相关的逻辑封装成 JavaScript 函数,然后再通过 Wasm 模块的 Import Section 导入到模块中供其使用。
因此,框架代码与 Glue Code 之间的协作方式可能会类似如下代码形式。首先是框架对应的 C++ 代码。
```
// framework.cpp
extern void createEmptyDivElement();
int main(int argc, char** argv) {
createEmptyDivElement(); // 创建一个空的 &quot;div&quot; 标签;
createEmptyDivElement();
...
return 0;
}
```
然后下面是 Glue Code 对应的 JavaScript 代码。
```
// glue.js
...
WebAssembly.instantiateStreaming(wasmBytes, {
env: {
// 将函数导入到 Wasm 模块中;
createEmptyDivElement: () =&gt; document.createElement('div'),
...
}
})
```
可以看到,在 Glue Code 代码中,我们将封装好的用于调用 “document.createElement” 这个 Web API 去创建空 div 标签的 JavaScript 函数 “createEmptyDivElement”传递给了用于实例化 Wasm 模块的 WebAssembly.instantiateStreaming 方法。
在框架所对应的 C++ 代码中,我们使用了这个从 JavaScript 环境导入到 Wasm 模块中的 “createEmptyDivElement” 函数。这里在代码中,所有通过 “extern” 指定的外部函数,都将会在编译至 Wasm 二进制模块后,从模块对应的 Import Section 中获取实际的函数体。
关于上述的代码示例,你大致有一个印象即可。我们会在“实战篇”中详细介绍一个 Wasm 项目从 0 到 1 的完整构建流程。
#### 跨上下文频繁调用的开销
除了上面提到的,即使将 Web 前端框架完全重写并编译至 Wasm我们也无法在完全脱离 JavaScript Glue Code 的情况下使用框架。另一个由此带来的问题在某些情况下可能会显得更加“致命”,那就是 “Wasm 与 JavaScript 两个上下文环境之间的函数调用开销”。
在早期的 Firefox 浏览器(版本 62 以前)上,由于实现问题,导致不管是使用 JavaScript 调用从 Wasm 模块中导出的函数,还是在 Wasm 模块内调用从 Web 浏览器导入到模块内的 JavaScript 函数,这两种方式的函数调用成本都十分高昂。在某些情况下,同样的函数调用过程会比 JavaScript 之间的函数调用过程慢约 20 倍。
但好在 Firefox 在 62 之后的版本中修复了这个问题。并着重优化了 JavaScript 与 Wasm 之间的函数调用效率。甚至在某些情况下JavaScript 与 Wasm 之间的函数调用效率要高于 JavaScript 之间的函数效率。
虽然这个问题在 Firefox 上得到了修复,但不可否认的是,在其他浏览器厂商的 Wasm 实现中,也可能会出现类似的性能问题。
Web 前端框架作为一个需要与 DOM 元素,以及相关 Web API 强相互依赖的技术产品,可想而知其在实际使用过程中,必然会通过 Glue Code 去完成 Wasm 与 JavaScript 之间的频繁函数调用。而以性能为重的 Web 前端框架,则无法忽视这些由于频繁函数调用带来的性能损耗。
### 使用 Wasm 重写现有框架的核心逻辑
在第二种方案下,我们将使用 Wasm 重写 Web 前端框架的核心逻辑,但并非全部。
如下图所示在这种情况下Web 应用的主要组成结构与上一种方案类似,唯一的不同是增加了 Web 框架所对应的 JavaScript 代码实现部分。
<img src="https://static001.geekbang.org/resource/image/ab/62/aba4ec5bfc67193d46615efba1ce7462.png" alt="">
相较于将整个框架都通过 Wasm 来实现,仅实现框架的核心逻辑部分,可以说更具有现实意义,而这也是现阶段大多数的框架开发者都在实践的方向。
所谓“核心逻辑”,其实依框架的具体实现不同,无法通过统一、准确的描述来定义。但可以遵循的原则是,这部分逻辑不会涉及与 DOM 或者 Web API 的频繁交互但其本身却又是“计算密集compute-intensive”的。
这里的“计算密集”可以理解为包含有大量的纯数学计算逻辑。我们知道Wasm 十分擅长处理这样的计算密集型逻辑。一个很具有代表性的,可以被 Wasm 重写的组件便是 React Fiber 架构中的 Reconciler主要用来计算 React 中 VDOM 之间的差异)。
### 使用 Wasm 配合框架增强应用的部分功能
我们继续逐渐递减 Wasm 与框架的“耦合”程度。
在第三种方案中,从本质上来看,框架本身的代码不会有任何的变化。而 Wasm 也不再着重于优化框架本身的性能。相对地,框架与 Wasm 将会配合起来使用,以优化整个应用的某一部分功能。下面这张图是在这个方案下,一个 Web 应用的基本组成结构。
<img src="https://static001.geekbang.org/resource/image/2a/8c/2a3a73af31e382f34e4a847fayy2b48c.png" alt="">
可以看到,这里 Wasm 本身只是作为一个模块,用于优化应用的某方面功能。而 Web 框架本身的源代码组成形式不会发生任何改变,应用仍然还是使用 JavaScript 来构建其主体结构。
事实上,这是 Wasm 在 Web 上的一种最为典型和常见的应用方式。Wasm 并不尝试取代 JavaScript而是通过利用其优势来补足或者加以提升 Web 应用在某方面的短板。一个最为常见的例子便是前端的“数据编解码”。
我们都知道,“编解码”实际上是十分单纯的数学计算,那么这便是 Wasm 能够大显身手的地方。通过替换 Web 应用中原有的基于 JavaScript 实现的编解码逻辑,使用 Wasm 来实现这部分逻辑则会有着明显的性能提升。而且由于这个过程不涉及与 Web API 的频繁交互Wasm 所能够带来的性能提升程度更是显而易见的。
### 使用其他语言构建 Web 前端框架
最后一种方案相较于之前的几种可能会稍显激进,但随着 Wasm 发展而不断出现的,一批又一批基于此方案实现的 Web 前端框架,值得让我们重新重视起来。
在此方案下,我们将使用诸如 C++ 和 Rust 等静态类型语言来实现 Web 前端框架。不仅如此,我们也同样需要使用这些语言来编写我们的 Web 应用。类似的框架有基于 Rust 语言的 Yew、Seed以及基于 Go 语言 Vugu 等等。
以相对较为“流行”的 Yew 框架为例,我们使用它来编写 Web 前端应用的大致思路,与 React 和 Vue.js 等传统 JavaScript Web 前端框架的形式十分类似。以下代码展示了如何使用 Rust 语言基于 Yew 框架,来构建一个基本的 Web 前端应用。
```
use yew::prelude::*;
pub struct App {}
pub enum Msg {}
impl Component for App {
type Message = Msg;
type Properties = ();
// 应用创建时执行的生命周期函数;
fn create(_: Self::Properties, _: ComponentLink&lt;Self&gt;) -&gt; Self {
App {}
}
// 应用视图更新时执行的生命周期函数;
fn update(&amp;mut self, _msg: Self::Message) -&gt; ShouldRender {
true
}
// 定义应用视图结构;
fn view(&amp;self) -&gt; Html {
html! {
&lt;p&gt;{ &quot;Hello, world!&quot; }&lt;/p&gt;
}
}
}
```
相信即使你不懂 Rust但如果你熟悉 React仍然可以发现基于 Yew 构建的 Web 前端应用,它的代码组织结构与 React 十分类似,整个应用也同样被划分为不同的“生命周期”。
比如在上面的代码中“create” 方法对应应用的创建时刻update 方法对应应用的状态更新时刻,以及最后用于渲染应用 UI 的 view 方法等等。不仅如此,在 Yew 中也同样拥有组件的概念,使用方式与 React 类似。
相对来说,抛开语言本身带来的成本不谈,单从性能来看,在目前 Wasm 的 MVP 标准下Yew 这类框架的潜力还没有实际的显露出来。Yew 希望能够借助 Wasm 的能力将视图VDOM差异的计算过程以更高性能的方式进行实现。但鉴于目前 MVP 标准下的一些限制实际上在最后的编译产物中Glue Code 执行时所带来的成本则会与 Wasm 带来的性能提升相互抵消。
不仅如此,考虑到目前 JavaScript 在构建 Web 应用时的丰富生态和资源,单从性能角度进行考量而使用 Yew 等框架也不是一个实际可行的方案。因此,未来这类“跨语言” Web 前端框架的生态会变得如何,也只能够让我们拭目以待了。
## 已有的讨论
在介绍了上述四种Wasm 可能与 Web 前端框架相互结合的方案后。我们再回过头来,看一看目前仍然流行的几种 JavaScript Web 前端框架有没有进行与 Wasm 结合的相关尝试。这里我选择了 React、Vue.js 以及 Ember.js 这三种 Web 框架。
### React
作为目前 Web 前端开发领域中最流行的框架之一。React 暂时还没有计划进行任何与 Wasm 相关的尝试。如下图所示,虽然社区中曾有人提议使用 Wasm 重写 React Fiber 架构中的 Reconciler 组件,但由于目前 Wasm 还无法直接操作 DOM 元素等标准上的限制,导致我们可预见,现阶段即使用 Wasm 重写 React 的 Fiber 算法,框架在实际处理 UI 更新时,可能也不会有着显著的性能提升。因此,对于 React 团队来说,投入产出比是一个值得考量的因素。
<img src="https://static001.geekbang.org/resource/image/63/25/634f8202eaf1fe730bd4bd2d81b98625.png" alt="">
### Vue.js
同 React 类似Vue.js 的社区内也曾有过类似的讨论,如下图所示。
<img src="https://static001.geekbang.org/resource/image/b9/5f/b93f7ee3d059fb9f0c3ffc461d7eb65f.png" alt="">
但与 React 所不同的是Vue.js 与 Wasm 的“结合”方式根据框架的具体实现细节可能有着更多的可能。不过一个不可否认的事实是Wasm 仍然处在快速的发展阶段。同样的,基于 Wasm 构建的各类应用也同样处在不稳定的维护状态中(比如,上述帖子中提到的 Walt 实际上于 2019 年便不再继续更新)。而目前,正是一个“百花齐放”的时代。
### Ember.js
最后我们要来讲讲 Ember.js。
Ember.js 的用户虽然没有 React 与 Vue.js 那么多,但它却是第一个宣布尝试与 Wasm 进行“深度整合”的 Web 前端框架Ember.js 在内部使用了名为 Glimmer VM 的渲染引擎。与 React 通过使用 Reconciler 组件计算 VDOM 差异来更新 UI 的策略有所不同Glimmer VM 通过将模板的构建过程分解为独立的虚拟机 “OpCode” 操作,来对 UI 中呈现的动态值进行细粒度更新。
<img src="https://static001.geekbang.org/resource/image/31/59/315de62de69f42c5cbf60dae5575e859.png" alt="" title="图片来自于EmberConf 2018">
在 EmberConf 2018 年的技术会议上,来自 Ember.js 团队的 Yehuda Katz 向我们介绍了 Glimmer VM 与 Wasm 的整合情况。你通过上图可以看到,除了 OpCode 模块相关的部分逻辑仍然在使用 JavaScript 构建以外,整个 VM 的大部分功能都已经完成到 Wasm 的迁移。并且该 Wasm 版本的 Glimmer VM 也已经通过了所有的测试集 Case。
但计划赶不上变化,回到 2020 年,我们再来看 Glimmer VM关于它与 Wasm 整合的消息貌似已经没有了太多的声音。
从 [Ember.js 官方论坛](https://discuss.emberjs.com/t/what-is-the-current-state-of-more-advanced-glimmer-vm-features/18114)中我们可以看到Ember.js 在与 Wasm 进行整合的过程中,其实遇到了很多问题,比如不支持 GC 导致 Wasm 线性内存中使用的资源无法被及时清理。Glimmer VM 还在继续为将来能够完全移植到 Wasm 做着准备。
但无论如何,这都不失为一次非常有意义的尝试。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在这节课里呢,我主要给你介绍了 Wasm 与 Web 前端框架的一些“故事”。
“Wasm 能否影响,或者说会如何影响现有的、基于 JavaScript 构建的现代 Web 前端框架呢?”这是一个被很多 Web 前端工程师所提及的问题。在这节课中,我尝试按照 Wasm 与 Web 前端框架的“整合程度”不同,将两者能够相互结合的可能方式大致分为了四种方案。
在第一种方案中,我们尝试将整个 Web 框架的全部功能,使用同样的 Wasm 版本进行代替,而应用代码仍然使用 JavaScript 进行编写。但由于现阶段 Wasm MVP 标准的限制,在这种方案下,我们不得不借助 JavaScript Glue Code 的帮助来实现框架的部分功能。
而当 Glue Code 的代码越来越多时JavaScript 函数与 Wasm 导出函数之间的相互调用会更加频繁,在某些情况下,这可能会产生严重的性能损耗。因此结合现实情况来看,整个方案的可用性并不高。
在第二种方案中,我们尝试仅使用 Wasm 来重写框架的核心部分,比如 React Fiber 架构中的 Reconciler 组件。这类组件通常并不含有过多需要与 Web API 打交道的地方,相对纯粹的计算逻辑更易于 Wasm 能力的发挥。同时这种方案也是现阶段大多数 Web 框架正在尝试的,与 Wasm 进行交互的“常规”方式。
在第三种方案中,我们仅使用 Wasm 来作为 Web 框架的辅助,以优化 Web 应用的某一方面功能。在这种方案中,框架本身的代码结构不会有任何的变化。实际上,这种方案也是传统 Web 应用在利用 Wasm 时的最常规方式。
在最后一个方案中,我们介绍了一种更为激进的方式。在这种方案下,包括 Web 框架和应用代码本身,都会由除 JavaScript 以外的,如 Rust、C++ 和 Go 等静态语言来编写。
但同样受限于现阶段 Wasm MVP 标准的限制,框架本身仍然离不开 JavaScript Glue Code 的帮助。同时考虑到实际的语言使用成本以及 JavaScript 生态的舍弃,这种方案的实际可行性仍有待时间的验证。
无论如何,相信随着 Wasm Post-MVP 标准的不断实现,上述各方案中使用的 Glue Code 代码量将会逐渐减少。随之提升的,便是 Web 框架以及应用的整体运行性能。
## **课后思考**
最后,我们来做一个思考题吧。
除了我们今天介绍的这四种 Web 框架与 Wasm 的结合使用方式,你还有哪些奇思妙想呢?
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,163 @@
<audio id="audio" title="10 | 有哪些已经投入生产的 WebAssembly 真实案例?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b8/ac/b82b4c48e012aa5d9638686f4f1ecdac.mp3"></audio>
你好,我是于航。
本节课,我们将不再“拘泥”于 Wasm 的实现细节,而是要从技术标准走向生产实践。作为应用篇中的第二节课,我们将一起来看看从 2017 年 Wasm MVP 标准的确定,直到如今 WASI 出现,使得 Wasm 走出 Web 的这几年时间里,现实世界中有哪些已经投入生产的 Wasm 真实案例?而这些案例又是怎样利用 Wasm解决了哪方面实际问题的呢这节课里介绍的几个案例均由我总结于网络上相关公司发布的文章或视频分享。
## eBay - Barcode Scanner
第一个我们要介绍的实际案例来自于 eBay 在 Wasm 上的一次尝试。
eBay 是一家知名的线上拍卖与购物网站,人们可以通过 eBay 来在线出售自己的商品。作为一家知名的购物网站为了优化用户录入待售商品的操作流程eBay 在自家的 iOS 与 Android 原生应用中提供了“条形码扫描”功能。
通过这个功能,应用可以利用移动设备的摄像头扫描产品的 UPC 条形码,然后在后台数据库中查找是否有已经提交过的类似商品。若存在,则自动填写“商品录入清单”中与该物品相关的一些信息,从而简化用户流程,优化用户体验。
### 问题所在
在 iOS 与 Android 原生应用中eBay 借助了自研的、使用 C++ 编写的条形码扫描库,来支持 UPC 条形码的扫描功能。而这对于诸如 iOS 与 Android 等 Native 平台来说,条形码的实际扫描性能得到了不错的保障,应用表现良好。
但是随着 eBay HTML5 应用的使用人数越来越多,为了能够使用户的商品录入流程与 Native 应用保持一致,“如何为 HTML5 应用添加高效的条形码扫描功能?”便成为了 eBay 工程师团队亟待解决的一个问题。
初期,技术团队使用了 GitHub 上的开源 JavaScript 版本条形码扫描器,来为 HTML5 应用支持 UPC 条形码的解析功能。但随着不断收到的用户反馈,团队发现 JavaScript 版本的条形码扫描器仅能够在 20% 的时间里表现良好,而在剩下 80% 的时间中,条形码的实际解析效率却不尽如人意,用户的每一次扫码过程都无法得到一致、流畅的用户体验。
出现这种问题的一个最为重要的原因,便是由于 JavaScript 引擎在实际优化代码执行的过程中,无法确保用户的每一次扫描过程都能够得到 JIT 的优化。JavaScript 引擎采用的“启发式”代码执行和优化策略,通常会首先通过 Profiling 来判断出“热代码”的具体执行路径,然后再调用 JIT 引擎来优化这段代码。而实际上,究竟哪段代码能够被优化,谁也无从得知。
### 可能的解决方案
那么,如何解决这个问题?其中的一个选择是等待 WICGWeb Incubator Community GroupWeb 孵化社区群组)曾提出的 “Shape Detection API” 提案。这个提案提出了一系列的 API可以让 Web 平台应用直接利用硬件加速或者系统相关的资源,来支持如人脸识别、条形码识别等功能。但该提案目前仍处于起步阶段,要实现跨浏览器的兼容性还有很多路要走。
eBay 技术团队所想到的另外一个方案,便是 Wasm。从下图所示的 V8 引擎编译管道中你可以看出。相较于 JavaScript 而言,浏览器引擎在执行 Wasm 字节码时不需要经过诸如“生成 AST”、“生成 Bytecode 字节码”、“生成 IR” 以及“收集运行时信息”等多个步骤。JavaScript 引擎的优化编译器后端可以直接将 Wasm 字节码转换为经过优化的机器码,进而以接近 Native 代码的效率来执行。
<img src="https://static001.geekbang.org/resource/image/27/57/272112ea462c9d8c426ce331cca7e957.png" alt="">
不仅如此Wasm 字节码在实际的执行过程中,也不会存在类似 JavaScript 代码的“去优化”过程,因此性能表现会更加稳定。
另一方面,借助于 Wasm 相关编译工具链的帮助eBay 技术团队可以直接使用曾经为 Native 平台设计开发的 C++ 条形码扫描库。总的来说eBay 技术团队不需要为 Wasm 重新编写这部分功能,而仅需要对已有的代码库进行少量改动即可。
### 项目架构
当方案确定之后,条形码扫描功能的具体工作流程如下所示。
- 使用 Web Worker API 从主线程创建一个工作线程Worker Thread用于通过 JavaScript 胶水代码来加载和实例化 Wasm 模块;
- 主线程将从摄像头获得到的视频流数据传递给工作线程,工作线程将会调用从 Wasm 模块实例中导出的特定函数,来处理这些视频流像素。函数在调用完成后,会返回识别出的 UPC 字符串或者返回空字符串,以表示没有检测到有效的条形码内容;
- 应用在运行时会通过设置“阈值时间”的方式,来检测是否读取到有效的条形码信息。当扫描时间超过这个阈值时,应用会弹出提示信息以让用户重试,或选择手动输入二维码序列。当然,阈值超时可能意味着两种情况:一种是用户没有扫描到有效的条形码;第二种是读取到的二维码视频流无法被应用使用的算法正确解析。
项目中使用到的 Wasm 模块以及 JavaScript 胶水代码,均是通过 Emscripten 工具链编译已有的 C++ 条形码扫描库得来的。整个方案的工作流程如下图所示。
<img src="https://static001.geekbang.org/resource/image/92/f4/929021573b6a7b9e38e8937986af42f4.png" alt="">
### 一致化的编译管道
作为工程化的一部分,如何将 Wasm 模块的开发和编译流程,也一并整合到现有的 Web 前端项目开发流程中,是每个实际生产项目都需要考虑的事情。
一个 Wasm 模块,或者说是 Wasm Web 应用的完整开发流程涉及到多个部分。除了组成应用最基本的 HTML、CSS 以及 JavaScript 代码外,对于 Wasm 模块的开发和编译,我们还需使用到由 Rust 和 C++ 等系统级编程语言编写的模块源文件、相关的标准库,以及用于编译这些源代码的编译工具链,比如 Emscripten。
为了确保每次都能够在一个一致的环境中来编译和生成 Wasm 模块,同时简化整个项目中 Wasm 相关开发编译环境的部署流程。eBay 技术团队尝试采用了 Docker 来构建统一的 Wasm 编译管道。这样在每次编译 Wasm 模块时Docker 都会启动一个具有相同环境的容器,来进行模块的编译流程,从而磨平了不同开发环境下可能带来的编译结果差异。
不仅如此,通过结合 NPM 下 “package.json” 文件中的自定义脚本命令,我们还可以让 Wasm 模块的开发与编译流程,与现有的 Web 前端应用开发编译流程,更加无缝地进行整合。举个例子,比如我们可以按照如下形式来组织 “package.json” 文件中的应用编译命令。
```
{
&quot;name&quot;: &quot;my-wasm-app&quot;,
&quot;scripts&quot;: {
&quot;build:emscripten&quot;: &quot;docker run --rm -v $(pwd)/src:/src trzeci/emscripten ./build.sh&quot;,
&quot;build:app&quot;: &quot;webpack .&quot;,
&quot;build&quot;: &quot;npm run build:emscripten &amp;&amp; npm run build:app&quot;,
// ...
},
// ...
}
```
其中,命令 “build:emscripten” 主要用于启动一个带有完整 Emscripten 工具链开发环境的 Docker 容器。并且在容器启动后,通过执行脚本 “./build.sh” ,来编译当前目录下 “src” 文件夹内的源代码到对应的 Wasm 二进制模块。“build:app” 命令则用于编译原有 Web 应用的 JavaScript 代码。最后我们将两部分再进行整合,便得到了最终的 “build” 命令。
### 并不理想
以上基于 Wasm 的方案看起来十分理想。但经过实际测试后eBay 技术团队发现,虽然基于 Wasm 的实现可以在 1 秒的时间内处理多达 50 帧的画面,但实际的识别成功率却只有 60%。剩下 40% 的失败情况大多是因为采样的画面角度不好,进而使得条形码的拍摄图像质量不高。产生问题的关键点,在于当前应用使用的是自研的 C++ 条形码扫描库。
自研的 C++ 条形码扫描库其一大特征为条形码的识别解析算法效率高,但仅适用于条形码成像质量较高的情况下。因此,急需一种方式来弥补在成像质量偏低时的条形码识别。
此时,团队将目光锁定到了另外一个业界十分有名的、基于 C 语言编写的开源条形码扫描库 —— ZBar。通过实验发现当使用 ZBar 作为条形码扫描库时,在所设置的阈值时间范围内,整个应用的扫描成功率提高到了 80%。
但 80% 的成功率对于产品的用户体验来说仍然不够。团队继续对 ZBar 和自研的 C++ 条形码扫描库进行测试。在经过一段时间后,他们发现在某些 ZBar 超时的情况下,自研的 C++ 库却能够快速地得到扫描结果。显然,基于不同的条形码图像质量,这两个库的执行情况有所不同。
### 竞争取胜
为了能够同时利用 ZBar 和自研的 C++ 库eBay 技术团队选择了一个“特殊的方案”。我想你肯定也能够猜到方案的大致内容。
在这个方案中,应用会启动两个工作线程,一个用于 ZBar另一个用于自研的 C++ 库,两者同时对接收到的视频流进行处理。当主线程接收到有效的识别结果时,便结束所有工作线程的执行。若超时,则显示错误信息。
经过测试,条形码在不同模拟测试场景中的识别成功率,可以提高到 95%。
无独有偶的是,当尝试把 JavaScript 版本的条形码扫描器实现同样作为工作线程,加入到竞争“队列”中时,整个应用的条形码扫描识别成功率达到了将近 100%。这样的结果让人感到惊喜。应用的最终架构可以通过下图很好地进行展示。
<img src="https://static001.geekbang.org/resource/image/8b/cb/8b317339247449e611fd0c0711e1d7cb.png" alt="">
产品上线后的最终效果如下图所示。
<img src="https://static001.geekbang.org/resource/image/f4/1b/f47db948d3d7yyda6ce7fa45a47a211b.png" alt="" title="图片来源于 eBay 官方博客">
产品在上线使用了一段时间后eBay 技术团队对应用的条形码扫描情况进行了统计,结果发现有 53% 的成功扫描来自于 ZBar34% 来自于自研的 C++ 库。剩下的 13% 则来自于第三方的 JavaScript 库实现。可见,其中通过 Wasm 实现(自研 C++ 库、Zbar得到的扫描结果占据了总成功次数的 87%。
虽然文章中没有提及,但实际上,设备对 Wasm 的兼容性也是需要考量的一个因素。你可以思考一下,我们怎样做可以在上述方案的基础上,来同时兼容旧设备上的条码扫描功能。
## AutoCAD Web
第二个我们要介绍的案例来自于一个有着将近 40 年历史的知名设计软件 —— AutoCAD。
AutoCAD 是一款由 Autodesk 公司设计研发的,用于进行 2D 绘图设计的应用软件,它被广泛地用于土木建筑、装饰装潢、工业制图等多个领域中。相信大部分的工科同学,也一定在大学本科期间参与过 AutoCAD 的课程与相关考试。如下图所示,是该应用桌面端版本的运行截图。
<img src="https://static001.geekbang.org/resource/image/52/4c/526a41fafae9ffafa80e2c152dd55b4c.png" alt="" title="图片来源于 YouTube">
### 历史负担
AutoCAD 桌面端软件的发展有着将近 40 年的历史。而随着应用的不断发展,随之而来便是逐渐变大的代码库体积,以及不断复杂化的软件架构。截止 2018 年AutoCAD 桌面端应用已经有着超过 1500 万行的 C/C++ 代码,并且仍然在以肉眼可见的速度增长着。
### 移动互联网浪潮
随着 2008 年移动互联网浪潮的逐渐兴起,越来越多的用户开始使用 PC 甚至是移动设备上的 Web 浏览器,来完成日常工作的一部分内容。感知到趋势的 Autodesk ,便开始着手将自家的 AutoCAD 应用从 PC 端的原生应用逐渐向 Web 应用进行移植。
初期,由于 AutoCAD 原生应用本身的代码库过于庞大AutoCAD 团队决定从头开始编写 AutoCAD 的 Web 版应用。在当时那个年代HTML5 刚刚标准化浏览器在功能特性上的支持还不够全面并且跨浏览器的兼容性也很难得到保障。因此AutoCAD 移植 Web 应用的第一版本便是基于 Adobe Flash 重新编写的,这个应用发布于 2010 年。
为了能够进一步利用 Web 标准,来优化 AutoCAD Web 应用的性能,并使得整个 Web 应用的技术架构更加贴近基于 JavaScript 构建的 Web 应用标准AutoCAD 团队于 2013 开始着手进行 AutoCAD 标准 Web 应用的移植工作。并且此时的 AutoCAD 团队还有着更大的“野心”。
他们首先基于 C++ ,重写了为 iOS 移动端 Native 应用准备的轻量版代码库。然后通过交叉编译Tangible的方式将这些 C++ 代码编译为了 Java 代码供 Android 设备使用。最后,在 Google Web Toolkit一个 Google 开发的可以使用 Java 语言开发 Web 应用的工具集)的帮助下,又将这些 Java 代码转译为了 Web 平台可用的 JavaScript 代码。
但事实上,由于 GWT 本身作为转译工具,会产生很多额外的胶水代码,并且经由 C++ 交叉编译而来的 Java 代码本身质量也并不高,因此这导致了最后生成的 Web 应用代码库十分庞大,且在浏览器中的运行性能并不可观。这个“粗糙版”的 Web 应用发布于 2014 年。
时间来到 2015 年,彼时 ASM.js 作为 Wasm 的“前辈”正展露着头角。AutoCAD 团队借此机会,在 Emscripten 工具链的帮助下,直接从 AutoCAD PC 版原有的 C++ 代码库中移植了一部分主要功能到 Web 平台上ASM.js 所带来的性能提升,让团队对 AutoCAD Web 应用的进一步发展充满了期待。
2018年3月基于 Wasm 构建的 AutoCAD Web 应用诞生。
<img src="https://static001.geekbang.org/resource/image/86/1a/8666fd68e6658aede43b4c2814fc1b1a.png" alt="" title="图片来源于网络">
### 应用架构
整个 AutoCAD Web 应用的组成结构你可以参考下面这张图。在应用的右侧是绘图区域,该区域由 HTML 中的 Canvas 元素与相关 Web API 进行渲染,运行在独立工作线程中的 Wasm 模块实例则负责控制这部分区域的实际绘图效果。
左侧的 UI 控制区域由 TypeScript 基于 React 框架进行构建,基于组件化的构建方式与我们日常开发的 Web 前端应用项目基本保持一致。UI 部分的交互操作则会通过 “postMessage” 等 Web API 通知到工作线程中的 Wasm 实例,并对输出到 Canvas 中的画面进行实时处理。
<img src="https://static001.geekbang.org/resource/image/9c/51/9c8ebbyy28f279363ce940699b096151.png" alt="" title="图片来源于 YouTube">
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在这节课里,我们举了两个比较有代表性、在现实生活中的 Wasm 生产实践案例。第一个是 eBay 在其 Web H5 应用中添加的条形码扫描功能。eBay 技术团队在初期使用了第三方的 JavaScript 版本条形码识别库,来进行条形码的识别,但无奈识别成功率较低。
而随着后期 ASM.js 与 Wasm 的出现和普及eBay 技术团队选择将自研的,原先被应用于 Native 平台的 C++ 识别库编译到 Wasm ,并整合到 Web 应用中使用。此时虽然识别成功率有所上升,但在某些成像质量较差的场景下,条形码仍然无法被正确识别。
为了解决这个问题,团队成员又以同样的方式,将基于 C 语言开发的知名第三方条形码识别库 ZBar 编译到了 Wasm。并通过多个工作线程“竞争”的方式尝试同时整合 JavaScript 版本实现、ZBar 与自研的 C++ 识别库,让应用的整体识别成功率有了一个质的提高。
在第二个案例中,我们介绍了 AutoCAD 在移动互联网浪潮兴起的这十年时间里,不断尝试将其 Native 应用移植到 Web 平台所使用的一些方式。而在这些众多的方案中,基于 Wasm 的方案给予了 AutoCAD 能够在 Web 平台上流程运行的可能。
最后,希望这些真实的案例能够给予你对 Wasm 更多的信心和思考。
## **课后练习**
最后,我们来做一个思考题吧。
你觉得将 Native 应用移植到 Web 应用时可能会存在哪些问题呢?或者说 Native 应用与 Web 应用在执行流程或组成方式上有哪些区别呢?欢迎大家各抒己见。
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,149 @@
<audio id="audio" title="11 | WebAssembly 在物联网、多媒体与云技术方面有哪些创新实践?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9e/e3/9efa1aab747d342b4b758ea3446025e3.mp3"></audio>
你好,我是于航。
我们继续接着上节课的内容,来一块看看 Wasm 在应用实践领域有哪些“新鲜事”。今天我们要来聊的是 Wasm 在物联网、多媒体与云技术领域内的一些创新性实践。我们一直说 Wasm 虽然“出身”于 Web但实际上却也可以 out-of-web。
Wasm 本身作为一种新的虚拟机字节码格式,其使用场景从来都不会被仅仅限制在某个领域。鉴于 Wasm 在这些领域内的相关实践数量众多,因此在本节课里,我们仅挑选一些比较典型且具有一定现实意义的创新性实践来进行介绍。同时也欢迎你在评论区和我进行互动,补充一下你所知道的 Wasm 在这些或者其他领域内的相关实践。
## 物联网IoT
物联网Internet of Thing我们一般简称为 IoT。是指相对于传统的手机、笔记本电脑等大型电子设备来说其可使用资源被有所限制比如单核的 CPU、仅有几百 KB 的内存和硬盘容量、有限的网络上传速度,或仅需纽扣电池进行供电等)的小型嵌入式设备。
因此,相较于为传统 PC计算机等大型电子设备开发应用程序而言为嵌入式设备开发程序则需要特殊的编程实践方法以用来应对有限的软硬件资源。
### 统一的编程接口
在 IoT 刚刚走入人们视野的最初几年,人们通常只能够使用 C/C++ 甚至是汇编语言,来为这些物联网嵌入式设备编写应用程序。
后来随着互联网技术的不断发展,以及从易用性、流行程度、生态系统等其他多方面进行考虑,诸如 JavaScript、Lua 以及 Python 等高抽象层次的脚本语言,也被逐渐应用在嵌入式设备上,“性能”已经不再成为人们选择嵌入式设备编程语言时所要考虑的第一要素。
但现实情况是,并非所有的嵌入式设备,都可以直接满足这些高级编程语言的使用要求。由于不同语言的运行时差异性,并且考虑到实现成本,嵌入式设备无法独立地为每种语言运行时都提供单独的编程接口(一般为 C/C++),以供其与嵌入式设备进行交互。此时 Wasm 字节码的高密度、高性能以及可移植性,便使得人们有了可尝试的新选择。
如下图所示,通过将 Wasm 字节码作为嵌入式设备的中间媒介表示形式IR来向所有的外部高级编程语言宿主运行时提供统一的基于 Wasm 的编程接口。对相关 Wasm 编程接口的调用,将会由嵌入式设备上的独立 Wasm 运行时来执行。
<img src="https://static001.geekbang.org/resource/image/32/02/32dbb78bfae32928d88a27641bd32702.png" alt="">
这样,我们不仅可以直接利用已有的 C/C++ 编程接口(编译到 Wasm 字节码),同时还能够向外界的宿主运行时提供统一的编程接口调用方式。关于这两者之间的具体交互方式,你可以通过 Web 浏览器中 JavaScript 与 Wasm 二进制模块之间的交互方式来进行类比。当然,细节的实现依据不同的编程语言将会有所不同。
### 微内核 - Unikernel
另一个比较有意思的想法源自于一个曾在 IoT ,或者说嵌入式领域比较火的概念 — “微内核”。
如下图左侧所示,在传统的操作系统内核架构中,有着用来支持各类功能的底层驱动、框架、接口以及组件库。实际上对于具有某一特定功能的嵌入式设备来说,其中的大部分内核底层功能都没有存在的必要性,但却仍然占用着一部分的硬件资源。
是否可以只把整个嵌入式硬件需要使用的内核底层组件单独提取出来,使其成为一个面向某一类特定功能或应用的专有内核呢?答案是当然可以,这就是“微内核”的概念。
<img src="https://static001.geekbang.org/resource/image/c5/73/c5c961d71f4d3234f02462e4aabb5673.png" alt="" title="图片来源于 YouTube">
相较于传统的类 Unix 操作系统内核(一般称之为宏内核),微内核有着许多的优势,比如:更快的启动速度、更小的 ROM 体积,以及更高的硬件资源使用率。 [Unikraft](http://www.unikraft.org/) 便是这样一款可以用来制作微内核的系统工具。
关于 Unikraft 的更多信息,你可以点击上一段文字中的超链接进行参考。你现在需要知道的就是,通过使用 Unikraft我们可以构建一个基于 Wasm 运行时的操作系统微内核。
相较于其他基于 JavaScript 等高级编程语言运行时(比如 V8构建的微内核而言基于 Wasm 的微内核将有着更高的程序执行效率、更少的硬件资源占用率,以及更快的操作系统冷启动速度。这都是源自于 Wasm 本身作为一种 V-ISA 所带来的优势。
比如我们可以为“树莓派(一种嵌入式开发板)”,构建一个拥有如下“服务栈”的 Wasm 微内核。在这个微内核中除了最下层必要的内核组件以外我们还为其添加了用于支持“图形界面”、“多线程”、“网络通信”以及“C 标准库”等功能的必要组件。
位于最上层的便是由 Wasm 虚拟机WAMR构建出的“执行层”。在这里我们可以通过提供 Wasm 字节码的方式,来与整个微内核的其他组件功能进行交互。
<img src="https://static001.geekbang.org/resource/image/5b/17/5bc9bd3c76916dd7901c02bca9a60217.png" alt="">
在这样的一个架构中,整个微内核的大小只有 468KB。当上述的 Wasm 微内核被运行在一个普通的树莓派开发板上时,整个内核的启动时间仅需要 20 毫秒。并且内存资源的使用率以及应用程序代码的大小,也都处在一个对于嵌入式设备来说十分可观的量级下。这样的系统冷启动速度以及资源使用率,对于 Serverless 相关的应用领域来说,不得不引起关注。
## 多媒体Multimedia
Wasm 在“多媒体”领域内的实践可谓是数不胜数。可以说,“音视频及图像的在线处理”是 Wasm 在基于现阶段 MVP 标准的情况下,其可以大显身手的一个重要场景。因为对多媒体资源的处理始终离不开“编解码”的需求,编解码过程本身又是一个“计算密集”的数据处理过程。
就目前的大多数 Web 浏览器实现而言,当 Wasm 不再需要频繁地与 JavaScript 环境之间传递大量数据时JavaScript 引擎便可以按照“最优”的策略来执行 Wasm 代码,从而减少在两个上下文环境间相互切换时所产生的性能损耗。这便给予了 Wasm 在“终端密集计算”这个场景中以机会,于是基于 Wasm 的 Web 端音视频处理方案便如雨后春笋般涌现。
### ogv.js
[ogv.js](https://github.com/brion/ogv.js) 是一个由“维基百科”技术团队开发的,可以在 Web 浏览器中使用的多媒体播放器。它能够播放如 “Ogg”、“WebM” 以及 “AV1” 等多种音视频格式。如下图所示为其整体架构设计。
<img src="https://static001.geekbang.org/resource/image/8a/1c/8a1b0eaa59ecfe8f9a68dabd25e3db1c.png" alt="" title="图片来源于 YouTube">
可以看到,位于主线程中的 Demuxer 作为整个播放器的核心组件,主要用于解码并提取各类型媒体文件中的音视频内容。位于各个工作线程中的音视频解码过程,也同样属于整个播放器的核心逻辑。因此,这两部分计算密集的逻辑便交由 Wasm 来进行处理。
同时为了保证性能和兼容性ogv.js 还使用了 ASM.js 实现来作为 Wasm 的一个兼容性补偿,以便在一些不支持 Wasm 的浏览器中,通过 ASM.js 来进行加速。在最不济的情况下ogv.js 便可以直接退回到 JavaScript 的方案(将 ASM.js 代码视作普通 JavaScript 来执行)。
不仅如此ogv.js 还可以同时利用浏览器支持的 “Multi-Cores Worker” 特性(每一个工作线程都使用 CPU 上的一个独立核心),来对整个解码过程进行加速。与此同时,随着 Wasm 最新的 SIMD 标准被越来越多的浏览器实现ogv.js 在处理视频像素矩阵以及各类相关编解码工作时,还可以利用该特性来做到进一步的加速。
另一个值得讲的便是 ogv.js 对第三方编解码库libogg、libvorbis、libtheora 等等的复用。ogv.js 在构建时,直接使用了已有的一些 C/C++ 编解码库来完成对音视频流的编解码过程,而没有选择自己从头开始编写这部分功能。因此,得益于 Emscripten 提供的对 C/C++ 代码到 ASM.js / Wasm 代码的转译功能ogv.js 的整个开发过程变得更加方便快速。
### WXInlinePlayer
同 ogv.js 类似,[WXInlinePlayer](https://github.com/qiaozi-tech/WXInlinePlayer) 也是一个 Web 端的音视频播放器。不过相较于 ogv.js 本身基于“维基百科”自身业务需求的出身而言WXInlinePlayer 的出身则显得更加“有趣”。
国内的大多数移动端浏览器厂商(或提供者)通常会在其自家浏览器内,对 HTML5 网页中基于`&lt;video&gt;`标签进行的视频播放行为,采用很多的 “魔改”。比如:使用单独的窗口来播放视频、视频播放完毕后推送广告信息等等。
在某些视频无法正常播放的情况下开发者甚至无法捕捉到任何的异常信息。而这便为那些需要提供一致性用户体验的产品设置了阻碍。WXInlinePlayer 的诞生便源于对此的迫切需求。
同大多数的`&lt;video&gt;`标签替代方案一样WXInlinePlayer 会自行解码收到的 FLV 视频流,然后再通过 WebGL 与 WebAudio 的方式,来将视频画面与音频播放出来。
在实际的解码过程中WXInlinePlayer 便会利用 Wasm 进行加速。同样地,它也使用了 ASM.js 作为 Wasm 的降级方案。在解码时,也同样利用了诸如 “de265” 等现有的 C/C++ 解码库。
相较于大多数公司采用的“直接将 FFmpeg 编译到 Wasm 来进行视频解码”的解决方案WXInlinePlayer 会相对更加轻量,且在某种程度上解码的效率更高。
FFmpeg 由于其自身的代码库体积过大,导致 Web 浏览器在实际加载和实例化 Wasm 模块时,会消耗更多的时间。并且 FFmpeg 天生并非针对 Web 场景,因此其内部的很多优化策略可能无法直接在 Web 平台上使用。而 WXInlinePlayer 仅把涉及到解码和 Remux 的部分单独提取出来,因此可以有效地针对某个解码方案进行优化。
你可以通过下图来参考一下, WXInlinePlayer 与其他同类型方案的视频播放性能对比结果。
<img src="https://static001.geekbang.org/resource/image/37/51/37bd4365097f4f5abea9cfa4c5862351.png" alt="" title="图片来源于 GitHub">
## 云Cloud
最后,我们来看看 Wasm 在云技术领域的一些实践。
**注**:由于我对云原生领域并不是特别熟悉,但我还是很想和你分享一些在该领域内比较重要的 Wasm 尝试。关于这些创新项目的更多内容,你可以参考 GitHub 上的相关文档,也欢迎你在评论区和我一块探讨。
### Krustlet
Kubernetes 是目前云原生领域中最常用的一种容器编排引擎。Kubernetes 由 Google 开源通过它我们可以方便地管理云平台上众多物理主机中运行的容器化应用。Kubernetes 使容器化应用的部署和管理变得更加简单和高效。
而 [Krustlet](https://github.com/deislabs/krustlet) 旨在作为一个 Kubernetes Kubelet ,以运行在整个 Kuberneters 集群中的各个服务节点上。它的设计与 Virtual Kubelet 十分类似,会在 Kubernetes API 事件流上监听新的 Pods。
根据指定的 Kubernetes TolerationKubernetes API 能够将特定的 Pod 调度到 Krustlet 上,然后将它们运行在基于 WASI 的 Wasm 运行时上。因此借助于 Krustlet我们可以方便快捷地在 Kubernetes 集群中部署 Wasm 应用。
### Embly
[Embly](https://www.embly.run/) 是一个基于 Wasm 的 Severless 框架。它使得我们可以在服务器上执行 Wasm 字节码函数并访问完成任务所需要的网络和系统资源。Embly 通过一种“声明式”的配置文件来定义不同的 Wasm 服务。配置文件中包含有对服务函数的编译、部署以及声明所需依赖等多种任务流程的配置项。
不仅如此Embly 还实现了 Actor 模型(一种并发计算模型),这样可以允许一个 Wasm 函数生成另一个 Wasm 函数并且函数之间可以相互传递数据。通过这种方式Embly 足以应对更加复杂的计算模式。同时,基于 Wasm 模块的沙箱机制也使得 Embly 可以支持多租户的特性。
一个简单的 Embly 示例配置文件如下所示。
```
function &quot;hello&quot; {
runtime = &quot;rust&quot;
path = &quot;./hello&quot;
}
gateway {
type = &quot;http&quot;
port = 8765
route &quot;/&quot; {
function = &quot;${function.hello}&quot;
}
}
```
在这个文件中,我们定义了一个名为 “hello” 的函数,以及一个 HTTP 类型的网关。在网关的配置项中通过 “route” 字段,我们可以声明所有通过端口 8765 发送进来的,对根目录(“/”的HTTP 请求,都会被转发到函数 “hello” 的对应实现中进行处理。
在 Embly 中使用到的所有 Wasm 函数,均需要由 Rust 语言进行编写。在函数定义中,我们可以使用由 Embly 提供的用于访问各类系统资源(包括 HTTP 对应的请求和响应)的数据类型。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在本节课中,我们主要介绍了 Wasm 在“物联网”、“多媒体”以及“云”这三个技术领域内的一些创新性实践。当然,限于篇幅和我所掌握的知识,我无法将每个领域内的每个 Wasm 实践项目都解释得十分清楚。
但无论如何,我希望通过这节课,能够给你这样一种认知,即:随着将近 5 年时间的发展,对 Wasm 的应用实践已经不再单单局限于 Web 平台,而是已经开始向各种各样的其他领域进军。虽然其中的大部分实践项目都还处于“实验性”阶段,但我相信,距离它们可以被真正使用在生产环境的那一天并不遥远。
## **课后思考**
最后,我们来做一个思考题吧。
你最期待 Wasm 能够在哪个应用/技术领域内有所创新?
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="12 | 有哪些优秀的 WebAssembly 编译器与运行时?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/e9/0d1b729ceb80e893841f22312190bce9.mp3"></audio>
你好,我是于航。
本节课我们来一起看看,目前业界有哪些优秀的 Wasm 运行时。这些运行时都是 Wasm 可以在 out-of-web 领域大显身手的最基本保障。它们其中有些支持 WASI 抽象系统接口,有些支持 Wasm Post-MVP 标准中的部分提案还有一些可以被专门用在诸如嵌入式、IOT 物联网以及甚至云AI 和区块链等特殊的领域和场景中。
不仅如此,还有一些更具创新性的尝试,比如 “Wasm 包管理”。这一概念类比 npm 之于 JavaScriptPyPi 之于 Pythoncrates.io 之于 Rust相信这一定可以为 Wasm 生态添砖加瓦。
这一切,我们都要先从“字节码联盟”这个特殊的组织开始说起。
## 字节码联盟Bytecode Alliance
“字节码联盟”成立于2019年末是一个由个人和公司组成的团体。最初的一批创始成员为 Mozilla、Fastly、Intel 以及 Red Hat。联盟旨在通过协作的方式来共同实现 Wasm 及 WASI 相关标准,并通过提出新标准的方式来共同打造 Wasm 在浏览器之外的未来。
对于开发者来说,联盟希望能够为开发者提供健全的、基于各类安全策略构建的成熟开发工具链(虚拟机、编译器以及底层库)生态。这样开发者便可以将目光更多地专注于应用本身的设计与研发上,同时可以在各类环境中,快速地构建可运行在浏览器之外的 Wasm 应用,并且不用考虑安全性等基本问题。
### 背景 —— 问题所在
回顾 20 年前,各大互联网公司在软件开发的过程中,对于跨应用的代码复用能力是非常弱的。但 20 年之后,诸如 NPM、PyPi 等代码包管理平台,让我们不再需要从头到尾完全“独立”地开发一个完整应用。
模块化的软件开发模式,让我们可以大量重用社区中现存的、已经十分成熟的第三方代码库。但在方便地利用这些代码库的同时,我们也不得不面对的另外一个问题,那就是随着第三方代码而来的代码安全性问题。
一个应用在运行时会依赖第三方代码库所提供的功能,因此在基于传统软件的开发模式中(譬如 Node.js 应用),第三方代码同样共享着应用程序所拥有的各类系统接口权限(如 Socket 网络通信、File 文件系统等),以及资源访问(内存、硬盘等)权限。
不仅如此,如下图所示,由于模块化的开发方式,使得代码的整体依赖成为了树状关系,因此整颗依赖树上的所有模块代码,都会共享同样的代码权限。这种共享无疑大大降低了应用整体的安全性,给第三方代码中所可能包含的恶意代码或漏洞以可乘之机。
<img src="https://static001.geekbang.org/resource/image/af/8a/af8665b4120e9e00cb5af6e23c13358a.png" alt="">
通过对一个真实的,第三方模块恶意代码窃取用户数字货币的案例进行总结,我们可以发现攻击者通常会按照以下时间顺序(仅选择了关键节点)来对终端用户逐步发起攻击。
- 第 0 天:攻击者创建了一个模块
- 第 2 天:攻击者将该模块作为可复用的第三方底层依赖库
- 第 17 天:攻击者为模块添加恶意代码
- 第 42-66 天:目标应用通过依赖升级而引入了恶意代码
- 第 90 天:攻击被用户察觉
通常来说,以上述案例为例,恶意代码需要同时具备以下两类权限才能够对终端用户成功地发起攻击:
1. 操作系统资源的访问权限。包括用于存放诸如“秘钥”等敏感数据的内存资源、用于发送窃取数据的 Socket 资源等;
1. 操作系统接口的调用权限。包括对文件资源的读写权限,以及 Socket 的操作权限。
据 GitHub 官方调查统计,自 2017 年到 2019 年NPM 上包含有恶意代码的模块数量逐年增加,并且攻击者的恶意代码攻击目标逐渐向具体的某类终端用户聚焦。攻击实施者更加具有耐心,企图进行可以暗中实施的、经过精心策划的攻击。
先抛开经由第三方模块主动发起的恶意代码攻击不谈,应用自有的代码漏洞也同样为攻击者提供了可乘之机,比如“经典”的 ZipSlip 任意文件覆盖漏洞。
ZipSlip 没有对解压缩文件时的目标地址进行校验,而是直接进行拼接。因此当遇到包含有恶意代码文件的压缩包时,这些文件便可经由此漏洞被解压到整个文件系统的任意位置。当然前提是应用的运行者拥有这些文件目录的写权限。
而在 NPM 中,具有类似漏洞的第三方代码库只有 59% 被修复。另外有超过 40% 的代码库依赖于拥有至少包含一个已知漏洞的 NPM 第三方模块。从现实的情况来看,此类“恶意代码”或者“代码漏洞”问题无法被完全避免,因此我们需要考虑其他的方式来保证应用的运行时安全。
究其根源,发生类似的安全性问题其主要原因在于,恶意代码拥有了本不该拥有的系统资源和系统接口访问权限。我们不能够 100% 地相信代码本身的行为方式,能够完全满足我们对安全性的要求。但基于 Wasm我们可以在一定程度上解决这个问题。
类比于操作系统上每个原生应用在运行时的独立进程,实际上,每一个 Wasm 模块在 out-of-web 环境中实例化运行时,也都有着自己独立的运行时沙盒环境,并且对应着独立的可用内存资源以及调用栈。但 Wasm 模块之间的隔离却不一定需要通过独立进程的方式来实现因此从运行模型上来看Wasm 的方式会更加轻量且高效。
不仅如此,正如我们在讲解 WASI 时所介绍的那样,与传统操作系统中的“进程”不同,每一个实例化的 Wasm 模块,都只能够在实例化时使用被主动分配的系统资源(内存)与接口能力(系统调用),因此对于模块实例所拥有权限的控制力度会更为细腻。
而且相对于传统进程需要通过“序列化”与“反序列化”才能够在进程间传递信息IPC的方式不同Wasm 实例之间的消息传递可以通过更加轻量的方式来完成。
### 解决方案 —— 纳米进程Nano-Process
根据上面讲过的 Wasm 在资源及权限控制上的相对优势,我们可以提出一种新的 Wasm 应用构建模式 —— “Wasm Nanoprocess”。
一般来说,一个完整的大型 Wasm 应用,可能会同时包含有多个相互依赖的底层 Wasm 模块。由于每一个模块实例都拥有着自己独立的数据资源及可用权限,因此我们可以称每一个实例化的模块为一个独立的 “nanoprocess”翻译过来也就是“纳米进程”。
当一个含有恶意代码的 Wasm 模块被“链接”到整个应用的依赖树中时,应用各依赖模块所能够使用的资源及系统接口权限,便全部来自于最上层的调用者。也就是说需要在应用运行的入口模块中被指定,然后再由该模块向下层依赖模块进行分发。
当恶意模块的内部代码需要使用某种未经授权的额外资源或能力时,整个模块依赖树的 “Import Section” 签名便会发生错误,这个错误会在运行时向上层用户抛出对应异常,提示某个模块的某些特定资源或者权限没有被导入。在这种情况下,特殊的权限调用便会引起人们的注意。
即便恶意代码获得了特定操作系统接口的执行权限,但恶意代码想要从其他应用依赖模块的实例中,获取对应内存段中的敏感信息,也并非易事。
由于每个 Wasm 模块实例都拥有独立、隔离的线性内存段来存储数据,因此只有在模块主动向外部暴露(通过 “Export Section” )特定数据,或者直接调用(动态链接)目标模块内的方法时,才能够将自身内存段中的数据传递过去。
如下图所示通过限制恶意代码对数据以及系统接口权限的访问和使用“Wasm Nanoprocess” 这种应用构建模式,可以在最大程度上保证 Wasm 应用及其所依赖第三方模块的运行时安全性。
<img src="https://static001.geekbang.org/resource/image/1c/2a/1cc308f68bca6cbcc67f4be261yy6c2a.png" alt="">
## 虚拟机运行时
我们为了能够基于 “Nanoprocess” 模式来构建安全可靠的 Wasm 应用,一定少不了在 out-of-web 领域提供 Wasm 字节码解析和执行能力的基础设施。并且在一定程度上,我们还需要它们为我们提供的 WASI 系统接口的访问能力。
而“字节码联盟”便负责培养和发展这样一批能够提供这些能力的优秀基础设施及相关组件。它们主要包括Wasm 运行时虚拟机、Wasm 运行时组件(实现)以及 Wasm 语言相关的工具。
下面我便挑选其中一些具有代表性的运行时虚拟机,来给你进行介绍。当然,在字节码联盟之外,也有一批十分优秀的开源 Wasm 底层基础设施正在以惊人的速度蓬勃发展,这里我会同时选择一些有代表性的项目和你分享。
### Wasmtime
Wamtime 是字节码联盟旗下的一个独立的 Wasm 运行时,它可以被独立作为 CLI 命令行工具进行使用或者是被嵌入到其他的应用程序或系统中。Wamtime 具有很高的可配置性和可扩展性,因此可以被应用到很多的场景中,譬如 IoT 与云原生领域。
Wasmtime 基于优化的 Cranelift 引擎构建因此它可以在运行时快速地生成高质量的机器码。Cranelift 是一个低层次的、可重定向的代码生成器。它可以将与目标无关的中间代码表示形式IR转换为可执行的机器代码。
除此之外Wasmtime 还支持部分的 WASI 系统接口以及 Wasm Post-MVP 提案,以及对于诸如 C 和 Python 等语言的运行时绑定。这样你便可以在这些语言的代码中,直接使用 Wasmtime 所提供的能力。关于它的更多信息可以在[这里](http://wasmtime.dev/)查看。
<img src="https://static001.geekbang.org/resource/image/ea/ae/eabaa3b2d28401c391a727106685d4ae.png" alt="">
### WAMR
WAMRWebAssembly Micro Runtime同样是一款字节码联盟旗下的独立 Wasm 运行时,它基于 C 语言开发。不过相较于 Wasmtime它更倾向于被应用在诸如 IoT、嵌入式芯片等对功耗和硬件资源要求较为严格的 Wasm 场景中。
WAMR 支持多种 Wasm 字节码的运行时“翻译”模式,比如 JIT 模式、AOT 模式以及解释器模式。其中在解释器模式下,整个运行时的大小仅有 85KB。在 AOT 模式下,仅有 50KB。不仅如此它可以在将近 100 微秒的时间内启动应用,并在最小 100KB 的内存资源下,便可以启动一个 Wasm 实例。
WAMR 也同样支持 WASI 以及部分的 Wasm Post-MVP 提案。同时附带地,它还提供了一个用于快速构建 Wasm 应用的 WAMR 应用框架。关于它的更多信息可以在[这里](https://github.com/bytecodealliance/wasm-micro-runtime)查看。
<img src="https://static001.geekbang.org/resource/image/3f/fd/3fba571041818bb599158e4da0f50bfd.png" alt="">
### Wasmer
Wasmer 是另外一款独立于字节码联盟优秀的 Wasm 运行时。
不同于 Wasmtime 与 WAMRWasmer 基于 Rust 编写,它在支持 Wasm 核心标准、部分 WASI 系统接口以及部分 Wasm Post-MVP 标准的基础之上,还同时提供了对多达数十种编程语言的 Wasm 运行时绑定支持。这意味着,你可以在其他编程语言中使用 Wasmer 的能力来解析和执行 Wasm 字节码。
除此之外有一个很有趣的尝试, Wasmer 同时提供和维护 Wasm 包管理平台 —— Wapm。通过这个平台你可以发布新的或直接使用已有的 Wasm 包。这些包都借助于 WASI 抽象操作系统接口,提供了与本地应用相同的系统资源访问能力。
关于它的更多信息可以查看[这里](https://wasmer.io/)。
<img src="https://static001.geekbang.org/resource/image/ef/21/efff12222d29c4eaf594c8f32fcb0821.png" alt="">
### SSVM
最后一个要介绍的 Wasm 运行时是 SSVM。它是一个专门针对云、AI 以及区块链应用程序设计的高性能、可扩展且经过硬件优化的 Wasm 虚拟机。
SSVM 的 Wasm 运行时针对 ONNX AI 模型进行了硬件优化。同时也可以作为区块链平台的智能合约运行时引擎。关于它的更多信息可以查看[这里](https://github.com/second-state/SSVM)。
<img src="https://static001.geekbang.org/resource/image/4e/d4/4e3309795530422d2912890de1a41fd4.png" alt="">
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在本节课程中,我首先给你介绍了伴随 Wasm 发展而出现的一个新的组织 —— “字节码联盟”。字节码联盟由 Mozilla 等一批科技公司作为创始成员,通过协作的方式来共同打造 Wasm 在 out-of-web 领域的未来发展。
紧接着,我们讲解了字节码联盟出现的背景,也就是当前传统应用构建模式在安全性上的不足。基于 Wasm 与 WASI 的天然安全特性,我们可以按照 “Nanoprocess” 纳米进程模型,来构建更加安全的 Wasm 应用。字节码联盟出现的目的之一,便是为我们提供、培养一系列必备的底层基础设施与相关组件,这样我们可以在未来轻松便捷地构建这类安全应用。
在这节课的最后,我们一起快速浏览了一些当前在字节码联盟旗下,以及其他优秀的、开源的 Wasm 运行时。这些运行时都支持 Wasm 的 MVP 标准以及部分 Post-MVP 标准中的提案。不仅如此,这些运行时还都有选择性地支持了部分的 WASI 操作系统接口并以不同的实现方式擅长于一个或多个不同的实际应用场景比如嵌入式、IoT、AI 甚至是云计算领域。
## **课后思考**
最后,我们来做一个思考题吧。
你觉得当前传统的软件构建模式还存在着哪些安全性问题?欢迎大家各抒己见。
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="13 | LLVM如何将自定义的语言编译到 WebAssembly" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/48/48246900b06afaacd58a16cd266e8248.mp3"></audio>
你好,我是于航。
应用 Wasm 的常见方式有几种类型,一种方式是通过 Web 浏览器提供的 JavaScript API 与 Web API ,来在 Web 应用中调用从 Wasm 模块中导出的函数。通过这种方式,我们可以充分利用 Wasm 的安全、高效及可移植性等优势。
另一种方式是通过 WASI 抽象系统调用接口,以便在 out-of-web 应用中使用 Wasm。这种使用方式与 Web 端大同小异,不过区别是可以借助底层运行时的能力,使得我们构建出的 Wasm 应用可以在 Web 浏览器外的 Native 环境中与操作系统打交道,并同样享受着 Wasm 本身所带来的安全、高效及可移植性。
而今天我们要介绍的另外一个 Wasm 的应用场景,则相对有些特殊。在大多数时候,我们都是将由诸如 C/C++ 以及 Rust 等语言编写的源代码,编译至 Wasm 字节码格式来使用。假设此时我们想要设计开发一款自定义的静态编程语言,那么怎样才能够方便快捷地为它的编译器添加一个能力,可以让编译器支持将 Wasm 作为编译目标呢?
关于这个问题,我们要先从传统的编译器链路开始说起。
## 传统编译器链路
对于传统的静态语言编译器来说通常会采用较为流行的“三段式”链路结构。如下图所示三段式结构分别对应着整个编译器链路中三个最为重要的组成部分编译器前端Compiler Frontend、中间代码优化器Optimizer以及编译器后端Compiler Backend
<img src="https://static001.geekbang.org/resource/image/d4/yy/d49e7f9fec5d2e4d4394b4a50219f3yy.png" alt="">
其中,“编译器前端”主要用于对输入的源代码进行诸如:词法、语法及语义分析,并生成其对应的 AST 抽象语法树,然后再根据 AST 来生成编译器内部的中间代码表示形式IR
“中间代码优化器”则主要用于对这些 IR 代码进行一定的优化,以减小最后生成的二进制文件大小,并同时提高二进制代码的执行效率。
最后的“编译器后端”则负责进行与本地架构平台相关的代码生成工作,主要会根据优化后的 IR 代码来进行寄存器分配和调优之类的工作,并生成对应的机器码,存储在构建出的二进制可执行文件中。当然,流程的细节根据具体编程语言实现可能有所不同。
这种分段式编译器链路的优势在于,当我们想要为其添加多种源语言或目标编译平台的支持时,我们只需要重新编写其中的一个“分段”,便可以很轻松地复用整个编译链路中的其他部分。你可以形象地通过下图来感受这种关系。
<img src="https://static001.geekbang.org/resource/image/1e/3d/1e2b5b2270dd973291423d9d3c99a13d.png" alt="">
比如当我们需要为编译器添加对另外一种源语言的支持时,我们只需要编写整个链路中的“编译器前端”部分即可。
但是满足这种“链路可分离”要求的一个前提需要整个链路中用于对接各个阶段的“中间产物IR其存在形式必须是确定且不变的。编译器前端“输送”给中间优化器的 IR 代码格式,必须对所有为各种源语言设计的编译器前端保持一致。同理,从中间优化器输入到编译器后端的“中间产物”也是如此。
然而一个现实的情况是,实际上在 LLVM 出现之前,在各类编程语言的编译器链路中,并没有采用完全统一的中间产物表示形式(包括 IR、AST 等在内)。因此如果想要对编译器链路中的某一部分进行重用,这个过程仍然会十分困难。
这就造成了每当人们需要重新设计一款编程语言时,需要将整个编译器的编译链路重新编写。但实际上编译器针对不同编程语言变化的部分,可能就只有编译器前端而已。
编译器链路的分段模式还有另外的一个好处,它可以让编译器开发者的分工更加明确。比如擅长编译器前端的开发者,便可以更加专注地来实现编译器的前端逻辑,为编译器提供针对新源语言的前端,而不用去考虑优化以及编译器后端的逻辑(对于这部分功能可以直接复用已有的编译器链路)。这对于需要投入到商业化运作中的编译器来说,十分有利。
## LLVM
LLVM 的全称为 “Low Level Virtual Machine”翻译成中文即“低层次虚拟机”。最初的 LLVM 是 Chris Lattner 和 Vikram Adve 两人于2000年12月研发的一套综合性的软件工具链。在这套工具链中包含了众多可用于开发者使用的相关组件这些组件包括语言编译器、链接器、调试器等操作系统底层基础构建工具。
LLVM 在开发初期,被定位为一套具有良好接口定义的可重用组件库。这意味着,我们可以在所开发的第三方应用程序中,使用由 LLVM 提供的众多成熟高效的编译链路解决方案。大到“中间代码优化器”,小到代码生成器中的一个 “SelectionDAG 图生成组件”。这些方案以“组件化”的形式被管理在整套 LLVM 工具集中,可用于支持整个编译链路中各个阶段遇到的各种问题。
除此之外LLVM 还提供了众多可以直接使用的命令行工具。通过这些工具(如 llvm-as、llc、llvm-dis 等等),我们也可以快速地对经由 LLVM 组件生成的中间表示产物,进行一定的变换和处理,这极大地方便了我们的应用开发和调试流程。
### LLVM-IR
在整个 LLVM 工具链体系中,最重要的组成部分,便是其统一的,用于表示编译器中间状态的代码格式 —— LLVM-IR。在一个基于 LLVM 实现的编译器链路中,位于链路中间的优化器将会使用 LLVM-IR 来作为统一的输入与输出中间代码格式。
在整个 LLVM 项目中,扮演着重要角色的 LLVM-IR 被定义成为一类具有明确语义的轻量级、低层次的类汇编语言,其具有足够强的表现力和较好的可扩展性。通过更加贴近底层硬件的语义表达方式,它可以将高级语言的语法清晰地映射到其自身。不仅如此,通过语义中提供的明确变量类型信息,优化器还可以对 LLVM-IR 代码进行更进一步的深度优化。
因此,通过将 LLVM-IR 作为连接编译器链路各个组成部分的重要中间代码格式,开发者便可以以此为纽带,来利用整个 LLVM 工具集中的任何组件。唯一的要求是所接入的源语言需要被转换为 LLVM-IR 的格式(编译器前端)。同样,对任何新目标平台的支持,也都需要从 LLVM-IR 格式开始,再转换成具体的某种机器码(编译器后端)。
在 LLVM-IR 的基础上,我们上面所讲的分段式编译链路可以被描绘成下图的形式。
<img src="https://static001.geekbang.org/resource/image/be/3b/be6d8648a2950ac4fa4ca2f5bbdcfd3b.png" alt="">
### 命令行:基于 LLVM 生成 Wasm 字节码
既然基于 LLVM-IR我们可以方便快捷地为整个编译链路添加新的前端源语言或者是后端目标平台。因此 Wasm 也同样可以作为一种目标平台,被实现在 LLVM 中Wasm 作为一种 V-ISA其实本身与 I386、X86-64 等架构平台没有太大的区别)。
无独有偶的是,在 LLVM 中,已经存在了可用于 Wasm 目标平台的编译器后端。接下来,我们将尝试把一段 C/C++ 代码通过 LLVM 转换为 Wasm 字节码。
这里为了能够完成整个编译流程,我们将使用到 LLVM 工具集中的一个 CLI 命令行工具 —— llc以及用于将 C/C++ 源代码编译为 LLVM-IR 中间代码的编译器 Clang。Clang 是一个业界知名的,基于 LLVM 构建的编译器,可用于编译 C/C++ 以及 Objective-C 等语言代码。
首先,我们给出如下这段 C/C++ 代码。
```
// add.cc
extern &quot;C&quot; {
int add (int a, int b) {
return a + b;
}
}
```
接下来,我们通过下面这个命令,将上面的代码编译为 LLVM-IR 中间代码对应的文本格式。
```
clang -S -emit-llvm add.cc
```
其中我们通过指定 “-S” 与 “-emit-llvm” 两个参数,使 Clang 在编译源代码时生成对应的 LLVM-IR 文本格式。命令执行完毕后,我们可以得到一个名为 “add.ll” 的文件。通过文本编辑器打开这个文件,你可以看到如下截图所示的 LLVM-IR 内容。
<img src="https://static001.geekbang.org/resource/image/15/93/159a4157a4d059f05af03f60c4796a93.png" alt="">
关于 LLVM-IR 的具体内容,你对它有一个大致的概念即可。接下来,我们继续使用 “llc” 工具,来将上面这部分 LLVM-IR 中间代码转换为对应的 Wasm 字节码。
“llc” 是 LLVM 的静态编译器,它可以将输入的 LLVM-IR 代码编译到平台相关的机器码。我们可以通过命令 “llc --version” 来查看它所支持的编译目标平台。如下图所示,我们可以看到其支持名为 “wasm32” 与 “wasm64” 两种 Wasm 的目标平台,这里我们使用第一个 “wasm32”。
<img src="https://static001.geekbang.org/resource/image/9f/97/9ff3c9f34ea013a926d40e8908345a97.png" alt="">
通过如下命令行,我们便可以将上述生成的 LLVM-IR 代码编译为最终的 Wasm 字节码。
```
llc add.ll -filetype=obj -mtriple=wasm32 -o add.wasm
```
### 组件库Wasm 编译器后端
在上面这个小实践环节中,我们通过使用 LLVM 工具链提供的命令行工具,将基于 C/C++ 代码编写的函数 “add” 编译成了对应的 Wasm 字节码格式。
那相对的,既然 LLVM 中存在着命令行工具可以进行类似的转换,那么在代码层面,便也存在着相应的组件库,能够实现从 LLVM-IR 到 Wasm 字节码的转换过程。
在实际的编码过程中,你可以通过 “llvm::TargetRegistry::lookupTarget” 这个 API 来设置和使用 Wasm 对应的目标编译器后端,以编译中间的 LLVM-IR 格式代码。关于这部分内容,你可以参考 LLVM 的[官方文档](https://llvm.org/docs/)来查阅更多的细节信息。
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在本节课中,我们主要介绍了传统“三段式”编译器链路的一些特点,即分段式的结构更易于编译链路中对各重要组件的复用。同时,“三段式”也能够让编译链路的扩展变得更加轻松。 LLVM 的出现,使分段式编译链路的优点被发挥利用到最大。
LLVM 是一套综合性的软件工具链。内部提供了一系列基于 LLVM-IR、可用于构建编译相关系统工具的各类组件比如代码优化器、生成器等等。不仅如此LLVM 还为我们提供了诸如 “llc” 等命令行工具,可用于方便地对 LLVM-IR 等格式进行转换和编译。
最后LLVM 也同样整合了可用于编译 LLVM-IR 到 Wasm 字节码的编译器后端,因此这对于我们来说,只要能够将我们自定义的编程语言代码编译到 LLVM-IR那么我们就可以直接利用 LLVM 已有的 Wasm 后端,来将这些 IR 编译到 Wasm 字节码格式。不仅如此,我们还能够直接复用 LLVM 已有的优化器组件,来优化我们的生成代码,进而简化整个编译器的开发工作。
## **课后思考**
最后,我们来做一个思考题吧。
你知道在上面 “llc” 的帮助信息中,所支持平台 “wasm32” 与 “wasm64” 两者有什么区别吗?
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,187 @@
<audio id="audio" title="14 | 有哪些正在行进中的 WebAssembly Post-MVP 提案?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ef/64/ef1e69d512b9c3bd7929127473182c64.mp3"></audio>
你好,我是于航。
作为“应用篇”的最后一节课,我们来一起看看自 Wasm MVP 标准发布之后2017年3月的这三年时间里Wasm 还有哪些行进中的后续标准正在被设计和实现?这些标准将会分别影响整个 Wasm 生态的哪些组成部分?借助于这些新的标准,现有的 Wasm 应用能否被进一步得到优化Wasm 这项技术能否被应用到更多、更广的领域中呢?相信在学习完这节课后,对于上面这些问题,你会有着进一步的感悟。
实际上,在我们之前课程里所讲到的那些 Wasm 案例,均是在现有 MVP 标准所提供能力的基础上进行构建的。但 MVP 标准并不代表着 Wasm 的最终版本,相反,它正是标志着 Wasm 从枯燥的技术理论走向生产实践的一个起点。
## MVP
MVPMinimum Viable Product的全称为“最小可行产品”这个我们之前也提到过。既然是“最小可行产品”那就意味着在这个版本中包含有能够支持该产品正常使用的最少同时也是最重要的组成部分。对于 Wasm 来说,便是我们之前在“核心原理”篇中介绍的那些内容。
那在这里让我先来总结一下Wasm 在 MVP 标准中都定义了哪些“功能”?
### 可编译目标
在本课程的第 [03](https://time.geekbang.org/column/article/283436) 讲中我们曾介绍过Wasm 实际上是一种新的 V-ISA 标准。“ISA” 我们都知道,翻译过来即“指令集架构”。同 X86、ARM 等其他常见的物理指令集架构类似,这意味着我们可以将诸如 C/C++ 等高级静态编程语言的代码,编译为对应这些 (V)ISA 的机器代码。
这里 ISA 的前缀 “V” ,代表着它是一种“虚拟的”指令集架构。也就是说,不同于我们上面提到的 X86 和 ARMWasm 指令集架构中的指令并不是为真实的物理硬件设计的。相反,这些虚拟指令被设计和应用于一种“概念性”的机器。而对于这个概念性机器的具体实现细节,则交由各个 VM 虚拟机以及 Runtime 运行时来负责。
而这便是 MVP 标准“赋予” Wasm 的第一个能力 —— 可编译目标。
作为一种指令集架构MVP 标准下的 Wasm 仅提供了包括“分支指令”、“内存操作指令”、“数学运算指令”以及“类型转换指令”等几种最为常用的指令类型。因此我们说Wasm 这项技术在当前 MVP 标准下的能力是十分有限的,而“数学密集计算”这个场景便是它暂时所能够很好支持的几个重要的实践场景之一。
<img src="https://static001.geekbang.org/resource/image/20/44/2091238a0b8517b9a06982d512369b44.png" alt="">
### 字节码格式
在定义好 Wasm 是什么V-ISA之后此时我们已经有了这样一套新的 V-ISA 标准,在其中有着各种各样的虚拟指令。下一个需要在 MVP 标准中确定的部分就是,我们应该如何在计算机中表示这些指令的一个集合呢?
或者说同我们常见的 Windows 上的 “.exe” 可执行文件,以及 Linux 下的 ELF 可执行文件类似,一个包含有虚拟指令的 Wasm 文件,它的内部组成结构应该是怎样的?
关于这部分内容,我在第 [04](https://time.geekbang.org/column/article/284554) 和 [05](https://time.geekbang.org/column/article/285238) 讲中为你给出了答案。同样,这也是 Wasm MVP 标准中最为重要的一部分定义,即 “Wasm 字节码组成结构”。在其中定义了 Wasm 以 “Section” 为单元的模块内部组成结构,以及这些结构在二进制层面的具体编码方式等。
<img src="https://static001.geekbang.org/resource/image/a9/d7/a9b0945dbf66319f9412133f473deed7.png" alt="">
### Web 可交互性
在定义好 “Wasm 是什么?”以及“如何在计算机中表示?”这两个问题之后,接下来便是“从理论到实践的最后一公里”,即“如何使用 Wasm” 这个问题。这里 MVP 标准便为我们提供了相应的 JavaScript API 与 Web API 以用于实现 Wasm 与 Web 的可交互性,这部分内容我们曾在第 [08](https://time.geekbang.org/column/article/288704) 讲中介绍过。
但 MVP 标准中所定义的“可交互性”,仅满足了 Web 与 Wasm 之间的最简单“交流方式”。在这种交流方式下JavaScript 环境与 Wasm 环境之间仅能够传递最基本的数字值。
而对于复杂数据类型的传递,则需要通过 Wasm 线性内存段进行中转。不仅如此,对于诸如 JavaScript 引擎等宿主环境中的“不透明数据“,我们也无法直接在 Wasm 模块中使用。而这便是 MVP 标准暂时所欠缺的部分。
<img src="https://static001.geekbang.org/resource/image/53/25/538f1eaace64d7a815255c265961c325.png" alt="">
## Post-MVP
至此,我们知道对于 Wasm 的 MVP 版本标准来说,其实它并不在于想要一次性提供一个大而完整的新技术体系。相反,它希望能够在人们的实际生产实践中,去逐渐验证 Wasm 这项新技术是否真的有效,是否真的可以解决现阶段开发者遇到的问题,然后再同时根据这些来自实际生产实践的反馈,与开发者共同制定 Wasm 的未来发展方向。
那话不多说,让我们来看看在 MVP 标准发布之后的这几年时间里Wasm 又发展出了哪些新的提案,而这些提案目前都处于怎样的进展中。
### 多线程与原子操作
顾名思义,”多线程与原子操作”提案为 Wasm 提供了多线程与原子内存操作相关的能力。从细节来看,该提案为 Wasm 标准提供了一个新的“共享内存模型”,以及众多的“内存原子操作指令”。这使得我们可以方便地在 Web 端构建出 Wasm 的多线程应用。如下图所示为基于此提案构建的 Wasm Web 多线程应用其基本结构。
<img src="https://static001.geekbang.org/resource/image/7b/d8/7b5522e978a9218565289043d706eed8.png" alt="">
“共享内存模型”你可以简单地理解为,它是一块可以同时被多个线程共享的线性内存段。你可以再看一下上面这张图。在 Web 平台中SharedArrayBuffer 对象便被用来作为这样的一个“共享内存对象”,以便支持在多个 Worker 线程之间数据共享能力。
多线程模式的一个特征就是,每个 Worker 线程都将会实例化自己独有的 Wasm 对象,并且每个 Wasm 对象也都将拥有自己独立的栈容器用来存储操作数据。
如果再配合浏览器的 “Multi-Cores Worker” 特性,我们便能够真正地做到基于多个 CPU 核心的 Wasm 多线程,而到那个时候 Wasm 应用的数据处理能力便会有着更进一步的提升。
对于“原子内存操作”你可以把它简单理解为,当你在多个线程中通过这些原子内存操作指令来同时访问同一块内存中的数据时,不会发生“数据竞争”的问题。
每一个操作都是独立的事务,无法被中途打断,而这就是“原子”的概念。不仅如此,通过这些原子内存操作,我们还能够实现诸如“互斥锁”,“自旋锁”等各类并发锁结构。
目前,该提案已经可以在 Chrome 和 Firefox 的最新版本中使用。关于该提案的更多信息可以点击[这里](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md)。
### SIMD
SIMD 的全称为 “Single Instruction, Multiple Data”即“单指令多数据流”。SIMD 是一种可以通过单一指令,对一组向量数据同时进行操作的一种并行性技术。你可以通过下图来直观地了解 SIMD 下的乘法与普通标量乘法运算之间的区别。
<img src="https://static001.geekbang.org/resource/image/99/51/9999bf62f2df931399373259583bc651.png" alt="">
在左侧的“标量乘法运算”中针对每一个乘法操作An x Bn我们都需要使用一条独立的乘法操作指令来执行因此对于这四组操作我们便需要使用四条指令。
而在右侧的 SIMD 版本中,针对 A1 到 A4 这四组乘法运算,我们可以仅通过一条 SIMD 指令就能够同时完成针对这四组数字的对应乘法运算。相较于普通的标量乘法运算来说SIMD 会使用特殊的寄存器来存储一个向量中的一簇数据然后再以整个“向量”为单位进行运算。因此相较于传统的标量计算SIMD 的性能会有着成倍的增长。
在 Wasm Post-MVP 标准中便提供了这样的一系列指令,可以让 Wasm 利用硬件的 SIMD 特性来对特定的向量计算过程进行加速。可想而知,这对于需要同时对大量数据单元(如像素点)进行相同计算过程的应用场景(如“静态图像及视频处理”),会有着十分明显的性能提升。
不过遗憾的是,目前该提案还暂时无法在任何的浏览器中使用。 Chrome 和 Firefox 仍然在努力地实现中。关于该提案的更多信息你可以点击[这里](https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md)。
### Wasm64
在目前的 MVP 标准中所有关于内存操作的相关指令,都仅能够使用 32 位长度的“偏移地址”,来访问 Wasm 模块线性内存中某个位置上的数据。而这便意味着,我们能够在 Wasm 实例中使用的内存资源最多仅能有 4GiB。因此我们一般会将 MVP 标准下的 Wasm 模型称为 “wasm32”。
而随着 Post-MVP 的不断发展Wasm 也将开始支持 64 位长度的内存指针(偏移),来访问大于 4GiB 的内存资源。相信更多更加庞大和复杂化的 Wasm Web 应用,也将会伴随着 “wasm64” 模型的出现而逐渐涌现。Web 端的能力将逐渐与 Native 平台靠拢。
现阶段,该提案还没有被任何浏览器实现。关于该提案的更多信息你可以点击[这里](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md)。
<img src="https://static001.geekbang.org/resource/image/81/a9/81d2232389b63d1860b3a0e7120a21a9.png" alt="">
### Wasm ES Module
相信 “ES Module” 对 Web 前端开发的同学来说,可谓是再熟悉不过了。作为一种官方的 JavaScript 模块化方案“ES Module” 使得我们能够通过 “export” 与 “import” 两个关键字,来定义一个 JavaScript 模块所需要导入,以及可以公开导出给外部使用的资源。
那么试想一下,我们是否也可以为 Wasm 提供类似的能力呢?借助于该提案,我们可以简化一个 Wasm 模块的加载、解析与实例化过程。并且可以通过与 JavaScript 一致的方式,来使用从 Wasm 模块中导出的资源。
```
import { add } from &quot;./util.wasm&quot;;
console.log(add(1, 2)); // 3;
```
可以看到在上面的代码中,相较于我们之前介绍的通过 JavaScript API 来加载和实例化 Wasm 模块的方式,使用 import 的方式会相对更加简洁。不仅如此,在该提案下,我们也可以通过 `&lt;script type="module"&gt;`标签的方式来加载和使用一个 Wasm 模块。
现阶段,该提案还没有被任何浏览器实现。关于该提案的更多信息可以点击[这里](https://github.com/WebAssembly/esm-integration/tree/master/proposals/esm-integration)。
### Interface Type
我们知道在目前的 Wasm MVP 标准中Wasm 与宿主环境(比如 JavaScript之间只能够互相传递“数字值”类型的数据。而对于诸如“字符串”、“对象”等复杂、特殊的数据类型则需要通过编解码的方式来在两个不同环境之间传递和使用。这无疑增加了应用的整体执行成本和开发者的负担。而 “Interface Type” 提案的出现便可以在一定程度上解决这个问题。
该提案通过在宿主环境与 Wasm 模块之间添加“接口适配层”,来满足从 Wasm 模块的“低层次”数据类型到外界宿主环境“高层次”数据类型之间的相互转换过程。借助于这个提案Wasm 模块与宿主环境之间的可交换数据类型将会变得更加丰富,同时数据的交换成本也会变得更低。
当然,目前该提案仍然处在不断地修改和变化当中,能够支持的高层次数据类型还有待最终的确定。现阶段,该提案还没有被任何浏览器实现。关于该提案的更多信息你可以点击[这里](https://github.com/WebAssembly/interface-types/blob/master/proposals/interface-types)。
<img src="https://static001.geekbang.org/resource/image/1b/de/1beb558c9f82bbb385eb28da34c4e2de.png" alt="">
## Wasm W3C 提案流程
事实上,同 TC39 对 ECMAScript 的提案流程一样,自 Wasm 成为 W3C 的一项“官方”标准之后,核心团队对 Wasm Post-MVP 提案的发布也有了相应的标准化流程。这个流程与 TC39 所使用的 “Stage0-4” 的“分阶段式”提案发布流程大同小异。
### 六个流程
一项新的 Wasm 提案从想法的诞生到最后被正式加入标准,一共需要经历如下的六个阶段:
`0.` Pre-Proposal [Individual Contributor]<br>
`1.`Feature Proposal [Community Group]<br>
`2.` Proposed Spec Text Available [Community + Woking Group]<br>
`3.`Implementation Phase [Community + Working Group]<br>
`4.`Standardize the Feature [Working Group]<br>
`5.`The Feature is Standardized [Working Group]
关于这六个阶段的“进入条件”,以及每个阶段要做的事情,你可以点击这里进行[查看](https://github.com/WebAssembly/meetings/blob/master/process/phases.md)。当然Wasm CGCommunity Group社区是完全开放和透明的只要你有合适的想法能够提升或改善 Wasm 在某一方面的能力,那就可以加入到提案的流程中来。
### 提案总览
最后,我在这里将目前处在各个阶段的 Wasm 后续提案全部罗列出来,如果你对其中的某个提案感兴趣,可以点击对应的链接了解详情。甚至更进一步,你可以提出对这些提案的想法,帮助 CG 和 WG 改善提案的相关细节,一起共建 Wasm 的未来!
- Phase 4[Reference Types](https://github.com/WebAssembly/reference-types)
- Phase 4[Bulk Memory Operations](https://github.com/WebAssembly/bulk-memory-operations)
- Phase 3[Tail Call](https://github.com/WebAssembly/tail-call)
- Phase 3[Fixed-width SIMD](https://github.com/webassembly/simd)
- Phase 3[Multiple Memories](https://github.com/WebAssembly/multi-memory)
- Phase 3[Custom Annotation Syntax in the Text Format](https://github.com/WebAssembly/annotations)
- Phase 2[Threads](https://github.com/webassembly/threads)
- Phase 2[ECMAScript Module Integration](https://github.com/WebAssembly/esm-integration)
- Phase 2[Exception Handling](https://github.com/WebAssembly/exception-handling)
- Phase 2[Typed Function References](https://github.com/WebAssembly/function-references)
- Phase 2[Type Reflection for WebAssembly JavaScript API](https://github.com/WebAssembly/js-types)
- Phase 1[Type Imports](https://github.com/WebAssembly/proposal-type-imports)
- Phase 1[Garbage Collection](https://github.com/WebAssembly/gc)
- Phase 1[Interface Types](https://github.com/WebAssembly/interface-types)
- Phase 1[WebAssembly C and C++ API](https://github.com/WebAssembly/wasm-c-api)
- Phase 1[Conditional Sections](https://github.com/WebAssembly/conditional-sections)
- Phase 1[Extended Name Section](https://github.com/WebAssembly/extended-name-section)
- Phase 1[Memory64](https://github.com/WebAssembly/memory64)
- Phase 1[Flexible Vectors](https://github.com/WebAssembly/flexible-vectors)
- Phase 1[Numeric Values in WAT Data Segments](https://github.com/WebAssembly/wat-numeric-values)
- Phase 1[Instrument and Tracing Technology](https://github.com/WebAssembly/instrument-tracing)
- Phase 1[Call Tags](https://github.com/WebAssembly/call-tags)
- Phase 0[Web Content Security Policy](https://github.com/WebAssembly/content-security-policy)
- Phase 0[Funclets: Flexible Intraprocedural Control Flow](https://github.com/WebAssembly/funclets)
- Phase 0[Module Types](https://github.com/WebAssembly/module-types)
- Phase 0[Constant Time](https://github.com/WebAssembly/constant-time)
## 总结
好了,讲到这,今天的内容也就基本结束了。最后我来给你总结一下。
在今天的课程中,我们主要介绍了 Wasm 从 MVP 标准到 Post-MVP 标准所经历的变化。在 MVP 标准中,主要定义了关于 Wasm 的一些核心基础性概念,比如 Wasm 作为 V-ISA 时的一些基本指令Wasm 作为二进制模块时的文件内部组成结构及数据编码规则,以及用于支持 Wasm 与 Web 可交互性的一些基本 API 等。
对于 Post-MVP 的众多提案,则将会在 MVP 的基础之上再进一步拓展 Wasm 的能力。这里我选择了五个比较重要且易于理解的提案给你介绍。关于其他后续提案的更多信息,你可以参考我在本文最后放置的列表。在这个列表中,我给你整理了目前正在行进中的、处在各个发展阶段的 Wasm Post-MVP 提案。
当然,你需要知道的是,尽管其中的大部分提案看起来都十分复杂,但 Wasm 被作为一种高级语言的最终编译目标,无论是对于 MVP 还是 Post-MVP 标准中的大多数提案,它们都不会对我们日常使用 Wasm 的方式产生任何改变。
这些提案所带来的新特性或者优化机制,将由各个编译器和虚拟机来为我们实现。对于我们来说,甚至可以在不进行任何代码变更的情况下,便直接享受到这些由 Post-MVP 标准带来的好处。
## **课后思考**
最后,我们来做一个思考题吧。
你觉得 Wasm 的提案流程与 ECMAScript 的提案流程有哪些异同之处?
今天的课程就结束了,希望可以帮助到你,也希望你在下方的留言区和我参与讨论,同时欢迎你把这节课分享给你的朋友或者同事,一起交流一下。