第一章 WebAssembly 初识

一、概述

WebAssembly 是 W3C 标准化组织制定的一个可移植、体积小、加载快并且兼容 Web 的全新格式。利用 WebAssembly 技术可以方便地将非 JavaScript 代码快速地“运行”在浏览器中,从而为前端场景提供了无限可能;此外,随着 WebAssembly 在开发者社区中越来越流行,也正在成为服务端以及云计算平台上的新锐。

作为本课程的开篇,本文首先介绍 WebAssembly 的发展历程,正所谓 “以史为镜,可以知兴替”,从其历史演变中我们可以深入了解 WebAssembly 技术的来龙去脉,以及它的核心诉求和价值;接下来,我们会梳理和总结 WebAssembly 的使命和优势;最后,我们通过一个简单的 WebAssembly 浏览器应用示例,带领读者一起身临其境地感受下 WebAssembly 在 Web 环境中的真实使用体验。

二、WebAssembly 历史

WebAssembly 技术源于浏览器,其发展历程可以说是一部浏览器性能优化史。在 Web 前端领域,JavaScript 语言是编写运行在浏览器上的 Web 应用的首选;与此同时,Node.js 等非浏览器 JavaScript 运行时环境的出现, JavaScript 变得越来越流行;然而,各类应用随着功能逐渐复杂化,对性能的要求越来越高;而由于 JavaScript 语言本身的一些特性,已经很难满足日益增长的应用性能需求。

为了满足应用日益增长的性能需求,针对 JavaScript 语言本身缺陷带来的瓶颈优化,逐步形成了三个阶段性优化产物,他们分别是 asm.js、NaCl/PNaCl 以及 WebAssembly,正是这三个阶段的优化逐步推动了 WebAssembly 技术的发展。接下来,我们将逐一从这三个阶段来阐述其各自的诉求及其价值,从而了解 WebAssembly 起源及其目标和优势。

2.1 asm.js阶段

JavaScript 是前端开发的首选语言,由于 JavaScript 本身是一种动态、弱类型编程语言;因此,只有在程序运行时才能确定执行上下文中对象具体数据类型;不仅如此,JavaScript 语言还允许同一个变量在不同时刻可以绑定不同类型的对象。JavaScript 的弱类型导致虚拟机只能在执行时刻进行类型推断,同时,其动态性又进一步导致当前的执行结果无法被复用,因为代码所执行的对象类型和逻辑都可能随时改变。

为了解决 JavaScript 语言自身带来的上述弊端,asm.js 便应运而生。asm.js 设计的出发点直指 JavaScript 语言的设计缺陷,asm.js 可以在 JavaScript 代码运行之前便确定程序中变量的具体类型;与此同时,它还进一步保证程序中变量的类型不会在程序运行的过程中发生改变。基于 asm.js 对弱类型和动态性的约束,虚拟机在执行过程中可以利用确定性的类型进行编译优化,并且编译结果可以复用而不需要重复执行相同源代码的编译优化过程,从而使得 Web 应用的运行效率有巨大的提升。

asm.js 标准始于 2013 年 8 月,它是 JavaScript 的一个严格子集, 是一种可用于编译器的低层级的、高效的目标语言。相较 JavaScript 而言,asm.js 使用了一 种名为 Annotation 的类型声明方式来对变量类型进行约束,其中 Annotation 的形式采用 | T 表示。如下述 asm.js 模块代码所示,赋值语句 n = n | 0 通过对变量与 0 进行使用"按位或"操作的声明方式,可以让虚拟机在解析 asm.js 代码时强制将该变量 n 视为一个 32 位的整数,并且该变量所能够存储的数据类型在运行过程中无法被更改。语法格式如下:

 function fast_fib_module(stdlib, foreign, heap) {
    "use asm";
    function fib(n) {
        n = n|0;
        if (n >>> 0 < 3) {
            return 1|0;
        }
        return (fib((n-1)|0) + fib((n-2)|0))|0;
    }
    return fib;
}

从整体上看,asm.js 模块是一个标准的 JavaScript 函数。在 asm.js 的模块语法规则中,不仅需要对函数所传入的参数进行类型声明,而且这些是必需且强制的;此外,asm.js 标准中只定义了对数值类型的 Annotation 声明方式,这使得 asm.js 的应用场景很大程度上集中在数值计算密集型 Web 应用的优化处理上;再加上各大浏览器厂商对 asm.js 标准的支持程度和实现方式也不尽相同。

asm.js 技术本身存在的问题和局限性,大大阻碍了 asm.js 的发展,从而也推动了 Web 应用优化进入了 NaCl 和 PNaCl 阶段。

2.2 NaCl 和 PNaCl 阶段

