WebAssembly入门

声明:部分文档直接翻自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目标

  1. 快速、高效、可移植
  2. 可读性和可调试性 —— 虽然Wasm是低级的类汇编语言,但是其有着可读的文本格式,可以进行读、写、debug等;
  3. 保持安全性 —— 运行在一个安全的沙箱环境,遵循同源策略和权限策略;
  4. 不会破坏web —— 可以与web技术共存并且有向后兼容性;

WebAssembly如何适应web平台

web平台可以认为由两部分组成:

  1. 运行Web App代码的虚拟机,通常是JavaScript
  2. 一系列能够控制web浏览器或者设备的Web API,如DOMBOMWebGLIndexedDBWebAudio等;

随着Web应用范围更加广阔(3D游戏、AR/VR、音视频处理、计算机视觉等),功能更加复杂,Javascript加载、解析和执行的性能瓶颈逐渐突出;

WebAssembly的出现不是为了替代JavaScript,而是一种补充,同时可以与Javascript共同工作,这样web开发者可以结合两者的优势开发;WebAssembly代码的基本单元叫做模块,其与ES2015模块对称;


WebAssembly核心概念

  1. Module: 与ES2015 module类似,module是无状态的,定义了输入和输出,表示一段由浏览器编译形成的可执行的机器码;
  2. Memory:一个可调整大小的ArrayBuffer,它包含由WebAssembly的低级内存访问指令读写的线性字节数组;
  3. Table:一个可调整大小的类型化数组—— 包含那些原本不能以原始字节存储在内存中(由于安全和可移植性原因)的引用(如函数)
  4. Instance:可以理解为module在实际运行环境中的实例,包括运行时相关的MemoryTable及导入值等;

JavaScript Api提供了能力用于创建module/memory/table/instance,并且能够完全控制Wasm的加载、编译和执行,因此甚至可以把它当做是一致高性能的JS封装函数;JS实例与wasm实例也可以相互调用;

未来,Wasm将会支持loadable<script type="module"),这样就可以支持直接加载一个wasm模块;


emscripten

根据官网定义:

Emscripten是一个使用LLVM把C/C++等编译生成WebAssembly的完整的编译器工具链,特别关注速度、大小和Web平台。

  1. 移植性
    编译使用C/C++、或者其他使用LLVM的语言现有项目到浏览器、node、或者wasm运行时
  2. APIs
    Emscripten把OpenGL转为WebGL,并且支持了类似的API如SDL(c语言实现的跨平台的多媒体和游戏开发库,提供了处理声音、图像等相关封装函数)、线程、POSIX、Web APIs及Javascript
  3. 快速
    LLVMEmscriptenBinaryenWebAssembly的共同作用下,输出文件被压缩并且可以以接近原生的速度运行

LLVM
LLVM项目是模块化、可重用的编译器以及工具链技术的集合。

传统编译器架构
在这里插入图片描述
就像不同种群的人类讲不同的语言,不同架构的机器也需要不同的机器码才能理解人类需要它做什么;编译器的功能就是将多种高级语言统一转为特定机器可理解的机器码 —— 其中FE主要将各种高级编程语言转为IR,然后BE将IR转为适用于特定架构的汇编码;

如果每个高级语言对应到不同的架构机器码,这样会庞杂并且不怎么高效;
在这里插入图片描述
因此可以设计一个IR,高级语言统一转为IR,然后在不同的系统架构上再将IR转为特定的机器码;
在这里插入图片描述

LLVM架构
在这里插入图片描述
与Clang关系

Clang是LLVM项目的一个子项目,基于LLVM架构的C/C++/Objective-C编译器前端。相对gcc的优点包括编译速度快、占用内存小、模块化设计、拓展性强、可读性及可调试性强;
在这里插入图片描述

在这里插入图片描述

Binaryen

是以c++编写的用于WebAssembly的一个基础库,使用了Binaryen的编译器包括AssemblyScriptwasm2jsGrain等;与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主要有四类入口点:

  1. 使用Emscripten导入C/C++应用
  2. 在汇编层面直接编写和生成wasm
  3. 编写Rust应用并把wasm作为编译目标
  4. 使用AssemblyScript(类似于TypeScript)并且编译成wasm二进制码

这里仅介绍第一种

Porting from C/C++

C/C++导入主要有两种方式:使用线上wasm assember或者Emscripten;在线编译工具有WasmFiddleWasmFiddle++等;

Emscripten能够把任何的C/C++源码转为三部分:

  • 一个.wasm模块;
  • 一份加载和执行该模块的JS“粘合”代码
  • 显示代码结果的html;

在这里插入图片描述

在内核中,该过程如下:

  • EmscriptenC/C++输入到clang + LLVM —— 一套成熟的开源C/C++编译工具链,比如作为OXS中XCode的一部分;
  • Emscriptenclang + 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的过程:

  1. clang FE将C转为LLVM IR;
  2. LLVM optimizer对IR进行优化;
  3. BE将IR转为.wasm —— 这里BE一般有两种:LLVM WASM BEEMSCRIPTEN using asm2wasm
  4. wasm转为可以在特定架构上运行的机器码(x86ARM);当然,还有一些库也能实现,比如依赖于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;它们有一部分是对模块是必须的,有一部分是可选的;

必选项

  1. Type: 包含此模块中定义的函数和任何导入函数的函数签名
  2. Function: 为此模块中定义的每个函数提供索引
  3. Code: 模块中每个函数的真实函数体

可选项

  1. Export: 使functions/memories/tables/全局变量在JS其他module可用;这允许分离编译的模块动态地链接在一起;这是一个.dll的WebAssembly版本;
  2. **Import: ** 指定要从其他WebAssembly模块或JavaScript导入的函数、内存、表和全局变量;
  3. Start: 当WebAssembly模块加载时自动运行的函数(基本类似于主函数);
  4. Global: 声明该module的全局变量
  5. Memory: 模块需要使用的内存;
  6. Table: 使得可以映射到WebAssembly模块之外的值,比如JavaScript对象。这对于允许间接函数调用特别有用。
  7. **Data: ** 初始化导入的或本地内存
  8. **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,需要自己搭建本地服务器

简单解析


参考文献

  1. WebAssembly
  2. WebAssembly Concepts
  3. Emscripten官网
  4. asm.js 和 Emscripten 入门教程
  5. asm.js Work Draft
  6. 简述 LLVM 与 Clang 及其关系
  7. Compiling a New C/C++ Module to WebAssembly
  8. mdn/webassembly-examples
  9. asm.js 和 Emscripten 入门教程
  10. 深入浅出让你理解什么是LLVM
  11. WebAssembly 不完全指北
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Neil-

你们的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值