声明:部分文档直接翻自https://developer.mozilla.org/相关模块
简介
WebAssembly
WebAssembly is a new type of code that can be run in modern web browsers — it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++, C# and Rust with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.
webAssembly
是一种新型的能够运行在现代web浏览器中运行的代码 —— 这是一种低级的压缩的二进制格式的类汇编语言,能以近乎native的性能运行并且提供将c/c++
、c#'
、Rust
等高级语言编译为可以运行在web
端的目标版本;它也被设计为允许与JavaScript
一起运行;
WebAssembly
已经被收录为W3C WebAssembly Community Group
的开放标准,使用WebAssembly JavaScript API
,你可以通过它们加载WebAssembly
模块到一个Web App
(node和JavaScript)中并且在两者间共享功能;
需要注意的是`WebAssembly并不是汇编
WebAssembly目标
- 快速、高效、可移植
- 可读性和可调试性 —— 虽然
Wasm
是低级的类汇编语言,但是其有着可读的文本格式,可以进行读、写、debug等; - 保持安全性 —— 运行在一个安全的沙箱环境,遵循同源策略和权限策略;
- 不会破坏web —— 可以与web技术共存并且有向后兼容性;
WebAssembly如何适应web平台
web平台可以认为由两部分组成:
- 运行Web App代码的
虚拟机
,通常是JavaScript
; - 一系列能够控制web浏览器或者设备的
Web API
,如DOM
、BOM
、WebGL
、IndexedDB
、WebAudio
等;
随着Web应用范围更加广阔(3D游戏、AR/VR、音视频处理、计算机视觉等),功能更加复杂,Javascript
加载、解析和执行的性能瓶颈逐渐突出;
WebAssembly
的出现不是为了替代JavaScript,而是一种补充,同时可以与Javascript
共同工作,这样web开发者可以结合两者的优势开发;WebAssembly
代码的基本单元叫做模块,其与ES2015
模块对称;
WebAssembly核心概念
- Module: 与
ES2015 module
类似,module
是无状态的,定义了输入和输出,表示一段由浏览器编译形成的可执行的机器码; - Memory:一个可调整大小的ArrayBuffer,它包含由WebAssembly的低级内存访问指令读写的线性字节数组;
- Table:一个可调整大小的类型化数组—— 包含那些原本不能以原始字节存储在内存中(由于安全和可移植性原因)的引用(如函数)
- Instance:可以理解为
module
在实际运行环境中的实例,包括运行时相关的Memory
、Table
及导入值等;
JavaScript Api提供了能力用于创建module
/memory
/table
/instance
,并且能够完全控制Wasm
的加载、编译和执行,因此甚至可以把它当做是一致高性能的JS封装函数;JS实例与wasm
实例也可以相互调用;
未来,Wasm
将会支持loadable
(<script type="module"
),这样就可以支持直接加载一个wasm
模块;
emscripten
根据官网定义:
Emscripten是一个使用LLVM把C/C++等编译生成WebAssembly的完整的编译器工具链,特别关注速度、大小和Web平台。
- 移植性
编译使用C/C++、或者其他使用LLVM的语言现有项目到浏览器、node、或者wasm运行时 - APIs
Emscripten把OpenGL转为WebGL,并且支持了类似的API如SDL
(c语言实现的跨平台的多媒体和游戏开发库,提供了处理声音、图像等相关封装函数)、线程、POSIX
、Web APIs及Javascript
; - 快速
在LLVM
、Emscripten
、Binaryen
、WebAssembly
的共同作用下,输出文件被压缩并且可以以接近原生的速度运行
LLVM
LLVM项目是模块化、可重用的编译器以及工具链技术的集合。
传统编译器架构
就像不同种群的人类讲不同的语言,不同架构的机器也需要不同的机器码才能理解人类需要它做什么;编译器的功能就是将多种高级语言统一转为特定机器可理解的机器码 —— 其中FE主要将各种高级编程语言转为IR,然后BE将IR转为适用于特定架构的汇编码;
如果每个高级语言对应到不同的架构机器码,这样会庞杂并且不怎么高效;
因此可以设计一个IR,高级语言统一转为IR,然后在不同的系统架构上再将IR转为特定的机器码;
LLVM架构
与Clang关系
Clang
是LLVM项目的一个子项目,基于LLVM架构的C/C++/Objective-C编译器前端。相对gcc
的优点包括编译速度快、占用内存小、模块化设计、拓展性强、可读性及可调试性强;
Binaryen
是以c++编写的用于WebAssembly
的一个基础库,使用了Binaryen
的编译器包括AssemblyScript
、wasm2js
、Grain
等;与clang + LLVM + Emscripten
一起作用于c/c++ => wasm
的转换过程;
可以用于读、写或转换wasm
;某些情况下可以把Binaryen
看做是没有LLVM的编译器后端(支持多核和pipeline);
整个流程可以描述为下:
WebAssembly与编译器架构
上面说了,WebAssembly
并不是真正的汇编;它与汇编的不同之处在于它并不是对应一个真实的物理机,而是一个概念上的机器,也就是不会做不同架构物理机的特定映射,因此WebAssembly
的指令也称作是虚拟指令
;
相对JS源码,wasm
具有更直接的到机器码的映射(但是不是对应特定的硬件设备的机器码)
为什么WebAssembly
比对应的JS运行更快
环境搭建
以ubuntu环境搭建为`例:
环境准备
Emsdk
不会在系统中安装任何工具,所有的变更都发生在/emsdk
目录下,而Emsdk
核心脚本都是python
脚本,因此需要单独安装python
;
# Install Python
sudo apt-get install python3
# Install CMake (optional, only needed for tests and building Binaryen or LLVM)
sudo apt-get install cmake
# install git
sudo aptget install git
拉取镜像
# Get the emsdk repo
git clone https://github.com/emscripten-core/emsdk.git
# Enter that directory
cd emsdk
安装与激活
更新emsdk
版本并安装;激活emsdk
并设置环境变量
# Fetch the latest version of the emsdk (not needed the first time you clone)
git pull
# Download and install the latest SDK tools.
./emsdk install latest
# Make the "latest" SDK "active" for the current user. (writes .emscripten file)
./emsdk activate latest
# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh
每次重新登陆或者新建 Shell 窗口,都要执行一次这行命令
使用Docker镜像搭建环境
如何在web App应用
目前,wasm
主要有四类入口点:
- 使用
Emscripten
导入C/C++
应用 - 在汇编层面直接编写和生成
wasm
- 编写
Rust
应用并把wasm
作为编译目标 - 使用
AssemblyScript
(类似于TypeScript
)并且编译成wasm
二进制码
这里仅介绍第一种
Porting from C/C++
由C/C++
导入主要有两种方式:使用线上wasm assember
或者Emscripten
;在线编译工具有WasmFiddle、WasmFiddle++等;
Emscripten
能够把任何的C/C++
源码转为三部分:
- 一个
.wasm
模块; - 一份加载和执行该模块的JS“粘合”代码
- 显示代码结果的html;
在内核中,该过程如下:
Emscripten
将C/C++
输入到clang + LLVM
—— 一套成熟的开源C/C++编译工具链,比如作为OXS中XCode的一部分;Emscripten
将clang + LLVM
的编译结果进一步转换为wasm
二进制码;- 由于目前
wasm
自身不能直接访问dom等,它只能够调用JS传入整型和浮点型的基本数据类型;因此它同时输出了JS“粘合”代码及html以实现访问web API
;
JS“粘合”代码包括一些库的方法JS实现以及对wasm
模块的获取、加载和执行的wasm
JavaScript API;
生成的HTML加载JS“粘合”代码
,并将输出置于一个textarea
中;
Note: There are future plans to allow WebAssembly to call Web APIs directly.
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合。LLVM是伊利诺伊大学的一个研究项目,提供一个现代化的,基于SSA的编译策略能够同时支持静态和动态的任意编程语言的编译目标
编译为.wasm
目前,支持WebAssembly
较多的编译器工具链是LLVM,有许多不同的FEs和BEs可以添加到LLVM由于支持WebAssembly
;
下图是从C语言编译为.wasm
的过程:
clang FE
将C转为LLVM IR;LLVM optimizer
对IR进行优化;- BE将
IR
转为.wasm
—— 这里BE一般有两种:LLVM WASM BE
或EMSCRIPTEN using asm2wasm
; - 将
wasm
转为可以在特定架构上运行的机器码(x86
或ARM
);当然,还有一些库也能实现,比如依赖于indexedDB
的文件系统;
在JS中加载.wasm
文件
function fetchAndInstantiate(url, importObject) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results =>
results.instance
);
}
WebAssembly
模块与JS模块不同,它的入参和返回值只能是整型或者浮点数类型;而对于字符串等其他数据类型,必须要使用WebAssembly
模块的memory
—— 它模拟了C/C++等语言中的堆概念;
而JavaScript
中的内存管理是依赖GC自动管理的;因此在JS中我们需要借助ArrayBuffer
实现(数组的索引作为内存地址);
ArrayBuffer
是一个字节数组,用来表示通用的、固定长度的原始二进制数据缓冲区;ArrayBuffer
中的内容我们无法直接操作,需要通过DataView
对象或者类型化数组对象操作,以此以特定的格式读取缓冲区中的内容;
如果想要在JS和
WebAssembly
之间传递一个字符串,你需要将字符转换为它们对应的字符码;
然后你需要将它们写入memory array
;由于索引是整数,索引可以传入到WebAssembly
函数中;
之后,字符串的第一个字符的索引将会被作为指针;
开发供web开发人员使用的WebAssembly模块的人很可能会为该模块创建一个包装器。这样,作为模块的使用者,您就不需要了解内存管理。(具体可以参考working with WebAssembly modules )
.wasm
的结构
了解.wasm
的结构有助于我们更好地理解整个转换过程;
int add42(int num) {
return num + 42;
}
对应的.wasm
如下:
00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B
其中,“num + 42” 大概是这样子的
代码如何在堆栈机里工作
WebAssembly
的工作机制就像堆栈机一样,运算时,运算相关的值在运算前会被一起在栈中排队等待运算执行;比如add
操作,只需要两个2个操作数,操作符会直接从栈顶取出2个操作数进行计算;由于不需要SI和DI寄存器,因此.wasm
的体积会更小,因此可以节约下载时间;
尽管WebAssembly是根据堆栈机指定的,但它在物理机上不是这样工作的。当浏览器将WebAssembly转换为运行浏览器的机器的机器码时,它将使用寄存器。由于WebAssembly代码没有指定寄存器,它为浏览器提供了更大的灵活性,可以为该机器使用最佳的寄存器分配。
Sections of the module
在.wasm
文件中,除了add42
函数自身外,可以看到还有很多其他组成部分,这里称作sections
;它们有一部分是对模块是必须的,有一部分是可选的;
必选项
- Type: 包含此模块中定义的函数和任何导入函数的函数签名
- Function: 为此模块中定义的每个函数提供索引
- Code: 模块中每个函数的真实函数体
可选项
- Export: 使
functions
/memories
/tables
/全局变量在JS
和其他module
可用;这允许分离编译的模块动态地链接在一起;这是一个.dll
的WebAssembly版本; - **Import: ** 指定要从其他WebAssembly模块或JavaScript导入的函数、内存、表和全局变量;
- Start: 当WebAssembly模块加载时自动运行的函数(基本类似于主函数);
- Global: 声明该
module
的全局变量 - Memory: 模块需要使用的内存;
- Table: 使得可以映射到WebAssembly模块之外的值,比如JavaScript对象。这对于允许间接函数调用特别有用。
- **Data: ** 初始化导入的或本地内存
- **Element: ** 初始化导入的或本地Table
Demo
首先,新建一个hello.c
文件
#include <stdio.h>
int main() {
printf('hello world\n');
return 0;
}
运行以下命令得到输出产物
emcc tests/hello.c -o tests/hello.html
使用浏览器打开html
文件查看效果,这里需要注意的是打开文件是以file
协议而不是以http
协议;部分浏览器默认不开启WebAssembly
,需要手动开启 —— Chrome打开chrome://flags并且开启Experimental WebAssembly
,部分FireFox需要在about:config
中开启javascript.options.wasm
运行结果如下图:
如果运行失败,并有以下报错both async and sync fetching of the wasm failed
,需要自己搭建本地服务器
简单解析