NaCl(Google Native Client) 是 2011 Google Chrome 团队意在把基于原生 C/C++ 语言编写的本地应用安全、高效地运行在 Web 浏览器端的一项技术。该技术主要方式是将原生 C/C++ 语言编写的本地应用编译为标准的 NaCl 模块,每个NaCl模块是一个以 “.nexe” 为后缀的 ELF 格式二进制文件,该文件可以直接在 Chrome 中加载并运行。基于 NaCl 技术开发出来的应用可以以接近原生 C/C++ 应用的效率在浏览器端稳定地运行,但在实际项目使用时,需要为每种不同的处理器架构分别单独编译对应版本的 NaCl 二进制模块文件;这种方式既不方便也不符合开源软件的便携特性;不仅如此,NaCl 模块这种直接存储针对底层处理器架构机器码的方式也使得模块本身失去了可移植性。

为了解决由于 NaCl 模块的平台依赖性导致的互联网上自由地分发的问题,有推出了新的 PNaCl (Chrome Portable Native Client) 技术。PNaCl 并不会直接将应用的 C/C++ 源代码编译成依赖特定处理器架构的底层机器码,PNaCl 首先会将应用的 C/C++ 源代码编译成一种基于 LLVM 生成的抽象中间二进制模块 ,以 “.pexe” 为后缀。这种模块不依赖具体的处理器架构,因此可以在互联网上被随意地分发。在浏览器中运行 PNaCl 应用时,浏览器会首先将 pexe 二进制模块加载到内存中,并根据当前处理器架构通过内置的 AOT 编译器将 pexe 二进制模块编译为特定处理器架构的机器码,随后被浏览器直接执行。

NaCl 和 PNaCl 技术没有被除 Chrome 以外的任何其他浏览器支持;此外,基于 C/C++ 语言也大大增加了 NaCl 和 PNaCl 应用的开发难度和开发成本,使得 NaCl 和 PNaCl 基本脱离了技术快速迭代的前端开发领域。

NaCl 和 PNaCl 技术本身存在的问题和局限性,阻碍了它成为浏览器的事实标准,从而也推动了 Web 应用优化进入到了当前的 WebAssembly 阶段。

2.3 WebAssembly 阶段

基于前两个阶段的探索,为了提升 Web 应用的性能,2015 年 Mozilla 在 asm.js 的基础上发布了一种新型的二进制代码格式 “WebAssembly”。这种二进制文件可以用类似 JavaScript 模块加载的方式被浏览器快速、 高效地执行;不仅如此,WebAssembly 并不像 NaCl 那样需要区分浏览器所运行的处理器架构,这使得它可以自由地在互联网上分发,具有很好的浏览器兼容性和用户体验。 鉴于 WebAssembly 的优势,2017年,Firefox、Chrome、Edge 和 Webkit 四大浏览器厂商在 WebAssembly MVP (最小可用版本) 标准的设计上达成共识;同年,WebAssembly Working Group (以下简称 WWG) 成立,标志着 WebAssembly 成为 W3C 标准技术体系的一部分;2019 年 12 月,宣布 WebAssembly 成为第 4 种 Web 语言,同时 WebAssembly 1.0 标准正式落地。

随着 WebAssembly 在开发者社区中越来越流行,WebAssembly 的潜在价值从 Web 逐渐开始向其他领域,比如云原生、AI 以及区块链等;2019年12月,Bytecode Alliance 字节码联盟宣布正式成立,联盟旨在通过协作的方式,来共同实现 WebAssembly 及 WASI 相关标准,并通过提出新标准的方式来共同打造 WebAssembly 在浏览器之外的未来生态。

在最近的2022年, WebAssembly 2.0 草案正式发布,相比 WebAssembly 1.0,WebAssembly 2.0 草案中加入了很多值得关注的新特性,比如引用类型(Reference Types)、固定宽度的 SIMD(Fixed-width SIMD)、批量内存操作(Bulk Memory Operations)等等。

三、WebAssembly 优势

相比于传统的JavaScript,WebAssembly的核心优势主要体现在如下几个方面:

  • 性能 : WebAssembly 是静态强类型低级语言,通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。此外,WebAssembly 是一个轻量的二进制格式,提供了友好的高效冷启动,轻量部署能力。
  • 跨平台、可移植 : WebAssembly 是一个可移植、体积小、语言和平台无关的二进制格式,开发者可以使用各种自身熟悉的语言开发,生成的 WebAssembly 作为平台无关的应用发布形式;发布的 WebAssembly 模块可以在众多平台上运行,例如浏览器、后端、终端设备、移动设备、IoT等都有广阔的应用场景。
  • 沙箱环境 : WebAssembly 运行在一个独立的沙箱中,一方面可以避免了数据的泄露和侧信道攻击,另一方面恶意代码只能影响自身沙箱环境而不会影响应用本身。
  • 标准化 : W3C WebAssembly Working Group 制定相关的标准,可以保证标准的通用性和各厂商的兼容性。
  • 网络安全 : WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容,WebAssembly 被限制运行在一个安全的沙箱执行环境中,像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
  • 灵活的开发模式 : WebAssembly 制定了标准化的中间指令格式,开发者可以使用多种不同的开发语言,如 C/C++,Java/Kotlin, TypeScript, Rust 等,利用工具链可以转化为统一的 WebAssembly 的中间指令格式。

