跳到主要内容

WebAssembly

WebAssembly 是一种底层类汇编语言,能够在所有当代桌面浏览器及很多移动浏览器上以接近本地的速度运行。

WebAssembly 被设计为编译目标,因此用C++Rust和其他语言编写的代码现在可以在 Web 上运行了。

后端开发者可以利用 WebAssembly 来提高代码复用度或者无须重写就将自己的代码移植到 Web 中。Web 开发者也可以从创建新库、改进现有库,以及提高自己代码中大计算量部分的性能中获益。

JavaScript 是一种解释型编程语言,使用解释型语言时,不需要提前编译代码,这意味着它启动的速度更快。但缺点是,解释器必须在每次运行代码时将指令转换为机器码。

其他编程语言(如C++)并不是解释型的。使用这类语言时,需要利用称为编译器的特定程序预先将指令转换为机器码。使用编译型编程语言时,需要一些时间将指令转换为机器码,然后才能运行它们,但其优点是有更多时间来优化代码的执行;一旦指令编译为机器码,就不需要再次编译。

WebAssembly 工作原理

由开发者提前编译为 WebAssembly 二进制格式文件,浏览器加载 WebAssembly 文件时,由于变量类型都是预知的,可以简单地将这段代码的二进制格式编译为机器码。

WebAssembly 工作原理

浏览器厂商已经通过不同方式提升WebAssembly的性能。其中一种方式是引入了一种称为流编译的技术,在浏览器下载和接收wasm 文件时,该技术可以将WebAssembly代码编译为机器码。流编译支持WebAssembly模块下载完毕即进行初始化,这样会显著加速模块的启动过程。

编译器工作原理

开发者以更接近于人类语言的语言编写代码,但计算机处理器只能理解机器语言。因此,你编写的代码必须转化为机器码才能运行。

如果将每种编程语言都直接编译为机器码的各个版本,那么效率会很低。取而代之的是,编译器中称为前端的部分会将你所编写的代码编译为一种中间表示(intermediate representation,IR)。创建好IR代码后,编译器的后端部分会接收IR代码,对其进行优化,然后将其转换为所需要的机器码。

编译器原理

由于浏览器可以在若干不同的处理器(比如,从桌面计算机到智能手机和平板设备)上运行,因此为每个可能的处理器发布一个WebAssembly 代码的编译后版本会非常繁复。取而代之的是,取得IR代码后,并通过一个专门的编译器来运行,这个编译器将IR代码转换为一种专用字节码并放入后缀为 .wasm 的文件中。

WebAssembly编译器原理

Wasm 文件中的字节码还不是机器码,它只是支持 WebAssembly 的浏览器能够理解的一组虚拟指令。当加载到支持WebAssembly的浏览器中时,浏览器会验证这个文件的合法性,然后这些字节码会继续编译为浏览器所运行的设备上的机器码。

浏览器识别WebAssembly

WebAssembly 定位

WebAssembly被设计为JavaScript的一个组件,而不是替代品。JavaScript 仍然是更好的选择。在一些情况下,网站可能需要包含WebAssembly来进行快速计算或提供底层支持。

为Web浏览器编程时,基本上有两个主要组件:JavaScript VM(WebAssembly模块运行于其中)以及Web API(比如DOM、WebGL、Web worker等),WebAssembly模块可以与JavaScript通信,但是还不能与任何Web API直接交流(但这个问题正在解决之中,未来可能会发生变化)。

WebAssembly 值类型

目前WebAssembly只能使用4种值类型:

  • 32位整型;
  • 64位整型;
  • 32位浮点型;
  • 64位浮点型;

布尔值用32位整型表示,0为false,非0值为true。所有其他值类型(如字符串)需要在模块的线性内存空间中表示。

哪些语言可用来创建WebAssembly模块

WebAssembly的最初关注点在CC++语言上,但后来RustAssemblyScript(一种新编译器,它接受TypeScript并将其转换为WebAssembly)这样的语言也增加了支持。

GitHub网站上维护了一个语言列表(Awesome WebAssembly Language),其中的语言可以编译到WebAssembly,或者将其VM放入WebAssembly。

创建自己的第一个WebAssembly模块

Emscripten 工具包

Emscripten工具包是将C/C++代码编译为 WebAssembly 字节码的最成熟工具包。

Emscripten使用LLVM编译器,这个编译器工具链目前具有最多的WebAssembly支持。Emscripten编译器使用Clang,后者类似于C++中的GCC,可以作为前端编译器将C或C++代码转换为LLVM IR,如图3-2所示。然后Emscripten会接收LLVM IR并将其转换为一种二进制字节码。

使用LLVM IR的编译器前端

安装

首先从官网GitHub仓库下载 zip 包,或者 clone 仓库代码。

然后执行下面命令:

# 进入 emsdk 项目目录中
cd ./emsdk

# 执行 emsdk 程序,下载最新的 SDK 工具包
./emsdk install latest

# 激活最新的SDK
./emsdk activate latest

# 使得当前终端窗口了解环境变量
source ./emsdk_env.sh

也可以在 .bash_profile 或者 .zshrc 文件中添加 alias,这样就可以随时使用 emsdk 相关命令了。

vi ~/.zshrc

# 增加 alias
export alias emsdk="xxxxx/emsdk/emsdk"

为了将C代码编译为WebAssembly模块,需要用控制台窗口来运行emcc(Emscripten编译器)命令,也可以给 emcc 添加 alias:

vi ~/.zshrc

# 增加 alias
export alias emsdk="xxxxx/emsdk/upstream/emscripten/emcc"

VScode 相关配置

vscode 中需要安装 C/C++Code Runner 插件。

Emscripten输出选项

根据目标的不同,可以用几种方式创建WebAssembly模块:

  • 让Emscripten生成WebAssembly模块JavaScript plubming文件,以及HTML模板文件。
  • 让Emscripten生成WebAssembly模块JavaScript plumbing文件。
  • 让Emscripten只生成WebAssembly模块

各文件的作用:

  • JavaScript plumbing,根据给定的命令行参数,这个文件的内容可能会有所不同。这个文件的代码会自动下载WebAssembly文件并在浏览器中将其编译和实例化。这个 js文件还包含若干辅助函数,使得主机与模块更容易相互交流。

  • HTML模板文件,相当于是一个简易 demo,如果是在WebAssembly学习过程中,要想在深入理解模块加载和实例化涉及的细节前专注于C/C++编译,生成 HTML 模板文件是有必要的。

  • WebAssembly模块,我们想要的 wasm 文件;

Emscripten会生成WebAssembly、JavaScript和HTML文件

写一个 C++ 程序

下面将实现编写一个名为IsPrime的辅助函数,这个函数会接受一个整型值作为参数,我们将检查这个值是否为素数。如果是,函数会返回1。否则,函数会返回0。

我们会在 main 函数中用 printf 函数将字符串传给Emscripten的JavaScript代码。然后JavaScript代码会接收这些字符串,将其显示在网页上的文本框和浏览器开发者工具的控制台窗口中。