image.png
传统开发模式一般如图 1 所示,通过特定的语言生态开发、发布和运行应用。这种模式在大型系统中不可避免的暴露出其劣势:

  • 首先,对于开发者来说,为了满足项目中不同部分的需求,需要采用不同的语言来开发,开发者需要掌握多种不同类型的语言,技术栈负担重。
  • 其次,大型软件需要解决不同开发语言的协作问题(交互和集成),不同语言的交互一般需要语言特定的 FFI,而没有统一的集成方式和接口(组合爆炸💥问题);交互过程中会引入额外的损耗(内存和性能)
  • 再次,不同的语言发布的程序采用各自的方式部署和运行,对安全性,隔离性,跨平台等方面都会各自面临相同的问题,而没有统一的机制解决。
  • 最后,不同的语言是为解决特定的问题,会在其他方面做妥协(例如,脚本语言 JavaScript, Ruby 开发者门槛低,但性能也低),对于已有模块的复用和迁移代价较大,无论是语言间相互转换 (transcompile) 还是重写都需要付出极大的代价。

WebAssembly 为应用开发者提供了不一样的技术架构选型的可能性。WebAssembly 作为一个可安全隔离,高效,体积小、跨平台,多语言支持的可移植二进制中间形式,为解决上述问题提供了可实施的路径,不同的开发者可以利用 WebAssembly 的特性来满足业务需求。

对于 JavaScript、Python 等脚本语言来说,为了追求更高的性能,可以将性能热点模块通过 WebAssembly 来实现,从而获取高性能执行的收益。对于 Rust 开发者来说,利用语言的特性可以获取高性能和高安全性,但为了让开发者获得更低的开发门槛,可以编译为WebAssembly 模块提供给类似 JavaScript、Python 等脚本语言使用,降低开发者门槛;对于 C++ 开发者来说,可以获得高性能,但 C++ 不完备的安全性机制可能会使应用存在安全隐患,可以将其编译为 WebAssembly 在轻量级安全沙箱中运行,从而使得安全机制做到开箱即用(安全性保障需要安全领域的专业支持,门槛很高),如图 2 所示。

image.png

四、WebAssembly Web 应用体验

为了更直观地了解 WebAssembly 的优势,本小节将搭建 Mandelbrot Web 应用来展示 WebAssembly 是如何与 JavaScript 语言结合,以及如何无缝的在 Web 环境中运行。其中,Mandelbrot [8] 是在复平面上组成分形的点的集合,其图形绘制过程需要进行大量的复平面数值计算,通过 WebAssembly 实现可以有效的提高计算和绘制性能。Mandelbrot 图形绘制算法将采用 AssemblyScript [9] 来编写,并通过 AssemblyScript 编译器生成 WebAssembly 二进制模块。

接下来,我们将创建 Mandelbrot Web 应用并运行通过运行来体验一下WebAssembly的优势。为了能够将 AssemblyScript 编译为 WebAssembly 二进制模块。

1,首先,通过如下命令创建项目工程,并预先安装 AssemblyScript 编译器。

npm init
npm install --save-dev assemblyscript

2,接下来,Mandelbrot 图形绘制函数 computeLine 保存为 Mandelbrot.ts,当完成工程创建后,通过如下命令行可以将 Mandelbrot.ts 编译为 WebAssembly 二进制文件 Mandelbrot.wasm。

npx asc Mandelbrot.ts --target release -o Mandelbrot.wasm

Mandelbrot.ts为源码如下:

// file: Mandelbrot.ts


/** Number of discrete color values on the JS side. */
const NUM_COLORS = 2048;


/** Computes a single line in the rectangle `width` x `height`. */
export function computeLine(y: u32, width: u32, height: u32, limit: u32): void {
    var translateX = width * (1.0 / 1.6);
    var translateY = height * (1.0 / 2.0);
    var scale = 10.0 / min(3 * width, 4 * height);
    var imaginary = (y - translateY) * scale;
    var realOffset = translateX * scale;
    var stride = (y * width) << 1;
    var invLimit = 1.0 / limit;


    var minIterations = min(8, limit);


    for (let x: u32 = 0; x < width; ++x) {
        let real = x * scale - realOffset;


        // Iterate until either the escape radius or iteration limit is exceeded
        let ix = 0.0, iy = 0.0, ixSq: f64, iySq: f64;
        let iteration: u32 = 0;
        while ((ixSq = ix * ix) + (iySq = iy * iy) <= 4.0) {
            iy = 2.0 * ix * iy + imaginary;
            ix = ixSq - iySq + real;
            if (iteration >= limit) break;
            ++iteration;
        }


        // Do a few extra iterations for quick escapes to reduce error margin
        while (iteration < minIterations) {
            let ixNew = ix * ix - iy * iy + real;
            iy = 2.0 * ix * iy + imaginary;
            ix = ixNew;
            ++iteration;
        }


        // Iteration count is a discrete value in the range [0, limit] here, but we'd like it to be
        // normalized in the range [0, 2047] so it maps to the gradient computed in JS.
        // see also: http://linas.org/art-gallery/escape/escape.html
        let col = NUM_COLORS - 1;
        let sqd = ix * ix + iy * iy;


        if (sqd > 1.0) {
            let frac = Math.log2(0.5 * Math.log(sqd));
            col = <u32>((NUM_COLORS - 1) * clamp<f64>((iteration + 1 - frac) * invLimit, 0.0, 1.0));
        }
        store<u16>(stride + (x << 1), col);
    }
}


/** Clamps a value between the given minimum and maximum. */
@inline
function clamp<T extends number>(value: T, minValue: T, maxValue: T): T {
    return min(max(value, minValue), maxValue);
}

运行上面的命令后,项目中将会生成一个二进制文件 Mandelbrot.wasm。

3,为了展示 Mandelbrot 效果,可以通过在 Web 页面[11] 中来加载 Mandelbrot.wasm,并调用 computeLine 函数来计算 Mandelbrot 集来完成图形绘制。此处,我们新建一个index.html的测试文件,代码如下:

// file: index.html


// Set up the canvas with a 2D rendering context
var cnv = document.getElementsByTagName("canvas")[0];
var ctx = cnv.getContext("2d");
var bcr = cnv.getBoundingClientRect();


// Compute the size of the viewport
// var width = bcr.width | 0;
// var height = bcr.height | 0;
// var ratio = window.devicePixelRatio || 1
// ...
// ctx.scale(ratio, ratio);


// Compute the size of and instantiate the module's memory
const memory = new WebAssembly.Memory({
    initial: ((byteSize + 0xffff) & ~0xffff) >>> 16
});
const mem = new Uint16Array(memory.buffer);
const imageData = ctx.createImageData(width, height);
const argb = new Uint32Array(imageData.data.buffer);


// Fetch and instantiate the module
fetch("build/Mandelbrot.wasm")
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.instantiate(buffer, {
    env: {
        memory,
        "Math.log": Math.log,
        "Math.log2": Math.log2
    },
}))
.then(module => {
    const exports = module.instance.exports;
    const computeLine = exports.computeLine;
    const updateLine = function (y) {
        var yx = y * width;
        for (let x = 0; x < width; ++x) {
            argb[yx + x] = colors[mem[yx + x]];
        }
    };


    // Compute an initial balanced version of the set.
    const limit = 40;
    for (let y = 0; y < height; ++y) {
        computeLine(y, width, height, limit);
        updateLine(y);
    }


    // Keep rendering the image buffer.
    (function render() {
        if (animate) requestAnimationFrame(render);
        ctx.putImageData(imageData, 0, 0);
    })();
    // ...
}).catch(err => {
    alert("Failed to load WASM: " + err.message + " (ad blocker, maybe?)");
    console.log(err.stack);
});

4,按上述步骤,我们已经生成 Mandelbrot.wasm 二进制模块,并在 index.html 中完成模块加载和图形绘制的逻辑实现,最后,我们可以在本地建立 http 服务来加载和展示 Mandelbrot 图形效果,启动命令如下。

cd $webassembly_tech/samples/mandelbrot
npx serve


> ┌──────────────────────────────────────────┐
> │  Serving!                                │
> │                                          │
> │   - Local:    http://localhost:3000      │
> │                                          │
> │   Copied local address to clipboard!     │
> │                                          │
> └──────────────────────────────────────────┘

image.png

虽然上述步骤中,我们已经详细描述了 Mandelbrot Web 应用中 WebAssembly 的生成和 Web 页面集成的各个步骤;但为了方便读者构建原型,webassembly_tech [11] 提供了源代码的下载,并提供了 Mandelbrot 项目使用文档快速构建和查看效果。

五、总结

至此,我们已经完整地介绍了 WebAssembly 技术的来龙去脉以及它的核心价值,并通过 Mandelbrot 示例带领读者一起身临其境地感受了 WebAssembly 在 Web 环境中的真实使用体验。然而,随着 WebAssembly 技术的不断发展,其应用领域和应用场景也越来越广阔,为了更好的了解 WebAssembly 的价值及其应用生态,我们将在接下来的文章中会对 WebAssembly使用场景和未来发展趋势做进一步的介绍和探索。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值